带你从头看完java集合框架源码之Set

带你从头看完java集合框架源码之Set

目录

  1. java集合框架源码之总体预览
  2. java集合框架源码之List
  3. java集合框架源码之Queue
  4. java集合框架源码之Set

上一篇文章我们介绍了Queue接口的实现类,这一篇我们来看Set下的各种接口以及类

抽象类AbstractSet:

public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E>

没有什么特殊的东西,重写了equals方法、hashcode方法和removeAll方法,

public boolean equals(Object o) {
    	//这个不用解释
        if (o == this)
            return true;
        if (!(o instanceof Set))
            return false;
        Collection<?> c = (Collection<?>) o;
        if (c.size() != size())
            return false;
        try {
            //两个set equals的条件是所有元素都相同
            return containsAll(c);
        } catch (ClassCastException unused)   {
            return false;
        } catch (NullPointerException unused) {
            return false;
        }
    }
//set的hashCode是所有元素hashCode相加
public int hashCode() {
        int h = 0;
        Iterator<E> i = iterator();
        while (i.hasNext()) {
            E obj = i.next();
            if (obj != null)
                h += obj.hashCode();
        }
        return h;
    }

public boolean removeAll(Collection<?> c) {
        Objects.requireNonNull(c);
        boolean modified = false;

        if (size() > c.size()) {
            for (Iterator<?> i = c.iterator(); i.hasNext(); )
                modified |= remove(i.next());
        } else {
            for (Iterator<?> i = iterator(); i.hasNext(); ) {
                if (c.contains(i.next())) {
                    i.remove();
                    modified = true;
                }
            }
        }
        return modified;
    }

看完抽象类,Set接口有一个继承的接口,叫SortSet(排序集),继承了SortedSet的接口还有NavigableSet,我们来看这两个接口

接口SortedSet:

特点:插入的元素按升序排序(元素必须实现Comparable),SortedSet的equals

注释中有这么一句话:

Note that the ordering maintained by a sorted set (whether or not an explicit comparator is provided) must be consistent with equals if the sorted set is to correctly implement the Set interface

请注意,如果排序集要正确实现set接口,则排序集维护的顺序(无论是否提供显式比较器)必须与equals一致。

什么叫维护的顺序与equals一致呢?文档中提示我们参考Comparable接口或Comparator接口来得知定义,翻阅Comparable接口,看到这样一句话

The natural ordering for a class C is said to be consistent with equals if and only if e1.compareTo(e2) == 0 has the same boolean value as e1.equals(e2) for every e1 and e2 of class C

当且仅当e1.compareTo(e2)==0对于类C的每个e1和e2具有与e1.equals(e2)相同的布尔值时,类C的自然顺序称为与equals一致

也就是说,两个对象e1和e2,如果e1.compareTo(e2)==0,那么e1.equals(e2)则为true,这样就叫equals一致

SortedSet定义的方法:

//返回比较器
Comparator<? super E> comparator();
//跟subList一样,分割SortedSet
SortedSet<E> subSet(E fromElement, E toElement);
//返回小于toElement的集合,修改该集合会反映在原集合上
SortedSet<E> headSet(E toElement);
//返回大于toElement的集合,修改该集合会反映在原集合上
SortedSet<E> tailSet(E fromElement);
//返回set的第一个元素(最小的)
E first();
//返回set的最后一个元素(最大的)
E last();
@Override
    default Spliterator<E> spliterator() {
        return new Spliterators.IteratorSpliterator<E>(
                this, Spliterator.DISTINCT | Spliterator.SORTED | Spliterator.ORDERED) {
            @Override
            public Comparator<? super E> getComparator() {
                return SortedSet.this.comparator();
            }
        };
    }

排序集SortedSet接口我们看完了,我们看看继承他的NavigableSet

接口NavigableSet:

NavigableSet顾名思义,是为了更好的浏览SortedSet而产生的接口,它提供了一系列方法来让我们更好的去访问排序集,我们来看一下他的方法

//返回Set中严格比e小的中最大的元素,没有返回null,如果e是null且集合不允许存null则抛异常
E lower(E e);
//返回Set中小于等于e的中最大的元素,没有返回null,如果e是null且集合不允许存null则抛异常
E floor(E e);
//返回Set中大于等于e的中最小的元素,没有返回null,如果e是null且集合不允许存null则抛异常
E ceiling(E e);
////返回Set中严格大于e的中最小的元素,没有返回null,如果e是null且集合不允许存null则抛异常
E higher(E e);
//移除并返回第一个元素
E pollFirst();
//移除并返回最后一个元素
E pollLast();
//返回一个按照升序遍历的迭代器
Iterator<E> iterator();
//返回一个逆序的Set
NavigableSet<E> descendingSet();
//返回一个按照降序遍历的迭代器
Iterator<E> descendingIterator();
//返回set的子集,boolean参数表示是否包含端点,如果fromElement = toElement且boolean参数只有一个为真,则返回空集
NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
                           E toElement,   boolean toInclusive);
//inclusive为true时,返回小于等于toElement元素的集合,为false时,返回小于toElement元素的集合
NavigableSet<E> headSet(E toElement, boolean inclusive);
//inclusive为true时,返回大于等于toElement元素的集合,为false时,返回大于toElement元素的集合
NavigableSet<E> tailSet(E fromElement, boolean inclusive);
//等价于subSet(fromElement, true, toElement, false)
SortedSet<E> subSet(E fromElement, E toElement);
//等价于headSet(toElement, false)
SortedSet<E> headSet(E toElement);
//等价于tailSet(fromElement, true)
SortedSet<E> tailSet(E fromElement);

