Java源码解析 —— HashSet

前言

今天来介绍下HashSet。前面,我们已经系统的对List和Map进行了学习。接下来,我们开始可以学习Set。相信经过Map的了解之后,学习Set会容易很多。毕竟,Set的实现类都是基于Map来实现的(HashSet是通过HashMap实现的)。

构造图如下:
蓝色线条:继承
绿色线条:接口实现

HashSet

HashSet简介

对于HashSet而言,它是基于HashMap来实现的,底层采用HashMap来保存元素。

定义

1
2
3
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable

HashSet是一个没有重复元素的集合
它是由HashMap实现的,不保证元素的顺序,而且HashSet允许使用null元素
HashSet是非同步的。如果多个线程同时访问一个hashSet,而其中至少一个线程修改了该set,那么它必须保持外部同步。这通常是通过对自然封装该set的对象执行同步操作来完成的。如果不存在这样的对象,则应该使用 Collections.synchronizedSet方法来“包装”set。最好在创建时完成这一操作,以防止对该set进行意外的不同步访问:

1
Set s = Collections.synchronizedSet(new HashSet(...));

HashSet通过iterator()返回的迭代器是fail-fast的

HashSet属性

1
2
3
4
5
// 底层使用HashMap来保存HashSet的元素
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
// 由于Set只使用到了HashMap的key,所以此处定义一个静态的常量Object类,来充当HashMap的value
private static final Object PRESENT = new Object();

看到这里就明白了,和我们前面说的一样,HashSet是用HashMap来保存数据,而主要使用到的就是HashMap的key。

看到private static final Object PRESENT = new Object();不知道你有没有一点疑问呢。这里使用一个静态的常量Object类来充当HashMap的value,既然这里map的value是没有意义的,为什么不直接使用null值来充当value呢?比如写成这样子private final Object PRESENT = null;我们都知道的是,Java首先将变量PRESENT分配在栈空间,而将new出来的Object分配到堆空间,这里的new Object()是占用堆内存的(一个空的Object对象占用8byte),而null值我们知道,是不会在堆空间分配内存的。那么想一想这里为什么不使用null值。想到什么吗,看一个异常类java.lang.NullPointerException,Java号称没有指针,但是处处碰到NullPointerException。所以为了从根源上避免NullPointerException的出现,采用浪费8个byte,在下面的代码中就不会出现if (xxx == null) { … } else {….}。

HashSet构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 使用HashMap的默认容量大小16和默认加载因子0.75初始化map,构造一个HashSet
*/
public HashSet() {
map = new HashMap<E,Object>();
}

/**
* 构造一个指定Collection参数的HashSet,这里不仅仅是Set,只要实现Collection接口的容器都可以
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<E,Object>(Math. max((int) (c.size()/.75f) + 1, 16));
// 使用Collection实现的Iterator迭代器,将集合c的元素一个个加入HashSet中
addAll(c);
}

/**
* 使用指定的初始容量大小和加载因子初始化map,构造一个HashSet
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<E,Object>(initialCapacity, loadFactor);
}

/**
* 使用指定的初始容量大小和默认的加载因子0.75初始化map,构造一个HashSet
*/
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity);
}

/**
* 不对外公开的一个构造方法(默认default修饰),底层构造的是LinkedHashMap,dummy只是一个标示参数,无具体意义
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}

源码解析

增加和删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 利用HashMap的put方法实现add方法
*/
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}

/**
* 利用HashMap的remove方法实现remove方法
*/
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}

/**
* 添加一个集合到HashSet中,该方法在AbstractCollection中
*/
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
// 取得集合c迭代器Iterator
Iterator<? extends E> e = c.iterator();
// 遍历迭代器
while (e.hasNext()) {
// 将集合c的每个元素加入到HashSet中
if (add(e.next()))
modified = true;
}
return modified;
}

/**
* 删除指定集合c中的所有元素,该方法在AbstractSet中
*/
public boolean removeAll(Collection<?> c) {
boolean modified = false;

// 判断当前HashSet元素个数和指定集合c的元素个数,目的是减少遍历次数
if (size() > c.size()) {
// 如果当前HashSet元素多,则遍历集合c,将集合c中的元素一个个删除
for (Iterator<?> i = c.iterator(); i.hasNext(); )
modified |= remove(i.next());
} else {
// 如果集合c元素多,则遍历当前HashSet,将集合c中包含的元素一个个删除
for (Iterator<?> i = iterator(); i.hasNext(); ) {
if (c.contains(i.next())) {
i.remove();
modified = true;
}
}
}
return modified;
}

是否包含

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 利用HashMap的containsKey方法实现contains方法
*/
public boolean contains(Object o) {
return map.containsKey(o);
}

/**
* 检查是否包含指定集合中所有元素,该方法在AbstractCollection中
*/
public boolean containsAll(Collection<?> c) {
// 取得集合c的迭代器Iterator
Iterator<?> e = c.iterator();
// 遍历迭代器,只要集合c中有一个元素不属于当前HashSet,则返回false
while (e.hasNext())
if (!contains(e.next()))
return false;
return true;
}

由于HashMap基于hash表实现,hash表实现的容器最重要的一点就是可以快速存取,那么HashSet对于contains方法,利用HashMap的containsKey方法,效率是非常之快的。

容量检查

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Returns the number of elements in this set (its cardinality).
*/
public int size() {
return map.size();
}

/**
* Returns <tt>true</tt> if this set contains no elements.
*/
public boolean isEmpty() {
return map.isEmpty();
}

HashSet遍历方式

通过Iterator遍历HashSet

  1. 根据iterator()获取HashSet的迭代器。
  2. 遍历迭代器获取各个元素。
1
2
3
4
for(Iterator iterator = set.iterator();
iterator.hasNext(); ) {
iterator.next();
}

通过for-each遍历HashSet

  1. 根据toArray()获取HashSet的元素集合对应的数组。
  2. 遍历数组,获取各个元素。
1
2
3
String[] arr = (String[])set.toArray(new String[0]);
for (String str : arr)
System.out.printf("for each : %s\n", str);

总结

HashSet和HashMap的区别

HashMap HashSet
HashMap实现了Map接口 HashSet实现了Set接口
HashMap存储键值对KV HashSet仅仅存储对象
使用put()方法将元素放进map中 使用add()方法将元素放到set中
HashMap使用key对象来计算hashcode值 HashSet中使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以用equals()方法来判断对象的相等性,如果两个对象不同返回false
HashMap比较快,因为用唯一的key来获取对象 较HashMap来说比较慢