Java 集合学习笔记:HashMap

Posted 笑虾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合学习笔记:HashMap相关的知识,希望对你有一定的参考价值。

Java 集合学习笔记:HashMap

UML

简介

基于Hash tableMap接口实现。实现了 Map 定义的所有方法,并允许 keyvaluenull。(除了非线程安全和允许 null 之外,HashMapHashtable 大致相同)这个类不保证 map 的顺序;尤其是,它不能保证顺序随时间的推移保持不变。

这个实现为基本操作(getput)提供了恒定时间的性能,假设 hash 函数将元素适当地分散到桶中。在集合视图上迭代所需的时间与HashMap实例的容量(桶的数量)加上它的大小(键-值映射的数量)成正比。因此,如果迭代性能很重要,就不要将初始容量设置得太高(或负载因子设置得太低)。

HashMap 的实例有两个参数影响其性能:初始容量负载因子容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。负载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了负载因子与当前容量的乘积时,则要对该哈希表进行rehash操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。

通常,默认负载因子 (0.75) 在时间和空间成本上寻求一种折衷。负载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 getput 操作,都反映了这一点)。在设置初始容量时应该考虑到映射中所需的条目数及其负载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以负载因子,则不会发生 rehash 操作。

如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。

注意,此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须 保持外部同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)这一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,以防止对映射进行意外的非同步访问,如下所示:

Map m = Collections.synchronizedMap(new HashMap(...));

由所有此类的“collection 视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。

注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException 。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

此类是 Java Collections Framework 的成员。

阅读源码

属性字段

1. 静态属性

属性默认值说明
DEFAULT_INITIAL_CAPACITY1<<4==16默认的初始容量 16。必须是2的幂。
MAXIMUM_CAPACITY1<<30==230table 的最大容量 ,必须小于等于该值。且必须是2的幂。
230=01000000 00000000 00000000 00000000
DEFAULT_LOAD_FACTOR0.75f默认负载因子
TREEIFY_THRESHOLD8树化阈值。当向哈希桶中添加元素时,如果 结点数 >= TREEIFY_THRESHOLD - 1 则将链表转换为。取值范围(2, 8]
(还需要满足前提条件 MIN_TREEIFY_CAPACITY)
UNTREEIFY_THRESHOLD6取消树化阈值。在split时如果发现桶中的结点数 <=此阈值,则将红黑树转为链表
取值应小于 TREEIFY_THRESHOLD,且最多为6
MIN_TREEIFY_CAPACITY64触发树化的前提条件。
除了 哈希桶 中链表长度达到阈值 外还需要 哈希表.容量 >= 64,才能触发树化。
(否则,如果一个bin中有太多的结点,则会调整表的大小。)应该至少是4 * TREEIFY_THRESHOLD,以避免调整大小和树化阈值之间的冲突。

2. 成员属性

  • 默认都是:null
  • transient :被此关键字修饰的字段不会序列化
属性说明
transient Node<K,V>[] table哈希表本质是一个数组。HashMap 是由 数组+链表or红黑树实现的,这就是那个数组。它的每个索引位置是一个哈希桶,桶中存放的是链表的头结点树的根结点
在刚new出来的新 HashMap 对象中table = null
在第一次使用时通过 resize() 初始化,并根据需要调整哈希表大小。分配的长度必定是2的幂。(We also tolerate length zero in some operations to allow bootstrapping mechanics that are currently not needed. 这句只知道他说允许长度为0,但他所指的“当前不需要的引导机制”不知道是指的啥。不会翻555)
transient Set<Map.Entry> entrySet缓存entrySet()的结果。作为一个当前 HashMap 所有键值对的视图。
transient int sizeMap 中实际包含的元素(键-值)数量。
transient int modCount结构修改次数。结构修改是指改变 HashMap 中的映射数量或以其他方式修改其内部结构(例如,重新哈希)的操作。该字段用于 HashMap 迭代器的并发冲突检测。(见ConcurrentModificationException)。
int threshold扩容阈值。键值对(entry)的个数大于阈值时,会触发扩容。( threshold = 容量 * 负载因子)。
final float loadFactor哈希表的负载因子。
———————————————————

HashMap 结构

静态工具方法

访问修饰符&返回类型方法描述
static final inthash(Object key)计算 keyhash 值。为了尽量避免碰撞,使用 异或位移。是出于性能考虑。
static Class<?>comparableClassFor(Object x)如果x的形式是Class C implements Comparable<C> 返回 C.class。否则返回 null
static intcompareComparables(Class<?> kc, Object k, Object x)如果 x的类型kc 就返回 k.compareTo(x) 的结果,否则返回 0
static final inttableSizeFor(int cap)返回大于 cap 的最近的一个 2 的倍数。
—————————————————————

