前言

在工作中一次排查慢接口时,查到了一个函数耗时较长,最终定位到是通过 List 去重导致的。

由于测试环境还有线上早期数据较少,这个接口的性能问题没有引起较大关注,后面频繁超时,才引起重视。.

HashSet源码

image.png

看类注释上,我们可以得到的信息有:

  • 底层实现基于 HashMap,所以迭代时不能保证按照插入顺序,或者其它顺序进行迭代;
  • add、remove、contanins、size 等方法的耗时性能,是不会随着数据量的增加而增加的,这个主要跟 HashMap 底层的数组数据结构有关,不管数据量多大,不考虑 hash 冲突的情况下,时间复杂度都是 O (1);
  • 线程不安全的,如果需要安全请自行加锁,或者使用 Collections.synchronizedSet;
  • 迭代过程中,如果数据结构被改变,会快速失败的,会抛出 ConcurrentModificationException 异常。

刚才是从类注释中看到,HashSet 的实现是基于 HashMap 的,在 Java 中,要基于基础类进行创新实现,有两种办法:

  • 继承基础类,覆写基础类的方法,比如说继承 HashMap , 覆写其 add 的方法;
  • 组合基础类,通过调用基础类的方法,来复用基础类的能力。

HashSet 使用的就是组合 HashMap,其优点如下:
继承表示父子类是同一个事物,而 Set 和 Map 本来就是想表达两种事物,所以继承不妥,而且 Java 语法限制,子类只能继承一个父类,后续难以扩展。
组合更加灵活,可以任意的组合现有的基础类,并且可以在基础类方法的基础上进行扩展、编排等,而且方法命名可以任意命名,无需和基础类的方法名称保持一致。
组合就是把 HashMap 当作自己的一个局部变量,以下是 HashSet 的组合实现:

// 把 HashMap 组合进来,key 是 Hashset 的 key,value 是下面的 PRESENT
private transient HashMap<E,Object> map;
// HashMap 中的 value
private static final Object PRESENT = new Object();

从这两行代码中,我们可以看出两点:

我们在使用 HashSet 时,比如 add 方法,只有一个入参,但组合的 Map 的 add 方法却有 key,value 两个入参,相对应上 Map 的 key 就是我们 add 的入参,value 就是第二行代码中的 PRESENT,此处设计非常巧妙,用一个默认值 PRESENT 来代替 Map 的 Value;

我们再来看看add方法:

public boolean add(E e) {
    // 直接使用 HashMap 的 put 方法,进行一些简单的逻辑判断
    return map.put(e, PRESENT)==null;
}

我们进入更底层源码java.util.HashMap#put:

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

o4wc41u9
1 声望0 粉丝