学习HashSet的时候需要先了解HashMap,如果不是很了解HashMap的请看HashMap
Java容器类的用途是“保存对象”,分为两类:Map——存储“键值对”组成的对象;Collection——存储独立元素。Collection又可以分为List和Set两大块。List保持元素的顺序,可以有相同的元素,而Set不能有重复的元素。
首先对Set接口进行简要的说明。
存入Set的每个元素必须是惟一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set不保证维护元素的次序。Set与Collection有完全一样的接口。
在没有其他限制的情况下需要Set时应尽量使用HashSet,因为它对速度进行了优化。
下面是HashSet的定义:
public class HashSet<E>extends AbstractSet<E>implements Set<E>, Cloneable, java.io.Serializable
HashSet继承了AbstractSet,实现了Set接口。其实AbstractSet已经实现Set接口了。AbstractSet继承自AbstractCollection,而AbstractCollection实现了Collection接口的部分方法,而Set接口和Collection接口完全一致,所以AbstractSet只是实现了AbstractCollection没有实现的Set接口的方法和重写了部分AbstractCollection已经实现的方法。
下面是HashSet定义的属性:
//使用HashMap,利用key-value来存储Set中key
private transient HashMap<E,Object> map;
//定义一个默认的value为object对象
private static final Object PRESENT = new Object();
为什么会有一个HashMap
//构造方法一:定义一个默认的HashMap
public HashSet() {
map = new HashMap<>();
}
// 构造方法二:利用给定的一个集合来初始化(HashMap中已经知道)
//Math.max((int) (c.size()/.75f) + 1, 16)为:创建HashMap中哈希表的大小,默认最小值为16,装载因子为默认0.75
// 调用addAll方法将c中的元素添加到HashSet对象中
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
//构造方法三:制定哈希表的容量和装载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
//构造方法四:指定哈希表的容量(默认装载因子0.75)
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 构造方法五:构造一个指定初始化容量和负载因子的LinkedHashMap
//dummy参数被忽略,只是用于区分其他的,包含一个int、float参数的构造方法(方法重载)
public HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
上面的构造方法都很简单,只有构造方法二中调用了addAll(Collection
//利用AbstractCollection中定义的方法
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c) //
if (add(e))
modified = true;
return modified;
}
//HashSet中重写的方法
public boolean add(E e) {
//直接调用HashMap中的方法
return map.put(e, PRESENT)==null;
}
看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的情况。
//迭代器(key)
public Iterator<E> iterator() {
return map.keySet().iterator();
}
//添加对象的个数
public int size() {
return map.size();
}
//判断是否为空
public boolean isEmpty() {
return map.isEmpty();
}
//是否包含对象key
public boolean contains(Object o) {
return map.containsKey(o);
}
//添加key
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//删除key
//remove(Object o)为什么还要判断结果呢?因为通过HashSet存入的元素,所对应的Value值都是PERSENT,如果传入的o不存在,map的remove方法返回为null,则对应的结果是HashSet的remove操作应该放回false,所以这里根据返回的结果判断是否移除成功。
public boolean remove(Object o) {
//删除对象(key)
return map.remove(o)==PRESENT;
}
//清空
public void clear() {
map.clear();
}
LinkedHashSet源码分析
LinkedHashSet具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按照元素的插入次序显示。(LinkedHashMap中还可以按照LRU先后顺序显示),
看LinkedHashSet的内容。
//调用HashSet中的第五个构造函数
public LinkedHashSet(int initialCapacity, float loadFactor){
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
super(16, .75f, true);
}
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
}
LinkedHashSet继承自HashSet,HashSet基于HashMap实现,看LinkedHashSet类只是定义了四个构造方法,也没看到和链表相关的内容,为什么说LinkedHashSet内部使用链表维护元素的插入顺序(插入的顺序)呢?
//先看下HashSet的中的第五个构造函数
public HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
如果还有对LinkedHashMap不明白的,请看LinkedHashMap