您的位置:首页 > 其它

HashSet及LinkedHashSet之源码分析

2016-05-17 10:21 417 查看


注意:下面是hashset的全部源码:

为了更了解HashSet的原理,下面对HashSet源码代码作出分析。package java.util;

public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;

// HashSet是通过map(HashMap对象)保存内容的
private transient HashMap<E,Object> map;

// PRESENT是向map中插入key-value对应的value
// 因为HashSet中只需要用到key,而HashMap是key-value键值对;
// 所以,向map中添加键值对时,键值对的值固定是PRESENT
private static final Object PRESENT = new Object();

// 默认构造函数
public HashSet() {
// 调用HashMap的默认构造函数,创建map
map = new HashMap<E,Object>();
}

// 带集合的构造函数
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<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
// 将集合(c)中的全部元素添加到HashSet中
addAll(c);
}

// 指定HashSet初始容量和加载因子的构造函数
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<E,Object>(initialCapacity, loadFactor);
}

// 指定HashSet初始容量的构造函数
public HashSet(int initialCapacity) {
map = new HashMap<E,Object>(initialCapacity);
}

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}

// 返回HashSet的迭代器
public Iterator<E> iterator() {
// 实际上返回的是HashMap的“key集合的迭代器”
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);
}

// 将元素(e)添加到HashSet中
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

// 删除HashSet中的元素(o)
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}

public void clear() {
map.clear();
}

// 克隆一个HashSet,并返回Object对象
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();
}
}

// java.io.Serializable的写入函数
// 将HashSet的“总的容量,加载因子,实际容量,所有的元素”都写入到输出流中
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 (Iterator i=map.keySet().iterator(); i.hasNext(); )
s.writeObject(i.next());
}

// java.io.Serializable的读取函数
// 将HashSet的“总的容量,加载因子,实际容量,所有的元素”依次读出
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// Read in any hidden serialization magic
s.defaultReadObject();

// Read in HashMap capacity and load factor and create backing HashMap
int capacity = s.readInt();
float loadFactor = s.readFloat();
map = (((HashSet)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor));

// Read in size
int size = s.readInt();

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}
}


Java容器类的用途是“保存对象”,分为两类:Map——存储“键值对”组成的对象;Collection——存储独立元素。Collection又可以分为List和Set两大块。List保持元素的顺序,而Set不能有重复的元素。

本文分析Set中最常用的HashSet类,并简单介绍和对比LinkedHashSet。

首先对Set接口进行简要的说明。

存入Set的每个元素必须是惟一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set不保证维护元素的次序。Set与Collection有完全一样的接口。

在没有其他限制的情况下需要Set时应尽量使用HashSet,因为它对速度进行了优化。

下面是HashSet的定义:

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


HashSet继承了AbstractSet,实现了Set接口。其实AbstractSet已经实现Set接口了。AbstractSet继承自AbstractCollection,而AbstractCollection实现了Collection接口的部分方法,而Set接口和Collection接口完全一致,所以AbstractSet只是实现了AbstractCollection没有实现的Set接口的方法和重写了部分AbstractCollection已经实现的方法。

下面是HashSet定义的属性:

1 private transient HashMap<E,Object> map;
2 private static final Object PRESENT = new Object();


为什么会有一个HashMap<E,Object>定义的属性?

想一下HashMap有什么特点:基于哈希表,存储键值对,Key不能相同等等。Key不能相同!这个特点是不是和Set的元素不能相同和类似?如果将Set的元素当成Map的Key,是否就保证了元素的不重复?!答案是肯定的。但是Map存储键值对,Key有了,那么Value呢?这正是第二个属性PERSENT的意义。看到PERSENT属性时一个Object对象,且是static和final的,它的用途就是当做Value存进map中。

总结一下,HashSet的实现方式大致如下,通过一个HashMap存储元素,元素是存放在HashMap的Key中,而Value统一使用一个Object对象。

这样看来HashSet应该很简单,应该只是使用了HashMap的部分内容来实现。

下面看具体的其它代码来验证上面的猜想。

构造方法:



1 // 构造方法一:调用默认的HashMap构造方法初始化map
2 public HashSet() {
3     map = new HashMap<E,Object>();
4 }
5 // 构造方法二:根据给定的Collection参数调用HashMap(int initialCapacity)的构造方法创建一个HashMap(这个构造方法的HashMap的源码分析里已经描述过了)
6 // 调用addAll方法将c中的元素添加到HashSet对象中
7 public HashSet(Collection<? extends E> c) {
8     map = new HashMap<E,Object>(Math.max((int) (c.size()/.75f) + 1, 16));
9     addAll(c);
10 }
11 // 构造方法三:构造一个指定初始化容量和负载因子的HashMap
12 public HashSet(int initialCapacity, float loadFactor) {
13     map = new HashMap<E,Object>(initialCapacity, loadFactor);
14 }
15 // 构造方法四:构造一个指定初始化容量的HashMap
16 public HashSet(int initialCapacity) {
17     map = new HashMap<E,Object>(initialCapacity);
18 }
19 // 构造方法五:构造一个指定初始化容量和负载因子的LinkedHashMap
20 // dummy参数被忽略,只是用于区分其他的,包含一个int、float参数的构造方法
21 HashSet(int initialCapacity, float loadFactor, boolean dummy) {
22     map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
23 }