hash

计算 keyhash 值。为了尽量避免碰撞,使用 异或位移。是出于性能考虑。

  • test
@Test
public void hashCodeTest()
    int h = 0b11111111111111110000000000000000;     // 0b开头表示二进制数
    int i = h >>> 16;                               // 无符号右移16位(包括符号位一起移)
    log.info("", Integer.toBinaryString( i ));    // 00000000000000001111111111111111 原本高位的16个1都移到了左边,左边空出的位置补0
    int hash = h ^ i;                               // 异或运算
    log.info("", Integer.toBinaryString( hash )); // 11111111111111111111111111111111 i高16位没东西,直接照搬 h,低16位,不同为1,相同为 0

  • hash(Object key)
  1. 如果 key 为 null 返回 0
  2. 否则调用对象的 hashCode() 获取 hash值。
  3. ^ + >>>,把 hash 值搅拌一下,尽可能的减少不同 key 出现 hash 相同的情况。
static final int hash(Object key) 
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

【算法学习】 ^ 加 >>> 减少碰撞

为何要使用异或位移来减少碰撞?

comparableClassFor(Object x)

如果x的形式是Class C implements Comparable<C> 返回其类的类型。否则为空。

  • test
    例如:当 x 的类型 C 和比较器的参数类型 Comparable<C> 一样时就返回 C
// 形式为 Class C implements Comparable<C>
Class C implements Comparable<C>;
C c = new C;
Class<?> clazz = comparableClassFor(c);
System.out.println(clazz.getName()); // C
// Class C implements Comparable<如果这里不是C> 返回 null
  • comparableClassFor(Object x)
  1. 检测:给定参数必需实现了 Comparable 否则直接返回 null
  2. 如果给定参数是String,直接返回 String.class,因为我们知道String实现了 Comparable<String>
  3. 否则:遍历对象实现的所有接口,逐个判断:
    3.1. 如果实现了Comparable 并且泛型参数不为空,则返回此参数类型。
static Class<?> comparableClassFor(Object x) 
	// 如果 x 是 Comparable 的实例。如果是继续,否则返回 null;
    if (x instanceof Comparable) 
    
        Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
        // 如果 x 是个 String 直接返回类型。
        if ((c = x.getClass()) == String.class) // bypass checks
            return c;
        // 获取 c 所实现的接口们(可能有多个所以放数组里)。如果不为 null 继续,否则返回 null;
        if ((ts = c.getGenericInterfaces()) != null) 
        	// 逐个遍历接口类型
            for (int i = 0; i < ts.length; ++i) 
                if (
                	// 1. 如果此接口 t 是某种 ParameterizedType(参数化类型,如 Collection<String>)
                	((t = ts[i]) instanceof ParameterizedType)
                	// 2. 并且,接口 t 的类型正好是 Comparable(为了调用 getRawType() 获取类型,做了强转)
                    && ((p = (ParameterizedType)t).getRawType() == Comparable.class)
                    // 3. 并且,获取 p 的参数类型数组。不为 null。(Comparable<T>就是这里替换 T 的实参)
                    && (as = p.getActualTypeArguments()) != null
                    // 4. 并且,只有 1 个
                    && as.length == 1 
                    // 5. 并且,Comparable<参数> 中的 ‘参数’ == 给定的 x 的类型。
                    && as[0] == c
                 ) 
                 return c;
            
        
    
    return null;

compareComparables(Class<?> kc, Object k, Object x)

如果 x的类型kc 就返回 k.compareTo(x) 的结果,否则返回 0
此方法是要配合上面的 comparableClassFor(Object x) 一起用的。

  • test
@Test
public void compareComparablesTest()
    String k = "jerry1";
    String x = "jerry2";
    Class<?> kc = comparableClassFor(k);
    int i = compareComparables(kc, k, x);
    log.info("", i); // -1

  • compareComparables(Class<?> kc, Object k, Object x)

k:就是 key,比如类型是我们最见的“字符串”。String 就实现了 Comparable<String>
kc : 通过 comparableClassFor(k) 获取 k 实现的 Comparable<T> 中的实参。在 HashMap 的源码 findtreeifyputTreeVal 这些方法中能看到它的身影。kc 都有先判断 非null 然后才使用。

以下情况中假设 k、x 的类型都是 String

  1. xnull 直接返回 0 (表示比个毛)
  2. kc 是从 k 上获取的比较器 Comparable<String> 的参数的类型 String.class
    如果 k 没有实现 Comparable<String>kcnull,否则 kcString.class
  3. x.getClass() != kc 表达的意思是:如果 k 没有实现 Comparable<String> 比较器,就没法比,直接返回 0
    换句话说只有 k 实现了 Comparable<X> 才会执行到 ((Comparable)k).compareTo(x) 这里。
