TreeSet 是一个有序的集合,它的作用是提供有序的Set集合。它继承于AbstractSet抽象类,实现了NavigableSet
, Cloneable, java.io.Serializable
接口。
TreeSet是基于TreeMap
实现的。TreeSet中的元素支持2种排序方式:自然排序 或者 根据创建TreeSet 时提供的 Comparator 进行排序。这取决于使用的构造方法。
TreeSet为基本操作(add、remove 和 contains)提供受保证的 log(n) 时间开销。
另外,TreeSet是非同步的。 它的iterator 方法返回的迭代器是基于fail-fast机制的迭代器。
以上便是TreeSet的定义和特征,TreeSet还是比较简单的,因为TreeSet是基于TreeMap而实现,接下来从源码层面剖析TreeSet的内部结构。
内部实现主要是从源码进行剖析,整个TreeSet的源代码并不多,因为很多实现复用了TreeMap中的实现。那么我们先从整体结构开始!
先上个继承图:
TreeSet的继承类图如上,其中直接实现NavigableSet,保证了一系列的导航操作,如:lower、floor、ceiling 和 higher等操作,后面会讲到这些操作方法。
/**
* 可以用指定的navigable map创建,TreeMap实现了该接口
*/
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
/**
* 虽然该构造方法无参数,但其实是new 了一个TreeMap并且调用如下的构造器:
*
* TreeSet(NavigableMap m) {
* this.m = m;
* }
*
*/
public TreeSet() {
this(new TreeMap<>());
}
/**
* 自定义比较器的构造方法
*/
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
/**
* 通过集合构造TreeSet
*/
public TreeSet(Collection<? extends E> c) {
//this()其实就是调用无参的构造器
this();
//把元素添加进入TreeMap
addAll(c);
}
对以上构造器的实现的分析:
new TreeMap
/**
* 本质就是TreeMap
*/
private transient NavigableMap<E, Object> m;
//与支持映射中的对象关联的虚值
private static final Object PRESENT = new Object();
//序列化版本号
private static final long serialVersionUID = -2479143000061671589L;
对内部成员的理解:
PRESENT
对象,相当于一个占位符。TreeSet的迭代器其实就是TreeMap中的迭代器的调用,接下来开始解析。
/**
* 按升序返回此集合中元素的迭代器。
*/
public Iterator<E> iterator() {
//可以看到是navigableKeySet()的迭代器
return m.navigableKeySet().iterator();
}
/**
* 按降序返回该集合中元素的迭代器。
*/
public Iterator<E> descendingIterator() {
//底层是TreeMap中的降序KeySet()
return m.descendingKeySet().iterator();
}
//该构造器位于TreeMap中
public NavigableSet<K> navigableKeySet() {
KeySet<K> nks = navigableKeySet;
//可以看到,是调用了TreeMap的KeySet。
return (nks != null) ? nks : (navigableKeySet = new KeySet<>(this));
}
迭代器的实现:
对于TreeSet的迭代测试:
public static void main(String[] args) {
TreeSet<String> treeSet=new TreeSet<>();
treeSet.add("123");
treeSet.add("3123");
treeSet.add("sdfsdfsdfdsf");
for (String s:
(treeSet.toArray(new String[0]))) {
System.out.println(s);
}
}
注意:TreeSet不支持foreach快速随机遍历,要使用foreach遍历必须先转化成为数组!推荐迭代器遍历。
求子集是Set的一个特色,TreeSet中提供了很多求取子集合的方法,如下:
/**
* 返回子Set,实际上是通过TreeMap的subMap()实现的。
*/
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive) {
return new TreeSet<>(m.subMap(fromElement, fromInclusive,
toElement, toInclusive));
}
// 返回Set的头部,范围是:从头部到toElement。
// inclusive是是否包含toElement的标志
public NavigableSet<E> headSet(E toElement, boolean inclusive) {
return new TreeSet<>(m.headMap(toElement, inclusive));
}
// 返回Set的尾部,范围是:从fromElement到结尾。
// inclusive是是否包含fromElement的标志
public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
return new TreeSet<>(m.tailMap(fromElement, inclusive));
}
// 返回子Set。范围是:从fromElement(包括)到toElement(不包括)。
public SortedSet<E> subSet(E fromElement, E toElement) {
return subSet(fromElement, true, toElement, false);
}
// 返回Set的头部,范围是:从头部到toElement(不包括)。
public SortedSet<E> headSet(E toElement) {
return headSet(toElement, false);
}
// 返回Set的尾部,范围是:从fromElement到结尾(不包括)。
public SortedSet<E> tailSet(E fromElement) {
return tailSet(fromElement, true);
}
子集方法的底层实现:
导航方法也是TreeSet的特点,如下:
// NavigableSet API methods
// 返回Set中小于e的最大元素
public E lower(E e) {
return m.lowerKey(e);
}
// 返回Set中小于/等于e的最大元素
public E floor(E e) {
return m.floorKey(e);
}
// 返回Set中大于/等于e的最小元素
public E ceiling(E e) {
return m.ceilingKey(e);
}
// 返回Set中大于e的最小元素
public E higher(E e) {
return m.higherKey(e);
}
// 获取第一个元素,并将该元素从TreeMap中删除。
public E pollFirst() {
Map.Entry<E, ?> e = m.pollFirstEntry();
return (e == null) ? null : e.getKey();
}
// 获取最后一个元素,并将该元素从TreeMap中删除。
public E pollLast() {
Map.Entry<E, ?> e = m.pollLastEntry();
return (e == null) ? null : e.getKey();
}
TreeSet中提供了一系列的导航方法,包括通过比较大小返回元素等等,这些方法的底层实现,先举其中的higher方法进行解析:
// 获取“大于key的最小键”
public final K higherKey(K key) {
return keyOrNull(subHigher(key));
}
TreeMap.Entry<K,V> subHigher(K key) { return absHigher(key); }
final TreeMap.Entry<K,V> absHigher(K key) {
//如果这个键太小,则返回最小值
if (tooLow(key))
return absLowest();
//获取比该值大一点的。
TreeMap.Entry<K,V> e = m.getHigherEntry(key);
return (e == null || tooHigh(e.key)) ? null : e;
}
final Entry<K,V> getHigherEntry(K key) {
Entry<K,V> p = root;
while (p != null) {
int cmp = compare(key, p.key);
if (cmp < 0) {
if (p.left != null)
p = p.left;
else
return p;
} else {
if (p.right != null) {
p = p.right;
} else {
Entry<K,V> parent = p.parent;
Entry<K,V> ch = p;
while (parent != null && ch == parent.right) {
ch = parent;
parent = parent.parent;
}
return parent;
}
}
}
return null;
}
对于导航比较方法的理解:
调用TreeMap的Key的比较方法。
比较的实现是通过: 二叉查找树+比较器进行比较。
在理解完红黑树、TreeMap的实现上,相信对TreeSet的理解是比较顺畅的。
HashSet 是一个没有重复元素的集合。
它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用 null 元素。
HashSet是非同步的。如果多个线程同时访问一个哈希 set,而其中至少一个线程修改了该 set,那么它必须 保持外部同步。这通常是通过对自然封装该 set 的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet 方法来“包装” set。最好在创建时完成这一操作,以防止对该 set 进行意外的不同步访问:
HashSet s = Collections.synchronizedSet(new HashSet(...));
特征:
由于HashSet的内部实现与TreeSet类似,都是基于对应的Map的实现,并且比TreeSet更加简单,因此只列出通过注释解析后的代码,不一一解释。
package java.util;
import java.io.InvalidObjectException;
import jdk.internal.misc.SharedSecrets;
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
static final long serialVersionUID = -5024744406713321676L;
//底层是HashMap
private transient HashMap<E, Object> map;
// 常量对象,占位作用
private static final Object PRESENT = new Object();
/**
* 构造HASHMAP
*/
public HashSet() {
map = new HashMap<>();
}
/**
* 通过集合构造HashSet
*/
public HashSet(Collection<? extends E> c) {
// 创建map。
// 为什么要调用Math.max((int) (c.size()/.75f) + 1, 16),从 (c.size()/.75f) + 1 和 16 中选择一个比较大的树呢?
// 首先,说明(c.size()/.75f) + 1
// 因为从HashMap的效率(时间成本和空间成本)考虑,HashMap的加载因子是0.75。
// 当HashMap的“阈值”(阈值=HashMap总的大小*加载因子) < “HashMap实际大小”时,
// 就需要将HashMap的容量翻倍。
// 所以,(c.size()/.75f) + 1 计算出来的正好是总的空间大小。
// 接下来,说明为什么是 16 。
// HashMap的总的大小,必须是2的指数倍。若创建HashMap时,指定的大小不是2的指数倍;
// HashMap的构造函数中也会重新计算,找出比“指定大小”大的最小的2的指数倍的数。
// 所以,这里指定为16是从性能考虑。避免重复计算。
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);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public Iterator<E> iterator() {
return map.keySet().iterator();
}
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}
public void clear() {
map.clear();
}
@SuppressWarnings("unchecked")
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// Write out any hidden serialization magic
s.defaultWriteObject();
// Write out HashMap capacity and load factor
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
// Write out size
s.writeInt(map.size());
// Write out all elements in the proper order.
for (E e : map.keySet())
s.writeObject(e);
}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();
// Read capacity and verify non-negative.
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}
// Read load factor and verify positive and non NaN.
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
// Read size and verify non-negative.
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}
// Set the capacity according to the size and load factor ensuring that
// the HashMap is at least 25% full but clamping to maximum capacity.
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
// Constructing the backing map will lazily create an array when the first element is
// added, so check it before construction. Call HashMap.tableSizeFor to compute the
// actual allocation size. Check Map.Entry[].class since it's the nearest public type to
// what is actually created.
SharedSecrets.getJavaObjectInputStreamAccess()
.checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
// Create backing HashMap
map = (((HashSet<?>) this) instanceof LinkedHashSet ?
new LinkedHashMap<>(capacity, loadFactor) :
new HashMap<>(capacity, loadFactor));
// Read in all elements in the proper order.
for (int i = 0; i < size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
/**
* 创建分裂迭代器
*/
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<>(map, 0, -1, 0, 0);
}
}