下面我首先抛出以下问题,让我们带着这些问题开始解析 HashMap:
原先是打算写一篇介绍,但是发现了一篇博客写的非常棒,我就不重复造轮子了。给出链接Java 8系列之重新认识HashMap
结合博客,再给出文章开始给出的问题的解答
HashMap 在原有的基础上增加了*红黑树 *结构,当链表长度大于8会转换为红黑树进行处理,当结点数目小于 6 则会还原成链表。
首先红黑树遍历的时间复杂度为 O(logn),长度为8 时,平均查找长度是 3 ,而此时链表的平均查找长度是 4,所以转换为树在遍历、查找时性能会得到提升。
不过这个答案并不严谨,选择数字 8 的原因如下文所述。
0: 0.60653066
1: 0.30326533
2: 0.07581633
3: 0.01263606
4: 0.00157952
5: 0.00015795
6: 0.00001316
7: 0.00000094
8: 0.00000006
理想情况下使用随机的哈希码,容器中节点分布在hash桶中的频率遵循泊松分布,具体可以查看泊松分布,按照泊松分布的计算公式计算出了桶中元素个数和概率的对照表,可以看到链表中元素个数为8时的概率已经非常小,再多的就更少了,所以原作者在选择链表元素个数时选择了8,是根据概率统计而选择的。
Equals 方法是用于比较对象是否相等,通过源码可以得知, HashMap 中结点的比较是根据 key 和 value 是否同时相等判断的。
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Person) {
Person e = (Person)o;
if (Objects.equals(id, e.getId()) &&
Objects.equals(name, e.getName()))
return true;
}
return false;
}
假设我们现在只重写 Equals 方法,而不重写 HashCode方法
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Person) {
Person e = (Person)o;
if (Objects.equals(id, e.getId()))
return true;
}
return false;
}
那么这种情况下,按照代码而言应该是,只要 Person 对象的 id 相等,则默认他们是相等的,但是当我们这么操作
Person p1 = new Person("1", "zhangsan");
Person p2 = new Person("2", "lisi");
// 此时 p1 和 p2 是相等的
System.out.println(p1.equals(p2));
HashMap map = new HashMap();
map.put(p1, "test");
System.out.println(map.get(p2));
按照道理来说这里应该是可以获取到 test ,然而事实上返回的是 null,因为在 HashMap 在进行 put() 和 get() 取出时都是通过 hash() 方法来获取对象的 hash 值,确定数组的下标。当重写了 hash 方法就可以确定 hash 值相同。
不是线程安全的,需要线程安全可以采用 ConcurrentHashMap 。
ConcurrentModificationException 异常时由于 modCount 和 exceptModCount 不同,这里使用的是 fastfial 机制,出现错误直接抛出异常。而 iterater 中的 remove() 方法则可以保证不出现异常。
使用HashMap时,要注意HashMap容量和加载因子的关系,这将直接影响到HashMap的性能问题。加载因子过小,会提高HashMap的查找效率,但同时也消耗了大量的内存空间,加载因子过大,节省了空间,但是会导致HashMap的查找效率降低。
如果能够确定大小,在初始化的时候一定要给出,避免不必要的扩容操作。
public class MyHashMap<K,V> {
// 数组的长度 默认数组长度是 16
private static final int length = 8;
// HashMap 的大下
private int size;
private Entry<K, V>[] table;
// 1. 实现构造方法
public MyHashMap() {
table = new Entry[length];
}
public V put(K key, V value) {
int hash = key.hashCode();
int index = hash % length;
for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
// 如果存在相同的 key 直接将原先的覆盖
if (entry.key.equals(key)) {
V oldValue = entry.value;
entry.value = value;
return oldValue;
}
entry = entry.next;
}
// 结点中并没有
addEntry(key, value, index);
return value;
}
private void addEntry(K key, V value, int index) {
size++;
table[index] = new Entry<>(key, value, table[index]);
}
public int size() {
return size;
}
public V get(K key) {
int hash = key.hashCode();
int index = hash % length;
if (table[index] == null) {
return null;
}
for (Entry<K, V> entry = table[index]; entry != null; entry = entry.next) {
if (entry.key.equals(key)) {
return entry.value;
}
}
return null;
}
public void clear() {
// 不为空则全部赋给 null, 将 size 修改为 o
Entry<K, V>[] newTable;
if ((newTable = table) != null && size > 0) {
size = 0;
for (int i = 0; i < newTable.length; ++i) {
newTable[i] = null;
}
}
}
class Entry<K, V>{
private K key;
private V value;
private Entry<K, V> next;
public Entry(K key, V value, Entry<K, V> next) {
this.key = key;
this.value = value;
this.next = next;
}
}
}
这个题目相对比较宽泛,怎么回答都可以。主要考察的是个人对于 HashMap 掌握的深度。
可以,put 两个相同的 key 的时候,会将之前 put 的值替换掉。
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
key 是可以为 null 的,HashMap 插入 值时会先计算 key 的 hash 值,我们可以看到当 key 为 null 时会返回 0, 也就是说位于数组的下标 0 处。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个问题我也理解的不是很清楚,有兴趣的可以好好研究一下给出的参考博客。
参考链接:
Java 8系列之重新认识HashMap
HashMap, ConcurrentHashMap 原理及源码,一次性讲清楚!
高并发编程:HashMap 深入解析