java.util.HashMap 是JDK里散列表的一个实现,主要用来提供对象与对象之间的映射。JDK6里采用位桶+链表的形式实现,Java8里采用的是位桶+链表/红黑树的方式,非线程安全。关于jdk1.6的实现,这篇博文 Java 集合系列10之 HashMap详细介绍(源码解析)和使用示例给出了很详细的分析。本篇主要说说java8中java.util.HashMap的不同实现。
首先介绍HashMap中几个重要字段的含义。
/** * JDK1.6中table字段为Entry数组。JDK1.8为Node数组,其中Node实现了Map.Entry接口 * static class Node<K,V> implements Map.Entry<K,V>。 *实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Node数组中的。 */ transient Node<K,V>[] table; /** * HashMap的集合视图对象。 * */ transient Set<Map.Entry<K,V>> entrySet; /** * HashMap中Key-Value的数量 */ transient int size; /** * HashMap被结构性更改的次数。结构性更改包括映射数量的变化和HashMap内部结构的变化(即Rehash) * 同时它用来实现HashMap集合视图的fail-fast机制。 * fail-fast是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。 * JDK1.6中该字段为 transient volatile int modCount;其中volatile是为了强制从主内存读写该字段,保证了多线程下其并发可见性。 *JDK1.8除去了volatile,毕竟该类本身是非线程安全类,volatile变量的读写需要Memory Barrier,会禁止编译器指令重排序,会降低性能。 */ transient int modCount; /** * 它是HashMap阀值。其大小为 容量*负载因子。当Key-Vaule的值超过了该值时会重新调整HashMap的大小,调用 resize()方法。 *并发条件下rehash是不安全的。 */ int threshold; /** * 哈希表的负载因子 */ final float loadFactor;
HashMap提供了4个构造函数
//使用指定 容量和 负载因子构造 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); } //使用初始容量 和 默认负载因子 来构造 public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); } //使用默认容量和默认负载因子构造 public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted } //使用另外一个Map构造 public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; putMapEntries(m, false); }
另外,JDK8对HashMap进行了优化,提供了以下几个字段,
static final int TREEIFY_THRESHOLD = 8;//当一个bin中的节点个数超过该值,则会调用treeifyBin来将bin中链表变为红黑树实现。 static final int UNTREEIFY_THRESHOLD = 6; static final int MIN_TREEIFY_CAPACITY = 64;
当我们使用put方法插入数据时候,会调用putVal方法。
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; //判断tab是否为空或者长度为0 则调用 resize()方法。 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //找到Key值对应的bin,并且为空,则直接插入 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; //若改节点是TreeNode,即节点的红黑树实现。则调用TreeNode相应方法 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //不是TreeNode,则遍历链表 else { for (int binCount = 0; ; ++binCount) { //到达链表尾部还没找到Key,则生成新的链表节点。同时判断是否满足优化条件,满足则把链表转化为红黑树,以提高查询效率 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } //更新已经存在Key的Value值 if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
如上所示,当满足条件if (binCount >= TREEIFY_THRESHOLD - 1) ,把链表变成treemap,这样查找效率从o(n)变成了o(log n)。
很久以前就一直想写博客,但一直怕写的不好。总归是要开始的,当做自己的学习笔记也不错。以上出于个人的理解,有疏漏错误之处还望大家指出。
另外,参考了另一篇博客