这 3 个 Set 集合的实现有点简单,那来做个总结吧
Set 接口是 Java Collections Framework 中的一员,它的特点是:不能包含重复的元素,允许且最多只有一个 null 元素。Java 中有三个常用的 Set 实现类:
- HashSet: 将元素存储在哈希表中,性能最佳,但不能保证元素的迭代顺序
- LinkedHashSet: 维护一个链表贯穿所有元素,按插入顺序对元素进行迭代
- TreeSet: 将元素存储在一个红黑树中,按元素大小排序的序列迭代
JDK 在实现时,这 3 个 Set 集合的核心功能其实分别委托给了: HashMap, LinkedHashMap 和 TreeMap,关于这 3 个 Map 的源码分析可查看本站发布的其他文章。
接下来对这 3 个 Set 集合的源码简单分析,并解决一些面试可能会遇到的问题。
HashSet
如果去除注释,HashSet 源码也就 200 行左右,除了序列化和克隆的方法,代码如下:
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
// 实际存储元素的对象
private transient HashMap<E,Object> map;
// 存储在 HashMap 中所有 key 的共享的 value 值
private static final Object PRESENT = new Object();
// 空构造函数
public HashSet() {
map = new HashMap<>(); // 0.75f 加载因子
}
// 使用已有集合填充并初始化
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
// 指定关联 HashMap 的初始容量和加载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
// 只指定初始容量
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
// 包访问权限的构造方法,仅用于 LinkedHashSet 初始化
// 使用 LinkedHashMap 作为底层存储
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
// HashSet 中的元素就相当于 HashMap 中的 key
public Iterator<E> iterator() {
return map.keySet().iterator();
}
// 以下这些方法,都是对 Set 接口中定义的方法的实现
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
// 所有键值对的 value 值都是 PRESENT 这个 Object 对象
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();
}
// JDK 8 提供的一种并行遍历机制 - 可分割迭代器
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
}
}
可以看到,底层使用 HashMap 用于实际存放数据,而 PRESENT 就是所有写入 map 的 value 值。实现比较简单,核心功能都委托给了 HashMap。
不管是 Set 还是 Map,存储的都是对象,在 Java 中,判断两个对象是否相等,都是通过 equals 和 hashCode 两个方法:
- 两个对象通过 equals 判断相等,那么它们肯定返回相同的 hashCode
- 反之,不要求必须拥有相同的 hashCode
所以,HashSet 存储的对象,都要正确覆盖实现 equals 和 hashCode 两个方法。
其实,HashSet 中的元素其实就是 HashMap 的 key,在插入时:
- 首先计算元素的 hashCode 值,找到底层数组存储位置
- 然后和该位置上的所有元素使用 equals 方法进行比较
- 如果都不相等,则插入;否则不插入,本质上这里做了一次 value 的更新,但 key 不变化。
关于迭代器,就是利用的 HashMap 中的 KeyIterator。
LinkedHashSet
LinkedHashSet 的代码就更简单了,它继承自 HashSet,代码如下:
public class LinkedHashSet<E> extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
// 调用父类特定的构造方法,初始一个 LinkedHashMap
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);
}
@Override
public Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.ORDERED);
}
}
全部代码就这些,值得注意的是构造方法中的 super 调用的是 HashSet 中的一个默认包访问权限的构造方法,核心功能都委托给了 LinkedHashMap。
像 HashSet 那样,它能在常量时间内完成集合的基本操作 add, contains 和 remove。性能略低于 HashSet,因为要额外维护一个链表。但有一个例外,在遍历时,LinkedHashSet 花费的时间与元素个数成比例,而 HashSet 花费时间较多,因为它与集合容量成比例。
TreeSet
TreeSet 是一个有序的 Set 集合,元素大小比较方式可以是自然顺序,也可以指定一个 Comparator 比较器。
它是对 TreeMap 的封装,提供了在有序集合上的遍历 API 比如,lower、floor、ceiling 和 higher 分别返回小于、小于等于、大于等于、大于给定元素的元素。能在 log(n) 时间内完成集合的基本操作 add, contains 和 remove。
有一点可以了解下,Set 接口定义的是使用 equals 方法比较元素是否相等,而 TreeSet 使用则是 compareTo 或者 compare 方法进行比较,这满足集合的行为,只不过没有遵守 Set 接口的规范。
TreeSet 源码也比较简单,毕竟只是对 TreeMap 封装了一下,这里不再贴出。
常用集合面试问题总结
之前分析了一部分常用集合的源码,这些集合都各有各的特点,它们的区别也经常出现在面试中,本文最后就对常见的面试题进行下总结。
ArrayList 与 LinkedList 有什么区别?
- 存储结构不同,ArrayList 底层使用数组;LinkedList 使用双向链表
- 性能上,ArrayList 能够随机访问,但增加和删除效率较慢,涉及到内存拷贝;LinkedList 只能顺序或逆序访问,占用内存稍大,但插入删除效率高
- LinkedList 还能当做栈和队列来使用
- 两者均与允许存储 null 也允许存储重复元素
- 两者都是线程不安全的,都可以使用 Collections.synchronizedList(List list) 方法生成一个线程安全的 List
ArrayList 与 Vector 有什么区别?
- ArrayList 非线程安全,Vector 线程安全
- 扩容时,ArrayList 增加 1.5 倍的容量 ; Vector 增加 2倍的容量
JDK 8 对 HashMap 做了哪些优化?
- 底层结构改为单链表 + 数组 + 红黑树的存储结构,在有大量哈希冲突时,将查询时间复杂度从 O(n) 降为 O(log(n))
- 优化哈希函数,将 1.7 中的4次位运算 + 5次异或运算,降低到1次位运算 + 1次异或运算
- 优化扩容机制,1.7 中会重新哈希计算新的位置,而 1.8 则是根据2的次幂扩展机制,不重新计算位置,只根据原散列值计算偏移量,要么位置不变,要么偏移旧数组容量的偏移量
HashMap 和 HashTable 的区别
- HashMap 线程不安全 ; HashTable 线程安全
- HashMap 允许 key 和 Vale 为 null ; HashTable 不允许 key、value 为 null
- HashMap 默认容量为 2^4 且容量一定是 2^n ; HashTable 默认容量是11(素数), 不一定是 2^n
- HashTable 直接使用模运算计算哈希桶下标 ; HashMap 使用 & 位运算 进行优化
HashMap 和 LinkedHashMap 的区别
- LinkedHashMap 继承自 HashMap 它们有相同的存储结构和扩容机制
- LinkedHashMap 内部需要额外维护一个链表
- LinkedHashMap 按插入顺序对元素进行迭代 ; 而 HashMap 迭代顺序不可预测
- LinkedHashMap 可按按访问顺序遍历元素,用于构建 LRU 缓存
什么是 fast-fail,原理是什么?
fast-fail,即快速失败,在遍历集合的过程中,如果发现集合结构发生了变化,会抛出 ConcurrentModificationException 运行时异常。
注意,在不同步修改的情况下,它不能保证会发生,它只是尽力检测并发修改的错误。
原理是通过一个 modCount 字段来实现的,这个字段记录了列表结构的修改次数,当调用 iterator() 返回迭代器时,会缓存 modCount 当前的值,如果这个值发生了不期望的变化,那么就会在 next, remove 操作中抛出异常。
小结
本文以及之前介绍的集合都是常规的,常用的,非线程安全的集合实现,接下来将会介绍 Java 并发包下的线程安全的集合,以及一些有特殊用途的集合实现。
这 3 个 Set 集合的实现有点简单,那来做个总结吧的更多相关文章
- Redis命令拾遗四(集合类型)—包含简单搜索筛选商品设计实例。
本文版权,归博客园和作者吴双共同所有.转载和爬虫请注明博客园蜗牛Redis系列文章地址 http://www.cnblogs.com/tdws/tag/NoSql/ Redis数据类型之集合(Set) ...
- 元组/字典/集合内置方法+简单哈希表(day07整理)
目录 二十三.元组内置方法 二十四.字典数据类型 二十五 集合内置方法 二十五.数据类型总结 二十六.深浅拷贝 补充:散列表(哈希表) 二十三.元组内置方法 什么是元组:只可取,不可更改的列表 作用: ...
- Java8 - Stream流:让你的集合变得更简单!
前段时间,在公司熟悉新代码,发现好多都是新代码,全是 Java8语法,之前没有了解过,一直在专研技术的深度,却忘了最初的语法,所以,今天总结下Stream ,算是一份自己理解,不会很深入,就讲讲常用的 ...
- c#将list集合转换为datatable的简单办法
public static class ExtensionMethods { /// <summary> /// 将List转换成DataTabl ...
- 详解MongoDB中的多表关联查询($lookup)
一. 聚合框架 聚合框架是MongoDB的高级查询语言,它允许我们通过转换和合并多个文档中的数据来生成新的单个文档中不存在的信息. 聚合管道操作主要包含下面几个部分: 命令 功能描述 $projec ...
- 详解MongoDB中的多表关联查询($lookup) (转)
一. 聚合框架 聚合框架是MongoDB的高级查询语言,它允许我们通过转换和合并多个文档中的数据来生成新的单个文档中不存在的信息. 聚合管道操作主要包含下面几个部分: 命令 功能描述 $projec ...
- 学习Redis你必须了解的数据结构——JS实现集合和ECMA6集合
集合类似于数组,但是集合中的元素是唯一的,没有重复值的.就像你学高中数学的概念一样,集合还可以做很多比如,并集,交集,差集的计算.在ECMA6之前,JavaScript没有提供原生的Set类,所以只能 ...
- 洛谷 P1466 集合 Subset Sums Label:DP
题目描述 对于从1到N (1 <= N <= 39) 的连续整数集合,能划分成两个子集合,且保证每个集合的数字和是相等的.举个例子,如果N=3,对于{1,2,3}能划分成两个子集合,每个子 ...
- Guava库介绍之集合(Collection)相关的API
作者:Jack47 转载请保留作者和原文出处 欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. 本文是我写的Google开源的Java编程库Guava系列之一,主要介 ...
随机推荐
- win10安装docker
配置首先需要Hyper-v和容器,这样就可以运行Linux的镜像了 如果是win10home版或者是其他版本就需要安装visulbox了, 然后去官网https://www.docker.com/pr ...
- 论文研读Unet++
Unet++: A Nested U-Net Architecture for Medical Image Segmentation Unet++ 论文地址 这里仅进行简要介绍,可供读者熟悉其结构与特 ...
- python的基本语法
编码 python3.0以上的版本,默认的源文件都是以UTF-8编码,所有的字符串都是unicode字符串,当然也可以为源文件指定不同的编码方式; 编码实例: #随机取一个变量 str = " ...
- Docker-Compose搭建单体SkyWalking
SkyWalking简介 SkyWalking是一款高效的分布式链路追踪框架,对于处理分布式的调用链路的问题定位上有很大帮助 有以下特点: 性能好 针对单实例5000tps的应用,在全量采集的情况下, ...
- HDU 1561:The more, The Better(有依赖的树型背包)
http://acm.hdu.edu.cn/showproblem.php?pid=1561 题意:有n个点,容量为m,每个点有一个价值,还给出n条边,代表选第i个点之前必须先选ai,问最多的价值能取 ...
- Django前端HTML通过JS实现表格可编辑,动态添加行,回车完成新建文件夹
功能描述: 点击“新建文件夹”按钮,在table的末尾增加一行:单击页面的新增行,使单元格td变成可编辑状态:输入内容后,当单元格失去焦点时,保存输入的内容:回车后通过AJAX提交后台完成新建文件夹. ...
- 关于过两级mux的时序约束的添加(一个非常经典的时序约束问题)
非常开心自己的微信公众号: <数字集成电路设计及EDA教程> 关注者超过了1700 里面主要讲解数字IC前端.后端.DFT.低功耗设计以及验证等相关知识,并且讲解了其中用到的各种EDA工具 ...
- mysql in与exists问题剖析
1 问题描述 发布当天发现一个日志分析的sql,在测试环境上执行良好,1秒内,而在线上环境上,执行要13秒左右. 嵌套sql一步一步分析后,发现出在in上,因时间紧迫,来补不及具体分析原因,尝试使 ...
- Java设计模式学习笔记(四) 抽象工厂模式
前言 本篇是设计模式学习笔记的其中一篇文章,如对其他模式有兴趣,可从该地址查找设计模式学习笔记汇总地址 1. 抽象工厂模式概述 工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问 ...
- MyBatis从入门到精通:update用法、delete用法
update用法: 1.接口类中添加的方法: int updateById(SysUser sysUser); 2.映射文件中添加的代码: <update id="updateById ...