本系列文章:
Java集合(一)集合框架概述
Java集合(二)List、ArrayList、LinkedList
Java集合(三)CopyOnWriteArrayList、Vector、Stack
Java集合(四)Map、HashMap、HashTable
Java集合(五)LinkedHashMap、TreeMap、ConcurrentHashMap
Java集合(六)Set、HashSet、LinkedHashSet、TreeSet使用及源码分析
Java集合(七)BlockingQueue、ArrayBlockingQueue、LinkedBlockingQueue
Java集合(八)PriorityBlockingQueue、DelayQueue
List是有序集合的根接口,Set是无序集合的根接口,无序也就意味着元素不重复
。更严格地说,Set集合不包含一对元素e1和e2 ,使得e1.equals(e2) ,并且最多一个空元素。
使用Set存储的特点与List相反:元素无序、不可重复。常用的实现方式:HashSet、LinkedHashSet和TreeSet。
具体实现 | 优点 | 缺点 |
---|---|---|
HashSet | 底层数据结构是哈希表,可以存储Null元素,效率高 | 线程不安全,需要重写hashCode()和equals()来保证元素唯一性 |
LinkedHashSet | 底层数据结构是链表和哈希表(链表保证了元素的顺序与存储顺序一致,哈希表保证了元素的唯一性),效率高 | 线程不安全 |
TreeSet | 底层数据结构是二叉树,元素唯一且已经排好序 | 需要重写hashCode和equals()来保证元素唯一性 |
当向HashSet结合中存入一个元素时,HashSet会调用该对象的hashCode()方法来得到该对象的hashCode值,然后根据 hashCode值来决定该对象在HashSet中存储位置。简单的说,HashSet集合判断两个元素相等的标准是两个对象通过equals方法比较相等,并且两个对象的hashCode()方法返回值也相等。
LinkedHashSet集合同样是根据元素的hashCode值来决定元素的存储位置,但是它同时使用链表维护元素的次序。这样使得元素看起来像是以插入顺序保存的,也就是说,当遍历该集合时候,LinkedHashSet将会以元素的添加顺序访问集合的元素。
在使用Set存储数据时,为保障元素唯一性,常常要重写hashCode。重写hashCode方法时,尽量遵循以下原则:
- 相同的对象返回相同的hashCode值
- 不同的对象返回不同的hashCode值,否则,就会增加冲突的概率
- 尽量的让hashCode值散列开(用异或运算可使结果的范围更广)
此处的没有顺序是指:元素插入的顺序与输出的顺序不一致
),HashSet允许使用Null元素,HashSet是非同步的。 至于具体使用哪种集合时,参考:
在List和Set两个分支中,ArrayList和HashSet是对应分支中适应性最广的,两者再比较,ArrayList则适用性更广一些。也就是说如果要确定用List,但不确定用哪种List,就可以使用ArrayList;如果确定用Set,但不确定用哪种Set,就可以使用HashSet。如果只知道用集合,就用ArrayList
。
HashSet的继承关系:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
HashSet是一个无序集合,其底层结构是HashMap,简单来说,HashSet是value是固定值(Object PRESENT = new Object()
)的HashMap。
底层实现是HashMap
(HashSet的值存放于HashMap的key上,HashMap的value是一个统一的值)。元素无序且不能重复
(从插入HashSet元素的顺序和遍历HashSet的顺序对比可以看出)。线程不安全
的。如果要保证线程安全,其中一种方法是将其改造成线程安全的类,示例: Set set = Collections.synchronizedSet(new HashSet(...));
允许存入null
。当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相同的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用 equals() 方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
public HashSet()
public HashSet(int initialCapacity)
public HashSet(int initialCapacity, float loadFactor)
public boolean add(E e)
public void clear()
public boolean contains(Object o)
public boolean isEmpty()
public Iterator<E> iterator()
public boolean remove(Object o)
public int size()
HashMap | HashSet |
---|---|
实现了Map接口 | 实现了Set接口 |
存储键值对 | 仅存储对象 |
调用put()向map中添加元素 | 调用 add()方法向Set中添加元素 |
HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false |
HashMap相对于HashSet 较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 |
由于HashSet的底层实现HashMap,所以其方法的实现基本都是对HashMap的操作。
变量:
//HashSet集合中的内容是通过HashMap数据结构来存储的
private transient HashMap<E,Object> map;
//向HashSet中添加数据,数据在上面的map结构是作为key存在的,而value统一都是
//PRESENT,这样做是因为Set只使用到了HashMap的key,所以此处定义一个静态的常
//量Object类,来充当HashMap的value
private static final Object PRESENT = new Object();
//直接 new 一个HashMap对象出来,采用无参的 HashMap 构造函数,
//HashMap对象具有默认初始容量(16)和加载因子(0.75)
public HashSet() {
map = new HashMap<>();
}
//指定初始容量和加载因子,创建HashMap实例
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
//指定初始容量,创建HashMap
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//以指定的initialCapacity和loadFactor构造一个新的空链接哈希集合。
//此构造函数为包访问权限,不对外公开,实际只是对LinkedHashSet的支持。
//实际底层会以指定的参数构造一个空LinkedHashMap实例来实现。
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
加载因子指的是能存储的元素占总的容量的比例。在 HashMap 中,能够存储元素的数量就是:总的容量*加载因子
。
每当向HashSet新增元素时,如果HashMap集合中的元素大于前面公式计算的结果,就必须要进行扩容操作,从时间和空间考虑,加载因子一般都选默认的0.75。
当把对象加入HashSet时,HashSet会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode 值的对象,这时会调用 equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,则覆盖旧元素。
//将 e 作为 key,PRESENT 作为 value 插入到 map 集合中,如果 e
//不存在,则插入成功返回 true;如果存在,则返回false
//本质上是调用HashMap的put方法来实现的
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//删除成功返回 true,删除的元素不存在会返回 false
//本质上是调用HashMap的remove方法来实现的
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
//调用 HashMap 的 containsKey(Object o) 方法,找到了返回 true,找不到返回 false
//因为HashSet的本质上是用HashMap来存储元素的,HashSet的值是HashMap中的key,所以
//此处调用了HashMap的containsKey方法来判断
public boolean contains(Object o) {
return map.containsKey(o);
}
这几个方法都是直接调用其底层实现HashMap的方法的,源码:
//清空集合
public void clear() {
map.clear();
}
//判断是否为空
public boolean isEmpty() {
return map.isEmpty();
}
//获取集合元素个数
public int size() {
return map.size();
}
//因为HashSet的本质上是用HashMap来存储元素的,HashSet的值是HashMap中的key,所以
//此处调用了HashMap的keySet方法来遍历HashSet中的元素
public Iterator<E> iterator() {
return map.keySet().iterator();
}
LinkedHashSet是有序集合,其继承关系:
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable
LinkedHashSet底层是通过LinkedHashMap来实现的,LinkedHashMap其实也就是value是固定值的LinkedHashMap。因此LinkedHashSet中的元素顺序是可以保证
的,也就是说遍历序和插入序是一致的。
LinkedHashSet所有的构造方法都是调用HashSet的同一个构造方法:
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
然后,通过调用LinkedHashMap的构造方法初始化Map:
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
可以看出:这里把accessOrder写死为false。所以,LinkedHashSet是不支持按访问顺序对元素排序的,只能按插入顺序排序
。
public LinkedHashSet()
public HashSet(int initialCapacity)
public HashSet(int initialCapacity, float loadFactor)
LinkedHashSet源码很简单,大都和其父类HashSet相同,此处只介绍其构造方法。
public LinkedHashSet(int initialCapacity, float loadFactor) {
//调用其父类HashSet的构造器,指定初始容量和增长因子,构造一个LinkedHashMap
super(initialCapacity, loadFactor, true);
}
public LinkedHashSet(int initialCapacity) {
//调用其父类HashSet的构造器,指定初始容量,增长因子为0.75,构造一个LinkedHashMap
super(initialCapacity, .75f, true);
}
public LinkedHashSet() {
//调用其父类HashSet的构造器,初始容量为16,增长因子为0.75,构造一个LinkedHashMap
super(16, .75f, true);
}
TreeSet是一个有序集合,基于TreeMap实现。TreeSet的继承关系:
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
//创建一个空的 TreeSet,使用自然排序
public TreeSet()
//指定比较器,如果比较器是 null 将使用自然排序
public TreeSet(Comparator<? super E> comparator)
//添加一个元素
public boolean add(E e)
//添加集合中的元素
public boolean addAll(Collection<? extends E> c)
public boolean remove(Object o)
public boolean contains(Object o)
//获取TreeSet元素个数
public int size()
//判断TreeSet是否为空
public boolean isEmpty()
//清空TreeSet
public void clear()
//返回此TreeSet中存在的最大元素
public E last() {
return m.lastKey();
}
//返回此TreeSet中存在的最小元素
public E first() {
return m.firstKey();
}
public E lower(E e)
public E higher(E e)
//返回在这个集合中小于或者等于给定元素的最大元素
public E floor(E e)
//返回在这个集合中大于或者等于给定元素的最小元素
public E ceiling(E e)
public E pollFirst()
public E pollLast()
//存储数据的底层数据结构
private transient NavigableMap<E,Object> m;
//由于 TreeSet 只需要使用 Key 进行存储,因此 Value 存储的是一个虚拟值
private static final Object PRESENT = new Object();
//使用 TreeMap 创建一个空的 TreeSet,使用自然排序,
//添加的元素需要实现 Comparable 接口,即是可比较的
public TreeSet() {
this(new TreeMap<E,Object>());
}
//指定比较器,如果比较器是 null 将使用自然排序
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
//构建一个treeSet,包含参数c中的元素,排序是基于元素的自然顺序
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
将集合C中的所有元素添加到TreeSet中。
public boolean addAll(Collection<? extends E> c) {
// Use linear-time version if applicable
if (m.size()==0 && c.size() > 0 &&
c instanceof SortedSet &&
m instanceof TreeMap) {
SortedSet<? extends E> set = (SortedSet<? extends E>) c;
TreeMap<E,Object> map = (TreeMap<E, Object>) m;
Comparator<?> cc = set.comparator();
Comparator<? super E> mc = map.comparator();
if (cc==mc || (cc != null && cc.equals(mc))) {
map.addAllForTreeSet(set, PRESENT);
return true;
}
}
return super.addAll(c);
}
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
public boolean contains(Object o) {
return m.containsKey(o);
}
//获取TreeSet元素个数
public int size() {
return m.size();
}
//判断TreeSet是否为空
public boolean isEmpty() {
return m.isEmpty();
}
//清空TreeSet
public void clear() {
m.clear();
}
//返回此TreeSet中存在的最大元素
public E last() {
return m.lastKey();
}
//返回此TreeSet中存在的最小元素
public E first() {
return m.firstKey();
}
public E lower(E e) {
return m.lowerKey(e);
}
public E higher(E e) {
return m.higherKey(e);
}
public E floor(E e) {
return m.floorKey(e);
}
public E ceiling(E e) {
return m.ceilingKey(e);
}
public E pollFirst() {
Map.Entry<E,?> e = m.pollFirstEntry();
return (e == null) ? null : e.getKey();
}
public E pollLast() {
Map.Entry<E,?> e = m.pollLastEntry();
return (e == null) ? null : e.getKey();
}