HashSet、TreeSet 两个类是在 Map 的基础上组装起来的类,两者都是线程不安全的。学习的侧重点,主要在于 Set 是如何利用 Map 现有的功能,来达成自己的目标的,也就是说如何基于现有的功能进行创新,然后再看看一些改变的小细节。
TreeSet
一:类注释
看源码先看类注释上,我们可以得到的信息有:
1:底层实现基于 HashMap,所以迭代时不能保证按照插入顺序,或者其它顺序进行迭代;
2:add、remove、contanins、size 等方法的耗时性能,是不会随着数据量的增加而增加的,这个主要跟 HashMap 底层的数组数据结构有关,不管数据量多大,不考虑 hash 冲突的情况下,时间复杂度都是 O (1);
3:线程不安全的,如果需要安全请自行加锁,或者使用Collections.synchronizedSet;迭代过程中,如果数据结构被改变,会快速失败的,会抛出ConcurrentModificationException 异常。
之前也看过 List、Map 的类注释,发现 2、3、4 点信息在类注释中都有提到,所以如果有人问 List、Map、 Set 三者的共同点,那么就可以说 2、3、4 三点。
二:HashSet 是如何组合 HashMap 的
刚才是从类注释 1 中看到,HashSet 的实现是基于 HashMap 的,在 Java 中,要基于基础类进行创新实现,有两种办法:
- 继承基础类,覆写基础类的方法,比如说继承 HashMap , 覆写其 add 的方法;
- 组合基础类,通过调用基础类的方法,来复用基础类的能力。
HashSet 使用的就是组合 HashMap,其优点如下:
- 继承表示父子类是同一个事物,而 Set 和 Map 本来就是想表达两种事物,所以继承不妥,而且 Java 语法限制,子类只能继承一个父类,后续难以扩展。
- 组合更加灵活,可以任意的组合现有的基础类,并且可以在基础类方法的基础上进行扩展、编排等,而且方法命名可以任意命名,无需和基础类的方法名称保持一致。
三:初始化
HashSet 的初始化比较简单,直接 new HashMap 即可,当有原始集合数据进行初始化的情况下,会对 HashMap 的初始容量进行计算,源码如下:
// 对 HashMap 的容量进行了计算
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
HashSet
TreeSet 大致的结构和 HashSet 相似,底层组合的是 TreeMap,所以继承了 TreeMap key 能够排序的功能,迭代的时候,也可以按照 key 的排序顺序进行迭代。
一:复用TreeMap的思路一:
场景一: TreeSet 的 add 方法,我们来看下其源码:
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
可以看到,底层直接使用的是 HashMap 的 put 的能力,直接拿来用就好了。
二:复用TreeMap的思路二:
场景二:需要迭代 TreeSet 中的元素,那应该也是像 add 那样,直接使用 HashMap 已有的迭代能力,比如像下面这样:
// 模仿思路一的方式实现
public Iterator<E> descendingIterator() {
// 直接使用 HashMap.keySet 的迭代能力
return m.keySet().iterator();
}
不过TreeSet的思路却恰恰相反,源码如下:
// NavigableSet 接口,定义了迭代的一些规范,和一些取值的特殊方法
// TreeSet 实现了该方法,也就是说 TreeSet 本身已经定义了迭代的规范
public interface NavigableSet<E> extends SortedSet<E> {
Iterator<E> iterator();
E lower(E e);
}
// m.navigableKeySet() 是 TreeMap 写了一个子类实现了 NavigableSet
// 接口,实现了 TreeSet 定义的迭代规范
public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}
三:TreeSet组合TreeMap实现的两种思路:
1:TreeSet直接使用TreeMap的某些功能,自己包装成为新的aip。
2:TreeSet定义自己想要的api,自己定义接口规范,让TreeMap实现。
面试题
一:如果我想实现根据 key 的新增顺序进行遍历怎么办?
要按照 key 的新增顺序进行遍历,首先想到的应该就是 LinkedHashMap,而 LinkedHashSet 正好是基于 LinkedHashMap 实现的,所以我们可以选择使用 LinkedHashSet。
二:如果我想对 key 进行去重,有什么好的办法么?
我们首先想到的是 TreeSet,TreeSet 底层使用的是 TreeMap,TreeMap 在 put 的时候,如果发现 key 是相同的,会把 value 值进行覆盖,所有不会产生重复的 key ,利用这一特性,使用 TreeSet 正好可以去重。
三:TreeSet 和 HashSet 两个 Set 的内部实现结构和原理?
HashSet 底层对 HashMap 的能力进行封装,比如说 add 方法,是直接使用 HashMap 的 put 方法,比较简单。
TreeSet 主要是对 TreeMap 底层能力进行封装复用,我发现了两种非常有意思的复用思路,重复 TreeSet 两种复用思路。