java集合之HashMap
- Map接口的基于哈希表的实现。 此实现提供所有可选的映射操作,并允许空null值和空null键。(除了非同步和允许使用 null 之外,
HashMap
类与Hashtable
大致相同)该类不保证映射的顺序; 特别是它不保证该顺序恒久不变。 - 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(
get
和put
)提供稳定的性能。迭代collection 视图需要的时间与 HashMap 实例的“容量”(桶的数量)加上它的大小(键-值映射的数量)成比例。 因此,如果迭代性能很重要,则不要将初始容量设置得太高(或负载因子太低),这一点非常重要。 - HashMap 的实例有两个影响其性能的参数:初始容量和负载因子(initial capacity and load factor)。 容量是哈希表中的桶数,初始容量就是哈希表创建时的容量。 负载因子是衡量哈希表在其容量自动增加之前允许达到多满的指标。 当哈希表中的条目数超过负载因子和当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
- 通常,默认负载因子 (.75) 在时间和空间成本之间提供了很好的权衡。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数
HashMap
类的操作中,包括get
和put
操作,都反映了这一点)。在设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以尽量减少 rehash 操作的次数。 如果初始容量大于最大条目数除以负载因子,则不会发生 rehash 操作。 - 如果很多映射关系要存储在
HashMap
实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。 请注意,使用具有相同 hashCode() 的多个键是降低任何哈希表性能的可靠方法。 为了改善影响,当键是 Comparable 时,此类可以使用键之间的比较顺序来帮助打破联系。 - 请注意,此实现不是同步的。 如果多个线程并发访问一个散列映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。)这通常是通过同步一些自然封装映射的对象来完成的 . 如果不存在这样的对象,则应使用 Collections.synchronizedMap 方法“包装”映射。 这最好在创建时完成,以防止对映射的意外不同步访问:
Map m = Collections.synchronizedMap(new HashMap(...));
- 此类的所有“集合视图方法”返回的迭代器都是快速失败的:如果在迭代器创建后的任何时间对映射进行结构修改,除了通过迭代器自己的 remove 方法外,迭代器将抛出 ConcurrentModificationException . 因此,面对并发修改,迭代器快速而干净地失败,而不是在未来不确定的时间冒着任意、非确定性行为的风险。
- 请注意,无法保证迭代器的快速失败行为,因为一般而言,在存在非同步并发修改的情况下不可能做出任何硬保证。 快速失败的迭代器会尽最大努力抛出 ConcurrentModificationException。 因此,编写一个依赖此异常来确保其正确性的程序是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
- 关于主数组的长度为2的倍数的原因:
- 扩容主数组的长度为2的倍数,因为这个length的长度,会影响 key的位置。
- key的位置的计算:
h & (length - 1);
实际上这个算法就是:h%length
,但是取模效率太低,所以用位运算效率代替,而此算法等效的前提就是 length必须是2的整数倍。 - 如果不是2的整数倍,则哈希碰撞的概率较高
- 关于装填因子 0.75:
- 如果装填因子大一点如1, 即数组满了再扩容,可以做到最大的空间利用率,但这是一个理想状态,元素不可能完全的均匀分布,很可能就哈西碰撞产生链表了。产生链表查询时间就长了。
—>空间好,时间不好 - 如果装填因子小一点如0.5,就浪费空间。可以做到逢0.5就扩容 ,哈希碰撞较少,不产生链表,查询效率较高 —>时间好,空间不好
- 在空间和时间中,取中间值,平衡两个因素就取值为 0.75
- 如果装填因子大一点如1, 即数组满了再扩容,可以做到最大的空间利用率,但这是一个理想状态,元素不可能完全的均匀分布,很可能就哈西碰撞产生链表了。产生链表查询时间就长了。
相关源码
public abstract class HashMap<K, V>
extends AbstractMap<K, V> //继承的AbstractMap中,已经实现了Map接口,又实现了这个接口
implements Map<K, V>, Cloneable, Serializable {
//重要属性:
static final int DEFAULT_INITIAL_CAPACITY = 16;//哈希表主数组的默认长度
/*定义了一个float类型的变量,以后作为:默认的装填因子,加载因子是表示Hsah表中元素的填满的程度,太大容易引起哈西冲突,太小容易浪费 0.75是经过大量运算后得到的最好值,这个值其实可以自己改,但是不建议改*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry<K, V>[] table;//主数组,每个元素为Entry类型
transient int size;
int threshold;//数组扩容的界限值,门槛值 16*0.75=12
final float loadFactor;//用来接收装填因子的变量
public HashMap() { //空构造器:内部相当于:this(16,0.75f);调用了当前类中的带参构造器
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}
//带参数构造器:对一些数值进行初始化
public HashMap(int initialCapacity, float loadFactor) {
//对capacity赋值,capacity的值一定是 大于传进来的initialCapacity的 2^n 中最小的数
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
//对loadFactor赋值,将装填因子0.75赋值给loadFactor
this.loadFactor = loadFactor;
//数组扩容的界限值,门槛值
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
//对table数组赋值,初始化数组长度为16
table = new Entry[capacity];
}
public V put(K key, V value) {//put方法:
if (key == null)//空值的判断
return putForNullKey(value);
int hash = hash(key);//调用hash方法,获取哈希码
int i = indexFor(hash, table.length);//得到key对应在数组中的位置
//如果你放入的元素,在主数组那个位置上没有值,e==null 那么不走这个循环;当在同一个位置上放入元素时,进行链表操作
for (Entry<K, V> e = table[i]; e != null; e = e.next) {
Object k;
//哈希值一样 并且 equals相比一样或者是同一个对象
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);//addEntry添加这个节点
return null;
}
//hash方法返回这个key对应的哈希值,内部进行二次散列,为了尽量保证不同的key得到不同的哈希码!
final int hash(Object k) {
int h = 0;
if (useAltHashing) {
if (k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h = hashSeed;
}
//k.hashCode()函数调用的是key键值类型自带的哈希函数,
//由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。
h ^= k.hashCode();
/*此函数可确保在每个位位置仅相差常数倍的 hashCode 具有有限数量的冲突(在默认加载因子下约为 8)。接下来的一串与运算和异或运算,称之为“扰动函数”,扰动的核心思想在于使计算出来的值在保留原有相关特性的基础上,增加其值的不确定性,从而降低冲突的概率。往右移动的目的,就是为了将h的高位利用起来,减少哈希冲突*/
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {//返回int类型数组的坐标
//算法等效为为取模运算:h%length,取模效率不如位运算
return h & (length - 1);
}
void addEntry(int hash, K key, V value, int bucketIndex) {
//size的大小 大于等于 16*0.75=12的时候
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);//主数组扩容为2倍
hash = (null != key) ? hash(key) : 0;//重新调整当前元素的hash码
bucketIndex = indexFor(hash, table.length);//重新计算元素位置
}
//将hash,key,value,bucketIndex 封装为一个Entry对象:
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K, V> e = table[bucketIndex];//获取bucketIndex位置上的元素给e
//将hash, key, value封装为一个对象,然后将下一个元素的指向为e (链表的头插法)并将新的Entry放在table[bucketIndex]的位置上
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;//集合中加入一个元素 size+1
}
void resize(int newCapacity) { //数组扩容
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
//创建长度为newCapacity的主数组
Entry[] newTable = new Entry[newCapacity];
boolean oldAltHashing = useAltHashing;
useAltHashing |= sun.misc.VM.isBooted() &&
(newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
boolean rehash = oldAltHashing ^ useAltHashing;
//转让方法:将老数组中的东西都重新放入新数组中
transfer(newTable, rehash);
//老数组替换为新数组
table = newTable;
//重新计算
threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K, V> e : table) {
while (null != e) {
Entry<K, V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
//将哈希值,和新的数组容量传进去,重新计算key在新数组中的位置
int i = indexFor(e.hash, newCapacity);
//头插法
e.next = newTable[i];//获取链表上元素给e.next
newTable[i] = e;//然后将e放在i位置
e = next;//e再指向下一个节点继续遍历
}
}
}
}
构造器
- HashMap() 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
- HashMap (int initialCapacity) 构造一个具有指定初始容量和默认负载因子 (0.75) 的空 HashMap。
- HashMap (int initialCapacity, float loadFactor) 构造一个具有指定初始容量和负载因子的空 HashMap。
- HashMap (Map<? extends K, ? extends V> m) 构造一个与指定 Map 具有相同映射关系的新 HashMap。
方法
Modifier and Type | Method | Description |
---|---|---|
void | clear() | 从此映射中删除所有映射。 |
Object | clone() | 返回这个 HashMap 实例的浅拷贝:键和值本身没有被克隆。 |
boolean | containsKey (Object key) | 如果此映射包含指定键的映射,则返回 true。 |
boolean | containsValue (Object value) | 如果此映射将一个或多个键映射到指定值,则返回 true。 |
Set<Map.Entry<K, V>> | entrySet() | 返回此映射中包含的映射的 Set 视图。 |
V | get (Object key) | 返回指定键映射到的值,如果此映射不包含该键的映射,则返回 null。 |
boolean | isEmpty() | 如果此映射不包含键值映射,则返回 true。 |
Set | keySet() | 返回此映射中包含的键的 Set 视图。 |
V | put (K key, V value) | 将指定值与此映射中的指定键关联。 |
void | putAll (Map<? extends K, ? extends V> m) | 将所有映射从指定映射复制到此映射。 |
V | remove (Object key) | 从此映射中删除指定键的映射(如果存在)。 |
int | size() | 返回此映射中键值映射的数量。 |
Collection | values() | 返回此映射中包含的值的集合视图。 |
- V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
- 尝试计算指定键及其当前映射值的映射(如果没有当前映射,则为 null)。
- V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
- 如果指定的键尚未与值关联(或映射为 null),则尝试使用给定的映射函数计算其值,并且 除非为空,否则将其输入此地图。
- V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
- 如果指定键的值存在且非空,则尝试计算给定键及其的新映射 当前映射值。
- V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
- 如果指定的键尚未与值关联或与空值关联,则将其与给定的值关联 非空值。
增加
V put(K key, V value)
将指定的值与此映射中的指定键关联(可选操作)。如果此映射以前包含一个该键的映射关系,则用指定值替换旧值(当且仅当 m.containsKey(k) 返回 true 时,才能说映射 m 包含键 k 的映射关系)。- key - 与指定值关联的键;value - 与指定键关联的值
- 返回:以前与 key 关联的值,如果没有针对 key 的映射关系,则返回 null。(如果该实现支持 null 值,则返回 null 也可能表示此映射以前将 null 与 key 关联)。
HashMap<Integer,String> map = new HashMap<>();
System.out.println(map.put(1,"fyz")); //null
map.put(6,"yjk");
map.put(8,"xhr");
System.out.println(map.put(1,"fyznb")); //fyz
map.put(5,"zsh");
System.out.println(map.size()); //4
System.out.println(map); //{1=fyznb, 5=zsh, 6=yjk, 8=xhr}
示意图
删除
clear() remove(Object key)
map.remove("zsh"); //移除
System.out.println(map);//{xhr=8, fyz=2, yjk=7}
map.clear(); //清空
System.out.println(map); //{}
查看
entrySet() get(Object key) keySet() size() values()
System.out.println(map.entrySet()); //[1=fyznb, 5=zsh, 6=yjk, 8=xhr]
System.out.println(map.get(5)); //zsh
System.out.println(map.keySet()); //[1, 5, 6, 8]
System.out.println(map.size()); //4
System.out.println(map.values()); //[fyznb, zsh, yjk, xhr]
使用视图遍历
System.out.println("\n---------keySet()---------");
//keySet()对集合中的key进行遍历查看:
Set<Integer> set = map.keySet();
for(Integer s:set){
System.out.print(s + s.hashCode() + "\t");
}
/*---------keySet()---------
2 10 12 16 */
System.out.println("\n---------values()---------");
//values()对集合中的value进行遍历查看:
Collection<String> values = map.values();
for(String i:values){
System.out.print(i + "\t");
}
/*---------values()---------
fyznb zsh yjk xhr */
System.out.println("\n---------get(Object key) keySet()---------");
Set<Integer> set2 = map.keySet();
for(Integer s:set2){
System.out.print(map.get(s) + "\t");
}
/*---------get(Object key) keySet()---------
fyznb zsh yjk xhr */
System.out.println("\n---------entrySet()---------");
Set<Map.Entry<Integer, String>> entries = map.entrySet();
for(Map.Entry<Integer, String> e:entries){
System.out.print(e.getKey()+"----"+e.getValue() + "\t");
}
/*---------entrySet()---------
1----fyznb 5----zsh 6----yjk 8----xhr */
判断
containsKey(Object key) containsValue(Object value) equals(Object o) isEmpty()
System.out.println(map.containsKey(5)); //true
System.out.println(map.containsValue("fyz")); //false
System.out.println("判断是否为空:"+map.isEmpty()); //判断是否为空:false
HashMap<Integer,String> map2 = new HashMap<>();
map2.put(1,"fyz");
map2.put(6,"yjk");
map2.put(8,"xhr");
map2.put(1,"fyznb");
map2.put(5,"zsh");
System.out.println("判断是否相等 == :" + (map == map2)); //判断是否相等 == :false
System.out.println("判断是否相等equals:"+map.equals(map2));//true,equals进行了重写,比较的是集合中的值是否一致
map2 = new HashMap<>();
map2.put(1,"fyz");
map2.put(6,"yjk");
map2.put(8,"xhr");
map2.put(1,"fyznb");
map2.put(5,"zsh");
System.out.println("判断是否相等 == :" + (map == map2)); //判断是否相等 == :false
System.out.println("判断是否相等equals:"+map.equals(map2));//true,equals进行了重写,比较的是集合中的值是否一致