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

map的区别

  • hashtable、hashmap和treemap都是常见的一些map实现,是以键值对的形式存储和操作数据的容器类型
  • hashtable是早期java类库提供一个哈希表的实现,本身是同步的,不支持null键和值,由于同步导致的性能开销,所以已经很少被推荐使用
  • hashmap应用更加广泛的哈希表实现,行为和hashtable一致,主要区别在于hashmap不是同步的,支持null键和值等。通常情况下,hashmap进行put和get操作,可以达到常数时间的性能,所以它是绝大多数利用键值对存取场景的首选
  • treemap则基于红黑树的一种提供顺序访问的map,和hashmap不同,它的get、put、remove操作都是O(log(n))的时间复杂度,具体顺序可以指定的Comparator来决定,或者根据键的自然顺序来判断

map

map的集合类图
Hashtable 比较特别,作为类似 Vector、Stack 的早期集合相关类型,它是扩展了 Dictionary 类的,类结构上与 HashMap 之类明显不同。

HashMap 等其他 Map 实现则是都扩展了 AbstractMap,里面包含了通用方法抽象。不同 Map 的用途,从类图结构就能体现出来,设计目的已经体现在不同接口上。

HashMap 的性能表现非常依赖于哈希码的有效性, hashCode 和 equals 的一些基本约定,比如:

  • equals 相等,hashCode 一定要相等
  • 重写了 hashCode 也要重写 equals
  • hashCode 需要保持一致性,状态改变返回的哈希值仍然要一致
  • equals 的对称、反射、传递等特性

LinkedHashMap 和 TreeMap

LinkedHashMap 和 TreeMap 都可以保证某种顺序,但二者还是非常不同的

  • 虽然 LinkedHashMap 和 TreeMap 都可以保证某种顺序,但二者还是非常不同的
  • LinkedHashMap 通常提供的是遍历顺序符合插入顺序,它的实现是通过为条目(键值对)维护一个双向链表。注意,通过特定构造函数,我们可以创建反映访问顺序的实例,所谓的 put、get、compute 等,都算作“访问”
  • 对于 TreeMap,它的整体顺序是由键的顺序关系决定的,通过 Comparator 或 Comparable(自然顺序)来决定

hashmap源码分析

hashmap内部结构可以看做是数组和链表的组合结构,数组被分成一个个桶(bucket),通过哈希值决定键值对这个数组的寻址;哈希值相同的键值对,则以链表形式存储。注意,如果链表大小查过阈值(TREEIFY_THRESHOLD, 8),则链表就会被改造成树形结构。
1、从非拷贝构造函数来看看,这个数组并没有在最初初始化好,仅仅设置一些初始值而已。hashmap也许是按照lazy-load原则,在首次使用时被初始化(拷贝构造函数除外)

public HashMap(int initialCapacity, float loadFactor){  
    // ... 
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

2、看看put

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

put方法实现的内部是putVal的调用:

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()).legth;
    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);
        //  ... 
     }
}

如果数组为null,resize方法会初始化它
resize方法兼顾两个职责,创建初始化存储数组,如果容量不满足需求的时候,进行扩容
在放置新的键值对的过程中,如果发生下面条件,就会发生扩容。

if (++size > threshold)
    resize();

具体键值对在哈希表中的位置(数组 index)取决于下面的位运算:

i = (n - 1) & hash

这个hash并不是key的hashcode,而是hashmap内部的另一个hash方法。另外,为啥需要将高位数据移到低位进行异或运算?因为有些数据计算出的哈希值差异主要在高位,而hashmap里的哈希寻址是可以忽略容量以上的高位的,那么这种情况下就可以有效避免类似情况下的哈希碰撞。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

3、看看resize

final Node<K,V>[] resize() {
    // ...
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACIY &&
                oldCap >= DEFAULT_INITIAL_CAPAITY)
        newThr = oldThr << 1; // double there
       // ... 
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {  
        // zero initial threshold signifies using defaultsfults
        newCap = DEFAULT_INITIAL_CAPAITY;
        newThr = (int)(DEFAULT_LOAD_ATOR* DEFAULT_INITIAL_CAPACITY;
    }
    if (newThr ==0) {
        float ft = (float)newCap * loadFator;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
    }
    threshold = neThr;
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newap];
    table = n;
    // 移动到新的数组结构 e 数组结构 
   }

不考虑极端情况,容量理论极限由MAXIMUM_CAPACITY 指定,数值为1<<30,也就是2的30次方,可以归纳为:
门限值=负载因子*容量,如果构建hashmap没有指定他们,则使用默认的常量值
门限值通常以倍数进行调整: new = old << 1,在putval逻辑中,当元素个数超过门限大小时,则调整map大小
扩容后,需要将老的数组中元素重新放置到新的数组中,这是扩容的一个主要开销来源
4、容量、负载因子和树化
hashmap要存取的键值对数量,可以预先设置合适大小,符合的条件:

负载因子*容量 > 元素个数   同时   容量是2的幂数

负载因子:
若没特殊需求,不要轻易更改,jdk自身的默认负载因子符合通用场景需求
建议不要设置超过0.75,因为会显著增加冲突,降低hashmap性能
负载因子过小,按照上面的公式,预设容量也进行调整,可能会频繁的扩容,增加无谓的开销,本身性能也会受到影响
5、树化改造
对应逻辑主要在 putVal 和 treeifyBin 中

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        // 树化改造逻辑
    }
}

如果容量小于 MIN_TREEIFY_CAPACITY,只会进行简单的扩容。
如果容量大于 MIN_TREEIFY_CAPACITY ,则会进行树化改造。

6、hashmap为啥要树化?
本质是安全问题。因为在放置元素的时候,如果一个对象哈希冲突,会被放到一个桶中,则会形成链表,链表的查询是线性的,严重影响存取性能
在现实世界中,构造哈希冲突的数据并不是复杂的事情,恶意代码就可以利用这些数据大量与服务器端交互,导致服务端CPU大量占用,这就构成了哈希碰撞拒绝服务攻击

猜你喜欢

转载自blog.csdn.net/weixin_31351409/article/details/80495060