Java Map
Java Map
Map 概述
Map 是一种依照键/值对(key/value)存储元素的容器,提供通过键快速获取、删除和更新键/值对的功能。
Map 的核心特性如下:
- 键(key)类似数组的下标,但数组下标固定为整数,Map 的键可以是任意类型的对象。
- 不允许重复键,若插入已存在的键,对应的值会被覆盖。
- 一个键与对应的值构成一个条目(Entry),条目整体存储在 Map 中。
![image]()
Map 类型及关系
Map 主要有三种实现类,其通用特性定义在 Map 接口中,类关系及具体实现如下:
核心接口与类结构
- 顶层接口:
Map<K, V> - 子接口:
SortedMap<K, V>、NavigableMap<K, V> - 抽象类:
AbstractMap<K, V> - 具体实现类:
HashMap<K, V>、LinkedHashMap<K, V>、TreeMap<K, V>
![image]()
各实现类特性
| 实现类 | 核心特性 | 排序方式 | 关键说明 |
|---|---|---|---|
| HashMap | 高效的增删改查,底层为散列表(数组+链表/红黑树) | 无序 | 允许 null 键和 null 值,初始容量 16 |
| LinkedHashMap | 继承 HashMap,扩展链表实现 | 插入顺序或访问顺序 | 无参构造默认插入顺序,指定 accessOrder=true 为访问顺序 |
| TreeMap | 基于红黑树实现,遍历键时高效 | 自然排序或自定义排序 | 键需实现 Comparable 接口或通过 Comparator 指定排序规则,不允许 null 键,运行 null 值 |
关键接口方法
SortedMap接口额外提供:firstKey():返回第一个键lastKey():返回最后一个键headMap(toKey):返回键小于 toKey 的子 MaptailMap(fromKey):返回键大于 fromKey 的子 Map
NavigableMap接口额外提供:floorKey(key):返回小于等于 key 的最大键ceilingKey(key):返回大于等于 key 的最小键lowerKey(key):返回小于 key 的最大键higherKey(key):返回大于 key 的最小键pollFirstEntry():移除并返回第一个条目pollLastEntry():移除并返回最后一个条目
![image]()
简单示例代码
package com.map;
import java.util.*;
/**
* @author Jing61
*/
public class MapDemo {
public static void main(String[] args) {
// HashMap,默认无序
Map<Integer, String> map1 = new HashMap<>();
map1.put(1, "Peppa");
map1.put(2, "Emily");
map1.put(3, "Candy");
map1.put(4, "Jorge");
map1.put(5, "Pedro");
System.out.println(map1);
// 如果重复的键值,则覆盖
map1.put(1, "Pedro");
System.out.println(map1);
// 如果键不存在,则添加,否则不添加
map1.putIfAbsent(2, "Jorge");
// 获取key的value
System.out.println(map1.get(2));
System.out.println(map1.get(1));
// 获取不存在的key的value,则返回null
System.out.println(map1.get(3));
// 获取不存在的key的value,则返回默认值
System.out.println(map1.getOrDefault(3, "Not Found"));
// 允许存入空键值
map1.put(null, null);
System.out.println(map1);
// 删除键值对
map1.remove(null);
// 获取键值对个数
System.out.println(map1.size());
// 判断map是否为空
System.out.println(map1.isEmpty());
// 是否包含某个键
System.out.println(map1.containsKey(1));
// 是否包含某个值
System.out.println(map1.containsValue("Pedro"));
// 修改某个键值对,可以通过put覆盖,也可以通过replace
map1.replace(1, "Dannie");
/*
迭代map
1.获取map中所有的key,通过key迭代
*/
Set<Integer> keys = map1.keySet();
for (Integer key : keys) {
System.out.println(key + ":" + map1.get(key));
}
/*
2.获取map中所有的value,通过value迭代
*/
Collection<String> values = map1.values();
for (String value : values) {
System.out.println(value);
}
/*
3.获取map中所有的键值对,通过键值对迭代
*/
Set<Map.Entry<Integer, String>> entries = map1.entrySet();
for (Map.Entry<Integer, String> entry : entries) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
/*
4.forEach
*/
map1.forEach((key, value) -> System.out.println(key + ":" + value));
// 清空
map1.clear();
// LinkedHashMap,插入有序
Map<String, Integer> map2 = new LinkedHashMap<>();
map2.put("Peppa", 5);
map2.put("Emily", 6);
map2.put("Candy", 12);
map2.put("Jorge", 3);
map2.put("Pedro", 6);
map2.put("Dannie", 4);
System.out.println(map2);
// LinkedHashMap,按照最后一次访问的顺序进行排序
Map<String, Integer> map3 = new LinkedHashMap<>(16, 0.75f, true);
map3.put("Peppa", 5);
map3.put("Emily", 6);
map3.put("Candy", 12);
map3.put("Jorge", 3);
map3.put("Pedro", 6);
map3.put("Dannie", 4);
System.out.println(map3.get("Emily"));
System.out.println(map3.get("Pedro"));
System.out.println(map3);
// TreeMap,默认有序,不能插入null键,但允许插入null值
Map<String, Integer> map4 = new TreeMap<>();
map4.put("Peppa", 5);
map4.put("Emily", 6);
map4.put("Candy", 12);
map4.put("Jorge", 3);
map4.put("Pedro", 6);
map4.put("Dannie", 4);
System.out.println(map4);
}
}
实战:统计单词出现次数
package com.map;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* @author Jing61
*/
public class HashMapDemo {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
try (BufferedReader reader = new BufferedReader(new FileReader("test.txt"))){
//循环读取每一行
String line = null;
while((line = reader.readLine()) != null) {
String[] words = line.split("[^a-zA-Z]+");
for(String src : words) {
if(map.containsKey(src)) {//表示map中已经存在key为当前迭代的单词,需要对该key对应的value+1
int count = map.get(src);
map.replace(src, count + 1);
}else
map.put(src, 1);
}
}
//1.如果map中存储该单词,就对该单词次数进行+1,else 就将该单词存放进map中
} catch (IOException e) {
e.printStackTrace();
}
/**
* 遍历Map:
* 1.可以获取所有的key=====>keySet
* 2.可以获取所有的values====>values
* 3.遍历所有的条目
* 4.JDK8增强:forEach
*/
// Set<String> keys = map.keySet();
// for(String key : keys)
// map.get(key);
//map.values();
Set<Map.Entry<String, Integer>> entries = map.entrySet();
for(Map.Entry<String, Integer> entry : entries) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
//jdk 8增强
//map.forEach((key,value)->System.out.println(key + ":" + value));
}
}
HashMap 原理概述
底层数据结构
HashMap 底层采用散列表(哈希表),本质是“数组+链表”的结合,兼具两者优点。
- 数组:占用空间,连续寻址容易,查询速度快,但是,增加和删除效率非常低。
- 链表:占用空间不连续,寻址困难,查询速度慢,但是,增加和删除效率非常高。
- 哈希表:即查询快,增删效率也高。
- JDK8 优化:当链表长度大于 8 时,链表转换为红黑树,进一步提升查找效率。
核心源码解析
关键常量与属性
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 初始容量 16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认负载因子
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table; // 核心数组(位桶数组)
...
}
Node 类结构
Node[] table 就是 HashMap 的核心数组结构,我们也称之为“位桶数组”。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 键的哈希值
final K key; // 键
V value; // 值
Node<K,V> next; // 下一个节点(链表指针)
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
return o instanceof Map.Entry<?, ?> e
&& Objects.equals(key, e.getKey())
&& Objects.equals(value, e.getValue());
}
}
Node 对象存储结构
一个Node对象存储了:
key:键对象value值对象。next:下一个节点。hash:键对象的hash值。
显然每一个Entry对象就是一个单向链表结构,我们使用图形表示一个Entry对象的典型示意:

