概览 本文使用 JDK1.8, 我们先借助 IDE 对 HashMap 储存 key-value 的结构有个大致的了解 先生成一个有 100 个数据的 map,key 是我自定义的对象,value 是一个随机 Integer,然后用 IDE 透视:
可以看到,最主要的是 table,和一些设置的数值。 map 最核心的结构是 table,是一个 node 的数组,node 里面存着 key 和 value,而 node 本身是一个链表。或者是一个红黑树的节点 TreeNode。
我们用图直观的看一下:
这个样例的 map 的生成方法在扩展 5,读完本文,你或许也可以做一个高度碰撞的 HashMap。
构造函数 我们常用的 HashMap 构造方法
1 2 3 4 5 HashMap<String, String> map1 = new HashMap<String, String>(); HashMap<String, String> map2 = new HashMap<String, String>(16 ); HashMap<String, String> map3 = new HashMap<String, String>(16 ,0.75F );
查看源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class HashMap <K ,V > extends AbstractMap <K ,V > implements Map <K ,V >, Cloneable , Serializable // HashMap 构造函数 public HashMap () { this .loadFactor = DEFAULT_LOAD_FACTOR; } public HashMap (int initialCapacity) { this (initialCapacity, DEFAULT_LOAD_FACTOR); } 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); }
用到了几个变量,下面提到可以再回来找
K
key 的泛型类型
V
value 的泛型类型
| float loadFactor | 负载因子,默认是 0.75 | | int initialCapacity | 初始容量,用来计算 threshold | | int threshold | 扩容门槛,默认是 0 如果 HashMap 内数据超过 threshold * loadFactor 则会进行扩容操作。 |
无参数的构造方法比较平常,构造函数为其设置了 loadFactor ,而另外两个构造函数则设置了 initialCapacity 和 自定的 loadFactor。当传入 initialCapacity 之后,通过一个函数,我们最终计算出最重要的 threshold 属性。来看下这个函数,主要是通过位运算,让 threshold 始终是 2 的 平方数(方便扩容时重新落位)
1 2 3 4 5 6 7 8 9 10 11 12 static final int tableSizeFor (int cap) { int n = cap - 1 ; n |= n >>> 1 ; n |= n >>> 2 ; n |= n >>> 4 ; n |= n >>> 8 ; n |= n >>> 16 ; return (n < 0 ) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1 ; }
这里有个技巧,如果 HashMap 大小已知,则建议预设好 threshold 和 loadFactor。防止 map 多次扩容(默认为 0)。 如现有 100 个数据,为防止扩容,则先计算扩容临界值 100/loadFactor(默认 0.75) = 133,则 initialCapacity 应设置为 2 ^ 8 = 256
put()方法 平时用法
debug 之,note 里面如果不知道变量是什么,可以查看代码下方的变量表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); } static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); } final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0 ) n = (tab = resize()).length; 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; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this , tab, hash, key, value); else { for (int binCount = 0 ; ; ++binCount) { if ((e = p.next) == null ) { p.next = newNode(hash, key, value, null ); if (binCount >= TREEIFY_THRESHOLD - 1 ) treeifyBin(tab, hash); break ; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break ; p = e; } } 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 ; }
这里用到了一些变量
key
put 方法的传入 key 值
value
put 方法传入的 value 值
hashcode
对象的 hashcode
hash
对 hashcode 进行了二次计算,缩小了 hashcode 的大小
hash 方法的意义在于,使得 hashcode 更加均匀,使 hash 值的位值在高低位上尽量分布均匀
Node<K,V> implements Map.Entry<K,V>
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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; } `
key/value结构将被包装成Node这个数据结构 | | Node<K,V>[] table | 存放数据的Node的数组 |
hash和槽位的算法: 高16bit不变,低16bit和高16bit做了一个异或,可以在资源消耗比较低的情况下,打乱一下 hash,让计算出来的槽位下标更加均匀。通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16)
,主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时不会有太大的开销
get()方法 平时用法
源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public V get (Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; } final Node<K,V> getNode (int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1 ) & hash]) != null ) { if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null ) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null ); } } return null ; }
resize()方法 这是一个 HashMap 的私有方法,是非常重要的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null ) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0 ; if (oldCap > 0 ) { if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } else if ((newCap = oldCap << 1 ) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1 ; } else if (oldThr > 0 ) newCap = oldThr; else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int )(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } if (newThr == 0 ) { float ft = (float )newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float )MAXIMUM_CAPACITY ? (int )ft : Integer.MAX_VALUE); } threshold = newThr; @SuppressWarnings ({"rawtypes" ,"unchecked" }) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; if (oldTab != null ) { for (int j = 0 ; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null ) { oldTab[j] = null ; if (e.next == null ) newTab[e.hash & (newCap - 1 )] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this , newTab, j, oldCap); else { Node<K,V> loHead = null , loTail = null ; Node<K,V> hiHead = null , hiTail = null ; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0 ) { if (loTail == null ) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null ) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null ); if (loTail != null ) { loTail.next = null ; newTab[j] = loHead; } if (hiTail != null ) { hiTail.next = null ; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
两倍扩容的算法 因为是两倍扩容,n-1 永远 是 1 组成,所以重新计算槽位时,要么是在前面 加个 0,要么是前面加个 1
如果是加 0,那么槽位不动,如果是加 1,那么等于移动了一个原容量
移动的过程相当于全部重新插入,所以是比较费时的
扩展 扩展 1:Hashcode 的计算规则 HashMap 中,hash 由 hashcode 计算得出,那么对象的 hashcode 如何计算呢?
Object 类的 hashCode.返回对象的内存地址经过处理后的结构,由于每个对象的内存地址都不一样,所以哈希码也不一样。这个是 native 方法,这个取决于 JVM 的设计,一般是某种地址的偏移。
String 类的 hashCode.根据 String 类包含的字符串的内容,根据一种特殊算法返回哈希码,只要字符串的内容相同,返回的哈希码也相同。
Integer 等包装类,返回的哈希码就是 Integer 对象里所包含的那个整数的数值,例如 Integer i1=new Integer(100),i1.hashCode 的值就是 100 。由此可见,2 个一样大小的 Integer 对象,返回的哈希码也一样。
int,char 这样的基础类,它们没有 hashCode,如果需要存储时,将进行自动装箱操作,计算方法同上。
扩展 2:HashMap 与 HashTable 的主要区别 他们的主要区别其实就是 Table 加了线程同步保护
HashTable 线程更加安全,代价就是因为它粗暴的添加了同步锁,所以会有性能损失。
有更好的 concurrentHashMap 可以替代 HashTable
扩展 3:自定义对象作为 key 有什么要求 必须要重写 equals 方法和 hashCode 方法,且放入 map 之后不要再修改对象 重写 hashCode 方法,可以使用 Objects.hash(item1, item2, item3) 来多并一 如果不重写,则相同内容的两个对象,如果内存地址不同,则会在 map 中有两个 node 最要命的是,你重新 new 一个自定义对象查询时(get()),如果内存地址和 map 内的 node 的内存地址不一致,将找不到你之前存的,内容一样的 node
扩展 4:HashSet 和 HashMap 有什么异同 HashSet 实际上用的是 HashMap,HashSet 的值是 HashMap 的 key,HashSet 在 HashMap 中的 value 是 new Object()
1 2 3 4 5 6 7 8 9 10 public boolean add (E e) { return map.put(e, PRESENT)==null ; } private static final Object PRESENT = new Object();public boolean contains (Object o) { return map.containsKey(o); }
扩展 5:生成一个高度碰撞的 HashMap 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class Main { public static void main (String[] args) { Random random = new Random(); Map<EasyCollision, Integer> map = Stream.generate(random::nextInt). limit(100 ).collect(Collectors.toMap(EasyCollision::new , Function.identity())); } static class EasyCollision { private Integer hashSource; public EasyCollision (Integer hashSource) { this .hashSource = hashSource; } @Override public boolean equals (Object o) { if (o == null || getClass() != o.getClass()) return false ; EasyCollision that = (EasyCollision) o; return Objects.equals(hashSource, that.hashSource); } @Override public int hashCode () { return hashSource % 15 ; } @Override public String toString () { return "EasyCollision{" + "hashSource=" + hashSource + '}' ; } } }
参考资料 Java HashMap 工作原理及实现