[Java]Map接口有关总结

Map接口

1、HashMap和Hashtable的区别

  • 线程安全方面HashMap是非线程安全的,Hashtable是线程安全的。因为Hashtable内部方法基本都经过synchronized修饰。但是如果要保证线程安全推荐使用ConcurrentHashMap

  • 效率方面。因为线程安全问题HashMap要比Hashtable效率高一点。但Hashtable基本被淘汰,尽量不要在代码中使用。

  • 对null的支持方面HashMapkeyvalue都支持null,作为key只能有一个为null,作为value可以有多个``nullHashtablekeyvalue都不允许有null值。

  • 初始容量

    • 创建时不指定容量:Hashtable的默认初始容量是11,之后每次扩充,容量变为原来的2n+1HashMap的默认初始容量是16,每次扩容变为原来的2倍。
    • 创建时指定容量:Hashtable会直接使用给定的大小,而HashMap会扩容为2的幂次方大小。

    所以HashMap总是使用2的幂作为哈希表的大小。

  • 底层数据结构方面JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间。Hashtable 没有这样的机制。

2、HashMap和HashSet的区别

HashSet的底层是基于HashMap实现的。HashSet 的源码很少,除了 clone()writeObject()readObject()HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

HashMap HashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用putmap中添加元素 调用addSet中添加元素
使用Key计算hashcode HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以equals()方法用来判断对象的相等性

3、HashMap和TreeMap的区别

TreeMapHashMap 都继承自AbstractMap ,但是TreeMap还实现了NavigableMap接口和SortedMap 接口。

实现 NavigableMap 接口让 TreeMap 有了对集合内元素的搜索的能力。

实现SortedMap接口让 TreeMap 有了对集合中的元素根据键排序的能力。

默认是按 key 的升序排序,不过我们也可以指定排序的比较器。例如:

// 通过传入匿名内部类的方式实现
TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {
    @Override
    public int compare(Person person1, Person person2) {
        int num = person1.getAge() - person2.getAge();
        return Integer.compare(num, 0);
    }
});

// 也可以通过 Lambda 表达式实现
TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> {
  int num = person1.getAge() - person2.getAge();
  return Integer.compare(num, 0);
});

相比于HashMap来说 TreeMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力

4、HashTable和ConcurrentHashMap的区别

ConcurrentHashMapHashtable 的区别主要体现在实现线程安全的方式上不同。

底层数据结构,在JDK1.8之前ConcurrentHashMap底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。

HashtableJDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的。

主要就是实现线程安全的方式的不同:

  • JDK1.7 的时候,ConcurrentHashMap 对整个桶数组进行了分割分段(Segment,分段锁),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。
  • 到了 JDK1.8 的时候,ConcurrentHashMap 已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronizedCAS 来操作。(JDK1.6 以后 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
  • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

来看看一些对比图:

对于JDK1.7ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。

Segment 数组中的每个元素包含一个 HashEntry 数组,每个 HashEntry 数组属于链表结构。

对于JDK1.8ConcurrentHashMap 不再是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红黑树。不过,Node 只能用于链表的情况,红黑树的情况需要使用 TreeNode。当冲突链表达到一定长度时,链表会转换成红黑树。

5、HashSet怎么检查重复?

JDK1.8 中,HashSetadd()方法只是简单的调用了HashMapput()方法,并且判断了一下返回值以确保是否有重复元素。

浅看一下HashSet的源码:

/**
 * Adds the specified element to this set if it is not already present.
 * More formally, adds the specified element <tt>e</tt> to this set if
 * this set contains no element <tt>e2</tt> such that
 * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
 * If this set already contains the element, the call leaves the set
 * unchanged and returns <tt>false</tt>.
 *
 * @param e element to be added to this set
 * @return <tt>true</tt> if this set did not already contain the specified
 * element
 */
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

HashMap的put方法内部调用了putVal方法,通过查看源码可以看到关于putVal的说明:

// Returns : previous value, or null if none
// 返回值:如果插入位置没有元素返回null,否则返回上一个元素
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
...
}

6、HashMap的长度为什么是2的幂次方

原因是为了减少Hash碰撞,尽量使Hash算法的结果均匀。

Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。

所以用之前要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。

为什么取模运算是 (n - 1) & hash 呢?

取模(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)

又因为采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。

7、HashMap的7种遍历方式

  • 使用迭代器(Iterator)EntrySet 的方式进行遍历

    Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<Integer, String> entry = iterator.next();
        System.out.println(entry.getKey());
        System.out.println(entry.getValue());
    }
    
  • 使用迭代器(Iterator)KeySet 的方式进行遍历;

    Iterator<Integer> iterator = map.keySet().iterator();
    while (iterator.hasNext()) {
        Integer key = iterator.next();
        System.out.println(key);
        System.out.println(map.get(key));
    }
    
  • 使用 For Each EntrySet 的方式进行遍历;

    for (Map.Entry<Integer, String> entry : map.entrySet()) {
        System.out.println(entry.getKey());
        System.out.println(entry.getValue());
    }
    
  • 使用 For Each KeySet 的方式进行遍历;

    for (Integer key : map.keySet()) {
        System.out.println(key);
        System.out.println(map.get(key));
    }
    
  • 使用 Lambda 表达式的方式进行遍历;

    map.forEach((key, value) -> {
        System.out.println(key);
        System.out.println(value);
    });
    
  • 使用 Streams API 单线程的方式进行遍历;

    map.entrySet().stream().forEach((entry) -> {
        System.out.println(entry.getKey());
        System.out.println(entry.getValue());
    });
    
  • 使用 Streams API 多线程的方式进行遍历。

    map.entrySet().parallelStream().forEach((entry) -> {
        System.out.println(entry.getKey());
        System.out.println(entry.getValue());
    });
    

通过 Oracle 官方提供的性能测试工具 JMH 对7种遍历方式进行性能测试可以得出结论:

其中 Units 为 ns/op 意思是执行完成时间(单位为纳秒),而 Score 列为平均执行时间, ± 符号表示误差。

因为 parallelStream 为多线程版本性能一定是最好的,所以就不参与测试了。

从以上结果可以看出,两个 entrySet 的性能相近,并且执行速度最快,接下来是 stream ,然后是两个 keySet,性能最差的是 KeySet

从以上结果可以看出 entrySet 的性能比 keySet 的性能高出了一倍之多,因此我们应该尽量使用 entrySet 来实现 Map 集合的遍历

8、HashMap多线程操作死循环问题

并发先的rehash操作会产生死循环问题。

详细文章解析可参考:https://coolshell.cn/articles/9606.html

posted @   knqiufan  阅读(55)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
点击右上角即可分享
微信分享提示
欢迎阅读『[Java]Map接口有关总结』