对比Hashtable、HashMap、TreeMap有什么不同?

文章目录

  • 对比Hashtable、HashMap、TreeMap有什么不同?
  • 典型回答
  • 考点分析&知识拓展
    • Map整体结构
    • hashCode和equals
    • LinkedHashMap 和 TreeMap
      • LinkedHashMap
      • TreeMap
    • HashMap源码分析
    • HashMap内部结构
    • put方法的实现
    • 容量和负载因子
    • 思考题

对比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[] 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[] tab; Node 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

你可能感兴趣的:(Java,基础)