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_CAPACITY16默认的初始容量-必须是2的幂。
MAXIMUM_CAPACITY230最大容量,必须小于等于该值。且必须是2的幂。
DEFAULT_LOAD_FACTOR0.75f默认加载因子
TREEIFY_THRESHOLD8树化阈值。当向bin(桶)中添加元素时,如果 binCount >= TREEIFY_THRESHOLD - 1 则,bin将(由列表)转换为。取值范围(2, 8]
UNTREEIFY_THRESHOLD6在调整大小操作期间取消树化的阈值。应小于 TREEIFY_THRESHOLD,且最多为6
MIN_TREEIFY_CAPACITY64可以对容器进行树化的最小表容量。(否则,如果一个bin中有太多的节点,则会调整表的大小。)应该至少是4 * TREEIFY_THRESHOLD,以避免调整大小和树化阈值之间的冲突。

2.成员属性

默认都是 null

属性说明
transient Node<K,V>[] table表,在第一次使用时初始化,并根据需要调整大小。分配时,长度总是2的幂。(我们还允许某些操作的长度为0,以允许当前不需要的引导机制。)
transient Set<Map.Entry<K,V>> entrySet保存缓存的 entrySet()。注意,AbstractMap 字段用于 keySet()values()
transient int sizeMap 中包含的键-值对的数量。
transient int modCount结构修改是指改变 HashMap 中的映射数量或以其他方式修改其内部结构(例如,重新哈希)的修改。该字段用于使 HashMap 集合视图上的迭代器快速失败。(见ConcurrentModificationException)。
int threshold要调整大小的下一个大小值( 容量 * 加载因子)。
final float loadFactor哈希表的加载因子。
———————————————————

静态内部类

class Node<K,V>

基本的哈希 bin (桶)节点,用于大多数键值对。(参见下面的TreeNode子类,以及LinkedHashMap的Entry子类。)

static class Node<K,V> implements Map.Entry<K,V> 
        final int hash; // 散列值。通过静态方法 hash(Object key) 计算 key 生成的
        final K key;	// 没错就是用我算出的 hash
        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;
        
		// 1. 如果地址相等,直接 true
		// 2. 如果 o 是 Map.Entry(键值对实体)的实例,且 key、value 都一样则 true
		// 3. 否则不相等 false
        public final boolean equals(Object o) 
            if (o == this)
                return true;
            if (o instanceof Map.Entry) 
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            
            return false;
        
    

静态工具方法

访问修饰符&返回类型方法描述
static final inthash(Object key)获取 key 的 hash 值。为了尽量避免碰撞,使用 异或位移。是出于性能考虑。
static Class<?>comparableClassFor(Object x)如果x的形式是Class C implements Comparable<C> 返回它的类的类型。否则为空。
static intcompareComparables(Class<?> kc, Object k, Object x)如果 x的类型kc 就返回 k.compareTo(x) 的结果,否则返回 0
static final inttableSizeFor(int cap)返回大于 cap(给定目标容量)的最小 2 次幂数。
—————————————————————

hash(Object key)

获取 key 的 hash 值。为了尽量避免碰撞,使用 异或位移。是出于性能考虑。

  • 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)
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)
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 然后才使用。

@SuppressWarnings("rawtypes","unchecked") // 压制强转警告
static int compareComparables(Class<?> kc, Object k, Object x) 
	// 以下情况中假设 k、x 的类型都是 String
	// 1. x 为 null 直接返回 0 (表示比个毛)
	// 2. kc 是从 k 上获取的比较器(Comparable<String>)的参数的类型(String.class)。
	//    如果 k 没有实现 Comparable<String> 则 kc 为 null,否则 kc 为 String.class
	// 3. x.getClass() != kc 意思是:如果 k 没有实现 Comparable<String> 比较器,就没法比,直接返回 0
	//    换句话说只有 k 实现了 Comparable<X> 才会执行到 ((Comparable)k).compareTo(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

从下而定个例子也可以看出,当有 6 位时,第三次移动时,就已经得到全是1的效果了。

// ========================= 6位 =========================
100000
0100000      // 第1次
------------------
110000
00110000	 // 第2次
------------------
111100
0000111100	 // 第3次
------------------
111111 		 // 6位第次就搞定了

// ========================= 再来个全的32位 =========================
10000000000000000000000000000000
01000000000000000000000000000000 	// >>> 1
------------------------------------
11000000000000000000000000000000	// |
00110000000000000000000000000000	// >>> 2
------------------------------------
11110000000000000000000000000000	// |
00001111000000000000000000000000	// >>> 4
------------------------------------
11111111000000000000000000000000	// |
00000000111111110000000000000000	// >>> 8
------------------------------------
11111111111111110000000000000000	// |
00000000000000001111111111111111	// >>> 16
------------------------------------
11111111111111111111111111111111

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

static final int tableSizeFor(int cap)  // cap = 50
    int n = cap - 1;	// 此处 -1 确保当正好是2的二次幂时,最后的 +1 能还原此数。
    // 这一通 >>> 与 | 配合下来,能得到原最高位后所有位都变成1.
    // 如: 100000 to 111111; 101010 to 111111;
    n |= n >>> 1;		
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    // 小于0直接返回 1
    // 如果大于最大值直接返回最大值,否则当前值 +1 返回。
    // +1 能保存是 2的二次幂。因为最高位后所有都是1时,再+1,肯定是一个2的倍数。
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;

构造方法

访问修饰符&返回类型方法描述

HashMap(int initialCapacity, float loadFactor)

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and load factor.
 *
 * @param  initialCapacity the initial capacity
 * @param  loadFactor      the load factor
 * @throws IllegalArgumentException if the initial capacity is negative
 *         or the load factor is nonpositive
 */
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;
    this.threshold = tableSizeFor(initialCapacity);

HashMap(int initialCapacity)

/**
 * Constructs an empty <tt>HashMap</tt> with the specified initial
 * capacity and the default load factor (0.75).
 *
 * @param  initialCapacity the initial capacity.
 * @throws IllegalArgumentException if the initial capacity is negative.
 */
public HashMap(int initialCapacity) 
    this(initialCapacity, DEFAULT_LOAD_FACTOR);

HashMap()

/**
 * Constructs an empty <tt>HashMap</tt> with the default initial capacity
 * (16) and the default load factor (0.75).
 */
public HashMap() 
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted

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

/**
 * Constructs a new <tt>HashMap</tt> with the same mappings as the
 * specified <tt>Map</tt>.  The <tt>HashMap</tt> is created with
 * default load factor (0.75) and an initial capacity sufficient to
 * hold the mappings in the specified <tt>Map</tt>.
 *
 * @param   m the map whose mappings are to be placed in this map
 * @throws  NullPointerException if the specified map is null
 */
public HashMap(Map<? extends K, ? extends V> m) 
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);

  • putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
/**
 * Implements Map.putAll and Map constructor
 *
 * @param m the map
 * @param evict false when initially constructing this map, else
 * true (relayed to method afterNodeInsertion).
 */
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) 
    int s = m.size();
    if (s > 0) 
        if (table == null

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

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

Java 集合学习笔记:HashMap

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

java集合类学习笔记之LinkedHashMap

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

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