文章目录
对比Hashtable、HashMap、TreeMap有什么不同?
典型回答
Hashtable、HashMap、TreeMap都是最常见的一些Map实现,是以键值对的形式存储和操作数据的容器类型。
Hashtable是早期Java类库提供的一个哈希表实现。
是同步的,不支持null键和null值。
由于同步导致的性能开销,所以不推荐使用。
HashMap是应用更为广泛的哈希表实现,行为大致与Hashtable一致。
主要区别在于HashMap不是同步的,支持null键和null值。
通常情况下HashMap进行put或get操作,可以达到常数时间的性能,所以他是绝大部分利用键值对存取场景的首选。
TreeMap则是基于红黑树的一种提供顺序访问Map。
和HashMap不同,他的put、get、remove之类的操作都是O(log(n))的时间复杂度。
具体顺序可以由指定的Comparator来决定,或者根据键的自然顺序来排序。
考点分析&知识拓展
Map整体结构
Map虽然通常被包括在Java集合框架里,但其本身不是狭义上的集合框架(Collection)。
Hashtable 比较特别,作为类似Vector、Stack的早期集合相关类型,它是扩展了Dictionary 类的,类结构上与 HashMap 之类明显不同。
HashMap 等其他 Map实现则是都扩展了AbstractMap,里面包含了通用方法抽象。不同 Map的用途,从类图结构就能体现出来,设计目的已经体现在不同接口上。(为什么继承了抽象类还要实现接口?)
hashCode和equals
HashMap 的性能表现非常依赖于哈希码的有效性,请务必掌握 hashCode 和 equals 的一些基本约定:
- equals 相等,hashCode 一定要相等。
- 重写了 hashCode 也要重写 equals。
- hashCode 需要保持一致性,状态改变返回的哈希值仍然要一致。
- equals 的对称、反射、传递等特性。
- 等
LinkedHashMap 和 TreeMap
虽然LinkedHashMap和TreeMap都可以保证某种顺序,但二者还是非常不同的。
LinkedHashMap
LinkedHashMap 通常提供的是遍历顺序符合插入顺序,
它的实现是通过为条目(键值对)维护一个双向链表。
通过特定构造函数,我们可以创建反映访问顺序的实例,
所谓的 put、get、compute 等,都算作“访问”。
这种行为适用于一些特定应用场景,例如,我们构建一个空间占用敏感的资源池,希望可以自动将最不常被访问的对象释放掉,这就可以利用 LinkedHashMap 提供的机制来实现。
TreeMap
对于TreeMap,它的整体顺序是由键的顺序关系决定的,
通过Comparator或Comparable(自然顺序)来决定。
HashMap源码分析
主要围绕:
- HashMap内部实现基本点分析。
- 容量(capacity)和负载因子(load factory)。
- 树化。
HashMap内部结构
HashMap的内部结构可以看作是数组(Node<K,V>[] table)和链表的结合。即链表数组。
数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组的寻址。哈希值相同的键值对,则以链表形式存储。
put方法的实现
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 onlyIfAbent,
boolean evit) {
Node<K,V>[] tab; Node<K,V> p; int , i;
if ((tab = table) == null || (n = tab.length) = 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == ull)
tab[i] = newNode(hash, key, value, nll);
else {
// ...
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for first
treeifyBin(tab, hash);
// ...
}
}
resize方法兼顾两个职责,创建初始存储表格,或者在容量不满足需求的时候,进行扩容(resize)。(扩容后,需要将老的数组中的元素重新放置到新的数组,这是扩容的一个主要开销来源。)
具体键值对在哈希表中的位置(数组 index)通过下面的位运算:
i = (n - 1) & hash。
仔细观察哈希值(hash)的源头,我们会发现,它并不是 key 本身的 hashCode,而是来自于HashMap内部的另外一个hash方法。
static final int hash(Object kye) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16;
}
为什么这里需要将高位数据移位到低位进行异或运算呢?这是因为有些数据计算出的哈希值差异主要在高位,而HashMap里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。
容量和负载因子
容量和负载因子决定了可用的桶的数量,空桶太多会浪费空间,太满则会严重影响操作的性能。
关于负载因子的建议:
- 如果没有特别需求,不要轻易进行更改,因为JDK自身的默认负载因子是非常符合通用场景的需求的。
- 如果确实需要调整,建议不要设置超过0.75的数值,因为会显著增加冲突,降低HashMap的性能。
思考题
题:解决哈希冲突有哪些典型方法?
答:开发地址法、拉链法等。
- https://blog.csdn.net/l494926429/article/details/52435509