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;
}
}

本文深入探讨Java中的HashMap数据结构,包括其底层实现原理、默认长度选择的原因、高并发下的死锁问题及解决方案,以及Java 8中对HashMap的优化措施。
414

被折叠的 条评论
为什么被折叠?



