问题

(1)集合(Collection)和集合(Set)有什么区别?

(2)HashSet怎么保证添加元素不重复?

(3)HashSet是否允许null元素?

(4)HashSet是有序的吗?

(5)HashSet是同步的吗?

(6)什么是fail-fast?

简介

集合,这个概念有点模糊。

广义上来讲,java中的集合是指java.util包下面的容器类,包括和Collection及Map相关的所有类。

中义上来讲,我们一般说集合特指java集合中的Collection相关的类,不包含Map相关的类。

狭义上来讲,数学上的集合是指不包含重复元素的容器,即集合中不存在两个相同的元素,在java里面对应Set。

具体怎么来理解还是要看上下文环境。

比如,面试别人让你说下java中的集合,这时候肯定是广义上的。

再比如,下面我们讲的把另一个集合中的元素全部添加到Set中,这时候就是中义上的。

HashSet是Set的一种实现方式,底层主要使用HashMap来确保元素不重复。

源码分析

属性

    // 内部使用HashMap
private transient HashMap<E,Object> map; // 虚拟对象,用来作为value放到map中
private static final Object PRESENT = new Object();

构造方法

public HashSet() {
map = new HashMap<>();
} 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);
} public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
} // 非public,主要是给LinkedHashSet使用的
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

构造方法都是调用HashMap对应的构造方法。

最后一个构造方法有点特殊,它不是public的,意味着它只能被同一个包或者子类调用,这是LinkedHashSet专属的方法。

添加元素

直接调用HashMap的put()方法,把元素本身作为key,把PRESENT作为value,也就是这个map中所有的value都是一样的。

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

删除元素

直接调用HashMap的remove()方法,注意map的remove返回是删除元素的value,而Set的remov返回的是boolean类型。

这里要检查一下,如果是null的话说明没有该元素,如果不是null肯定等于PRESENT。

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

查询元素

Set没有get()方法哦,因为get似乎没有意义,不像List那样可以按index获取元素。

这里只要一个检查元素是否存在的方法contains(),直接调用map的containsKey()方法。

public boolean contains(Object o) {
return map.containsKey(o);
}

遍历元素

直接调用map的keySet的迭代器。

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

全部源码

package java.util;

import java.io.InvalidObjectException;
import sun.misc.SharedSecrets; public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L; // 内部元素存储在HashMap中
private transient HashMap<E,Object> map; // 虚拟元素,用来存到map元素的value中的,没有实际意义
private static final Object PRESENT = new Object(); // 空构造方法
public HashSet() {
map = new HashMap<>();
} // 把另一个集合的元素全都添加到当前Set中
// 注意,这里初始化map的时候是计算了它的初始容量的
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);
} // 只指定初始容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
} // LinkedHashSet专用的方法
// dummy是没有实际意义的, 只是为了跟上上面那个操持方法签名不同而已
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
} // 迭代器
public Iterator<E> iterator() {
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);
} // 添加元素
public boolean add(E e) {
return map.put(e, PRESENT)==null;
} // 删除元素
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
} // 清空所有元素
public void clear() {
map.clear();
} // 克隆方法
@SuppressWarnings("unchecked")
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(e);
}
} // 序列化写出方法
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
// 写出非static非transient属性
s.defaultWriteObject(); // 写出map的容量和装载因子
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor()); // 写出元素个数
s.writeInt(map.size()); // 遍历写出所有元素
for (E e : map.keySet())
s.writeObject(e);
} // 序列化读入方法
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
// 读入非static非transient属性
s.defaultReadObject(); // 读入容量, 并检查不能小于0
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " +
capacity);
} // 读入装载因子, 并检查不能小于等于0或者是NaN(Not a Number)
// java.lang.Float.NaN = 0.0f / 0.0f;
float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
} // 读入元素个数并检查不能小于0
int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " +
size);
}
// 根据元素个数重新设置容量
// 这是为了保证map有足够的容量容纳所有元素, 防止无意义的扩容
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
HashMap.MAXIMUM_CAPACITY); // 再次检查某些东西, 不重要的代码忽视掉
SharedSecrets.getJavaOISAccess()
.checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity)); // 创建map, 检查是不是LinkedHashSet类型
map = (((HashSet<?>)this) instanceof LinkedHashSet ?
new LinkedHashMap<E,Object>(capacity, loadFactor) :
new HashMap<E,Object>(capacity, loadFactor)); // 读入所有元素, 并放入map中
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
} // 可分割的迭代器, 主要用于多线程并行迭代处理时使用
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
}
}

总结

(1)HashSet内部使用HashMap的key存储元素,以此来保证元素不重复;

(2)HashSet是无序的,因为HashMap的key是无序的;

(3)HashSet中允许有一个null元素,因为HashMap允许key为null;

(4)HashSet是非线程安全的;

(5)HashSet是没有get()方法的;

彩蛋

(1)阿里手册上有说,使用java中的集合时要自己指定集合的大小,通过这篇源码的分析,你知道初始化HashMap的时候初始容量怎么传吗?

我们发现有下面这个构造方法,很清楚明白地告诉了我们怎么指定容量。

假如,我们预估HashMap要存储n个元素,那么,它的容量就应该指定为((n/0.75f) + 1),如果这个值小于16,那就直接使用16得了。

初始化时指定容量是为了减少扩容的次数,提高效率。

public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}

(2)什么是fail-fast?

fail-fast机制是java集合中的一种错误机制。

当使用迭代器迭代时,如果发现集合有修改,则快速失败做出响应,抛出ConcurrentModificationException异常。

这种修改有可能是其它线程的修改,也有可能是当前线程自己的修改导致的,比如迭代的过程中直接调用remove()删除元素等。