@SuppressWarnings("rawtypes","unchecked") // 压制强转警告
static int compareComparables(Class<?> kc, Object k, Object x) 
    return (x == null 
    		|| x.getClass() != kc ? 0 : ((Comparable)k).compareTo(x));

tableSizeFor(int cap)

返回大于 cap 的最近的一个 2 的倍数。

  • test
    以 cap = 50 为例:
@Test
public void tableSizeForTest()
    int cap = 50;
    int n = cap - 1;	// n: 49。
    int MAXIMUM_CAPACITY = 1 << 30; // 1_073_741_824
    //int x,y;
    //log.info("原值=;  =  | ; Binary:  =  |  ", x=y=n, x |= x >>> 1, y, y>>>1,Integer.toBinaryString(x), Integer.toBinaryString(y), toBinary(y>>>1, 6));
    n |= n >>> 1;       // 原值=49; 57 = 49 | 24; Binary: 111001 = 110001 | 011000
    n |= n >>> 2;       // 原值=57; 61 = 57 | 28; Binary: 111101 = 111001 | 011100
    n |= n >>> 4;       // 原值=63; 63 = 63 | 31; Binary: 111111 = 111111 | 011111
    n |= n >>> 8;       // 原值=63; 63 = 63 | 31; Binary: 111111 = 111111 | 011111
    n |= n >>> 16;      // 原值=63; 63 = 63 | 31; Binary: 111111 = 111111 | 011111
    n = (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    System.out.println(n); // 64

cap:目标容量传进来前已确保 >= 0

static final int tableSizeFor(int cap)  // cap = 50
	// 因为末尾需要 + 1 达到 0111 + 1 = 1000 效果。所以此处配合先 -1
	// 最终效果:确保当 cap 正好是 2的n次方时,最终结果是 cap 本身。
    int n = cap - 1;	
    // 这一通 >>> 与 | 配合下来,使得最高位的 1 不变,右边所有的 0 都变成 1
    // ( 也就是找到最高位并将其右侧所有位都设置成 1 )
    // 如: 1000 0000 变成 1111 1111; 
    //      0010 1010 变成 0011 1111;
    n |= n >>> 1;		
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    // 小于 0 直接返回 1 ( 2 的 0 次方 )
    // 如果大于最大值直接返回最大值,否则当前值 +1 返回。
    // +1 能保存是 2的二次幂。因为最高位后所有都是 1 时,再+1,肯定是一个 2 的幂。
    // 例:0000 1111 + 1 = 0001 0000 = 16
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

【算法学习】32 位全置 1

10000000 00000000 00000000 00000000 
01000000 00000000 00000000 00000000 // >>> 1
————————————————————————————————————// 或
11000000 00000000 00000000 00000000 
00110000 00000000 00000000 00000000	// >>> 2
————————————————————————————————————// 或
11110000 00000000 00000000 00000000 
00001111 00000000 00000000 00000000	// >>> 4
————————————————————————————————————// 或
11111111 00000000 00000000 00000000	
00000000 11111111 00000000 00000000	// >>> 8
————————————————————————————————————// 或
11111111 11111111 00000000 00000000
00000000 00000000 11111111 11111111	// >>> 16
————————————————————————————————————// 或
11111111 11111111 11111111 11111111 

构造方法

方法描述
HashMap(int initialCapacity, float loadFactor)指定初始容量负载因子,构造一个空的 HashMap 。
HashMap(int initialCapacity)指定初始容量,构造一个空的 HashMap 。负载因子 默认 0.75
HashMap()构造一个空的 HashMap【容量默认16 】,【负载因子默认0.75】
HashMap(Map<? extends K, ? extends V> m)用指定的 Map,构造一个新的 HashMap。【负载因子默认 0.75】,容量足够存放给定的 Map。

HashMap(int initialCapacity, float loadFactor)

指定初始容量负载因子,构造一个空的 HashMap。

/**
 * @param  initialCapacity 初始容量
 * @param  loadFactor      负载因子
 * @throws 如果【初始容量 < 0】,或【负载因子 <= 0】,抛 IllegalArgumentException
 */
public HashMap(int initialCapacity, float loadFactor) 
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
    // 【初始容量】超过上限就拉回来
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 【负载因子】非正或不是数字,抛锅。
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
    // 初始化负载因子
    this.loadFactor = loadFactor;
    // 返回大于 initialCapacity 的最小 2 次幂数。
    this.threshold = tableSizeFor(initialCapacity);

HashMap(int initialCapacity)

指定 初始容量,构造一个空的 HashMap。负载因子 默认 0.75

/**
 * @param  initialCapacity 初始容量
 * @throws 如果【初始容量 < 0】抛 IllegalArgumentException 
 */
public HashMap(int initialCapacity) 
    this(initialCapacity, DEFAULT_LOAD_FACTOR); // DEFAULT_LOAD_FACTOR =  0.75f;

HashMap()

构造一个空的 HashMap 容量默认16 负载因子默认0.75

public HashMap() 
    this.loadFactor = DEFAULT_LOAD_FACTOR; // DEFAULT_LOAD_FACTOR =  0.75f; 其它都是默认值。

HashMap(Map<? extends K, ? extends V> m)

用指定的 Map,构造一个新的 HashMap。负载因子使用默认的0.75容量等于给定的 Map 大小

/**
 * @param   m 用来创建 HashMap 的给定 map
 * @throws  如果给定 map 为空,抛 NullPointerException 
 */
public HashMap(Map<? extends K, ? extends V> m) 
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);

