HashSet、TreeSet 源码解析

HashSet、TreeSet 源码解析

HashSet、TreeSet 两个类是在 Map 的基础上组装起来的类,我们学习的侧重点,主要在于 Set 是如何利用 Map 现有的功能,来达成自己的目标的,也就是说如何基于现有的功能进行创新,然后再看看一些改变的小细节是否值得我们学习。

一:HashSet

1.1、HashSet 类注释

看源码,首先要看类注释,我们看看类注释上面都说了什么,部分截图如下图所示:
在这里插入图片描述
类注释主要讲了以下四点

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

我们之前也看过 List、Map 的类注释,我们发现 2、3、4 点信息在类注释中都有提到,所以如果有人问 List、Map、 Set 三者的共同点,那么就可以说 2、3、4 三点。

1.2、HashSet 是如何组合 HashMap 的

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

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

HashSet 使用的就是组合 HashMap,其优点如下:

  1. 继承表示父子类是同一个事物,而 Set 和 Map 本来就是想表达两种事物,所以继承不妥,而且 Java 语法限制,子类只能继承一个父类,后续难以扩展。
  2. 组合更加灵活,可以任意的组合现有的基础类,并且可以在基础类方法的基础上进行扩展、编排等,而且方法命名可以任意命名,无需和基础类的方法名称保持一致;

我们在工作中,如果碰到类似问题,我们的原则也是尽量多用组合,少用继承。

组合就是把 HashMap 当作自己的一个局部变量,以下是 HashSet 的组合实现:

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

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

  1. 们在使用 HashSet 时,比如 add 方法,只有一个入参,但组合的 Map 的 add 方法却有 key,value 两个入参,相对应上 Map 的 key 就是我们 add 的入参,value 就是第二行代码中的 PRESENT,此处设计非常巧妙,用一个默认值 PRESENT 来代替 Map 的 Value;
  2. 如果 HashSet 是被共享的,当多个线程访问的时候,就会有线程安全问题,因为在后续的所有操作中,并没有加锁;

HashSet 在以 HashMap 为基础进行实现的时候,首先选择组合的方式,接着使用默认值来代替了 Map 中的 Value 值,设计得非常巧妙,给使用者的体验很好,使用起来简单方便,我们在工作中也可以借鉴这种思想,可以把底层复杂实现包装一下,一些默认实现可以自己吃掉,使吐出去的接口尽量简单好用。

1.2.1、初始化

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 的其他方法就比较简单了,就是对 Map 的 api 进行了一些包装,如下的 add 方法实现:

扫描二维码关注公众号,回复: 11858743 查看本文章
public boolean add(E e) {
    
    
    // 直接使用 HashMap 的 put 方法,进行一些简单的逻辑判断
    return map.put(e, PRESENT)==null;
}

从 add 方法中,我们就可以看到组合的好处,方法的入参、名称、返回值都可以自定义,如果是继承的话就不行了。

1.2.2、小结

HashSet 具体实现值得我们借鉴的地方主要有如下地方,我们平时写代码的时候,完全可以参考参考:

  1. 对组合还是继承的分析和把握;
  2. 对复杂逻辑进行一些包装,使吐出去的接口尽量简单好用;
  3. 组合其他 api 时,尽量多对组合的 api 多些了解,这样才能更好的使用 api;

二:TreeSet

TreeSet 大致的结构和 HashSet 相似,底层组合的是 TreeMap,所以继承了 TreeMap key 能够排序的功能,迭代的时候,也可以按照 key 的排序顺序进行迭代,我们主要来看复用 TreeMap 时,复用的两种思路:

2.1、复用 TreeMap 的思路一

场景一: TreeSet 的 add 方法,我们来看下其源码:

public boolean add(E e) {
    
    
    return m.put(e, PRESENT)==null;
}

可以看到,底层直接使用的是 HashMap 的 put 的能力,直接拿来用就好了。

2.2、复用 TreeMap 的思路二

场景二:需要迭代 TreeSet 中的元素,那应该也是像 add 那样,直接使用 HashMap 已有的迭代能力,比如像下面这样:

// 模仿思路一的方式实现
public Iterator<E> descendingIterator() {
    
    
    // 直接使用 HashMap.keySet 的迭代能力
    return m.keySet().iterator();
}

这种是思路一的实现方式,TreeSet 组合 TreeMap,直接选择 TreeMap 的底层能力进行包装,但 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();
}

TreeMap 中对 NavigableSet 接口的实现源码如下:

// TreeMap 为了满足 Set 的功能,实现了 Set 定义的 NavigableSet 的接口
static final class KeySet<E> extends AbstractSet<E> implements NavigableSet<E> {
    
    
    private final NavigableMap<E, ?> m;
    KeySet(NavigableMap<E,?> map) {
    
     m = map; }

    public Iterator<E> iterator() {
    
    
        if (m instanceof TreeMap)
            return ((TreeMap<E,?>)m).keyIterator();
        else
            return ((TreeMap.NavigableSubMap<E,?>)m).keyIterator();
    }

