前言
书接上文,上一篇中对于 Set 接口与 AbstractSet 抽象实现类做了介绍与分析,本篇将对 Set 最终实现类 HashSet 与 LinkedHashSet 做介绍与分析。
先来看下 HashSet 的源码
/**
* 这个类实现了 Set 接口,有一个 hash table(事实上是一个 HashMap)支持。它对于套的迭代顺序不做保证,此处
* 特指,它不保证顺序能一直保持一致。这个类允许 null 元素,结社 hash 功能正确地在桶中分散了元素。
*
* 这个类为基本操作(add,remove,contains 和 size)提供了与事件一直的表现,迭代这个套锁需要的时间与
* HashSet 实例的长度(元素数量)加上支持的 HashMap 实例的“容量”(桶个数)成正比。因此,如果迭代性能很重要
* 的话,不将初始容量设置的太高(或者加载因子太低)就是很重要的。
*
* 注意这个实现类不是线程安全的。如果多线程并发地访问一个 hash set,并且至少有一个线程更改了 set,它必须从
* 外部实现线程安全。这通常通过对封装了 set 的对象实现线程安全来完成。
*
* 如果没有类似的对象存在,set 应该使用 Collections#synchronizedSet 方法“包装”。这最好在创建的时候完
* 成,来防止意外的对于 set 的非线程安全的访问: <pre>
* Set s = Collections.synchronizedSet(new HashSet(...));</pre>
*
* 这个类的 iterator 方法返回的迭代器是 fail-fast 的:如果 set 在被创建后的任何时间点被更改了,以除了通过
* 迭代器自己的 remove 方法的其他任何方式,迭代器会抛出一个 ConcurrentModificationException。因此,在
* 并发修改操作时,迭代器失败地快速和干净,而不是在未来某个不确定的时间点,冒险做武断的,描述不清的动作。
*
* 注意一个迭代器的 fail-fast 行为是不能被保证的,通常来说,不可能对出现的非线程安全的同时修改操作做任何硬性
*的保证。基于最佳性能的基础考虑,Fail-fast 迭代器抛出一个 ConcurrentModificationException。因此,建立
* 在这中异常上写出的程序的正确性将会是不健壮的:迭代器的 fail-fast 行为应当植被用于检查 bugs。
*
* HashSet 继承自 AbstractSet 抽象类,实现了 Set 接口,Cloneable 接口以及 Serializable 接口。
*/
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
/**不参与序列化的部分**/
private transient HashMap<E,Object> map;
/**与一个在支持 map 中的对象相关的壳值**/
private static final Object PRESENT = new Object();
/**
* 构造一个新的,空的 set,支持的 HashMap 实例有模式容量(16)和加载因子(0.75)
*/
public HashSet() {
map = new HashMap<>(); //构造一个空 HashMap
}
/**
* 构造一个新的包含指定数据结构中的元素的 set,HashMap 被使用默认加载因子(0.75)和一个足够包含指定数
* 据结构中元素的初始化容量来构造。
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); //传参构造一个空 HashMap
addAll(c); //调用 addAll 方法
}
/**
* 构造一个新的,空的 set,支持 HashMap 实例有指定的初始容量和指定的加载因子
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor); //传参构造一个空的 HashMap
}
/**
* 构造一个新的,空的 set,支持 HashMap 实例有指定的初始容量和模式的加载因子(0.75)。
*/
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity); //传参构造一个空的 HashMap
}
/**
* 构造一个新的,空的链接 hash set。(这个私有构造器只被 LinkedHashSet 使用。)支持 HashMap 实例是
* 一个由指定初始容量和指定加载因子构造的 LinkedHashMap。
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor); //传参构造一个空的 LinkedHashMap
}
/**
* 返回一个包含这个 set 中所有元素的迭代器。元素不以固定的顺序返回。
*/
public Iterator<E> iterator() {
return map.keySet().iterator(); //调用 HashMap#keySet 方法,返回结果的迭代器
}
/**
* 返回 set 中元素的个数(它的基数)。
*/
public int size() {
return map.size(); //返回 HashMap 的长度
}
/**
* 如果 set 中不包含元素,返回 true
*/
public boolean isEmpty() {
return map.isEmpty(); //返回 HashMap#isEmpty 方法结果
}
/**
* 如果 set 包含特定元素,返回 true。更正规地说,当且仅当这个 set 包含一个元素与指定对象相等时候返回
* true
*/
public boolean contains(Object o) {
return map.containsKey(o); //调用 HashMap#containsKey,传入指定对象
}
/**
* 如果当前 set 中不存在指定元素,则加入。更正规地说,如果当前 set 中不包含有与指定对象相等的元素,则加
* 入。如果 set 已经包含了元素,这个调用将不做任何改动的返回。
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null; //调用 HashMap#put 方法,将结果与 null 做比较
}
/**
* 如果当前 set 中存在指定对象,则从中移除。更正规地说,如果 set 中包含有与指定对象相等的对象,则移除。
* 如果当前 set 中包含指定对象(或者与当前 set 改变后的结果相等)。(一旦调用返回,当前 set 中将不再包
* 含元素。)
*/
public boolean remove(Object o) {
return map.remove(o)==PRESENT; //调用 HashMap#remove() 方法,比较返回值与 PRESENT
}
/**
* 移除当前 set 中的所有元素。方法调用后,set 将变为空
*/
public void clear() {
map.clear(); //调用 HashMap#claer 方法
}
/**
* 返回一个 HashSet 实例的浅拷贝:那些元素本身不被克隆。
*/
@SuppressWarnings("unchecked")
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone(); //调用 Object#clone 方法,缓存给 newSet
newSet.map = (HashMap<E, Object>) map.clone(); //调用 HashMap#clone 方法,并赋值给 newSet.map
return newSet; //返回 newSet
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
/**
* 保存当前 HashSet 实例的状态为一个流(为了序列化写)。
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
s.defaultWriteObject();
//写出 HashMap 的容量和加载因子
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
//写出 HashMap 的长度
s.writeInt(map.size());
//以正确的顺序写出所有元素
for (E e : map.keySet())
s.writeObject(e);
}
/**
* 从一个流中重构一个 HashSet 实例(为了反序列化读)。
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
//读出 HashMap 的容量并验证
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
}
//读出 HashMap 的加载因子并验证
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
}
//读出 HashMap 的长度并验证
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}
/**
* 通过长度和加载因子设计 set 的容量来保证 HashMap 至少 25% 被填满,直到容量最大值
*/
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY);
/**
* 构造支持 map 将在第一个元素添加的时候懒创建一个数组,所以在构造前检查它。调用
* HashMap.tableSizeFor 来计算实际分配的大小。检查 Map.Entry[].classs,既然它是与被创建的类
* 型最接近的公共类型。
*/
SharedSecrets.getJavaOISAccess()
.checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
/**建立支持 HashMap**/
map = (((HashSet<?>)this) instanceof LinkedHashSet ? //根据当前对象判断是建立新的 LinkedHashMap 还是 HashMap
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));
/**按正确的顺序读如所有元素**/
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT); //依次加入 map 中
}
}
/**创建一个包含所有这个 set 中的元素的 fail-fast 的并行迭代器。**/
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
}
}
通过 HashSet 的源码可以看到,HashSet 其实是维护了一个 HashMap 变量,并通过操作 HashMap 来完成对 HashSet 的操作,没有自己的迭代器,或者说这些迭代器是在 HashMap 中定义的,可以说 HashSet 本身只是一个 HashMap 的装饰器。
然后来看 LinkedHashSet 的源码
/**
* Set 接口的 Hash table 和链接链接实现类,伴随一个可以预计的顺序。这个实现类与 HashSet 不同的是它维护了
* 一个穿过它所有 entries 的双向链接链表。这个链接链表定义了迭代操作顺序,就是以元素加入 set 的顺序(插入-
* 顺序)。注意插入顺序不被相同对象的重新插入影响。(如果 s.add(e) 在 m.contains(e) 立即返回 true 之前被
* 调用时,那么一个键 key 是重新加入的。)
*
* 这个实现类使得客户端免受不确定的,通常是由 HashSet 提供的乱序,不带来与 TreeSet 相关的额外性能消耗。它
* 可以被用来产生一个与原始的 set 相同顺序的 set 的拷贝:
* <pre>
* void foo(Set s) {
* Set copy = new LinkedHashSet(s);
* ...
* }
* </pre>
* 如果一个模块将 set 作为输入,拷贝它,并且稍后返回一个有明确顺序的拷贝时候,这种技术是尤为有用的(客户端通
* 常对于以相同顺序返回的东西心存感激。)
*
* 这个类提供了所有 Set 的可选操作,并且允许 null 元素。就像 HashSet,在假设 hash 功能正确地分配元素到不
* 同的桶中时,它对于基础操作(add,contains 和 remove)提供时间相关的性能。性能看上去就比 HashSet 略微
* 低一些,因为维护链接链表需要额外成本,并伴随一个异常:对于一个 LinkedHashSet 的迭代操作时间与这个 set
* 的长度长正比,与它的容量无关。对于一个 HashSset 的迭代操作看上去更昂贵,时间与它的容量成正比。
*
* 有两个参数会影响到一个链接 hash set 的性能:初始化容量和加载因子。它们就像对于 HashSet 那样精确定义。注
* 意,然而,相比于 HashSet ,这个类选择过高的初始化容量值的代价没有这么严重,因为迭代操作时间不会受到容量的
* 影响。
*
* 注意这个实现类不是线程安全的。如果多线程同时并行地访问一个链接 hash set,并且至少有一个线程修改了 set,
* 它必须从外部实现线程安全。这通常是通过对于自然封装这个 set 的对象实现线程安全来完成的。
*
* 如果没有这样的对象,set 应该被使用 Collections#synchronizedSet 方法“包装”。这最好在被创建时完成,来
* 防止意外的对于这个 set 的非线程安全的访问 <pre>
* Set s = Collections.synchronizedSet(new LinkedHashSet(...));</pre>
*
* 通过本类的 iterator 方法返回的迭代器是 fail-fast 的:如果在迭代器创建后的任意时间点 set 被结构性的改变
* 了,只要不是通过迭代器自己的 ListIterator#remove 方法,迭代器将会抛出一个同时修改异常。因此,对于表面上
* 的同步更改操作,迭代器失败的快速和干净,而不是在未来的某个不确定的时间点冒险做一些不确定的行为。
*
* 注意一个迭代器的 fail-fast 行为是不能被保证的,通常来说,不可能对出现的非线程安全的同时修改操作做任何硬性
* 的保证。基于最佳性能的基础考虑,Fail-fast 迭代器抛出一个 ConcurrentModificationException。因此,建
* 立在这中异常上写出的程序的正确性将会是不健壮的:迭代器的 fail-fast 行为应当植被用于检查 bugs。
*
* LinkedHashSet 继承自 HashSet,实现了 Set 接口,Cloneable 接口以及 Serializable 接口
*/
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
private static final long serialVersionUID = -2851667679971038690L;
/**
* 使用指定初始容量和加载因子构造一个新的,空的链接 hash set
*/
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true); //调用 HashSet 的构造方法
}
/**
* 使用指定的初始化容量和默认加载因子(0.75)构造一个新的,空的链接 hash set
*/
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true); //调用 HashSet 的构造方法
}
/**
* 使用默认的容量(16)和默认加载因子(0.75)构造一个新的,空的链接 hash set
*/
public LinkedHashSet() {
super(16, .75f, true); //调用 HashSet 的构造方法
}
/**
* 使用指定数据结构中相同的元素构造一个新的链接 hash set。这个链接 hash set 被构造成足够装下指定数据
* 结构中的元素的大小,加载因子为默认加载因子(0.75)。
*/
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true); //调用 HashSet 的构造方法
addAll(c); //调用 AbstractCollection 中的 addAll 方法
}
/**
* 构造一个包含这个 set 中所有元素的延迟绑定并且 fail-fast 的并行迭代器。
*/
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
通过 LinkedHashSet 的源码可以看到,它基本上只是调用了 HashSet 父类中为 LinkedHashSet 特别预留的构造方法 HashSet(int initialCapacity, float loadFactor, boolean dummy),而这个方法的具体实现还是在 LinkedHashMap 中。纵观 Set 到 LinkedHashSet,可以认为 Set 提供的能力是一个没有下标操作的,元素不能重复的数据结构,HashSet 中看到这些能力的最终实现在 Map 实现类,而 LinkedHashSet 中看到实现有序的方式就是通过一个特殊的 Map 实现类构造器。
以上就是对于 Set 接口最终实现类 HashSet 以及它的子类 LinkedHashSet 的介绍与分析。下一篇将对 TreeSet 进行介绍与分析。