概述 HashMap位于java.util包,完整的定义:public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable ,可以看到实现了Map接口,并且继承自AbstractMap。但是你会发现AbstractMap这个类,也是实现了Map接口的,为什么呢?别怀疑,这就是一个错误。
它不是线程安全的,它的key和value都可以是null,键只能有一个null,此外它不保证顺序。
在JDK1.8之前是数组+链表的形式,1.8之后如果链表长度大于8且当前数组大于64 ,那么链表就会变成红黑树。注意两个条件要同时满足,因为当数组小的时候,没必要使用红黑树。
当创建HashMap而未指定初始大小的时候,默认长度是16,代码如下:
而且源码里你可以看到,它的构造方法中,只有设置了参数,并没有做其它的操作,说明其实在你创建HashMap的时候,并没有真正把它创建出来。
底层的结构如下图所示,本质上就是一个Node数组:
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 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; } 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; } 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 ; } }
当put的时候,会首先通过调用key这个类的hashCode()方法来计算哈希值,然后结合数组长度,采用某种算法来计算出应该放到Node数组中的索引值,如果索引的位置没有数据,那就直接放入;如果索引处有东西,那么就比较原先的值和现在将要插入的值的哈希值是否一致(这里说的不是很准确,代码里会有展示),如果不一致,那么就开辟空间。如果一致,那么就发生了哈希冲突,那么会继续调用equals方法来判断两者是否一致,相等则替换掉,不相等则和这个链条中的下一个继续比较,如果没有一个相等,就新增一个节点。
面试题:那么用的是什么算法呢?那还有别的方法呢?
答:首先根据key的hashcode方法计算出哈希值,然后和数组的长度进行无符号右移,异或和与这三个操作来计算出索引。别的方法:取余、随机数、平方取中。这些方法效率低,所以没有被采用。
threshold(阈值) = capacity(数组最大长度) * loadFactory(负载因子),一般数组长度为16,负载因子是0.75,所以当数组中有12个元素的时候,就需要扩容了(变为两倍)。
成员变量 序列化ID 1 private static final long serialVersionUID = 362498820763181265L ;
初始化容量 1 2 3 4 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4 ;
默认是16,注意注释里也写了容量必须是2的整数次幂。那为什么呢?源码中计算索引是通过hash&(length-1),只要length是2的整数次幂,那么hash&(length-1) 就等于hash%length,而且能够有效减少哈希碰撞。
简单说一下就是,如果length是2的整数幂,那么写成二进制就是10000…000,减一就所有全是1,那么与运算就不会去减少原来hash的位置信息。
构造函数 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 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); } 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 ; }
负载因子 1 static final float DEFAULT_LOAD_FACTOR = 0.75f ;
最大容量 1 static final int MAXIMUM_CAPACITY = 1 << 30 ;
应该也接触不到吧…
红黑树阈值 1 2 3 4 5 6 static final int MIN_TREEIFY_CAPACITY = 64 ;static final int TREEIFY_THRESHOLD = 8 ;static final int UNTREEIFY_THRESHOLD = 6 ;
为什么默认是8呢?因为树节点除了存储数据外,相比于链表还需要额外存储颜色、左子树右子树和父亲节点的信息,约为普通链表节点的两倍。根据泊松分布,链表长度达到8的概率是非常非常低的,
数组 1 transient Node<K,V>[] table;
缓存 1 transient Set<Map.Entry<K,V>> entrySet;
实际存放的键值对数目
方法 hash 1 2 3 4 5 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
该hash算法既十分简单,同时又充分利用了其高位和低位的值,能最大限度保证分散。注意!并非仅仅是获取了对象的哈希值,而是获取了哈希值之后还进行了高位和低位的异或操作。后续使用的均是这个处理过后的哈希值。
put 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 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); } final V putVal (int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { HashMap.Node<K, V>[] tab; HashMap.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 { HashMap.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 HashMap.TreeNode) e = ((HashMap.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 ; }
resize 扩容这里需要多说几句,首先我们要减少扩容的发生,因为发生扩容意味着之前的索引位置都需要重新计算。但是hashmap做的非常巧妙,因为每次扩容都是length乘以二,所以length%hash要么还是在原来的位置,要么就在原来的位置+旧容量这个位置。
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 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; }
treeifyBin 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 final void treeifyBin (Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1 ) & hash]) != null ) { TreeNode<K,V> hd = null , tl = null ; do { TreeNode<K,V> p = replacementTreeNode(e, null ); if (tl == null ) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null ); if ((tab[index] = hd) != null ) hd.treeify(tab); } }
remove 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 public V remove (Object key) { Node<K,V> e; return (e = removeNode(hash(key), key, null , false , true )) == null ? null : e.value; } final Node<K,V> removeNode (int hash, Object key, Object value, boolean matchValue, boolean movable) { Node<K,V>[] tab; Node<K,V> p; int n, index; if ((tab = table) != null && (n = tab.length) > 0 && (p = tab[index = (n - 1 ) & hash]) != null ) { Node<K,V> node = null , e; K k; V v; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) node = p; else if ((e = p.next) != null ) { if (p instanceof TreeNode) node = ((TreeNode<K,V>)p).getTreeNode(hash, key); else { do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) { node = e; break ; } p = e; } while ((e = e.next) != null ); } } if (node != null && (!matchValue || (v = node.value) == value || (value != null && value.equals(v)))) { if (node instanceof TreeNode) ((TreeNode<K,V>)node).removeTreeNode(this , tab, movable); else if (node == p) tab[index] = node.next; else p.next = node.next; ++modCount; --size; afterNodeRemoval(node); return node; } } return null ; }
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 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 ; }
遍历 获得所有的key、values 1 2 Set<String> keys = map.keySet(); Collection<Integer> values = map.values();
获得所有的键值对 1 2 3 4 5 Iterator<Map.Entry<String, Integer>> iterator = map.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry<String, Integer> entry = iterator.next(); System.out.println(entry.getKey() + " -- " + entry.getValue()); }
请千万不要先获取所有的key,然后再根据key去找value,这样效率会非常低。
JDK8给Map这个接口新增了一个叫forEach方法:
1 2 default void forEach (BiConsumer<? super K, ? super V> action) {...}
然后我们可以用lambda表达式轻松编写遍历:
1 2 3 map.forEach((key, value) -> { System.out.println(key + " --- " + value); });
超级轻松!
区别 HashTable
最明显的就是hashtable给自己的方法加了synchronized关键字,所以是线程安全的;而hashmap则没有。
第二个是hashtable不允许null的键,也不允许Null的值;而hashmap都允许。
hashmap中的哈希值其实是经过高低位异或过的,而hashtable是直接拿来用的。
emmm 其它基本上无差异….对性能几乎没有影响了。
HashSet 本质上就是一个HashMap,它就直接用这个hashmap进行处理。但是由于它不能重复,它直接就用了hashmap的key不能重复来实现的….所以hashmap是先计算出hashcode,进行高低位异或操作,然后拿着这个新的hashcode去计算索引,计算出来如果发现该位置上存在有对象,那就启动==判断和equals判断,只要两个中有一个符合就认为是一致的。
ConcurrentHashMap 上面也说了,因为HashTable给自己的方法都加上了synchronized关键字,所以它是线程安全的,但是!!考虑一下这个场景:你需要给一号桶加入数据,但是由于七号桶目前正在被使用,所以你并不能给一号桶加数据,但是我们都知道这两个操作根本不会互相影响,但是你就是做不了。
发生上面的最主要原因就是:加锁的粒度太粗了,于是就有了ConcurrentHashMap这个细粒度的加锁的同步HashMap。
它在JDK1.5被引入,主要就是实现了上面所说的功能,不锁一个方法,而是当你要进入到一个链表的时候才加锁。