接口到这我们就看完了,接下来我们看Set的三个实现类,分别是有序集TreeSet,无序集HashSet和LinkedHashSet

类TreeSet:

特点:基于TreeMap(底层是红黑树),线程不安全,添加、删除的时间复杂度为O(n)

红黑树:

红黑树也是二叉查找树,不同的是红黑树是自平衡的二叉查找树,又不同于另一个自平衡的二叉查找树AVL树,红黑树的平衡是黑平衡,想要了解的话可以看我的另一篇文章:好怕怕的红黑树(一文带你从2-3-4树理解红黑树)

成员变量:

//存放元素的map
private transient NavigableMap<E,Object> m;
//底层TreeMap中key对应的value
private static final Object PRESENT = new Object();
//序列化ID
private static final long serialVersionUID = -2479143000061671589L;

构造方法:

//根据一个NavigableMap的对象构造TreeSet
TreeSet(NavigableMap<E,Object> m) {
        this.m = m;
    }
//无参构造,底层是TreeMap
public TreeSet() {
        this(new TreeMap<E,Object>());
    }
//指定比较器构造
public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
//调用无参构造
public TreeSet(Collection<? extends E> c) {
        this();
        addAll(c);
    }
//用排序集对象构造TreeSet
public TreeSet(SortedSet<E> s) {
        this(s.comparator());
        addAll(s);
    }

通过构造方法我们可以看到,底层都是使用TreeMap

我们来看看添加操作和删除操作

public boolean add(E e) {
    	//调用的是TreeMap的put方法,添加的元素做key,成员变量PRESENT作一个默认的value值,无实际意义
    	//TreeMap底层是红黑树,key是不重复的,所以TreeSet也满足Set不重复的性质
    	// m.put(e, PRESENT)==null是用来判断插入值是否已经存在,如果插入值原先不存在就会返回null,即插入成功
    	//因为插入成功时是返回null,所以value值不可以用null来代替
    	//因为如果插入的值原先存在就会返回原先的value,如果value用null代替就会不知道插入是否成功
    	//这也就是为什么要用一个默认对象来充当value
        return m.put(e, PRESENT)==null;
    }
public boolean remove(Object o) {
    	//同样调用的是TreeMap的remove方法
        return m.remove(o)==PRESENT;
    }

翻阅整个TreeSet的源码基本上都是在调用TreeMap,也就是说我们要了解更多的细节需要去了解TreeMap,这个我们等讲Map接口的时候再详细讲

类HashSet:

特点:底层是HashMap实现,元素无序,可以添加null元素,线程不安全

成员变量:

//序列化ID
static final long serialVersionUID = -5024744406713321676L;
//存放元素的哈希表
private transient HashMap<E,Object> map;
//默认value值
private static final Object PRESENT = new Object();

构造方法:

//根据集合创建HashSet,初始容量是16和c.size/0.75中的最大值
//为什么是0.75?因为HashMap的加载因子是0.75
public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }
//创建具有指定容量和加载因子的HashSet
public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
//创建指定容量的HashSet,默认加载因子
public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
//提供给LinkHashSet的构造方法,创建具有指定容量和加载因子的HashSet
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

看一下添加和删除的方法

public boolean add(E e) {
    	//调用HashMap的put方法
        return map.put(e, PRESENT)==null;
    }
public boolean remove(Object o) {
    	//调用HashMap.remove方法
        return map.remove(o)==PRESENT;
    }

可以看到和TreeSet一样,都是调用底层map的方法,想要完全了解Set,map的底层源码不能放过

类LinkedHashSet:

特点:链表实现的HashSet(底层是LinkedHashMap),迭代顺序和插入顺序一致,可以插入null

Performance is likely to be just slightly below that of HashSet, due to the added expense of maintaining the linked list, with one exception: Iteration over a LinkedHashSet requires time proportional to the size of the set, regardless of its capacity. Iteration over a HashSet is likely to be more expensive, requiring time proportional to its capacity.

性能略低于HashSet,因为要维护链表节点会产生开销(与ArrayDeque对比LinkedList、Stack的性能原因差不多),但是迭代时间的代价仅限于元素的个数而不是容量,因为HashMap为底层的HashSet还有加载因子的影响

public class LinkedHashSet<E>
    extends HashSet<E>
    implements Set<E>, Cloneable, java.io.Serializable

可以看到是继承了HashSet的

成员变量只有一个序列化ID

构造方法:

//调用父类构造方法HashSet(int initialCapacity, float loadFactor, boolean dummy)
//指定容量和加载因子
public LinkedHashSet(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor, true);
    }
//指定容量,加载因子为0.75
public LinkedHashSet(int initialCapacity) {
        super(initialCapacity, .75f, true);
    }
//默认容量16,加载因子0.75
public LinkedHashSet() {
        super(16, .75f, true);
    }
//根据集合构造Set
public LinkedHashSet(Collection<? extends E> c) {
        super(Math.max(2*c.size(), 11), .75f, true);
        addAll(c);
    }

除此之外,源码中没有别的方法了

可以看到无论是TreeSet还是HashSet、LinkedHashSet,他的底层实现都是基于Map的,因此想要搞懂Set,就必须去阅读并理解Map接口的相关内容,Set接口到这里就结束了,接下来的文章我们要阅读Map接口

posted @ 2021-03-15 16:38  ForYou丶  阅读(77)  评论(0编辑  收藏  举报