浅谈java集合-->Hashtable、HashMap、TreeMap有什么不同


前言

在日常开发中Map是经常被用到的(面试中也是高频的考点),了解Map无论是在实际开发中和就职面试中都大有裨益。


一、HashMap、TreeMap、Hashtable定义

HashMap

HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以它是绝大部分利用键值对存取场景的首选,比如,实现一个用户 ID 和用户信息对应的运行时存储结构。

TreeMap

TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(log(n))的时间复杂度,具体顺序可以由指定的 Comparator 来决定,或者根据键的自然顺序来判断。

Hashtable

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

二、对比

类\项目初始化大小扩容率线程安全使用率
HashMap162n不安全
TreeMapnullnil不安全较高
Hashtable112n+1安全

三、应用场景

1 在不能重复的场景,如日期作为Key。

2 快速获取某个值是否存在。

三、扩展

谈谈HashMap

Map 整体结构
Map 虽然通常被包括在 Java 集合框架里,但是其本身并不是狭义上的集合类型(Collection)
在这里插入图片描述
HashMap 等其他 Map 实现则是都扩展了 AbstractMap,里面包含了通用方法抽象。不同 Map 的用途,从类图结构就能体现出来,设计目的已经体现在不同接口上。

大部分使用 Map 的场景,通常就是放入、访问或者删除,而对顺序没有特别要求,HashMap 在这种情况下基本是最好的选择。
HashMap 的性能表现非常依赖于哈希码的有效性,请务必掌握 hashCode 和 equals 的一些基本约定,比如:

equals 相等,hashCode 一定要相等。

重写了 hashCode 也要重写 equals。

hashCode 需要保持一致性,状态改变返回的哈希值仍然要一致。

equals 的对称、反射、传递等特性。

HashMap 源码分析

我们来一起看看 HashMap 内部的结构,它可以看作是数组(Node<K,V>[] table)和链表结合组成的复合结构,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数组的寻址;哈希值相同的键值对,则以链表形式存储,你可以参考下面的示意图。这里需要注意的是,如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),图中的链表就会被改造为树形结构。

在这里插入图片描述

public HashMap(int initialCapacity, float loadFactor){  
    // 没有在最初就初始化好。仅仅设置了一些初始值而已。
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}
 

putVal method

final V putVal(int hash, K key, V value, boolean onlyIfAbent,
               boolean evit) {
    Node<K,V>[] tab; Node<K,V> p; int , i;
    //如果表格是 null,resize 方法会负责初始化它,这从 tab = resize() 可以看出
   // resize 方法兼顾两个职责,创建初始存储表格,或者在容量不满足需求的时候,进行扩容(resize)。

// 在放置新的键值对的过程中,如果发生下面条件,就会发生扩容。
    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);
        //  ... 
     }
}
 

观察哈希值的源头,会发现,它并不是 key 本身的 hashCode,而是来自于 HashMap 内部的另外一个 hash 方法。注意,为什么这里需要将高位数据移位到低位进行异或运算呢?这是因为有些数据计算出的哈希值差异主要在高位,而 HashMap 里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。

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

依据 resize 源码,不考虑极端情况(容量理论最大极限由 MAXIMUM_CAPACITY 指定,数值为 1<<30,也就是 2 的 30 次方)

门限值等于(负载因子)x(容量),如果构建 HashMap 的时候没有指定它们,那么就是依据相应的默认常量值。

门限通常是以倍数进行调整 (newThr = oldThr << 1),我前面提到,根据 putVal 中的逻辑,当元素个数超过门限大小时,则调整 Map 大小。

扩容后,需要将老的数组中的元素重新放置到新的数组,这是扩容的一个主要开销来源。

容量、负载因子和树化

容量和负载系数决定了可用的桶的数量,空桶太多会浪费空间,如果使用的太满则会严重影响操作的性能。极端情况下,假设只有一个桶,那么它就退化成了链表,完全不能提供所谓常数时间存的性能。
负载因子
如果没有特别需求,不要轻易进行更改,因为 JDK 自身的默认负载因子是非常符合通用场景的需求的。

如果确实需要调整,建议不要设置超过 0.75 的数值,因为会显著增加冲突,降低 HashMap 的性能。

如果使用太小的负载因子,按照上面的公式,预设容量值也进行调整,否则可能会导致更加频繁的扩容,增加无谓的开销,本身访问性能也会受影响。

四 总结

1 如果无特别需求就使用HashMap

2 如果要线程安全就使用ConcurrentHashMap后面的浅谈专题我会介绍
3 如果想要保持最新值可以参考LinkedHashMap下面的代码

public static void main(String[] args) {
            LinkedHashMap<String, String> accessOrderedMap = new LinkedHashMap<String, String>(16, 0.75F, true){
                @Override
                protected boolean removeEldestEntry(Map.Entry<String, String> eldest) { // 实现自定义删除策略,否则行为就和普遍 Map 没有区别
                    return size() > 3;
                }
            };
            accessOrderedMap.put("Project1", "Valhalla");
            accessOrderedMap.put("Project2", "Panama");
            accessOrderedMap.put("Project3", "Loom");
            accessOrderedMap.forEach( (k,v) -> {
                System.out.println(k +":" + v);
            });
            // 模拟访问
            accessOrderedMap.get("Project2");
            accessOrderedMap.get("Project2");
            accessOrderedMap.get("Project3");
            System.out.println("Iterate over should be not affected:");
            accessOrderedMap.forEach( (k,v) -> {
                System.out.println(k +":" + v);
            });
            // 触发删除
            accessOrderedMap.put("Project4", "Mission Control");
            System.out.println("Oldest entry should be removed:");
            accessOrderedMap.forEach( (k,v) -> {// 遍历顺序不变
                System.out.println(k +":" + v);
            });
        }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值