然后,我们画出 Node[] 数组的结构(这也是HashMap的结构):

存储数据过程(put 方法)
- 获得 key 的 hashCode:调用 key 对象的
hashCode()方法得到哈希码。 - 计算 hash 值:通过两次散列优化分布,最终通过位运算
hash = hashcode & (数组长度-1)得到数组索引(数组长度为 2 的整数幂)。
散列源码:static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } - 生成 Node 对象:封装 key、value、hash 值和 next 指针(初始为 null)。
- 存入数组:
- 若索引位置无 Node,则直接存入。
- 若索引位置已有 Node,则将新 Node 作为链表尾节点(JDK8 中链表长度超 8 转为红黑树)。

总结如上过程:当添加一个元素(key-value)时,首先计算key的hash值,以此确定插入数组中的位置,但是可能存在同一hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表,同一个链表上的Hash值是相同的,所以说数组存放的是链表。JDK8中,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率。
获取数据过程(get 方法)
- 计算 hash 值:通过 key 的 hashCode 经散列算法得到索引位置。
- 遍历查找:在对应索引的链表/红黑树中,通过
equals()方法对比 key,直到碰到返回 true 的节点对象为止,找到匹配的 Node。 - 返回结果:返回匹配 Node 的 value。
扩容机制
- 触发条件:当数组中元素个数达到
负载因子(0.75)* 数组长度时,触发扩容。 - 扩容规则:数组长度变为原来的 2 倍,重新计算所有 Node 的 hash 值并迁移到新数组。
- 注意:扩容操作耗时,需尽量避免频繁扩容。
注意
Java 规定:两个通过 equals() 判定相等的对象,必须具有相同的 hashCode();若 equals() 为 true 但 hashCode() 不同,会导致 HashMap 中存储和查找异常。两个具有相同的 hashCode()的对象,不一定 equals() 判定相等。




浙公网安备 33010602011771号