Hashtable、HashMap、TreeMap有什么不同?


title:Hashtable、HashMap、TreeMap有什么不同?

map我们都知道来源于key-value的认识。以键值对的形式存储和操作数据的容器类型。而上面的这些都是Map的常见实现

Hashtable 是早期java类库类库提供的一个哈希表实现,本身是同步的,不支持null键和值,由于同步导致的性能开销,不推荐使用。

HashMap与Hashtable行为基本上一致,不一样的地方在于HashMap不是同步的,支持null键和值
通常,HashMap进行put或者get操作,可以达到常数时间的性能,因此使我们利用键值对存取场景的首选。比如:
用户ID --用户信息对应的运行时存储结构

Treemap是基于红黑树的一种提供访问顺序的Map,和HashMap不同,它的get,put,remove的时间复杂度都是O(log(n)),具体顺序由指定的Comparator来决定,或者由键的自然顺序来判断

HashMap的设计和实现细节
HashMap扩展了abstractMap,且非常依赖于哈希码的有效性:
1.equals相等,hashcode一定相等
2.hashcode改写以后也要改写equals
3.hashCode需要保持一致性,状态改变返回的哈希值仍然要一致
4.equals的对称,反射,传递等特性(下篇博客写)

有序Map
LinkedHashMap和TreeMap都可以保证某种顺序
1.linkedHashMap提供的遍历顺序符合插入顺序,它的实现是通过为键值对维护一个双向链表,
通过特定构造函数,我们可以创建反映访问顺序的实例,所谓的put,get,compute都算作“访问”。
场景:构建一个空间占用敏感化的资源池,希望可以自动将最不常被访问的对象释放掉,使用LInkedHashMap来实现
2.对于TreeMap,它的整体顺序是由键的顺序来决定,通过comparator或Comparable(自然顺序:必须符合约定:comparaTo的返回值需要和equals一致)来决定。
代码如下,经常复盘

public V put(K key, V value) {
    Entry<K,V> t = …
    cmp = k.compareTo(t.key);
    if (cmp < 0)
        t = t.left;
    else if (cmp > 0)
        t = t.right;
    else
        return t.setValue(value);
        // ...
   }

构建一个优先级的调度系统的问题,其本质就是一个典型的有限队列场景,Java标准类库提供了基于二叉堆实现的PriorityQueue,TreeMap和TreeSet也是依赖于此。

HashMap内部基本点分析
可以看作是数组(Node<K,V>[]table)链表组成的符合结构
数组被分为一个个桶,通过哈希值决定键值对在这个数组的寻址。
哈希值相同的键值对,就按照链表形式存储。
如果链表超过阈值,就会被改造成树形结构。

这个数组似乎并没有被初始化好,仅仅设置了一些初始值而已
看代码就知道

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

我们认为HashMap也许是按照lazy-load原则,在首次使用时初始化。
只有putVal的调用

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

如果表格是null,resize就会负责初始化它,这从tab=
resize()可以看出

resize方法兼顾两个职责,创建初始存储表格,或者在容量不满足要求的时候,进行扩容(resize)

在放置新的键值对的过程中,如果发生下面这种情况,就会发生扩容


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

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

i = (n - 1) & hash

学过数据结构的都知道,它并不是key本身的hashCode,而是来自于HashMap里面的另外一个hash方法。
为什么将高位数据移位到低位进行异或运算?
计算出的哈希值差异主要在高位,而HashMap里的哈希寻址是忽略容量以上的高位的,这种处理就可以有效避免类似情况下的哈希碰撞

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

为什么树化?对链表(bin)进行怎样的处理?

可以看出,putVal方法本身逻辑非常集中,从初始化,扩容到树化,都有参与
依据resize源码,在不考虑极端情况下的(容量理论最大极限由 MAXIMUM_CAPACITY指定,数值为1<<30,即2^30次方):
我们可以归纳为门限值=负载因子*容量,我们通常用默认值
**门限值(就是数组的最大参数)**通常以倍数调整,当元素个数超过门限大小是,即调整Map大小
扩容后,需要将老的数组中的元素重新放置到新的数组,这是扩容的一个主要开销来源

容量,负载因子,树化?
还是数据结构的原理
容量和负载因子都是数组的标准条件,相当于当时学过的寻找大于元素数量的最小的素数来当做数组。
就相当于桶的数量,太少和占用太满都会影响操作的性能

负载因子:不超过0.75
如果使用太小的负载因子,则用导致更加频繁的扩容,增加开销成本

树化改造,不外乎对应逻辑就是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){
                //树化逻辑改造
            }

数组判定条件:当bin的数量大于TREEIFY_THRESHOLD(默认是8)的条件下:(一个桶中有8个元素),允许红黑树化
—>接下来有判定条件:
如果容量小于MIN_TREEIFY_CAPACITY,只会进行简单的扩容
如果容量大于MIN_TREEIFY_CAPACITY(最小树形阈值)
则进行红黑树化

本质上是一个安全问题,如果哈希冲突,都会放置同一个桶里,形成链表,我们知道链表是线性的,会影响存取的性能。
第二是因为构造哈希冲突的数据并不是非常复杂的事情,恶意代码可以利用这些数据大量与服务器交互,从而让服务器CPU被大量占用,造成崩溃,所以允许树化,再不济进行扩容。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值