集合主要体系图
主要分为Collection(单列集合,通常存放单个对象),Map(双列集合,键值对形式),Collections,其中Collection和Map为接口类型,而Collcations为集合工具类。
Collrction 接口和常用方法
Collcation接口没有直接的实现类,而是通过其子接口List和Set来实现的。
在Collection中定义了单列集合操作的常用方法,下面做一个简单演示
/*
因为 Collection 接口不能实例化,因此使用实现类 ArrayList 来演示
*/
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("");// 添加元素
list.remove(""); // 按照类型删除元素
list.remove(0); // 按照索引删除元素
list.size(); // 获取集合元素个数
list.isEmpty(); // 判断集合是否为空
list.addAll(new ArrayList<>()); // 添加多个元素
list.removeAll(new ArrayList<>()); // 删除多个元素
list.contains("aa"); // 查找指定元素
list.clear(); // 清空集合中的元素
list.containsAll(new ArrayList<>()); // 查找多个元素是否存在
}
迭代器遍历
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()){
String next = iterator.next();
System.out.println(next);
}
}
增强 for 遍历
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("a");
list.add("a");
list.add("a");
list.add("a");
list.add("a");
for (String s : list) {
System.out.println(s);
}
}
增强 for 遍历的方式底层实际上也是迭代器的形式,通过观察遍历后的源码可知
public static void main(String[] args) { List<String> list = new ArrayList(); list.add("a"); list.add("a"); list.add("a"); list.add("a"); list.add("a"); list.add("a"); // 构造迭代器 Iterator var2 = list.iterator(); // 通过迭代器遍历 while(var2.hasNext()) { String s = (String)var2.next(); System.out.println(s); } }
List 接口和常用方法
List接口是Collection接口的子接口,List集合中的元素是有序的即添加和取出的元素顺序一致,List中的元素可重复,凡实现List接口的实现类,都具有以上特性。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
list.add(1, "bb"); // 指定位置插入元素
list.addAll(2, Arrays.asList("aa", "bb", "cc"));// 指定位置插入多个元素
list.get(1);// 获取指定索引位置的元素
list.indexOf("aa"); // 查找指定元素首次出现的位置
list.lastIndexOf("bb");// 查找指定元素在集合中末次出现的位置
list.remove(1);// 删除指定索引位置处的元素
list.remove("aa");// 按照位置删除元素
list.set(1, "cc");// 设置指定索引位置处的元素
list.subList(0, 3);// 返回指定索引区间的元素(左闭右开区间)
}
注意:
ArrayList可以存放null值;ArrayList是线程不安全的。
ArrayList 源码解读
ArrayList底层维护了一个 Object 类型的数组transient Object[] elementData;// transient 代表该属性不会被序列化,如果使用空参构造器来初始化ArrayList,则默认容量为0,构造器源码如下
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
其中
DEFAULTCAPACITY_EMPTY_ELEMENTDATA就是一个空的Object数组,源码如下private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
下面来研究添加元素时,ArrayList内部的实现细节;添加元素的add(E e)方法源码如下
public boolean add(E e) {
// 添加元素之前先确保集合的容量
ensureCapacityInternal(size + 1); // Increments modCount!!
// 将元素往数组指定索引位置赋值
elementData[size++] = e;
// 这期间没有异常的话,可以直接返回 true
return true;
}
查看ensureCapacityInternal(size + 1)源码
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
因为
size初始值为0,因此参数minCapacity在首次调用add(E e)方法添加元素时,值为1
先查看calculateCapacity(elementData, minCapacity)的源码
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
判断
elementData是否等于DEFAULTCAPACITY_EMPTY_ELEMENTDATA,这里使用==比较的是地址值,根据上面的源码分析可知,使用空参创建ArrayList时,会将DEFAULTCAPACITY_EMPTY_ELEMENTDATA的地址引用赋值给elementData,因此方法中的if条件满足;接下来调用
Math.max()方法,得到minCapacity和DEFAULT_CAPACITY中的最大值,ArrayList中DEFAULT_CAPACITY的值默认为10,这就是默认容量,在首次调用add(E e)方法添加元素时,minCapacity必然为1,则calculateCapacity(Object[] elementData, int minCapacity)在首次添加元素的情况下,返回值为10。
继续查看ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));方法的源码
private void ensureExplicitCapacity(int minCapacity) {
// 代表 ArrayList 的修改次数(每次执行增删改操作都会+1)主要为了防止并发修改当前集合
modCount++;
// 计算数组容量是否够用,也就是 minCapacity 是否大于 elementData.length 的情况,由此来判断是否需要扩容
if (minCapacity - elementData.length > 0)
// 扩容
grow(minCapacity);
}
此时,参数
minCapacity的值为10,因为ensureExplicitCapacity()接收的是calculateCapacity()的返回值,而elementData在空参构造器中被赋值为是一个空数组,因此其length值为0,if表达式成立。
因为是首次添加,继续执行grow(minCapacity);源码如下
private void grow(int minCapacity) {
// 记录当前数组的长度;首次添加元素时,oldCapacity 值被赋值为 0
int oldCapacity = elementData.length;
/*
利用当前数组的长度,计算新的容量;首次添加元素时,newCapacity 值被赋值为 0
新的容量 = 旧的容量 + 旧的容量 / 2,也就是旧容量的 1.5,使用右移运算效率较高
*/
int newCapacity = oldCapacity + (oldCapacity >> 1);// 首次添加时,oldCapacity 为 0,0 右移一位还是 0(右移运算可以理解为除以2)
// 如果计算出的新容量 newCapacity 小于 minCapacity,直接将 minCapacity 的值赋给 newCapacity;首次添加元素时,minCapacity 值被赋为 10
if (newCapacity - minCapacity < 0)
// newCapacity 值被赋值为 10
newCapacity = minCapacity;
// 检查 newCapacity 的值是否大于 MAX_ARRAY_SIZE(2147483693)
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 根据 newCapacity 使用 copyOf() 对 elementData 重新赋值,完成 elementData 首次添加元素时的扩容和原数据的复制
elementData = Arrays.copyOf(elementData, newCapacity);
}
以上可以看出,使用空参构造出的ArrayList在首次添加元素时,如果调用的是普通的add(E e)方法,则会初始化为默认容量10;
同时根据以上流程,也可以得出结论:如果首次添加元素调用的是addAll(Collection<? extends E> c)方法且addAll(Collection<? extends E> c)参数集合中的元素个数大于默认容量10,则使用参数集合元素个数作为初始化容量,否则依然使用默认容量10作为初始化容量。
后续添加元素时,在不超过集合当前容量的情况下,在执行 if (minCapacity - elementData.length > 0)时,就不会进入扩容逻辑了,但是在添加的元素个数超出集合当前容量时,情况就会发生变化,在调用add(E e)添加第11个元素时, if (minCapacity - elementData.length > 0)条件满足了,因为ArrayList首次扩容的容量为10,而此时添加的元素为第11个,具体的扩容细节源码如下
private void grow(int minCapacity) { // 当前 minCapacity 值为 11
// 当前 elementData.length; 值为 10
int oldCapacity = elementData.length;
// oldCapacity(10) + oldCapacity >> 1(5) = 15 扩容,新的容量为 15
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 15 - 11 = 4,扩容后的容量满足
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用扩容后的容量创建新集合,将旧集合中的数据复制到新集合中,再将新集合的引用赋值给 elementData
elementData = Arrays.copyOf(elementData, newCapacity);
}
以上是ArrayList的扩容流程,可以看出,ArrayList是在将集合放满后,下一次添加元素时开始扩容的,每次扩容为原来的1.5倍,如果计算出的扩容后的容量依旧不够,则直接将容量扩容为当前size+添加的元素个数,也就是:minCapacity
上面的分析都是调用add(E e)方法的情况下,如果首次添加元素调用的是addAll(Collection<? extends E> c)方法呢,继续分析addAll(Collection<? extends E> c)源码,下面是首次添加的情况
public boolean addAll(Collection<? extends E> c) {
// 将添加的多个元素转换为 Object 数组
Object[] a = c.toArray();
// 得到添加元素的个数 numNew
int numNew = a.length;
// 将当前集合中的 size + numNew 得到的 minCapacity 作为参数,使用 ensureCapacityInternal() 来判断是否需要扩容
ensureCapacityInternal(size + numNew); // Increments modCount
// 将 a 数组中,0 ~ numNew-1 索引处的元素,复制到 elementData 中,从 elementData 的 size 处开始复制
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
查看ensureCapacityInternal(size + numNew);源码
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
依旧先执行calculateCapacity(elementData, minCapacity),源码如下
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 首次添加元素,需要计算 DEFAULT_CAPACITY 和 minCapacity 之间的最大值,因为本次 addAll() 一次性添加了 11 个元素,所以返回值为 11,而不是像首次调用 add() 方法一样,返回 10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
继续追踪ensureExplicitCapacity()源码
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 首次添加元素,肯定需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
至于
grow()扩容逻辑则无需再看,结合前面的分析,直接可以推断在首次添加的情况下,集合扩容的最终大小实际上就是由calculateCapacity()方法的返回值决定的;对于add(E e)方法,首次添加一定会扩容为10,因为minCapacity不可能超过DEFAULT_CAPACITY默认的10;但这里调用的
addAll(Collection<? extends E> c)一次性添加了11个元素,大于了默认的10个元素,calculateCapacity()返回值就为11,那么最终初始化容量时,容量大小自然也就为11了。后续再添加元素时,和
add(E e)一样,如果容量足够,直接添加元素,否则扩容1.5倍,若扩容后的容量依旧不够,则直接扩容到minCapacity。
下面对ArrayList的扩容做一个总结
使用空参构造器的情况下,首次调用
add(E e),集合初始化容量为10,后续添加元素时,如果size+添加的元素个数:minCapacity超过当前容量elementData.length,则触发扩容方法grow(minCapacity),直接扩容为1.5倍,如果容量够了,就直接使用新的容量执行扩容,否则直接使用minCapacity作为新容量进行扩容。使用有参构造的情况下,通过参数值来决定初始化容量的大小,需要扩容时直接扩容为
1.5倍,不够则扩容为minCapacity;如果参数为0,那么会直接将EMPTY_ELEMENTDATA赋值给elementData,后续在首次调用add(E e)添加元素时,初始化容量依然为10,扩容机制不变,有参构造的源码如下public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } }
Vector 源码解读
Vector是一个线程同步的集合类,其也是List接口的实现类;Vector中的操作方法都使用synchronized修饰以保证线程安全。
下面对Vector的初始化和扩容源码进行一个简单分析,首先是采用无参构造的情况下
public Vector() {
this(10);
}
无参构造中继续调用了单个参数的重载构造方法,如下
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
继续调用如下
public Vector(int initialCapacity, int capacityIncrement) {
super();
// 使用无参构造器的情况下,initialCapacity 默认为 10
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
// 创建容量为 10 的对象数组
this.elementData = new Object[initialCapacity];
// 当向量的大小大于其容量时,向量的容量自动增加的量。如果容量增量小于或等于零,则每次需要增长时,向量的容量都会【增加】一倍
this.capacityIncrement = capacityIncrement;
}
通过以上分析,在使用Vector的空参构造情况下,初始化容量为10,后续需要扩容时,默认扩容为原来的2倍;如果使用有参构造,最终调用的构造方法和空参的一样,只不过初始化的容量为有参构造中指定的容量,扩容倍率不变,依然为2倍。
下面来研究扩容的细节,首先是添加元素的add(E e)方法
public synchronized boolean add(E e) {
// 记录集合的操作次数
modCount++;
// 检查集合容量
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
ensureCapacityHelper(elementCount + 1);源码如下
private void ensureCapacityHelper(int minCapacity) {
// 所需的最小容量大于当前元素数组的长度,则需要扩容
if (minCapacity - elementData.length > 0)
// 扩容
grow(minCapacity);
}
grow(minCapacity);源码如下
private void grow(int minCapacity) {
// 得到当前旧的容量
int oldCapacity = elementData.length;
// 如果 capacityIncrement 大于 0,则扩容 capacityIncrement 个容量,否则扩容 elementData.length 个容量
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// 扩容后的容量 newCapacity 是否大于所需的最小容量 minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用新容量创建新数组,复制数据
elementData = Arrays.copyOf(elementData, newCapacity);
}
注意,
Vector的默认扩容是直接在当前容量上,增加原来的1倍,这相当于扩容为原来的2倍
总结
以上就是Vector的扩容机制,每次扩容为原来的2倍,如果扩容后的容量不满足,则直接使用minCapacity的值作为新的容量。
Vector和ArrayList相比较,二者底层都是基于数组的;Vector线程安全,效率较低,ArrayList线程不安全,效率较高。
LinkedList 解析
LinkedList底层是基于双向链表的结构,线程不安全的,可添加null值,一个双向链表的简单实现如下
/**
* @description: DoubleNodeList 双向环形链表
* @author: dhj
* @date: 2021/12/18 21:26
* @version: v1.0
*/
public class DoubleLinkedList<T> {
private Node firstNode;// 头节点
private Node lastNode; // 尾节点
private int size; // 元素个数
public DoubleLinkedList() {
this.size = 0;
this.firstNode = null;
this.lastNode = null;
}
class Node {
private final T data; // 存放当前节点中的数据
private Node preNode; // 指向前一个节点
private Node nextNode; // 指向下一个节点
public Node(T data, Node preNode, Node nextNode) {
this.data = data;
this.preNode = preNode;
this.nextNode = nextNode;
}
}
/**
* 添加数据
*
* @param data 数据
* @return 返回是否添加成功
*/
public boolean add(T data) {
try {
// 头尾节点为 null,先初始化
if (this.firstNode == null || this.lastNode == null) {
// 初始化头节点,头节点不存储数据
this.firstNode = new Node(null, null, null);
// 初始化尾节点,尾节点不存储数据
this.lastNode = new Node(null, null, null);
// 使用数据创建新节点
Node newNode = new Node(data, null, null);
// 构建引用
this.firstNode.nextNode = newNode;
this.firstNode.preNode = this.lastNode;
newNode.preNode = this.firstNode;
newNode.nextNode = this.lastNode;
this.lastNode.nextNode = this.firstNode;
this.lastNode.preNode = newNode;
this.size++;
return true;
}
// 不是首次添加,找到尾节点的前一个节点,也就是上次添加的节点
Node tempNode = this.lastNode.preNode;
// 构造新节点
Node newNode = new Node(data, null, null);
// 构建引用
tempNode.nextNode = newNode;
newNode.preNode = tempNode;
this.lastNode.preNode = newNode;
newNode.nextNode = this.lastNode;
this.size++;
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 遍历数据
*/
public void show() {
// 得到头节点
Node tempNode = this.firstNode;
if (tempNode == null || tempNode.nextNode == null) {
System.out.println("没有数据!");
return;
}
// 顺序遍历
while (tempNode.nextNode != this.lastNode) {
Node nextNode = tempNode.nextNode;
System.out.println(nextNode.data.toString());
tempNode = tempNode.nextNode;
}
//// 反向遍历
//while (tempNode.preNode != this.firstNode) {
// Node preNode = tempNode.preNode;
// System.out.println(preNode.data.toString());
// tempNode = tempNode.preNode;
//}
}
/**
* 删除元素
*
* @param data 被删除的元素
* @return 返回删除元素的个数
*/
public int remove(T data) {
int removeCount = 0;
Node tempNode = this.firstNode.nextNode;
while (tempNode.nextNode != lastNode) {
// 找到被删除的元素
if (tempNode.data.equals(data)) {
// 被删除节点的前一个节点的下一个节点指向被删除节点的下一个节点
tempNode.preNode.nextNode = tempNode.nextNode;
// 被删除节点的下一个节点的前一个节点指向被删除元素前一个节点
tempNode.nextNode.preNode = tempNode.preNode;
removeCount++;
// 节点后移
tempNode = tempNode.nextNode;
continue;
}
// 当前不是被删除的元素,直接后移
tempNode = tempNode.nextNode;
}
return removeCount;
}
}
通过以上双向链表的简单实现,可以对
LinkedList的底层结构有一个大概了解,可以看出,使用链表结构,底层在添加元素时就无需涉及到扩容的问题,直接构建节点即可。
LinkedList 对比 ArrayList
LinkedList底层基于双向链表,ArrayList底层基于数组,LinkedList无需连续的内存空间,而ArrayList因为是数组结构,因此需要连续的内存空间
ArrayList的随机访问快(通过索引访问),尾部删除增加元素效率高(其他部分的删除和添加数据因为需要移动元素,因此效率低),而LinkedList的随机访问慢(需要遍历节点),头尾插入和删除元素的效率高(其他部分的插入和删除效率实际上也不是很高,因为需要遍历节点,修改引用)。
根据以上特性,对于需要经常访问和修改的数据,可以使用ArrayList集合来进行存储;相反的,需要经常修改变动的数据可以使用LinkedList。
Set 接口和常用方法
Set接口是Collction的子接口,Set接口实现类中的数据都是无序的(添加和取出的顺序不一致),没有索引,不允许重复的元素,最多可包含一个null。
因为Set是Collection的子接口,因此其常用方法和Collection接口中的一样,可以使用迭代器,增强for来进行遍历,但不能使用索引来访问
HashSet 说明
HashSet是Set接口的实现类,HashSet中存储的数据都是无需的,不允许存储重复的元素,可存储最多一个null值;底层是使用数组加链表的形式来存储元素的;关于不能存储重复元素这种特性,有如下代码进行演示
public static void main(String[] args) {
Set<Object> set = new HashSet<>();
set.add("a"); // true
set.add("a"); // false
set.add(new Person("张三")); // true
set.add(new Person("张三")); // true
set.add(new String("b")); // true
set.add(new String("b")); // false
System.out.println(set);
}
针对以上现象,先进行一个总结:为了防止重复元素的添加,
HashSet的底层结构HashMap会将当前添加的元素与已经存在的元素进行比较,首先是hash值的比较,如果hash值相等,继续比较二者的地址值,如果地址值相等,则认为这个两个元素相等,不会添加当前元素;若地址值不相等,则继续调用equals()方法进行比较,如果返回true,则当前元素无法添加。以上代码中,添加的两个
a和b之所以会失败,正是因为String内部重写了equals()方法,直接比较的是内部字符数组的内容;而两个Person之所以能够添加成功,是因为没有重写equals()方法,导致调用的是Object类中的原生equals()方法直接进行地址值的比较,因为都是new出来的,地址值不同,添加成功。后面会通过源码解读进行详细说明。
数组链表模拟
在进行源码解读之前,先对数组加链表进行一个简单的模拟,方便后续理解源码,数组链表的简单实现如下
/**
* @description: ArrayLinked 数组链表模拟
* @author: dhj
* @date: 2021/12/19 11:12
* @version: v1.0
*/
@SuppressWarnings({"all"})
public class ArrayLinked<T> {
private Object[] nodeArray = null; // 用于存储链表的数组
private static final int DEFAULT_CAPACITY = 16; // 数组的默认初始化容量
public ArrayLinked() {
this.nodeArray = new Object[DEFAULT_CAPACITY];// 初始化数组默认容量为16
}
/**
* 节点类
*/
class Node {
T data; // 数据域
Node nextNode;// 下一个节点
public Node(T data, Node nextNode) {
this.data = data;
this.nextNode = nextNode;
}
}
public boolean add(T data) {
// 先计算 hashCode 值
int hashCode = Math.abs(data.hashCode());
// 使用 hashCode 值计算索引下标
int index = hashCode % nodeArray.length;
// 此索引处还没有节点
if (nodeArray[index] == null) {
// 在该下标处构建 Node 节点,存放数据
Node newNode = new Node(data, null);
// 在该索引处进行赋值
nodeArray[index] = newNode;
return true;
}
/*
当前索引处有节点,遍历到最后一个节点处,构建新节点
*/
Node tempNode = (Node) nodeArray[index];
while (tempNode.nextNode != null) {
tempNode = tempNode.nextNode;
}
// 循环结束,代表找到了最后一个节点,构建新节点,修改引用
Node newNode = new Node(data, null);
tempNode.nextNode = newNode;
return true;
}
/**
* 遍历数组链表
*/
public void show() {
if (nodeArray == null) {
System.out.println("还没有元素可遍历");
return;
}
for (Object o : nodeArray) {
if (o == null) {
continue;
}
Node tempNode = (Node) o;
while (true) {
if (tempNode.nextNode == null) {
System.out.println(tempNode.data.toString());
break;
}
System.out.println(tempNode.data.toString());
tempNode = tempNode.nextNode;
}
}
}
public static void main(String[] args) {
ArrayLinked<String> list = new ArrayLinked<>();
list.add("aa");
list.add("bb");
list.add("cc");
list.add("dd");
list.add("di2hd3");
list.show();
}
}
整体结构为,将元素通过链表的形式进行存储,而多个链表又分别存储在不同的数组索引下,数组的索引通过
hashCode值进行计算得到,这就是数组链表结构,以上是直接添加的元素,没有考虑重复元素的情况,而在HashSet中,还要通过比较元素是否相同来决定是否要添加。
HashSet 源码解读
HashSet是Set接口较为常用的实现类,下面对其源码进行一个解读;首先是构造器,查看构造器底层发现,HashSet实际上是一个HashMap,源码如下
/*
构造一个新的空集; 后备HashMap实例具有默认的初始容量 (16) 和负载系数 (0.75)
*/
public HashSet() {
map = new HashMap<>();
}
这说明在使用空参构造器的情况下,
HashSet底层的存储结构HashMap的默认容量为16
下面开始执行添加元素的add(E e)方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
可以看出,在
HashSet中,底层的add(E e)方法实际上是调用的map.put()方法,同时将元素存储到map结构的key中,而value存储的是一个不变的Object对象:private static final Object PRESENT = new Object();。主要起一个占位作用。
接着便执行HashMap中的put()方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
在真正的执行
put()方法之前,会调用hash()方法来计算存入元素的hash值,便于后续利用hash值来计算索引,hash()方法源码如下static final int hash(Object key) { int h; /* key 为 null,则 hash 值为 0;否则调用 Object 中的 hashCode() 方法先计算出 key 的 hash 值 hashCode() 方法被 native 修饰,具体实现在操作系统或调用非 Java 代码的接口来实现; 得到的 hash 值先与 16 进行右移运算,再根据得出的值进行异或运算。 */ return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }通过以上算法,尽量让不同的
key得到的都是不同的hash值(注意:最终得到的hash值并不是hashCode()方法直接得到的hash值)
接下来利用得到的hash值,调用putVal(hash(key), key, value, false, true);来真正的存放元素,源码如下
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 定义一些辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 在首次添加之前,HashMap 的 table(Node 数组)为 null
if ((tab = table) == null || (n = tab.length) == 0)
/*
调用 resize() 初始化或加扩容 table
最终得到新的 table 赋值给 tab,返回新 table 的长度给 n
首次添加元素时,初始化容量为 16,n 也等于 16
*/
n = (tab = resize()).length;
/*
i = (n - 1) & hash] 计算当前 key 的 hash 值应该在 tab 表中哪一个索引位置进行存放
*/
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果该索引位置为 null,说明可以直接存放新节点(可能存在该索引位置已经有节点的情况)
// 构建新节点,分别存储:当前元素的 hash 值,当前元素,占位的 value,下一个节点域
tab[i] = newNode(hash, key, value, null);
else {
// 索引位置有元素的情况
Node<K,V> e; K k; // 定义辅助变量
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 操作次数 +1
++modCount;
// 如果当前元素个数大于了容量阈值,则触发 resize() 进行扩容
if (++size > threshold)
resize();
// 空方法,留给子类去实现重写的(void afterNodeInsertion(boolean evict) { })
afterNodeInsertion(evict);
return null;
}
resize()方法源码如下/* 初始化或加倍表大小。如果为空,则根据字段阈值中保留的初始容量目标进行分配。否则,因为我们使用的是两次幂扩展,所以每个bin中的元素必须保持相同的索引,或者在新表中以两次偏移量的幂移动。 */ final Node<K,V>[] resize() { // 首次添加时,table 为 null,oldTab 自然也被赋值为 null Node<K,V>[] oldTab = table; // oldCap 被赋值为 0,否则的话 oldCap 为当前 table 的 length int oldCap = (oldTab == null) ? 0 : oldTab.length; // threshold:下一个要调整大小的大小值 (容量阈值),初始化为 0 int oldThr = threshold; int newCap, newThr = 0; // 旧的数组容量大于 0 时;首次添加时不满足该if条件 if (oldCap > 0) { // 如果旧的数组容量大于等于最大容量 1073741824 if (oldCap >= MAXIMUM_CAPACITY) { // 直接将容量阈值提升为 int 最大值 2147483647 threshold = Integer.MAX_VALUE; // 返回旧的 table,这种情况下不再扩容 return oldTab; } /* 旧的容量小于 1073741824,将旧容量值执行左移操作(相当于翻倍),得到新的容量 newCap 新容量小于最大容量 1073741824 并且旧容量大于默认容量 DEFAULT_INITIAL_CAPACITY(16) 满足以上条件,将旧容量翻倍,同时作为新的容量阈值(左移操作) */ else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 双倍阈值 } else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; else { // 首次添加元素时,使用默认容量 16 作为初始化容量 newCap = DEFAULT_INITIAL_CAPACITY; // 首次添加元素时,新的扩容阈值 newThr 等于:默认容量 * 负载因子(0.75) newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } /* 在当前数组容量翻倍后,如果新的容量 newCap 大于 1073741824 且旧的容量大于等于默认容量 16 的情况下,才会执行此 if 中的逻辑,也就是不满足上面的 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) 情况下。 */ if (newThr == 0) { float ft = (float)newCap * loadFactor; // 新的容量 newThr 阈值为 int 最大值 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } // 容量阈值为新计算出的扩容阈值 threshold = newThr; @SuppressWarnings({"rawtypes","unchecked"}) // 使用新容量创建节点数组 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 复制旧 table 中的数组到新 table,首次添加时无需赋值 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; if (e.next == null) newTab[e.hash & (newCap - 1)] = e; else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // preserve order Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; do { next = e.next; if ((e.hash & oldCap) == 0) { if (loTail == null) loHead = e; else loTail.next = e; loTail = e; } else { if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } // 最终返回新 table return newTab; }以上就是
resize()方法的实现细节,由此可以得出结论,在首次put()元素时,table容量被默认初始化为16,负载因子默认为0.75:static final float DEFAULT_LOAD_FACTOR = 0.75f;,而容量阈值为16 * 0.75 = 12,最终初始化了一个容量为16,容量阈值为12的table返回。
以上是第一次add(E e)时,Set集合的内部执行逻辑,下面继续对第二次或者是多次add(E e)的执行逻辑进行分析
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 定义一些辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 在首次添加之前,HashMap 的 table(Node 数组)为 null
// 完成首次添加,table 初始化之后,此 if 条件的逻辑不会再执行
if ((tab = table) == null || (n = tab.length) == 0)
/*
调用 resize() 初始化或加扩容 table
最终得到新的 table 赋值给 tab,返回新 table 的长度给 n
首次添加元素时,初始化容量为 16,n 也等于 16
*/
n = (tab = resize()).length;
/*
i = (n - 1) & hash] 计算当前 key 的 hash 值应该在 tab 表中哪一个索引位置进行存放
*/
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果该索引位置为 null,说明可以直接存放新节点(可能存在该索引位置已经有节点的情况)
// 构建新节点,分别存储:当前元素的 hash 值,当前元素,占位的 value,下一个节点域
tab[i] = newNode(hash, key, value, null);
else {
/*
索引位置有元素的情况(第二次或者多次执行 add(E e) 方法且计算出的索引值处有元素时会执行此逻辑,但第一次执行add(E e)时肯定不会执行此逻辑)
*/
Node<K,V> e; K k; // 定义辅助变量
/*
如果索引位置的节点 p 的 hash 值与当前添加的元素的 hash 值相等并且索引位置的节点 p 的 key 的地址与添加的元素的地址相同又或者是二者调用 euqals() 的返回值为 true(内容相同),那么将索引位置处的节点 p 赋值给临时节点变量 e
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断 p 是否为一颗红黑树(jdk1.7 中,HashMap 底层是基于【数组加链表】实现的,而 jdk1.8 时,HashMap 底层使用【数组+(链表|红黑树)】实现)
else if (p instanceof TreeNode)
// 调用红黑树的 putTreeVal() 方法进行元素的添加,内部会对元素进行比较
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 以上if条件都不满足,说明该索引位置下是一个链表(因为添加的元素与索引位置元素不相等同时索引位置处也不是红黑树,说明索引位置处肯定为一个链表)
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 向链表末尾添加新节点
p.next = newNode(hash, key, value, null);
// 如果链表的长度等于 8 了(TREEIFY_THRESHOLD 值为 8)
if (binCount >= TREEIFY_THRESHOLD - 1)
// 将该索引位置处的链表进行树化
treeifyBin(tab, hash);
break;
}
// 将添加的元素和链表下的每一个节点中的 hash 和 key 进行比较,如果发现相同的,直接退出
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 存在重复的 key,因为根据以上逻辑的分析,添加成功的情况下,e 是为 null 的
V oldValue = e.value;
// 将旧的 value 值替换为新的 value 值(在使用 Map 结构时起作用)
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回值不为 null,代表添加失败
return oldValue;
}
}
// 操作次数 +1
++modCount;
// 如果当前元素个数大于了容量阈值,则触发 resize() 进行扩容
if (++size > threshold)
resize();
// 空方法,留给子类去实现重写的(void afterNodeInsertion(boolean evict) { })
afterNodeInsertion(evict);
return null;
}
treeifyBin(tab, hash);链表树化的逻辑final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; // 如果 tab 为空或者 tab 的容量小于 MIN_TREEIFY_CAPACITY(64)则不会进行树化 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 而是扩容 resize(); // 下面是树化逻辑 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; do { TreeNode<K,V> p = replacementTreeNode(e, null); if (tl == null) hd = p; else { p.prev = tl; tl.next = p; } tl = p; } while ((e = e.next) != null); if ((tab[index] = hd) != null) hd.treeify(tab); } }
经过以上源码分析,下面对HashSet的初始化,元素添加,树化和扩容机制进行一个总结
HashSet初始化时,底层使用的是HashMap,table为null,负载因子为0.75,容量阈值初始化为0,在首次添加元素后,table被初始化为默认容量为16的节点数组Node<K,V>[] table;;初始化后的容量阈值为12。后续在添加元素时,首先计算其
hash值,利用hash值计算索引值,判断该索引值在对应的节点数组处是否有元素,如果没有元素,直接在该索引位置处构造新节点;如果该索引位置处有元素,则比较当前添加的元素和索引位置处的第一个节点的hash值,equals()返回值,如果二者相同,则不会添加;否则的话,判断该索引位置处是否为一个红黑树,如果是红黑树,则调用红黑树的元素添加逻辑;如果红黑树也不是,那么该索引处就为一个链表,遍历链表的每一个节点判断是否和要添加的元素相同,如果有相同的情况,不会添加,否则继续遍历到最后一个节点处,通过尾插的方式将元素添加到链表尾部。之后便是
HashMap树化的逻辑,在元素添加完成后,立即判断链表的长度是否大于等于8了,如果是,则进入树化逻辑,树化需要满足两个条件:链表的长度大于等于8 && 节点数组 table 的长度大于等于 64,此时当前索引位置处的链表会进化为一颗红黑树结构。红黑树结构的特点是查找修改快,但是其占用空间也较多,同时也是为了预防Dos攻击,防止链表超长情况下导致性能下降。然后是扩容机制,
HashSet底层基于HashMap,由此HashSet的扩容实际上也是HashMap的扩容;在使用空参构造,初始化容量为16的情况下,默认负载因子为0.75,容量阈值为12,当元素个数等于12时,将节点数组table的容量扩容为原来的2倍,依次类推。
LinkedHashSet 说明
LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表的结构(实际上就是上面的数组链表模拟中,将链表改为双向的即可);LinkedHashSet使用双向链表来维护元素的次序,使得看起来元素是以插入顺序保存的,实际上元素的插入还是按照hash值来进行存储;LinkedHashSet不允许添加重复的元素。
为什么使用双向链表可以维护元素插入的顺序:每当一个元素添加进来时,上一个添加的元素的
nextNode节点域会执行刚刚添加的元素,而刚刚添加的元素的preNode节点域会指向上一个添加的元素,这种指向与其所在的数组索引位置无关,直接通过节点的节点域来维护顺序。示意图如下
HashSet & LinkedHashSet 总结
HashSet和LinkedHashSet底层使用的分别是HashMap和LinkedHashMap,而LinkedHashMap又是HashMap的子类,这说明HashSet和LinkedHashSet底层使用的存储结构没有变化,只不过LinkedHashSet底层的LinkedHashMap额外维护了双向链表的顺序引用,这使得LinkedHashSet能够保证元素的添加和取出顺序,至于HashSet和LinkedHashSet的扩容机制和初始化机制,因为最底层都是使用HashMap,所以二者相同。
HashSet中的元素是无序的,而LinkednHashSet底层维护的了双向链表的有序引用,因此是有序的。
Map 接口特点
Map中存储的是key-value的键值数据类型,key不允许重复,之前的HashSet和LinkedHashSet也是利用了这一特性,在Map中,key-value存在一对一的关系,通过key总能找到对应的value,Map的key可以为null,但只能有一个;value可以为null,可以有多个;当往Map中存储已经存在的相同key时,会使用新的value值替换掉旧的value值。
需要注意的是,
Map中的key在使用时,应该保证其不可变性,可能存在使用一个对象作为key时,对象中的内容发生变化,导致后续无法通过这个key来找到对应的value;String因其不可变性,通常作为Map的key来使用。
HashMap 源码解析
关于HashMap的源码,在前面的HashSet中已经解析过了,这里只节选关键的源码如下
首先是空参构造(使用有参构造的话,初始化容量根据参数来定)
public HashMap() {
// 默认加载因子 0.75
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
put()方法源码
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 定义一些辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 在首次添加之前,HashMap 的 table(Node 数组)为 null
// 完成首次添加,table 初始化之后,此 if 条件的逻辑不会再执行
if ((tab = table) == null || (n = tab.length) == 0)
/*
调用 resize() 初始化或加扩容 table
最终得到新的 table 赋值给 tab,返回新 table 的长度给 n
首次添加元素时,初始化容量为 16,n 也等于 16
*/
n = (tab = resize()).length;
/*
i = (n - 1) & hash] 计算当前 key 的 hash 值应该在 tab 表中哪一个索引位置进行存放
*/
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果该索引位置为 null,说明可以直接存放新节点(可能存在该索引位置已经有节点的情况)
// 构建新节点,分别存储:当前元素的 hash 值,当前元素,占位的 value,下一个节点域
tab[i] = newNode(hash, key, value, null);
else {
/*
索引位置有元素的情况(第二次或者多次执行 add(E e) 方法且计算出的索引值处有元素时会执行此逻辑,但第一次执行add(E e)时肯定不会执行此逻辑)
*/
Node<K,V> e; K k; // 定义辅助变量
/*
如果索引位置的节点 p 的 hash 值与当前添加的元素的 hash 值相等并且索引位置的节点 p 的 key 的地址与添加的元素的地址相同又或者是二者调用 euqals() 的返回值为 true(内容相同),那么将索引位置处的节点 p 赋值给临时节点变量 e
*/
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判断 p 是否为一颗红黑树(jdk1.7 中,HashMap 底层是基于【数组加链表】实现的,而 jdk1.8 时,HashMap 底层使用【数组+(链表|红黑树)】实现)
else if (p instanceof TreeNode)
// 调用红黑树的 putTreeVal() 方法进行元素的添加,内部会对元素进行比较
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 以上if条件都不满足,说明该索引位置下是一个链表(因为添加的元素与索引位置元素不相等同时索引位置处也不是红黑树,说明索引位置处肯定为一个链表)
else {
// 遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 向链表末尾添加新节点
p.next = newNode(hash, key, value, null);
// 如果链表的长度等于 8 了(TREEIFY_THRESHOLD 值为 8)
if (binCount >= TREEIFY_THRESHOLD - 1)
// 将该索引位置处的链表进行树化
treeifyBin(tab, hash);
break;
}
// 将添加的元素和链表下的每一个节点中的 hash 和 key 进行比较,如果发现相同的,直接退出
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 存在重复的 key,因为根据以上逻辑的分析,添加成功的情况下,e 是为 null 的
V oldValue = e.value;
// 将旧的 value 值替换为新的 value 值(在使用 Map 结构时起作用)
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
// 返回值不为 null,代表添加失败
return oldValue;
}
}
// 操作次数 +1
++modCount;
// 如果当前元素个数大于了容量阈值,则触发 resize() 进行扩容
if (++size > threshold)
resize();
// 空方法,留给子类去实现重写的(void afterNodeInsertion(boolean evict) { })
afterNodeInsertion(evict);
return null;
}
扩容源码
/*
初始化或加倍表大小。如果为空,则根据字段阈值中保留的初始容量目标进行分配。否则,因为我们使用的是两次幂扩展,所以每个bin中的元素必须保持相同的索引,或者在新表中以两次偏移量的幂移动。
*/
final Node<K,V>[] resize() {
// 首次添加时,table 为 null,oldTab 自然也被赋值为 null
Node<K,V>[] oldTab = table;
// oldCap 被赋值为 0,否则的话 oldCap 为当前 table 的 length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// threshold:下一个要调整大小的大小值 (容量阈值),初始化为 0
int oldThr = threshold;
int newCap, newThr = 0;
// 旧的数组容量大于 0 时;首次添加时不满足该if条件
if (oldCap > 0) {
// 如果旧的数组容量大于等于最大容量 1073741824
if (oldCap >= MAXIMUM_CAPACITY) {
// 直接将容量阈值提升为 int 最大值 2147483647
threshold = Integer.MAX_VALUE;
// 返回旧的 table,这种情况下不再扩容
return oldTab;
}
/*
旧的容量小于 1073741824,将旧容量值执行左移操作(相当于翻倍),得到新的容量 newCap
新容量小于最大容量 1073741824 并且旧容量大于默认容量 DEFAULT_INITIAL_CAPACITY(16)
满足以上条件,将旧容量翻倍,同时作为新的容量阈值(左移操作)
*/
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 双倍阈值
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
// 首次添加元素时,使用默认容量 16 作为初始化容量
newCap = DEFAULT_INITIAL_CAPACITY;
// 首次添加元素时,新的扩容阈值 newThr 等于:默认容量 * 负载因子(0.75)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/*
在当前数组容量翻倍后,如果新的容量 newCap 大于 1073741824 且旧的容量大于等于默认容量 16 的情况下,才会执行此 if 中的逻辑,也就是不满足上面的 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) 情况下。
*/
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
// 新的容量 newThr 阈值为 int 最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 容量阈值为新计算出的扩容阈值
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
// 使用新容量创建节点数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 复制旧 table 中的数组到新 table,首次添加时无需赋值
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 最终返回新 table
return newTab;
}
HashMap 树化与退化
HashMap在jdk1.7中底层基于数组+链表形式,而在jdk1.8中底层基于数组+链表|红黑树的形式;下面对HashMap的树化与退化进行说明
树化
当数组某个索引下的元素链表长度超过
8且当前数组的容量大于等于64时,该索引处的链表会进化为红黑树结构。
退化
情况1:在扩容时,如果对红黑树进行拆分,拆分后的元素个数 <= 6 则会退化链表。
情况2:remove 树上的元素节点时,若 root、root.left、root.right、root.left.left 有一个为 null ,也会退化为链表;注意,一定是在移除某个元素之前,root、root.left、root.right、root.left.left 上存在 null,如果我们直接移除 root、root.left、root.right、root.left.left 中的某一个,其余两个还在的话,树不会退化为链表,移除之后,再去移除其他元素,树才会退化为链表。root、root.left、root.right、root.left.left 判断的依据不是指这几个位置上固定的值,而是按照这几个位置上是否有元素来判断。
Hashtable 说明 & 源码分析
Hasntable实现了Map接口;Hashtable不能存储null值,否则会抛出NullPointerException;Hashtable是线程安全的,里面的方法都使用synchronized进行修饰,正因如此,Hashtable的效率不高。
Hasntable底层是基于Entry数组的形式的,源码如下
private transient Entry<?,?>[] table;
Entry是Hashtable的一个内部类,源码如下
private static class Entry<K,V> implements Map.Entry<K,V> {
// 元素的 hash 值
final int hash;
// 元素的 key
final K key;
// 元素值
V value;
// 节点域
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// Map.Entry Ops
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
接下来看构造器,先是空参
public Hashtable() {
// 默认初始化容量为 11,负载因子为 0.75
this(11, 0.75f);
}
接下来是有参构造
public Hashtable(int initialCapacity) {
// 负载因子默认 0.75,初始化容量由参数指定
this(initialCapacity, 0.75f);
}
底层调用如下
public Hashtable(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load: "+loadFactor); if (initialCapacity==0) initialCapacity = 1; // 负载因子 0.75 this.loadFactor = loadFactor; // 初始化容量为参数中的值 table = new Entry<?,?>[initialCapacity]; // 容量阈值一半为【参数值 * 0.75】,使用空参构造的情况下,容量阈值为【11 * 0.75】 threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); }
下面分析添加元素的put()方法,源码如下
public synchronized V put(K key, V value) {
// value 不允许为 null 值
if (value == null) {
throw new NullPointerException();
}
// 确保 key 不在 hash 表中
// 得到 table 的引用
Entry<?,?> tab[] = table;
// 计算 key 的 hash 值
int hash = key.hashCode();
// 计算当前 key 使得的索引
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 通过计算出的索引取出对应位置的 Entry
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 遍历 Entry 数组索引位置处的所有 Entry 链表
for(; entry != null ; entry = entry.next) {
// 判断是否有重复的 key
if ((entry.hash == hash) && entry.key.equals(key)) {
// 存在重复的 key,更新 value 值
V old = entry.value;
entry.value = value;
return old;
}
}
// 根据上面的源码:如果索引位置处为 null 或者没有重复的 key,则直接添加新的 Entry
addEntry(hash, key, value, index);
return null;
}
addEntry()源码如下
private void addEntry(int hash, K key, V value, int index) {
// 操作次数 +1
modCount++;
// 得到 table 引用
Entry<?,?> tab[] = table;
// 检查是否超过容量阈值
if (count >= threshold) {
// 刷新容量(扩容)
rehash();
tab = table;
hash = key.hashCode();
// 使用新 table 的容量重新计算索引
index = (hash & 0x7FFFFFFF) % tab.length;
}
// 创建新的 Entry
@SuppressWarnings("unchecked")
// 得到指定 index 处的 Entry
Entry<K,V> e = (Entry<K,V>) tab[index];
/*
将当前索引位置处的 Entry 作为新 Entry 的下一个 Entry,也就是 netx 节点域
同时 table 数组当前索引位置处的 Entry 变为新 Entry(这相当于头插法:每新一个元素,都在 Entry 链表的头部进行插入,然后之新插入的元素的 next 域指向插入之前的第一个元素)
*/
tab[index] = new Entry<>(hash, key, value, e);
// 操作数 ++
count++;
}
下面研究一下扩容逻辑rehash();,源码如下
protected void rehash() {
// 得到当前 table 的容量,也就是旧的容量
int oldCapacity = table.length;
// 临时变量,保存旧的 table
Entry<?,?>[] oldMap = table;
// 新的容量 = oldCapacity * 2 +1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
// 使用新容量创建新的 Entry 数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
// 计算新的容量阈值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 将旧 table 中的数据保存到新的扩容后的 table 中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
以上是
Hashtable的扩容逻辑,每次扩容为当前容量的2倍+1
补充:Hashtable中的key值不能为null,因为需要调用key的hashCode()方法计算hash值,源码如下
int hash = key.hashCode();
如果
key为null的话,调用hashCode()会抛出异常。
总结
Hashtable中的方法都被synchronized修饰,在需要保证线程安全的情况下存储键值数据的话,Hashtable可以作为一个选择。
Hashtable底层基于Entry数组实现。
Hashtable空参构造的初始化容量为11,负载因子0.75,容量阈值为8;若使用有参构造,则初始化容量由参数指定,负载因子不变,容量阈值根据负载因子*参数值计算得出。当前
Entry的个数大于等于容量阈值时,会触发Hashtable的扩容,每次扩容为当前容量的2倍+1。
Hashtable中元素的插入采用的是头插法。
TreeSet & TreeMap 了解使用
TreeSet能够对添加的元素进行排序,通过在构造器中传入Comparator接口的匿名实现类来指定排序规则,TreeSet在底层使用此排序规则对加入的元素进行排序,相同的元素则不会重复添加,TreeSet使用演示如下
public static void main(String[] args) {
TreeSet<Integer> treeSet = new TreeSet<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
treeSet.add(1);
treeSet.add(3);
treeSet.add(5);
treeSet.add(0);
// 打印出的结果为排序后的
System.out.println(treeSet);
}
TreeMap的使用规则和TreeSet一致,实际上TreeSet底层就是基于TreeMap的,简单使用演示如下
public static void main(String[] args) {
TreeMap<String, String> treeMap = new TreeMap<>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
treeMap.put("c", "c");
treeMap.put("a", "c");
treeMap.put("f", "c");
treeMap.put("e", "c");
treeMap.put("g", "c");
System.out.println(treeMap);
}
注意:TreeSet不允许添加null值,这说明TreeMap的key也不能为null值,但TreeMap的value可以为null值且可以有多个。
集合的使用选型规则
Java 中提供的集合种类繁多,哪怕是以上简单解析说明的,也有十多种,开发过程中应该怎么选择呢?下面对此进行一个规则性梳理,如下图
Collections 工具类
Collections是java.util下的一个集合工具类,包含对集合的多种操作,下面对一些常用操作进行一个简单演示
第一轮
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
list.add(i);
}
List<Integer> demoList; // 用于演示的集合,方便复用
demoList = new ArrayList<>(list);
System.out.println("集合反转之前:" + demoList);
Collections.reverse(demoList); // 反转集合中元素的顺序
System.out.println("集合反转之后:" + demoList);
System.out.println("--------------------------------------");
demoList = new ArrayList<>(list);
System.out.println("随机排序之前:" + demoList);
Collections.shuffle(demoList);// 对集合中的元素进行随机排序
System.out.println("随机排序之后:" + demoList);
System.out.println("--------------------------------------");
demoList = new ArrayList<>(list);
System.out.println("排序之前:" + demoList);
/*
按照指定规则对集合中元素进行排序
*/
Collections.sort(demoList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
System.out.println("排序之后:" + demoList);
System.out.println("--------------------------------------");
demoList = new ArrayList<>(list);
System.out.println("交换之前:" + demoList);
Collections.swap(demoList, 0, 1); // 交换集合中 i ,j 位置处的元素
System.out.println("交换之后:" + demoList);
}
打印输出如下
集合反转之前:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 集合反转之后:[10, 9, 8, 7, 6, 5, 4, 3, 2, 1] -------------------------------------- 随机排序之前:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 随机排序之后:[1, 8, 9, 6, 5, 10, 2, 7, 4, 3] -------------------------------------- 排序之前:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 排序之后:[10, 9, 8, 7, 6, 5, 4, 3, 2, 1] -------------------------------------- 交换之前:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 交换之后:[2, 1, 3, 4, 5, 6, 7, 8, 9, 10]
第二轮
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 10; i++) {
list.add(i);
}
List<Integer> demoList; // 用于演示的集合,方便复用
demoList = new ArrayList<>(list);
Integer max = Collections.max(demoList); // 根据给定集合中的自然顺序,返回元素中的最大值
System.out.println("集合内容为:" + demoList + ";最大值为:" + max);
System.out.println("--------------------------------------");
/*
根据给定的顺序返回元素中的最大值
*/
Integer max1 = Collections.max(demoList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
System.out.println("集合内容为:" + demoList + ";最大值为:" + max1);
System.out.println("--------------------------------------");
demoList.add(1);
int frequency = Collections.frequency(demoList, 1);
System.out.println("元素 1 出现了:" + frequency + " 次");
System.out.println("--------------------------------------");
System.out.println("替换之前:" + demoList);
Collections.replaceAll(demoList, 1, 11); // 将集合中所有的旧值替换为新值
System.out.println("替换之后:" + demoList);
System.out.println("--------------------------------------");
System.out.println("复制之前:" + demoList);
Collections.copy(demoList, list); // 将【参数2】集合中的数据复制到【参数1】集合中;注意:目标集合至少需要与源集合的长度一致
System.out.println("复制之后:" + demoList);
}
打印输出如下
集合内容为:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10];最大值为:10 -------------------------------------- 集合内容为:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10];最大值为:1 -------------------------------------- 元素 1 出现了:2 次 -------------------------------------- 替换之前:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1] 替换之后:[11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] -------------------------------------- 复制之前:[11, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] 复制之后:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]