    public Iterator<E> descendingIterator() {
    
    
        if (m instanceof TreeMap)
            return ((TreeMap<E,?>)m).descendingKeyIterator();
        else
            return ((TreeMap.NavigableSubMap<E,?>)m).descendingKeyIterator();
    }

	// 这些都是 NavigableSet 接口里面的方法
    public int size() {
    
     return m.size(); }
    public boolean isEmpty() {
    
     return m.isEmpty(); }
    public boolean contains(Object o) {
    
     return m.containsKey(o); }
    public void clear() {
    
     m.clear(); }
    public E lower(E e) {
    
     return m.lowerKey(e); }
    public E floor(E e) {
    
     return m.floorKey(e); }
    public E ceiling(E e) {
    
     return m.ceilingKey(e); }
    public E higher(E e) {
    
     return m.higherKey(e); }
    public E first() {
    
     return m.firstKey(); }
    public E last() {
    
     return m.lastKey(); }
    public Comparator<? super E> comparator() {
    
     return m.comparator(); }
    public E pollFirst() {
    
    
        Map.Entry<E,?> e = m.pollFirstEntry();
        return (e == null) ? null : e.getKey();
    }
    public E pollLast() {
    
    
        Map.Entry<E,?> e = m.pollLastEntry();
        return (e == null) ? null : e.getKey();
    }
    public boolean remove(Object o) {
    
    
        int oldSize = size();
        m.remove(o);
        return size() != oldSize;
    }
    public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                                  E toElement,   boolean toInclusive) {
    
    
        return new KeySet<>(m.subMap(fromElement, fromInclusive,
                                      toElement,   toInclusive));
    }
    public NavigableSet<E> headSet(E toElement, boolean inclusive) {
    
    
        return new KeySet<>(m.headMap(toElement, inclusive));
    }
    public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
    
    
        return new KeySet<>(m.tailMap(fromElement, inclusive));
    }
    public SortedSet<E> subSet(E fromElement, E toElement) {
    
    
        return subSet(fromElement, true, toElement, false);
    }
    public SortedSet<E> headSet(E toElement) {
    
    
        return headSet(toElement, false);
    }
    public SortedSet<E> tailSet(E fromElement) {
    
    
        return tailSet(fromElement, true);
    }
    public NavigableSet<E> descendingSet() {
    
    
        return new KeySet<>(m.descendingMap());
    }

    public Spliterator<E> spliterator() {
    
    
        return keySpliteratorFor(m);
    }
}

从 TreeMap 的源码中,我们可以看出 TreeMap 实现了 TreeSet 定义的各种特殊方法。

我们可以看到,这种思路是 TreeSet 定义了接口的规范,TreeMap 负责去实现,实现思路和思路一是相反的。

我们总结下 TreeSet 组合 TreeMap 实现的两种思路:

  1. TreeSet 直接使用 TreeMap 的某些功能,自己包装成新的 api;
  2. TreeSet 定义自己想要的 api,自己定义接口规范,让 TreeMap 去实现;

方案 1 和 2 的调用关系,都是 TreeSet 调用 TreeMap,但功能的实现关系完全相反,第一种是功能的定义和实现都在 TreeMap,TreeSet 只是简单的调用而已,第二种 TreeSet 把接口定义出来后,让 TreeMap 去实现内部逻辑,TreeSet 负责接口定义,TreeMap 负责具体实现,这样子的话因为接口是 TreeSet 定义的,所以实现一定是 TreeSet 最想要的,TreeSet 甚至都不用包装,可以直接把返回值吐出去都行。

我们思考下这两种复用思路的原因:

  1. 像 add 这些简单的方法,我们直接使用的是思路 1,主要是 add 这些方法实现比较简单,没有复杂逻辑,所以 TreeSet 自己实现起来比较简单;
  2. 思路 2 主要适用于复杂场景,比如说迭代场景,TreeSet 的场景复杂,比如要能从头开始迭代,比如要能取第一个值,比如要能取最后一个值,再加上 TreeMap 底层结构比较复杂,TreeSet 可能并不清楚 TreeMap 底层的复杂逻辑,这时候让 TreeSet 来实现如此复杂的场景逻辑,TreeSet 就搞不定了,不如接口让 TreeSet 来定义,让 TreeMap 去负责实现,TreeMap 对底层的复杂结构非常清楚,实现起来既准确又简单;

2.3、小结

TreeSet 对 TreeMap 的两种不同复用思路,很重要,在工作中经常会遇到,特别是思路二,比如说 dubbo 的泛化调用,DDD 中的依赖倒置等等,原理都是 TreeSet 第二种的复用思想。

三:总结

HashSet 对组合的 HashMap 类扩容的门阀值的深入了解和设计,值得我们借鉴,TreeSet 对 TreeMap 两种复用思路,值得我们学习,特别是第二种复用思路。

猜你喜欢

转载自blog.csdn.net/weixin_38478780/article/details/107979931