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 中,判断两个对象是否相等,都是通过 equalshashCode 两个方法:

  • 两个对象通过 equals 判断相等,那么它们肯定返回相同的 hashCode
  • 反之,不要求必须拥有相同的 hashCode

所以,HashSet 存储的对象,都要正确覆盖实现 equalshashCode 两个方法。

其实,HashSet 中的元素其实就是 HashMap 的 key,在插入时:

  1. 首先计算元素的 hashCode 值,找到底层数组存储位置
  2. 然后和该位置上的所有元素使用 equals 方法进行比较
  3. 如果都不相等,则插入;否则不插入,本质上这里做了一次 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 集合的实现有点简单,那来做个总结吧的更多相关文章

  1. Redis命令拾遗四(集合类型)—包含简单搜索筛选商品设计实例。

    本文版权,归博客园和作者吴双共同所有.转载和爬虫请注明博客园蜗牛Redis系列文章地址 http://www.cnblogs.com/tdws/tag/NoSql/ Redis数据类型之集合(Set) ...

  2. 元组/字典/集合内置方法+简单哈希表(day07整理)

    目录 二十三.元组内置方法 二十四.字典数据类型 二十五 集合内置方法 二十五.数据类型总结 二十六.深浅拷贝 补充:散列表(哈希表) 二十三.元组内置方法 什么是元组:只可取,不可更改的列表 作用: ...

  3. Java8 - Stream流:让你的集合变得更简单!

    前段时间,在公司熟悉新代码,发现好多都是新代码,全是 Java8语法,之前没有了解过,一直在专研技术的深度,却忘了最初的语法,所以,今天总结下Stream ,算是一份自己理解,不会很深入,就讲讲常用的 ...

  4. c#将list集合转换为datatable的简单办法

    public static class ExtensionMethods        {        /// <summary>        /// 将List转换成DataTabl ...

  5. 详解MongoDB中的多表关联查询($lookup)

    一.  聚合框架 聚合框架是MongoDB的高级查询语言,它允许我们通过转换和合并多个文档中的数据来生成新的单个文档中不存在的信息. 聚合管道操作主要包含下面几个部分: 命令 功能描述 $projec ...

  6. 详解MongoDB中的多表关联查询($lookup) (转)

    一.  聚合框架 聚合框架是MongoDB的高级查询语言,它允许我们通过转换和合并多个文档中的数据来生成新的单个文档中不存在的信息. 聚合管道操作主要包含下面几个部分: 命令 功能描述 $projec ...

  7. 学习Redis你必须了解的数据结构——JS实现集合和ECMA6集合

    集合类似于数组,但是集合中的元素是唯一的,没有重复值的.就像你学高中数学的概念一样,集合还可以做很多比如,并集,交集,差集的计算.在ECMA6之前,JavaScript没有提供原生的Set类,所以只能 ...

  8. 洛谷 P1466 集合 Subset Sums Label:DP

    题目描述 对于从1到N (1 <= N <= 39) 的连续整数集合,能划分成两个子集合,且保证每个集合的数字和是相等的.举个例子,如果N=3,对于{1,2,3}能划分成两个子集合,每个子 ...

  9. Guava库介绍之集合(Collection)相关的API

    作者:Jack47 转载请保留作者和原文出处 欢迎关注我的微信公众账号程序员杰克,两边的文章会同步,也可以添加我的RSS订阅源. 本文是我写的Google开源的Java编程库Guava系列之一,主要介 ...

随机推荐

  1. 确认过眼神,看清HTTP协议

    导读:什么是 HTTP?它有什么属性?我们常用的是什么呢?快来阅读本文,将会为你一一道来. 什么是 HTTP 协议? 在了解HTTP之前,我们需要了解什么是网络通信模型(也就是我们常说的 OSI 模型 ...

  2. happy machine learning(Second One)

    发现机器学习就根本停不下来 今天来用RNN算法来爽爽僵尸网络宿主预测 首先我们下载好数据,然后打开我们可爱的熊猫 import numpy as np import pandas as pd impo ...

  3. raft算法解析

    一.raft算法引入 在寻找一种易于理解的一致性算法的研究(In Search of an Understandable Consensus Algorithm-extended version) 论 ...

  4. Maven版本管理-Maven Release Plugin插件

    一.什么是版本管理 首先,这里说的版本管理(version management)不是指版本控制(version control),但是本文假设你拥有基本的版本控制的知识,了解subversion的基 ...

  5. re正则

    #转义字符和原生字符 import re # # # 转义 # text = 'apple price is $299' # ret = re.search('\$\d+',text) # print ...

  6. 关于Keepalive的那些事

    服务端很多同学包括自己对keepalive理解不清晰,经常搞不清楚,TCP也有keepalive,HTTP也有keepalive,高可用也叫keepalive,经常混淆这几个概念.做下这几个概念的简述 ...

  7. 【POJ - 3669】Meteor Shower(bfs)

    -->Meteor Shower Descriptions: Bessie听说有场史无前例的流星雨即将来临:有谶言:陨星将落,徒留灰烬.为保生机,她誓将找寻安全之所(永避星坠之地).目前她正在平 ...

  8. shell_chmod与目录权限

    此篇文档将讲解关于linux中文件权限常用命令chmod.为了达到一个比较好的效果,我会在需要的地方实际上机验证测试,并截图给朋友们看.我的linux机器装的是(opensuse-11.3),并且以文 ...

  9. CF1027D Mouse Hunt题解

    题目: 伯兰州立大学的医学部刚刚结束了招生活动.和以往一样,约80%的申请人都是女生并且她们中的大多数人将在未来4年(真希望如此)住在大学宿舍里. 宿舍楼里有nn个房间和一只老鼠!女孩们决定在一些房间 ...

  10. antd pro中如何使用mock数据以及调用接口

    antd pro的底层基础框架使用的是dva,dva采用effect的方式来管理同步化异步 在dva中主要分为3层 services  models  components models层用于存放数据 ...