上面的构造方法都很简单,只有构造方法二中调用了addAll(Collection<? extends E> c)方法。该方法在AbstractCollection中定义,HashSet通过继承拥有该方法。



1 public boolean addAll(Collection<? extends E> c) {
2     boolean modified = false;
3     Iterator<? extends E> e = c.iterator();
4     while (e.hasNext()) {
5         if (add(e.next()))
6         modified = true;
7     }
8     return modified;
9 }




这个方法通过遍历c中的元素,然后调用add(E e)方法添加元素。

1 public boolean add(E e) {
2     return map.put(e, PRESENT)==null;
3 }


看add(E e)方法只是调用了HashMap(构造方法中提供了创建LinkedHashMap的方式,但是LinkedHashMap是继承HashMap的,put方法也是调用HashMap的put方法)的put方法将e当做Key,PERSENT当做Value加入到map中并根据返回值判断是否添加成功。

因为HashMap的put方法在Key已经存在的情况下返回的是对应的Value值,若Key不存在则返回的是null,所以根据返回的是null可以确定新元素被添加到HashSet中了,如果返回的是其他值则说明Key已经存在,即元素已经在HashSet中已经存在,add(E e)返回的结果为false。虽然add(E e)返回false说明了HashSet添加元素失败,但实际上其中的map中的内容已经被替换,原先的值被PERSENT代替。

如果原先的值就是null呢?其实不用考虑这个问题,因为通过HashSet添加的元素,Value的内容都是PERSENT,不会出现null的情况。

iterator()

1 public Iterator<E> iterator() {
2     return map.keySet().iterator();
3 }


很清楚了,返回的是HashMap中KeySet的迭代器。

size()

1 public int size() {
2     return map.size();
3 }


size()方法同样返回的是map的大小,所以HashSet根本就没定义size属性。

1 public boolean isEmpty() {
2     return map.isEmpty();
3 }


既然size()用的是map的大小,那么isEmpty()自然也是判断map。



1 public boolean contains(Object o) {
2     return map.containsKey(o);
3 }
4 public void clear() {
5     map.clear();
6 }
7 public Object clone() {
8     try {
9         HashSet<E> newSet = (HashSet<E>) super.clone();
10         newSet.map = (HashMap<E, Object>) map.clone();
11         return newSet;
12     } catch (CloneNotSupportedException e) {
13         throw new InternalError();
14     }
15 }




这几个方法就不解释了。

1 public boolean remove(Object o) {
2     return map.remove(o)==PRESENT;
3 }


remove(Object o)为什么还要判断结果呢?因为通过HashSet存入的元素,所对应的Value值都是PERSENT,如果传入的o不存在,map的remove方法返回为null,则对应的结果是HashSet的remove操作应该放回false,所以这里根据返回的结果判断是否移除成功。

只能感叹HashMap太强大了,HashSet是完全使用HashMap来实现的。

---------------------------------------------------------------------------------------------------------------------------------

LinkedHashSet源码分析

LinkedHashSet具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按照元素的插入次序显示。

看LinkedHashSet的内容。



1 public class LinkedHashSet<E>
2     extends HashSet<E>
3     implements Set<E>, Cloneable, java.io.Serializable {
4
5     public LinkedHashSet(int initialCapacity, float loadFactor) {
6             super(initialCapacity, loadFactor, true);
7     }
8
9     public LinkedHashSet(int initialCapacity) {
10         super(initialCapacity, .75f, true);
11     }
12
13     public LinkedHashSet() {
14         super(16, .75f, true);
15     }
16
17     public LinkedHashSet(Collection<? extends E> c) {
18         super(Math.max(2*c.size(), 11), .75f, true);
19         addAll(c);
20     }
21 }




LinkedHashSet继承自HashSet,HashSet基于HashMap实现,看LinkedHashSet类只是定义了四个构造方法,也没看到和链表相关的内容,为什么说LinkedHashSet内部使用链表维护元素的插入顺序(插入的顺序)呢?

注意这里的构造方法,都调用了父类HashSet的第五个构造方法:HashSet(int initialCapacity, float loadFactor, boolean dummy)。如果还记得上面的内容应该明白为什么是基于链表,下面再给出这个构造方法的内容。

1 HashSet(int initialCapacity, float loadFactor, boolean dummy) {
2     map = new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
3 }


区别于其他的HashSet的构造方法,这个方法创建的是一个LinkedHashMap。LinkedHashMap继承自HashMap,同时自身有一个链表结构用于维护元素顺序,默认情况使用的是插入元素(见《LinkedHashMap源码分析》),所以LinkedHashSet既有HashSet的访问速度(因为访问的时候都是通过HashSet的方法访问的),同时可以维护顺序。

下面是一个HashSet和LinkedHashSet维护元素顺序的例子。



1 Set<String> linkedSet = new LinkedHashSet<String>();
2 linkedSet.add("First");
3 linkedSet.add("Second");
4 linkedSet.add("Thrid");
5 linkedSet.add("Fourth");
6 System.out.println("LinkedHashSet:"+linkedSet);
7 Set<String> hashSet = new HashSet<String>();
8 hashSet.add("First");
9 hashSet.add("Second");
10 hashSet.add("Thrid");
11 hashSet.add("Fourth");
12 System.out.println("HashSet:"+hashSet);
13 // LinkedHashSet:[First, Second, Thrid, Fourth]
14 // HashSet:[Fourth, Second, Thrid, First]
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: