Java集合(一)Map(1)

Map

HashMap和HashTable区别

  • 线程是否安全:HashMap线程不安全,HashTable线程安全。因为HashTable内部的方法都经过了synchronized关键字修饰。

    HashMap线程不安全例子:如果两个线程都要往HashMap中插入数据,但是发生哈希冲突,(hash 函数计算出的插入下标是相同的)。其中一个线程刚执行完Hash冲突判断后,时间片到了,另一个线程执行,直到另一个线程操作完成,第一个线程才再次执行。由于已经判断完哈希冲突,所以直接按照原来的下标,向其中插入数据,这样就覆盖了第二个线程插入的数据,导致了数据覆盖问题。

    HashTable用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

  • 底层数据结构:HashMap底层JDK1.8以后是数组/链表,红黑树。当链表长度大于阈值时,会进行判断。一般来说,如果长度大于64,会将链表转换为红黑树,从而减少搜索数据时间。

  • 效率:因为线程安全的原因,HashTable的效率要比HashMap要低。

  • 初始容量大小,和每次扩容的大小:

    HashMap初始容量一般是16,并且每次扩充,容量都变成原来的2倍。如果我们给定了容量初始值,HashMap会变成你给定容量的2的幂次方。

    HashTable初始容量一般是11,并且每次扩充,容量都变成原来的2n+1,如果我们给定了容量初始值,HashTable会变成你给定容量大小。

  • 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException

HashSet与TreeMap

HashSet

HashSet实现接口是Set,向容器中添加数据利用的是add()方法。但是HashSet的底层是基于HashMap实现的。

HashSet方法是如何避免元素不重复

当把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcode,HashSet 会假设对象没有重复出现,会将对象插入到相应的位置中。但是如果发现有相同 hashcode 值的对象,这时会调用对象的 equals() 方法来检查对象是否真的相同,如果相同,则 HashSet 就不会让重复的对象加入到 HashSet 中,这样就保证了元素的不重复。

为了更清楚的了解 HashSet 的添加流程,我们可以尝试阅读 HashSet 的具体实现源码,HashSet 添加方法的实现源码如下(以下源码基于 JDK 8):

// hashmap 中 put() 返回 null 时,表示操作成功
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
​
​
//从上述源码可以看出 HashSet 中的 add 方法,

 实际调用的是 HashMap 中的 put,那么我们继续看 HashMap 中的 put 实现:

// 返回值:如果插入位置没有元素则返回 null,否则返回上一个元素
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

 

从上述源码可以看出,HashMap 中的 put() 方法又调用了 putVal() 方法,putVal() 的源码如下:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K, V>[] tab;
    Node<K, V> p;
    int n, i;
    //如果哈希表为空,调用 resize() 创建一个哈希表,并用变量 n 记录哈希表长度
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    /**
     * 如果指定参数 hash 在表中没有对应的桶,即为没有碰撞
     * Hash函数,(n - 1) & hash 计算 key 将被放置的槽位
     * (n - 1) & hash 本质上是 hash % n 位运算更快
     */
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 直接将键值对插入到 map 中即可
        tab[i] = newNode(hash, key, value, null);
    else {// 桶中已经存在元素
        Node<K, V> e;
        K k;
        // 比较桶中第一个元素(数组中的结点)的 hash 值相等,key 相等
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            // 将第一个元素赋值给 e,用 e 来记录
            e = p;
            // 当前桶中无该键值对,且桶是红黑树结构,按照红黑树结构插入
        else if (p instanceof TreeNode)
            e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
            // 当前桶中无该键值对,且桶是链表结构,按照链表结构插入到尾部
        else {
            for (int binCount = 0; ; ++binCount) {
                // 遍历到链表尾部
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 检查链表长度是否达到阈值,达到将该槽位节点组织形式转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 链表节点的<key, value>与 put 操作<key, value>
                // 相同时,不做重复操作,跳出循环
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 找到或新建一个 key 和 hashCode 与插入元素相等的键值对,进行 put 操作
        if (e != null) { // existing mapping for key
            // 记录 e 的 value
            V oldValue = e.value;
            /**
             * onlyIfAbsent 为 false 或旧值为 null 时,允许替换旧值
             * 否则无需替换
             */
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 访问后回调
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 更新结构化修改信息
    ++modCount;
    // 键值对数目超过阈值时,进行 rehash
    if (++size > threshold)
        resize();
    // 插入后回调
    afterNodeInsertion(evict);
    return null;
}

从上述源码可以看出,当将一个键值对放入 HashMap 时,首先根据 key 的 hashCode() 返回值决定该 Entry 的存储位置。如果有两个 key 的 hash 值相同,则会判断这两个元素 key 的 equals() 是否相同,如果相同就返回 true,说明是重复键值对,那么 HashSet 中 add() 方法的返回值会是 false,表示 HashSet 添加元素失败。因此,如果向 HashSet 中添加一个已经存在的元素,新添加的集合元素不会覆盖已有元素,从而保证了元素的不重复。如果不是重复元素,put 方法最终会返回 null,传递到 HashSet 的 add 方法就是添加成功。

总结

HashSet 底层是由 HashMap 实现的,它可以实现重复元素的去重功能,如果存储的是自定义对象必须重写 hashCode 和 equals 方法。HashSet 保证元素不重复是利用 HashMap 的 put 方法实现的,在存储之前先根据 key 的 hashCode 和 equals 判断是否已存在,如果存在就不在重复插入了,这样就保证了元素的不重复


TreeMap

TreeMap 和 HashMap 都实现了 AbstractMap 接口,但是TreeMap多实现了 NavigableMap 接口和 SortedMap 接口。

TreeMap 继承关系图

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。NavigableMap 接口提供了丰富的方法来探索和操作键值对。而这些方法都是基于底层红黑树实现的。

底层结构

TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

而在1.8前,HashMap是使用了数组+链表组成的链表散列。

HashMap多线程引起的死循环问题

JDK1.7 及之前版本的 HashMap 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。

为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在数据覆盖的问题。并发环境下,推荐使用 ConcurrentHashMap

  • 31
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值