带你从头看完java集合框架源码之Set
带你从头看完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接口