引出问题
-
HashSet
、Set
、集合(Collection)
三者有什么联系与区别? - HashSet 怎么保证元素不重复?
- HashSet 能否添加null元素数据?
- HashSet 添加元素是有顺序的吗?
- HashSet 是线程安全的吗(即是同步的吗)?
-
fail-fast(快速失败)
是什么? - 应用场景是什么?
简介
JAVA集合是指 存在 java.unil
包中的 Collection
相关的类,包括 Collection
与 Map
相关的所有类。
本文的 HashSet 是 Set 的一种实现方式,底层主要使用 HashMap 来实现。
HashSet、Set、集合(Collection)三者有什么联系与区别?
先看下 HashSet
的类继承图。
由此可以看出:
- HashSet 是 Set 的一种实现方式,Set 又是 Collection 的一种实现。
- HashSet 继承了 AbstractSet 类,AbstractSet 是 Set 的默认实现, 接口同时继承了 AbstractCollection 类。
源码分析
先来一张 HashSet
的方法图:
这里我们主要分析 add 、remove 、contains 、clear、contains、size、isEmpty 几个方法。
- 属性
通过看源码发现有一个 私有的 map 属性和一个 Object 的静态常量:
/** 内部使用的 HashMap ,用来存放数据 */
private transient HashMap map;
/** 虚拟对象,用来作为 map 的 value 元素 */
private static final Object PRESENT = new Object();
这里的 map
属性 是做为 hashSet 的内部元素存储容器;PRESENT
用来 做为 map
元素的value 值。
- 构造函数
共有四个构造函数:
- 无参数
public HashSet() {
/*** 内部初始化 map 容器 */
map = new HashMap<>();
}
2.参数为 一个 Collection 的 集合
public HashSet(Collection extends E> c) {
/*** 内部初始化 map 容器 */
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
/** 把传进来的 集合 添加 到 HashSet 集合中 */
addAll(c);
}
- 参数为 initialCapacity(初始化大小),loadFactor (填充因子 )
public HashSet(int initialCapacity, float loadFactor) {
/*** initialCapacity 初始化大小 , loadFactor 填充因子 */
map = new HashMap<>(initialCapacity, loadFactor);
}
- 参数为 initialCapacity(初始化大小)
public HashSet(int initialCapacity) {
/*** initialCapacity 初始化大小 */
map = new HashMap<>(initialCapacity);
}
- 还有个 非 public 的构造函。
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
这个 方法有点特殊,不是 public 的,意味它只能被同一个包或者子类调用。在这里主要是给LinkHashSet
使用的。
构造 方法都是调用 HashMap 对应的构造方法 , 来初始化 map
属性。
- add (添加元素)
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
内部直接调用 HaskMap 的 put 方法,把元素的本身作为 key , 用 PRESENT
作为 value值,换句话说 就是在 map
中的所有value 都是一样了。
- remove (删除元素)
/*** 返回值为 boolean 值 */
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
举例 1:错误的删除,会抛出 ConcurrentModificationException 异常 , 具体 分析 请看下文 fail-fast(快速失败)
板块
static final String DELETE_KEY = "aa";
public static void main(String[] args) {
final Set all = new HashSet<>();
all.add("bb");
all.add("cc");\
all.add("aa");
all.add("111");
for (String s : all) {
if(DELETE_KEY.equals(s)){
// 这里会报错
all.remove(DELETE_KEY);
}
}
}
举例 2:正确的删除
final Set all = new HashSet<>();
/** 添加元素 */
all.add("aa");
all.add("bb");
all.add("cc");
all.add("111");
all.add("222");
all.add("333");
all.add("test");
/** 遍历删除元素,利用 iterator 删除 */
Iterator iter= all.iterator();
while (iter.hasNext()){
if("aa".equals(iter.next())){
iter.remove();
}
}
内部直接调用 HaskMap 的 remove 方法,将返回值与 PRESENT
做相等运算。这里返回的是 boolean 值。注意 HaskMap 的 remove 返回的是删除元素的value值
.
- contains (查询元素)
public boolean contains(Object o) {
return map.containsKey(o);
}
内部直接调用 HaskMap 的 containsKey方法,判断是否存在 与之相等 key 值.
这里要特别提示一下 , Set 集合中 是没有 get() 方法的哦!因为感觉 get 好像没有多大的意义,不能 向 List 集合 那样 用 下标(index)来访问元素。
- clear(删除所有元素)
public void clear() {
map.clear();
}
内部直接调用 HaskMap 的 clear方法。
- size (获取元素个数)
public int size() {
return map.size();
}
内部直接调用 HaskMap 的 size方法。
- isEmpty (判断是否包含任何元素)
public boolean isEmpty() {
return map.isEmpty();
}
内部直接调用 HaskMap 的 isEmpty方法。
- spliterator (遍历元素)
public Spliterator spliterator() {
return new HashMap.KeySpliterator(map, 0, -1, 0, 0);
}
可分隔的迭代器,主要用于多线程下迭代处理元素使用。
fail-fast(快速失败)
- 概念:
是JAVA集合中的一种错误机制。当我们使用 迭代器 迭代元素时,发现集合中有修改元素的情况,则快速做出效应,立即抛出 ConcurrentModificationException 异常。这种修改操作可能是其他线程在修改,也有可能是当前的线程自已修改导致的(例如:在 迭代过程中 调用 remove 方法等)。
直接附上代码:
static final String DELETE_KEY = "aa";
public static void main(String[] args) {
final Set all = new HashSet<>();
all.add("aa");
all.add("bb");
all.add("cc");
all.add("111");
all.add("222");
all.add("333");
for (String s : all) {
if(DELETE_KEY.equals(s)){
// 这里会报错
all.remove(DELETE_KEY);
}
}
}
结果:
注意:
并不是所有的 JAVA 集合容器都有 fail-fast 这个机制,如: ConcurrentHashMap 等 就是没得。实现原理
查看 HashMap 源码 (因为内部使用 HashMap 来存储数据)可以得出,有一个 属性
/**
* 被修改的次数
*
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount;
每次对 集合容器中的元素先进性修改时,这个 值 都会加 1,在遍历前记录这个属性到 expectedModCount 属性中,遍历中检查检查 expectedModCount 与 modCount 是否一致,如果不一致就说明 有修改线程修改的元素,则抛出 ConcurrentModificationException 异常。
回答问题
通过 查看源码知道 , 在使用 HashSet 集合作为存储容器的时候,操作 HashSet 容器 在内部 都是 间接的操作 HashMap 。
- HashSet 怎么保证元素不重复?
回答:HashSet 内部使用 HashMap 来存储数据,key 为元素本身对象,value 都是一样值;HashMap 的 key 不能重复。
- HashSet 能否添加null元素数据?
回答:HashSet 只能允许有一个元素为 null 元素,因为 HashMap 的 key 允许为空。
- HashSet 添加元素是有顺序的吗?
回答:HashSet 是无顺序的,因为 HashMap 的 key 是无顺序。
- HashSet 是线程安全的吗(即是同步的吗)?
回答:HashSet 不是线程安全的,因为 HashMap 不是线程安全的。
- 应用场景是什么?
回答:HashSet 内部使用 HashMap 来存储数据,由于HashMap 的特性,可以用于以下环境中。
不要求线程安全性、元素之间不要求顺序、元素之间不重复、并且不能 通过 get 获取到的场景。