HashMap可以说是Java中最常用的集合类框架之一,是Java语言中非常典型的数据结构,因此在面试中经常会被问到hashmap的问题。
1)hashmap的底层原理?
hashmap是通过数组+链表实现的。
调用put方法存储数据的时候,通过hashCode方法处理key,计算出Entry在数组中存放的位置(bucket,桶)。
index = (length - 1) & HashCode(Key) (取模效率比位运算低,所以并没有使用取模这种方式计算)
HashMap数组的每一个元素不止是一个Entry对象,也是一个链表的节点,每个Entry对象通过Next指针指向它的下一个Entry节点。当多个Entry被定位到一个数组的时候(碰撞),只需要插入到对应的链表即可。
调用get方法获取数据的时候,同样是通过hashCode方法处理key,计算出Entry在数组中存放的位置。然后通过equals()方法来寻找键值对。
2)hashmap的默认长度?为什么这么设置?
hashmap的默认长度是16。
之所为设置成16,是为了降低hash碰撞(两个元素虽然不相同,但是hash函数的值相同)的几率。
index = (length - 1) & HashCode(Key)
如果长度是16或者其他2的幂,length - 1的值是所有二进制位全为1(1111),index的结果等同于hashcode后几位的值只要输入的hashcode本身分布均匀,hash算法的结果就是均匀的。如果是非2的幂,可能会导致分配不均匀,甚至有的bucket永远分配不到。
所以HashMap给初始值、扩容的时候,容器大小都是2的幂次方。
3)高并发下,为什么hashmap会出现死锁?如何避免这种问题?
如果两个线程同时put,发现HashMap需要重新调整大小,这时候会产生条件竞争。(java8版本以下才有该问题,头部插入导致)
调整大小的条件:HashMap.Size >= index * LoadFactor(负载因子,默认值为0.75f,空间利用率和减少查询成本的折中,0.75的话碰撞最小)
需要遍历原Entry数组,然后把所有的Entry重新Hash到新数组(长度为原来的两倍)。因为length变化了,根据index = (length - 1) & HashCode(Key),index必然也会发生改变。
在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部。转移前链表顺序是1->2,那么转移后就会变成2->1。
如果多个线程同时操作的时候,链表容易形成环形链表1->2、2->1。这种时候如果get方法获取链表的话,就会陷入死循环。
高并发下可以使用CocurrentHashMap替代hashmap,CocurrentHashMap线程安全同时效率高。
4)java8中对hashmap做了什么优化?
1)HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树。提高了查询效率,红黑树查询时间是O(logn),链表是O(n)。
2)发生hash碰撞时,java 1.7 会在链表的头部插入,而java 1.8会在链表的尾部插入。头部插入效率高,不需要遍历尾部,但是容易产生环形链表,引入红黑树后没法头部插入,但是红黑树减少了插入的成本。
5)手写个hashmap
public class Node<K, V> { private K key; private V value; private Node<K, V> next; public Node(K key, V value, Node<K, V> next) { this.key = key; this.value = value; this.next = next; } public K getKey() { return key; } public void setKey(K key) { this.key = key; } public V getValue() { return value; } public void setValue(V value) { this.value = value; } public Node<K, V> getNext() { return next; } public void setNext(Node<K, V> next) { this.next = next; } }
public class MyHashMap<K, V> { Node<K, V>[] table = null; // 默认初始大小 static final int DEFAULT_INITIAL_CAPACITY = 16; // 负载因子 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 实际大小 static int size; public void put(K k, V v) { if (table == null) { table = new Node[DEFAULT_INITIAL_CAPACITY]; } int index = k.hashCode() & (DEFAULT_INITIAL_CAPACITY - 1); Node<K, V> node = table[index]; if (node == null) { table[index] = new Node<>(k, v, null); size++; } else { Node<K, V> newNode = node; while (newNode != null) { if (k.equals(newNode.getKey()) || k == newNode.getKey()) { newNode.setValue(v); return; } newNode = node.getNext(); } table[index] = new Node<K, V>(k, v, table[index]); size++; } } public V get(K k) { int index = k.hashCode() & (DEFAULT_INITIAL_CAPACITY - 1); Node<K, V> node = table[index]; if (k.equals(node.getKey()) || k == node.getKey()) { return node.getValue(); } else { Node<K, V> nextNode = node.getNext(); while (nextNode != null) { if (k.equals(nextNode.getKey()) || k == nextNode.getKey()) { return nextNode.getValue(); } } } return null; } }