非 public 方法

putMapEntries 填充当前对象

用给定的 map 填充当前 HashMap。主要是实现 Map.putAllMap 构造 的底层逻辑,供它们调用 .

  • putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
  1. 如果给定的 map 不为空,继续处理,否则结束。
  2. 如果 哈希表表为 null 则先初始化,否则直接检测并按需扩容。
  3. 然后遍历给定的 map 填充当前 HashMap
/**
 * @param m 	用来创建 HashMap 的 map
 * @param evict 在最初构造这个 Map 时为 false,否则为true(最终会传给 afternodeinsert 方法 )。                                                                                                                                        
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) 
	// 取出给定 map 的大小,好用来创建新的 HashMap
    int s = m.size();
    // 没有内容直接跳过
    if (s > 0) 
    	// 为 null 表示尚未初始化
        if (table == null)  // pre-size
        	// 计算所需的初始容量(向上取整尽量避免频繁扩容)
            float ft = ((float)s / loadFactor) + 1.0F;
            // 如果没超最大值,直接使用。否则用最大值。
            int t = ((ft < (float)MAXIMUM_CAPACITY) ? (int)ft : MAXIMUM_CAPACITY);
            // 如果大于扩容阈值,则计算出新的扩容阈值(大于 t 的最小 2 次幂)。
            if (t > threshold) // 初始化时  threshold = 0 必触发
                threshold = tableSizeFor(t);
        // 给定的 map 比当前 HashMap 大。进行扩容。
         else if (s > threshold)
            resize();
        // 遍历给定的 m 填充当前 HashMap
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) 
            K key = e.getKey();
            V value = e.getValue();
            // 被构造函数调用时 evict = false 
            putVal(hash(key), key, value, false, evict); 
        
    

【算法学习】 尽量避免新 Map 很快触发扩容

分析1:
((float)s / loadFactor) + 1.0F 通过+1.0F 努力避免刚出生没扑腾 put几下就触发扩容的尴尬。
1. (int)( float + 1 ) 的含义是向上取整。常用于小数部分不应舍弃的场景,比如1.5瓶饮料,需要2个瓶子装。
2. 以 s = 12 为例:如不 +1.0F 那么 threshold = (12 / 0.75) = 16 离下一次扩容近在咫尺。构造出来的新 map 几乎一抬脚就要触发一次扩容。
但如果有了 +1.0F 那么 (12 / 0.75) + 1 = 17 经过 tableSizeFor(17) 结果是 32。新 map 可以开心的扑腾put 不用尴尬了。

分析2:
在网上搜了一下,看到有一种说法,如果不+1,舍弃小数,会导致放不下(触发扩容浪费)。但是 size + 1 > size / 0.75 不可能成立,size > size / 0.75 更不可能。
更何况有tableSizeFor(t)的存在,size 要正好是 2的n次方,然后再满足上面的条件。
无论如何,实际PUT的个数永远不可能放不下啊????这个疑惑困扰了我几天5555

分析3:
看了 anlian523-JDK8 HashMap源码 putMapEntries解析 发现:我把程序员们想得太好了,我认为他们会遵守基本的编码规范(如果你不知道为何 loadFactor 是 0.75 请别动它),但是如果有人不用 0.75 非要传一个奇怪的小数,就可能满足 16 + 1 > 16 / 0.95 == true ,所以我是不是可以理解为,这个 +1 不是用来防止容量不够,而是用来防刁民的

getNode 获取结点

按给定的 hash

以上是关于Java 集合学习笔记:HashMap的主要内容,如果未能解决你的问题,请参考以下文章

Java 集合学习笔记:HashMap - 迭代器

Java 集合学习笔记:HashMap

Java集合源码学习笔记HashMap分析

java集合类学习笔记之LinkedHashMap

Java学习笔记5.4.1 Map接口 - HashMap类

java集合之HashMap与ConcurrentHashMap的自我理解