通过上两篇的 HashMap 和 LinkedHashMap 源码分析之后,本文将继续分析 JDK 集合之 Set 源码,因为有了以前的 Map 源码分析的铺垫,Set 源码就简单不少了,本文的篇幅也将比以前短不少。查看 Set 源码的构造参数就能够知道,Set 内部其实维护的就是一个 Map,只是单单使用了 Entry 中的 key 。那么本文将再也不赘述内部数据结构,而是经过部分的源码,来讲明两个 Set 集合与 Map 之间的关系。本文将从如下几部分叙述:java
图片来自互联网侵删面试
因为本篇文章主要叙述 Set 容器以及和 Map 容器之间关系,咱们只须要关注上述集合图谱中 Set 部分。能够看出 Set 主要的实现类有 HashSet
和 TreeSet
以及没有画出的 LinkedHashSet
。其中 HashSet
的实现依赖于 HashMap
, TreeSet
的实现依赖于 TreeMap
,LinkedHashSet
的实现依赖于 LinkedHashMap
。数组
从各个实现类的声明也能够看出其继承关系安全
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
public class LinkedHashSet<E>
extends HashSet<E>
implements Set<E>, Cloneable, java.io.Serializable
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
复制代码
在看 Set 的源码以前,咱们先归纳的说下 Set 集合的特色bash
HashSet 源码只有短短的 300 行,上文也阐述了实现依赖于 HashMap,这一点充分体如今其构造方法和成员变量上。咱们来看下 HashSet 的构造方法和成员变量:数据结构
// HashSet 真实的存储元素结构
private transient HashMap<E,Object> map;
// 做为各个存储在 HashMap 元素的键值对中的 Value
private static final Object PRESENT = new Object();
//空参数构造方法 调用 HashMap 的空构造参数
//初始化了 HashMap 中的加载因子 loadFactor = 0.75f
public HashSet() {
map = new HashMap<>();
}
//指按期望容量的构造方法
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//指按期望容量和加载因子
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
//使用指定的集合填充Set
public HashSet(Collection<? extends E> c) {
//调用 new HashMap<>(initialCapacity) 其中初始指望容量为 16 和 c 容量 / 默认 load factor 后 + 1的较大值
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
// 该方法为 default 访问权限,不容许使用者直接调用,目的是为了初始化 LinkedHashSet 时使用
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
复制代码
经过 HashSet 的构造参数咱们能够看出每一个构造方法,都调用了对应的 HashMap 的构造方法用来初始化成员变量 map ,所以咱们能够知道,HashSet 的初始容量也为 1<<4
即16,加载因子默认也是 0.75f。多线程
咱们都知道 Set 不容许存储重复元素,又由构造参数得出结论底层存储结构为 HashMap,那么这个不可重复的属性必然是有 HashMap 中存储键值对的 Key 来实现了。在分析 HashMap 的时候,提到过 HashMap 经过存储键值对的 Key 的 hash 值(通过扰动函数hash()处理后)来决定键值对在哈希表中的位置,当 Key 的 hash 值相同时,再经过 equals 方法判读是不是替换原来对应 key 的 Value 仍是存储新的键值对。那么咱们在使用 Set 方法的时候也必须保证,存储元素的 HashCode 方法以及 equals 方法被正确覆写。并发
HashSet 中的添加元素的方法也很简单,咱们来看下实现:函数
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
复制代码
能够看出 add 方法调用了 HashMap
的 put 方法,构造的键值对的 key 为待添加的元素,而 Value 这时有全局变量 PRESENT
来充当,这个PRESENT
只是一个 Object 对象。源码分析
除了 add 方法外 HashSet
实现了 Set 接口中的其余方法这些方法有:
public int size() {
return map.size();
}
public boolean isEmpty() {
return map.isEmpty();
}
public boolean contains(Object o) {
return map.containsKey(o);
}
//调用 remove(Object key) 方法去移除对应的键值对
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
// 返回一个 map.keySet 的 HashIterator 来做为 Set 的迭代器
public Iterator<E> iterator() {
return map.keySet().iterator();
}
复制代码
关于迭代器咱们在讲解 HashMap
中的时候没有详细列举,其实 HashMap
提供了多种迭代方法,每一个方法对应了一种迭代器,这些迭代器包括下述几种,而 HashSet
因为只关注 Key 的内容,因此使用 HashMap
的内部类 KeySet
返回了一个 KeyIterator
,这样在调用 next 方法的时候就能够直接获取下个节点的 key 了。
//HashMap 中的迭代器
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
复制代码
关于 HashSet 中的源码分析就这些,其实除了一些序列化和克隆的方法之外,咱们已经列举了全部的 HashSet 的源码,有没有感受巨简单,其实下面的 LinkedHashSet 因为继承自 HashSet 使得其代码更加简单只有短短100多行不信继续往下看。
在上述分析 HashSet
构造方法的时候,有一个 default 权限的构造方法没有讲,只说了其跟 LinkedHashSet 构造有关系,该构造方法内部调用的是 LinkedHashMap 的构造方法。
LinkedHashMap 较之 HashMap 内部多维护了一个双向链表用来维护元素的添加顺序:
// dummy 参数没有做用这里能够忽略
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
//调用 LinkedHashMap 的构造方法,该方法初始化了初始起始容量,以及加载因子,
//accessOrder = false 即迭代顺序不等于访问顺序
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
复制代码
LinkedHashSet的构造方法一共有四个,统一调用了父类的 HashSet(int initialCapacity, float loadFactor, boolean dummy)
构造方法。
//初始化 LinkedHashMap 的初始容量为诶 16 加载因子为 0.75f
public LinkedHashSet() {
super(16, .75f, true);
}
//初始化 LinkedHashMap 的初始容量为 Math.max(2*c.size(), 11) 加载因子为 0.75f
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
//初始化 LinkedHashMap 的初始容量为参数指定值 加载因子为 0.75f
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
//初始化 LinkedHashMap 的初始容量,加载因子为参数指定值
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
复制代码
完了..没错,LinkedHashSet 源码就这几行,因此能够看出其实现依赖于 LinkedHashMap 内部的数据存储结构。
以前分析了多篇关于 JDK 集合源码的文章,而这些集合源码中的知识点都是面试的时候常客,所以在本篇结尾做为 "充数" 的一节,咱们来以面试题的形式总结一下以前所分过的源码中的知识点,这些知识点在以前的文章中都有详细的分析,若是有疑问能够回顾一下以前的源码分析文章。
存储结构上 ArrayList 底层使用数组进行元素的存储,LinkedList 使用双向链表做为存储结构。
二者均与容许存储 null 也容许存储重复元素。
在性能上 ArrayList 在存储大量元素时候的增删效率 平均低于 LinkedList,由于 ArrayList 在增删的是须要拷贝元素到新的数组,而 LinkedList 只须要将节点先后指针指向改变。
在根据角标获取元素的时间效率上ArrayList优于 LinkedList,由于数组自己有存储连续,有 index 角标,而 LinkedList 存储元素离散,须要遍历链表。
不要使用 for 循环去遍历 LinkedList 由于效率很低。
二者都是线程不安全的,均可以使用 Collections.synchronizedList(List<E> list)
方法生成一个线程安全的 List。
Collections.synchronizedList(List<E> list)
的线程同步的集合,迭代器并不一样步,须要使用者去加锁。简述 HashMap 的工做原理 JDK 1.8后作了哪些优化
(e.hash & oldCap) == 0
来肯定节点新位置是位于扩容前的角标仍是以前的 2倍角标位置。HashMap 和 HashTable 的区别
HashMap
是线程不安全的,HashTable是线程安全的。
HashMap
容许 key 和 Vale 是 null,可是只容许一个 key 为 null,且这个元素存放在哈希表 0 角标位置。 HashTable
不容许key、value 是 null
HashMap
内部使用hash(Object key)
扰动函数对 key 的 hashCode
进行扰动后做为 hash
值。HashTable
是直接使用 key 的 hashCode()
返回值做为 hash 值。
HashMap
默认容量为 2^4 且容量必定是 2^n ; HashTable
默认容量是11,不必定是 2^n
HashTable
取哈希桶下标是直接用模运算,扩容时新容量是原来的2倍+1。HashMap
在扩容的时候是原来的两倍,且哈希桶的下标使用 &运算代替了取模。
HashMap 和 LinkedHashMap 的区别
LinkedHashMap 拥有与 HashMap 相同的底层哈希表结构,即数组 + 单链表 + 红黑树,也拥有相同的扩容机制。
LinkedHashMap 相比 HashMap 的拉链式存储结构,内部额外经过 Entry 维护了一个双向链表。
HashMap 元素的遍历顺序不必定与元素的插入顺序相同,而 LinkedHashMap 则经过遍历双向链表来获取元素,因此遍历顺序在必定条件下等于插入顺序。
LinkedHashMap 能够经过构造参数 accessOrder 来指定双向链表是否在元素被访问后改变其在双向链表中的位置。
fast-fail
规则 简单说明一下快速失败(fail—fast)在用迭代器遍历一个集合对象时,若是遍历过程当中集合对象中的内容发生了修改(增长、删除、修改),则会抛出ConcurrentModificationException
。
迭代器在遍历时直接访问集合中的内容,而且在遍历过程当中使用一个 modCount
变量。集合在被遍历期间若是内容发生变化,就会改变 modCount
的值。每当迭代器使用hasNext()/next()
遍历下一个元素以前,都会检测 modCount
变量是否为expectedmodCount
值,是的话就返回遍历值;不然抛出异常,终止遍历。
场景:java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程当中被修改)。
集合在使用 for 循环或者高级 for 循环迭代的过程当中不容许使用,集合自己的 remove 方法删除元素,若是进行错误操做将会致使 ConcurrentModificationException
异常的发生
Iterator 能够删除访问的当前元素(current),一旦删除的元素是Iterator 对象中 next 所正在引用的,在 Iterator 删除元素经过 修改 modCount 与 expectedModCount 的值,可使下次在调用 remove 的方法时候二者仍然相同所以不会有异常产生。
本文分析了 JDK 中 HashSet
和 LinkedHashSet
的源码实现,阐述了Set 与 Map 的关系,也经过最后一节的面试题总结复习了一下以前几篇源码分析文章的知识点。以后可能会继续分析一下 Android 中特有的 ArrayMap
和 SparseArray
源码分析。
集合源码分析文章目录,欢迎你们查看。