另外,并不是java中所有的集合都有fail-fast的机制。比如,像最终一致性的ConcurrentHashMap、CopyOnWriterArrayList等都是没有fast-fail的。

那么,fail-fast是怎么实现的呢?

细心的同学可能会发现,像ArrayList、HashMap中都有一个属性叫modCount,每次对集合的修改这个值都会加1,在遍历前记录这个值到expectedModCount中,遍历中检查两者是否一致,如果出现不一致就说明有修改,则抛出ConcurrentModificationException异常。


欢迎关注我的公众号“彤哥读源码”,查看更多源码系列文章, 与彤哥一起畅游源码的海洋。

死磕 java集合之HashSet源码分析的更多相关文章

  1. 死磕 java集合之PriorityQueue源码分析

    问题 (1)什么是优先级队列? (2)怎么实现一个优先级队列? (3)PriorityQueue是线程安全的吗? (4)PriorityQueue就有序的吗? 简介 优先级队列,是0个或多个元素的集合 ...

  2. 死磕 java集合之CopyOnWriteArraySet源码分析——内含巧妙设计

    问题 (1)CopyOnWriteArraySet是用Map实现的吗? (2)CopyOnWriteArraySet是有序的吗? (3)CopyOnWriteArraySet是并发安全的吗? (4)C ...

  3. 死磕 java集合之LinkedHashSet源码分析

    问题 (1)LinkedHashSet的底层使用什么存储元素? (2)LinkedHashSet与HashSet有什么不同? (3)LinkedHashSet是有序的吗? (4)LinkedHashS ...

  4. 死磕 java集合之DelayQueue源码分析

    问题 (1)DelayQueue是阻塞队列吗? (2)DelayQueue的实现方式? (3)DelayQueue主要用于什么场景? 简介 DelayQueue是java并发包下的延时阻塞队列,常用于 ...

  5. 死磕 java集合之PriorityBlockingQueue源码分析

    问题 (1)PriorityBlockingQueue的实现方式? (2)PriorityBlockingQueue是否需要扩容? (3)PriorityBlockingQueue是怎么控制并发安全的 ...

  6. 死磕 java集合之ConcurrentHashMap源码分析(三)

    本章接着上两章,链接直达: 死磕 java集合之ConcurrentHashMap源码分析(一) 死磕 java集合之ConcurrentHashMap源码分析(二) 删除元素 删除元素跟添加元素一样 ...

  7. 死磕 java集合之ArrayDeque源码分析

    问题 (1)什么是双端队列? (2)ArrayDeque是怎么实现双端队列的? (3)ArrayDeque是线程安全的吗? (4)ArrayDeque是有界的吗? 简介 双端队列是一种特殊的队列,它的 ...

  8. 【死磕 Java 集合】— ConcurrentSkipListMap源码分析

    转自:http://cmsblogs.com/?p=4773 [隐藏目录] 前情提要 简介 存储结构 源码分析 主要内部类 构造方法 添加元素 添加元素举例 删除元素 删除元素举例 查找元素 查找元素 ...

  9. 死磕 java集合之LinkedList源码分析

    问题 (1)LinkedList只是一个List吗? (2)LinkedList还有其它什么特性吗? (3)LinkedList为啥经常拿出来跟ArrayList比较? (4)我为什么把LinkedL ...

随机推荐

  1. windows7 dos修改mysql root密码

    第一步:打开mysql 安装路径  选择bin文件  同时按下Shift+鼠标右键  点击"在此处打开命令" 第二步:输入mysql -u root -p 按回车键会提示输入密码 ...

  2. (一)JUnit简介

    单元测试是测试应用程序的功能是否能够按需要正常进行,是一个对单一实体(类或方法)的测试. JUnit是一个Java编程语言的单元测试框架. 单元测试框架是一部分代码,可以确保另一端代码(方法)按预期工 ...

  3. Python-分支循环- if elif for while

    分支与循环 条件是分支与循环中最为核心的点,解决的问题场景是不同的问题有不同的处理逻辑.当满足单个或者多个条件或者不满足条件进入分支和循环,这里也就说明这个对相同问题处理执行逻辑依据具体参数动态变化, ...

  4. java判断一个字符串是否是数字的三种方法

    参考https://blog.csdn.net/ld_flex/article/details/7699161 1 用JAVA自带的函数 public static boolean isNumeric ...

  5. compare.go

    package clientv3 import (     pb "github.com/coreos/etcd/etcdserver/etcdserverpb" ) type C ...

  6. log.go

    ) //打开日志文件 以及文件操作权限     if err != nil {         return err     }     // 解析日志记录的等级信息     level, err : ...

  7. BZOJ_5180_[Baltic2016]Cities_ 斯坦纳树

    BZOJ_5180_[Baltic2016]Cities_ 斯坦纳树 题意: 给定n个点,m条双向边的图.其中有k个点是重要的.每条边都有一定的长度. 现在要你选定一些边来构成一个图,要使得k个重要的 ...

  8. i春秋----Misc

    好久没有写 博客今天更新多了一些 解题思路:题目做多了,自然能够想到是凯撒解密: 查看得到答案; flag{4c850c5b3b2756e67a91bad8e046dda} 2: 解题思路:是我想太多 ...

  9. 从壹开始 [Admin] 之四 || NetCore + SignalR 实现日志消息推送

    缘起 哈喽大家周一好呀,感觉好久没有写文章了,上周出差了一次,感觉还是比坐办公室好的多,平时在读一本书<时生>,感兴趣的可以看看

  10. 一起来学Spring Cloud | 第二章:服务注册和发现组件 (Eureka)

    本篇文章,很浅显的一步步讲解如何搭建一个能运行的springcloud项目(带所有操作截图).相信!看完本篇之后,你会觉得springcloud搭建如此简单~~~~ 一. Eureka简介: 1.1  ...