Set类集合通过组合不同的Map,实现各种Set的功能,HashSet组合的是HashMap,TreeSet组合的是TreeMap,而LinkedHashSet则是继承HashSet,HashSet和TreeSet之所以采用组合而不是继承的方式,是因为:
- 继承表示父子类是同一个事物,而 Set 和 Map 本来就是想表达两种事物,所以继承不妥,而且 Java 语法限制,子类只能继承一个父类,使用继承后续难以扩展;
- 组合更加灵活,可以任意的组合现有的基础类,并且可以在基础类方法的基础上进行扩展、编排等,而且方法命名可以任意命名,无需和基础类的方法名称保持一致;
在工作中,如果碰到类似问题,我们的原则也是尽量多用组合,少用继承。
HashSet源码解析
HashSet底层基于HashMap实现,所以HashMap的大部分特性HashSet也有,例如:
- 无法保证元素顺序,迭代时不能保证按照插入顺序,或者其它顺序进行迭代;
- 线程不安全,如果需要安全要自行加锁,或者使用 Collections.synchronizedSet;
- 迭代过程中,如果数据结构被改变,会快速失败,抛出 ConcurrentModificationException 异常;
HashSet组合的源码实现
// 把 HashMap 组合进来,key 是 Hashset 的 key,value 是下面的 PRESENT
private transient HashMap<E,Object> map;
// HashMap 中的 value
private static final Object PRESENT = new Object();
HashSet中的元素就是HashMap中的key,HashMap中的value是个固定值。
HashSet初始化
初始化源码如下:
// 对 HashMap 的容量进行了计算
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
从源码中可以学习到初始化HashMap时如何指定容量,使用Math.max((int) (c.size()/.75f) + 1, 16)
计算容量可以防止HashMap扩容,当我们往 HashMap 拷贝大集合时,也可以使用这种方式指定HashMap的初始容量。
TreeSet源码解析
TreeSet 大致的结构和 HashSet 相似,底层组合的是 TreeMap,所以继承了 TreeMap key 能够排序的功能,迭代的时候,也可以按照 key 的排序顺序进行迭代。
TreeSet组合TreeMap的过程中使用了两种方式,如下:
- 对于add()这类简单的方法,没有复杂的逻辑,TreeSet直接调用TreeMap的put()方法;
- 对于迭代器这类复杂的方法,TreeMap的迭代器并不能满足TreeSet,如果TreeSet自己实现迭代器,由于底层使用的是TreeMap,需要对TreeMap的底层实现特别清楚,难度较大,TreeSet采用的方式是,自己负责接口定义,TreeMap负责具体实现,因为接口是 TreeSet 定义的,所以实现一定是 TreeSet 最想要的;
LinkedHashSet源码解析
LinkedHashSet继承了HashSet,跟HashMap与LinkedHashMap的区别一样,LinkedHashSet中的元素也是有序的,可以通过存放顺序进行遍历,LinkedHashSet之所以有序,是因为LinkedHashSet底层组合的是LinkedHashMap,源码如下:
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
这种设计思路很巧妙,LinkedHashSet虽然继承的是HashSet,但是通过构造方法改变了底层的数据结构,在LinkedHashSet中只有构造方法,没有定义其他的方法,都是从HashSet继承的。