文章目录
- java 的集合的使用
- 1 集合的框架体系及主要的分类
- 2 Collection 接口和常用方法
- 3 List 接口和常用方法
- 4 ArrayList 底层结构和源码分析
- 5 Vector 底层源码分析
- 6 LinkedList 底层结构和源码分析
- 7 Set 接口和常用方法
- 8 HashSet 的底层结构和源码分析
- 9 LinkedHashSet 分析与使用
- 10 Map 接口
- 11 HashMap 底层结构和源码解读
- 12 Hashtabel 的解读
- 13 Properties 类的基本介绍
- 14 TreeSet 的解读与分析
- 15 TreeMap 的解读与分析
- 16 在实际开发中如何选择合适的集合实现类?
- 17 Collections 工具类(这里只介绍使用较多的)
- 17.1 基本介绍
- 17.2 排序操作(均为 static 方法)
- 17.3 查找、替换操作
- (1)Object max(Collection) :根据元素的自然顺序,返回给定集合中的最大元素
- (2)Object max(Collection, Comparator) :根据 Comparator 指定的顺序返回给定集合中的最大元素
- (3)Object min(Collection) :根据元素的自然顺序,返回给定集合中的最小元素
- (4)Object min(Collection, Comparator) :根据 Comparator 指定的顺序返回给定集合中的最小元素
- (5)int frequency(Collection, Object) :返回指定集合中指定元素的出现次数
- (6)void copy(List dest, List src) :将 src 中的内容复制到 dest 中
- (7)boolean replaceAll(List list, Object oldVal, Object newVal) :使用新值替换 List 对象的所有旧值
- (8)如果希望了解更多,可以查阅官方文档。点击以下链接即可直达!
java 的集合的使用
试想一下,如果我们需要保存多个数据,首先会想到用数组。但是数组有着以下的局限:
- 长度开始时必须指定,而且一旦指定,不能更改。
- 保存的必须为同一类型的元素。
- 使用数组进行增加/删除元素代码—比较麻烦。
数组扩容:
Person[] person = new Person[1];//大小为1
person[0] = new Person();
//增加新的对象
//创建一个新数组
Person[] person2 = new Person[person.length + 1];
for(){} //进行拷贝,这里略写
//添加新的对象
person2[person.length - 1] = new Person[];
鉴于数组的局限性,java 中引入了集合的概念。
集合的优点:
- 可以动态的保存任意多个对象,使用方便。
- 提供了一系列方便的操作对象的方法:add 、remove、set 、get等。
- 使用集合可以直接进行添加,删除新元素的操作。
1 集合的框架体系及主要的分类
java 中集合类很多,主要分为2大类:Collection 和 Map 。以下它们主要的继承、实现关系:


集合主要为2组:
- 单列集合
- 双列集合
//1. Collection 接口有两个重要的子接口 List 和 Set, 他们的实现子类都是单列集合
//2. Map 接口的实现子类 是双列集合,存放的 K-V
//Collection,存放单列数据
ArrayList arrayList = new ArrayList();
arrayList.add("jack");
arrayList.add("tom");
//Map,存放双列数据
HashMap hashMap = new HashMap();
hashMap.put("NO1", "北京");
hashMap.put("NO2", "上海");
2 Collection 接口和常用方法
(1)Collection 接口实现类的特点
- Collection 的实现子类可以存放多个元素,每个元素可以是 Object。
- 有些 Collection 的实现类,可以存放重复的元素,有些不可以。
- 有些 Collection 的实现类,有些是有序的(List),有些不是有序(Set)。
- Collection 接口没有直接的实现子类,是通过它的子接口 Set 和 List 来实现的。
(2)Collection 接口常用方法,以实现子类 ArrayList 进行案例演示
public class Collection_ {
public static void main(String[] args) {
List list = new ArrayList();
//add() 方法:添加单个元素
list.add("jack");//添加字符串
list.add(10);//自动装箱
//remove :删除指定元素
list.remove(0);//删除第一个元素
list.remove("jack");//指定删除某个元素
//contains :查找元素是否存在
System.out.println(list.contains("jack"));
//如果存在,则返回 true; 没有,则返回 false
//size() :获取元素个数
int n = list.size();
//isEmpty() :判断集合是否为空
boolean flag = list.isEmpty();
//如果为空,返回 true; 不为空,返回 false
//clear() :清空集合所有元素
list.clear();
//addAll() :添加多个元素
ArrayList list2 = new ArrayList();
list2.add("西游记");
list2.add("红楼梦");
list.addAll(list2);//即,addAll()里面可以传一个集合
//containsAll():查找多个元素是否都存在
boolean flag2 = list.containsAll(list2);
//如果都存在,则返回 true; 没有,则返回 false
//removeAll():删除多个元素
list.removeAll(list2);
}
}
(3)Collection 接口遍历元素的方法
1 使用 Iterator(迭代器)
1.1 基本介绍
Iterator 的结构如下图:

基本介绍如下:
- Iterator 对象称为迭代器,主要用于 Collection 集合中的元素。
- 所有实现了 Collection 接口的集合类都有一个 iterator() 方法,用以返回一个实现了 Iterator 接口的对象,即可以返回一个迭代器。
- Iterator 仅用于遍历集合, Iterator 本身并不存放对象。
1.2 Iterator 接口的方法和遍历的执行原理

执行原理:
得到一个集合的迭代器: Iterator iterator = coll.iterator();
判断是否还有下一个元素:hasNext()
while(iterator.hasNext()) {
System.out.println(iterator.next());
//next() 的作用:
//1.下移
//2.将下移以后,集合位置上的元素返回
}

如上图,一开始迭代器 iterator 指向集合中第一个元素的上一个位置,iterator.hasNext() 为对下一个位置进行判断,如果存在元素则返回 true,没有则返回 false。
iterator.next() 执行之后表示,iterator 指向了下一个位置,并返回此位置上的元素。
注意: 在调用 iterator.next() 方法之前,必须要调用 iterator.hasNext() 进行检测。如果没有调用 iterator.hasNext() 进行检测,且恰好下一条记录无效,若此时调用 iterator.next() ,系统将会抛出 NoSuchElementException 异常。
案例演示:
public class Collection_2 {
public static void main(String[] args) {
Collection col = new ArrayList();
col.add("三国演义");
col.add("西游记");
col.add("红楼梦");
//遍历集合并输出
//得到集合对应的迭代器
Iterator iterator = col.iterator();
while(iterator.hasNext()) {
System.out.println(iterator.next());
}
//如果希望再次进行遍历
//需要重置迭代器去
iterator = col.iterator();
}
}
2 使用 for 循环增强进行遍历
增强 for 循环,可以代替 iterator 迭代器,特点是:增强 for 循环就是简化版的 iterator,本质上是一样的。只能用于遍历集合或数组。
基本语法:
for(元素类型 元素名 : 集合名或数组名) {
访问元素;
}
例:
for(Object ob: col) {
System.out.println(ob);
}
3 List 接口和常用方法
3.1 基本介绍
List 接口是 Collection 接口的子接口
-
List 集合中元素有序(即添加顺序和取出的顺序一致)、且可重复。
List list = new ArrayList(); list.add("tom");//为第一个 list.add("jack");//为第二个 list.add("mary");//为第三个 //即存储形式类似数组 -
List 集合的每个元素都有其对应的顺序索引,即支持索引,索引从 0 开始。
-
List 容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素。序号也是从 0 开始的。
System.out.println(list.get(2)); //输出 mary,即为集合中第三个元素 -
JDK API 中 List 接口的实现类有:常用的有 ArrayList、LinkedList、Vector

3.2 List 接口的常用方法
通过代码进行演示:
importjava.util.ArrayList;
importjava.util.List;
publicclassListMethod{
@SuppressWarnings({"all"})
public static void main(String[] args){
List list=new ArrayList();
list.add("张三丰");
list.add("贾宝玉");
//void add(int index, Object ele):在 index 位置插入 ele 元素
//在 index = 1 的位置插入一个对象
list.add(1, "杨过");
System.out.println("list=" + list);
//boolean addAll(int index, Collection eles):
//从 index 位置开始将 eles 中的所有元素添加进来
List list2 = new ArrayList();
list2.add("jack");
list2.add("tom");
list.addAll(1, list2);
System.out.println("list=" + list);
//Object get(int index):获取指定 index 位置的元素
//int indexOf(Object obj):返回 obj 在集合中首次出现的位置
System.out.println(list.indexOf("tom"));//2
//int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
list.add("杨过");
System.out.println("list=" + list);
System.out.println(list.lastIndexOf("杨过"));
//Object remove(int index):移除指定 index 位置的元素,并返回此元素
list.remove(0);
System.out.println("list=" + list);
//Object set(int index, Object ele):
//设置指定 index 位置的元素为 ele , 相当于是替换.
list.set(1, "玛丽");
System.out.println("list=" + list);
//List subList(int fromIndex, int toIndex):
//返回从 fromIndex 到 toIndex 位置的子集合
// 注意返回的子集合 fromIndex <= subList < toIndex
List returnlist = list.subList(0,2);
System.out.println("returnlist="+returnlist);
}
}
3.3 List 的 3 种遍历方法(上文已经讲解过)
- 使用迭代器
- 使用增强 for 循环
- 使用普通的 for 循环
for(int i = 0; i < col.size(); i++) {
System.out.println(col.get(i));
}
4 ArrayList 底层结构和源码分析
4.1 注意事项:
-
permits all elements(允许所有元素), including null(包括 null), ArrayList 可以加入 null, 并且多个。
-
ArrayList 是由数组来实现数据存储的。
-
ArrayList 基本等同于 Vector ,除了 ArrayList 是线程不安全的(执行效率高)。观察源码可知,ArrayList 的方法都没有使用 synchronized 关键字。
在多线程的情况下,不建议使用 ArrayList。
4.2 结论
关于 ArrayList 的底层结构和源码,我们先给出结论,后续再进行代码的追踪。
-
ArrayList 中维护了一个 Object 类型的数组 elementData。
transient Object[] elementData;//transient 表示瞬间,短暂的,表示该属性不会被序列化 -
当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为0,第一次添加,则扩容 elementData 为 10,如果需要再次进行扩容,则扩容elementData为原来的1.5倍。
-
如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为原来的1.5倍。

4.3 源码分析
(1)使用无参构造器初始化
使用已下代码进行源码分析:
import java.util.ArrayList;
public class ArrayList01 {
public static void main(String[] args) {
//使用无参构造器
ArrayList arrayList = new ArrayList();
for(int i = 1; i <= 10; i++) {
arrayList.add(i);
}
}
}
当使用无参构造器时,elementData 会进行初始化。

以下为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 在ArrayList中的定义:
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
可以看出,当调用无参构造器时,elementData 数组将会初始化为一个空数组。
第一次增加元素
由于我们增加的是 int 类型,所以会进行一个自动装箱。

然后将进入 add() 方法中:

可以看出,进入 add() 方法之中,先会调用 ensureCapacityInternal() 方法,size 为集合中元素的个数,因为我们正在第一次增加元素,所以此时 size = 0,即我们传入的参数为 size + 1 = 1 。
接下来,我们进入 ensureCapacityInternal() 方法看看:

可以看到,将要执行 calculateCapacity() 方法,然后再返回来执行 ensureExplicitCapacity() 方法。
所以我们进入 calculateCapacity() 方法:

可以看到,进入之后,会进行一个 if 判断:分析之后易得,calculateCapacity() 方法返回的是DEFAULT_CAPACITY值为10
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果elementData是默认的空数组,则返回默认容量和最小容量中的较大值
// 这是因为当ArrayList初始化时使用的是空数组,此时需要根据传入的最小容量和默认容量来确定实际容量
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 如果elementData不是默认的空数组,直接返回传入的最小容量
// 这种情况通常发生在ArrayList已经初始化过,且有数据的情况下,此时只需根据传入的最小容量来确定容量即可
return minCapacity;
}
执行完之后,我们返回ensureCapacityInternal()方法,继续执行ensureExplicitCapacity() 方法,此时我们传入的参数为10

进入ensureExplicitCapacity() 方法:

private void ensureExplicitCapacity(int minCapacity) {
// 每次尝试修改ArrayList的容量时,modCount都会增加,这用于支持快速失败的迭代器
modCount++;
// 溢出敏感的代码,防止整数溢出
// 如果最小容量大于当前数组的长度,则需要扩容
if (minCapacity - elementData.length > 0)
// 调用grow方法进行扩容,传入最小容量
grow(minCapacity);
}
可以看到,minCapacity - elementData.length = 10 - 0 > 0 ,所以将会调用 grow() 方法对elementData 数组进行扩容,我们接下来继续进入grow() 方法:

private void grow(int minCapacity) {
// 获取当前数组的长度
int oldCapacity = elementData.length;
// 新容量为旧容量的1.5倍,通过位运算实现
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新容量仍小于最小容量,则将新容量设为最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于最大数组大小,则调用hugeCapacity方法计算新容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用Arrays.copyOf方法复制数组,扩容到新容量
// 由于minCapacity通常接近当前元素个数,所以这种方式效率较高
elementData = Arrays.copyOf(elementData, newCapacity);
}
流程为:
-
int oldCapacity = elementData.length此时,elementData 数组是一个空数组,所以长度为0 -
int newCapacity = oldCapacity + (oldCapacity >> 1);// 新容量为旧容量的1.5倍,通过位运算实现由于 oldCapacity 此时等于0,所以 newCapcity 计算出来也为0。
-
// 如果新容量仍小于最小容量,则将新容量设为最小容量 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; -
接下来,直接执行最后一句,数组的复制,这一句执行完成之后,elementData 数组的扩容便完成了。

然后,让我们返回梦开始的地方,add() 方法:

public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
//扩容成功之后,开始把元素存入elementData数组中
elementData[size++] = e;
return true;
}
后续继续添加元素,直到 elementData 数组满了,将会继续按上面的源码逻辑进行扩容。
总结:
ArrayList 使用无参构造器,elementData 数组会初始化为一个空数组,开始添加元素之后,如果是第一个元素,elementData 数组会自动扩容为 10,后续再次扩容时将会按照原来大小的 1.5 倍进行扩容。
(2)使用有参构造器指定大小
感兴趣的小伙伴,可以根据上述方法进行源码分析。由于通过构造器指定 elementData 数组大小与无参构造器差别较小,我在这里只说明不同的部分。
传入一个 int 型参数:
import java.util.ArrayList;
public class ArrayList01 {
public static void main(String[] args) {
//使用有参构造器
ArrayList arrayList = new ArrayList(2);
for(int i = 1; i <= 10; i++) {
arrayList.add(i);
}
}
}

可以看到,elementData 数组会通过我们传入的参数进行初始化。
5 Vector 底层源码分析
5.1 结论
- Vector 类的定义说明

- Vector 底层也是一个对象数组,
protected Object[] elementData; - Vector 是线程同步的,即线程安全,Vector 类的操作方法带有
synchronized

-
在开发中,需要线程同步安全时,考虑使用 Vector。
-
与 ArrayList 的比较:
| 底层结构 | 版本 | 线程安全(同步)效率 | 扩容倍数 | |
|---|---|---|---|---|
| ArrayList | 可变数组 | jdk 1.2 | 不安全,效率高 | 如果是有参构造器,按原来的1.5倍扩容 如果是无参构造器 1.第一次给一个默认容量10 2.从第二次开始,按原来的1.5倍扩容 |
| Vector | 可变数组 Object[] | jdk 1.0 | 安全,效率低 | 如果是无参构造器,默认10个容量,满之后,按原来的2倍扩容 如果是指定大小,则每次都按原来的2倍扩容 |
5.2 底层源码分析
(1)无参构造器
以下面的代码进行分析:
import java.util.Vector;
public class Vector_ {
public static void main(String[] args) {
Vector vector = new Vector();
for(int i = 1; i <= 10; i++) {
vector.add(i);
}
vector.add(11);
}
}
首先,我们调用无参构造器:

分析: 可以看到,无参构造器中,使用 this(10) 调用同类中的有参构造器,有参构造器中又调用另一个构造器 this(initialCapacity,capacityIncrement) ,我们进入这个构造器中:

执行之后,elementData 数组获得了10个容量。
在我们添加到10个元素之前,elementData 数组都不会进行扩容,直到我们开始添加第11个元素:

分析: 此时,我们进入 add() 方法中,将执行 ensureCapacityHelper() 方法:

分析: 此时,我们传入的 minCapacity 的值为 11,即我们需要 11 个容量才能装下数据。进行 if 判断,所需的最小容量大于 elementData 数组的容量,所以我们执行 grow() 方法进行扩容,接下来我们进入 grow() 方法:

分析: 扩容的算法为:
private void grow(int minCapacity) {
// 获取当前数组的容量
int oldCapacity = elementData.length;
// 计算新的容量,如果capacityIncrement大于0,
//则新的容量为旧容量加上capacityIncrement,
// 否则新的容量为旧容量的两倍,这是一种溢出意识的代码,防止整数溢出
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
// 如果新容量仍小于最小需要的容量,则将新容量设置为最小需要的容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新容量大于数组允许的最大容量,则调用hugeCapacity方法来确定新容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 使用Arrays.copyOf方法将原数组复制到新数组中,新数组的容量为newCapacity
elementData = Arrays.copyOf(elementData, newCapacity);
}
扩容成功之后,elementData 数组可以存放下,第11个元素,我们返回 add() 方法进行添加。

(2)使用有参构造器指定大小
这里不再过多赘述,只是在创建时,elementData 数组的初始容量有所变化,其他完全一样。
6 LinkedList 底层结构和源码分析
6.1 结论
- LinkedList 底层实现了双向链表和双端队列特点
- 可以添加任意元素(元素可以重复),包括可以添加 null
- 线程不安全,没有实现同步
- LinkedList 底层维护了一个双向链表
- LinkedList 中维护了两个属性 first 和 last 分别指向首节点和尾节点

- 每个节点(Node 对象),里面又维护了 prev、next、item 三个属性,其中通过 prev 指向前一个,通过 next 指向下一个节点。最终实现双向链表。

- 因此,LinkedList 的元素的 添加和删除 ,不是通过数组完成的,相对来说效率较高。
6.2 LinkedList 的增加
用以下代码进行分析:
import java.util.LinkedList;
public class LinkedList_ {
public static void main(String[] args) {
LinkedList list = new LinkedList();
//添加
list.add("one");
list.add("second");
list.add("third");
}
}
首先,调用无参构造器进行初始化:

初始化之后,list 的结构为:

开始添加,我们进入 add() 方法:

分析: 调用 linkLast() 方法,我们进入此方法:

void linkLast(E e) {
// 获取当前链表的最后一个节点
final Node<E> l = last;
// 创建一个新的节点,其前驱节点为当前最后一个节点,后继节点为null,元素为e
final Node<E> newNode = new Node<>(l, e, null);
// 将新节点设置为链表的最后一个节点
last = newNode;
// 如果链表原本为空(即最后一个节点为null),则将新节点也设置为第一个节点
if (l == null)
first = newNode;
// 否则,将原最后一个节点的后继节点指向新节点
else
l.next = newNode;
// 链表大小加1
size++;
// 修改计数加1,用于支持迭代器的快速失败机制
modCount++;
}
分析: 这是我们第一次添加元素,此时的链表是为空的。
final Node<E> l = last :此时,first 和 last 都为 null,所以执行之后 l = null
final Node<E> newNode = new Node<>(l, e, null); :这里创建一个新的节点,把 l 传给节点的 prev ,e 传给节点的 item,null 传给节点的 next。
last = newNode; :让 last 指向新节点。
此时,l == null 为真,执行 first = newNode :让 first 指向新节点。

开始添加第二个元素
我们进入 add() 方法:

同样,我们进入 linkLast() 方法:

void linkLast(E e) {
// 获取当前链表的最后一个节点
final Node<E> l = last;
// 创建一个新的节点,其前驱节点为当前最后一个节点,后继节点为null,元素为e
final Node<E> newNode = new Node<>(l, e, null);
// 将新节点设置为链表的最后一个节点
last = newNode;
// 如果链表原本为空(即最后一个节点为null),则将新节点也设置为第一个节点
if (l == null)
first = newNode;
// 否则,将原最后一个节点的后继节点指向新节点
else
l.next = newNode;
// 链表大小加1
size++;
// 修改计数加1,用于支持迭代器的快速失败机制
modCount++;
}
分析: 现在,我们正在添加第二个元素。
final Node<E> l = last; :新建一个 Node 为 l,此时,last 指向 第一个节点,所以这句执行之后,l 也指向了 第一个节点。

final Node<E> newNode = new Node<>(l, e, null); :这里创建一个新的节点,把 l 传给节点的 prev ,e 传给节点的 item,null 传给节点的 next。所以这句执行之后,新节点的 prev 指向了第一个节点。

last = newNode; :让 last 指向新节点。

if 进行判断,此时 l 不为 null ,所以执行 else 分支的语句,l.next = newNode; :执行之后,第一个节点的 next 指向了第二个节点:

至此,list 中已经成功的添加了2个元素。
6.3 LinkedList 的删除
使用以下代码进行分析:
import java.util.LinkedList;
public class LinkedList_ {
public static void main(String[] args) {
LinkedList list = new LinkedList();
//添加
list.add("one");
list.add("second");
list.add("third");
//删除
list.remove(1);
}
}
我们进入 remove() 方法:

首先,对传入的参数进行一个检验,使用 checkElementIndex() 方法:


检验完成之后,开始执行 unlink() 方法,我们进入此方法:
E unlink(Node<E> x) {
// 断言x不为空,确保传入的节点有效
// assert x != null;
// 获取要删除节点的元素
final E element = x.item;
// 获取要删除节点的后继节点
final Node<E> next = x.next;
// 获取要删除节点的前驱节点
final Node<E> prev = x.prev;
// 如果要删除的节点是第一个节点
if (prev == null) {
// 将第一个节点指向其后继节点
first = next;
} else {
// 将前驱节点的后继指向要删除节点的后继
prev.next = next;
// 断开要删除节点与其前驱的连接
x.prev = null;
}
// 如果要删除的节点是最后一个节点
if (next == null) {
// 将最后一个节点指向其前驱
last = prev;
} else {
// 将后继节点的前驱指向要删除节点的前驱
next.prev = prev;
// 断开要删除节点与其后继的连接
x.next = null;
}
// 清除要删除节点的元素引用,便于垃圾回收
x.item = null;
// 链表大小减1
size--;
// 修改计数加1,用于支持迭代器的快速失败机制
modCount++;
// 返回被删除节点的元素
return element;
}

final E element = x.item; :获取要删除节点的元素,我们要删除的是索引为1的节点, 由于索引从0开始,即要删除的节点为上图的中间节点。element = "second"
final Node<E> next = x.next; :获取要删除节点的后继节点,即 next 指向最后一个节点。
final Node<E> prev = x.prev; :获取要删除节点的前驱节点,即 prev 指向第一个节点。

由于我们要删除的是中间节点,所以 prev 和 next 都不为 null ,所以执行else 分支语句:
-
prev.next = next;:将前驱节点的后继指向要删除节点的后继x.prev = null;:断开要删除节点与其前驱的连接 -
next.prev = prev;:将后继节点的前驱指向要删除节点的前驱x.next = null;: 断开要删除节点与其后继的连接

到这里,再继续执行,元素就删除成功了。
6.4 LinkedList 和 ArrayList 的比较
| 底层结构 | 增删的效率 | 改查的效率 | |
|---|---|---|---|
| ArrayList | 可变数组 | 较低 数组扩容 | 较高 |
| LinkedList | 双向链表 | 较高 通过链表追加 | 较低 |
如何选择 ArrayList 和 LinkedLis:
- 如果我们改查的操作多,选择 ArrayList
- 如果我们增删的操作多,选择 LinkedList
- 一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择 ArrayList
- 在一个项目中,根据业务灵活选择,也可能是,一个模块使用 ArrayList ,另一个模块使用的是 LinkedList,也就是说,要根据业务进行选择。
7 Set 接口和常用方法
7.1 基本介绍
- 无序(添加和取出的顺序不一致),没有索引
- 不允许重复元素,所以最多包含一个 null
- JDK API 中 Set 接口的实现类有:

- 和 List 接口一样,Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样。
遍历方法:
-
使用迭代器
-
使用增强 for 循环
-
要注意不能使用索引来进行遍历
因为,Set 接口中没有 get() 方法
8 HashSet 的底层结构和源码分析
8.1 结论
- HashSet 实现了 Set 接口
- HashSet 实际上是 HashMap,从下图构造器可以看出:

- 可以存放 null 值,但是只能有一个 null
- HashSet 不保证元素是有序的,取决于 hash() 方法之后,再确定索引的结果。(即,不保证存放元素的顺序和取出的顺序一致)
- 不能有重复元素/对象。
8.2 源码分析
8.2.1 HashMap 简要介绍
HashSet 的底层是 HashMap,HashMap 的底层是(数组 + 链表 + 红黑树)。
HashMap 底层维护了一个 table 表,该表由数组 + 链表组成:


步骤为:
-
先获取元素的哈希值(hashCode 方法)。
-
对哈希值进行一个运算,得出一个索引值,即为要存放在哈希表中的位置号。
-
如果该位置上没有其他元素,则直接进行存放
如果该位置上已经有其他元素,则需要进行 equal() 方法判断(注意equals 方法是可以由程序员自己重写的),如果相等,则不再添加,如果不相等,则以链表的方式添加。
8.2.2 HashSet 源码分析
(1)结论
- HashSet 底层是 HashMap
- 添加一个元素时,先得到 hash 值,该值会转成索引值。
- 找到存储数据表 table,看这个索引位置是否已经存放了元素。
- 如果没有,直接加入。
- 如果有,调用 equals 比较,如果相同,就放弃添加;如果不相同,则添加到最后。
- 在 Java8 中,如果一条链表的元素个数到达
TREEIFY_THRESHOLD(默认值是8),并且 table 的大小 >=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树)。
(2)源码分析
用以下代码进行分析:
import java.util.HashSet;
public class HashSet_ {
public static void main(String[] args) {
HashSet hashSet = new HashSet();
//添加元素
hashSet.add("xiong");
hashSet.add("study");
hashSet.add("xiong");
}
}
我们直接从 add() 方法开始,第一次添加元素,进入 add() 方法:

其中,PRESENT 的定义为:它的作用是:为使用HashMap,而进行的一个占位。
private static final Object PRESENT = new Object();
我们继续进入 map.put() 的方法之中:

接下来,我们分析 hash() 方法,进入该方法:

static final int hash(Object key) {
int h;
// 如果键为null,返回0作为其哈希值
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// 否则,先获取键的hashCode值赋给h
// 然后将h与h右移16位后的值进行异或运算,得到最终的哈希值
// 这样做可以减少哈希冲突,使高位和低位的信息都能参与到哈希值的计算中
}
通过 hash() 方法,将会计算出一个 hash 值,这个值将用于哈希表的索引计算。
计算完 hash 值之后,我们接着进入 putVal() 方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 检查哈希表是否初始化,如果未初始化,则进行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 计算哈希值对应的索引位置
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果该位置为空,则直接插入新节点
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;
// 如果哈希表大小超过阈值,则进行扩容
if (++size > threshold)
resize();
// 插入节点后的钩子方法,用于支持子类的扩展
afterNodeInsertion(evict);
return null;
}
分析:
-
初始化检查
if ((tab = table) == null || (n = tab.length) == 0):检查哈希表是否已经初始化。如果未初始化,则调用resize()方法进行初始化,并获取新的表长n。
分析
resize()方法,由于我们是第一次添加,所以 table 为 null,进入resize()方法:final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; // 旧的哈希表 int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧的哈希表容量 int oldThr = threshold; // 旧的阈值 int newCap, newThr = 0; // 新的哈希表容量和新的阈值 // 如果旧的哈希表容量大于0 if (oldCap > 0) { // 如果旧的哈希表容量已经达到最大值 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; // 设置阈值为最大值 return oldTab; // 返回旧的哈希表 } // 否则,尝试将哈希表容量翻倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // 阈值也翻倍 } // 如果旧的哈希表容量为0,但阈值大于0,说明初始容量被放在阈值中 else if (oldThr > 0) newCap = oldThr; // 否则,使用默认的初始容量和负载因子 else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 如果新的阈值为0,计算新的阈值 if (newThr == 0) { float ft = (float)newCap * loadFactor; 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; // 更新哈希表引用 // 如果旧的哈希表不为空,重新分配节点到新的哈希表 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); // 如果是树节点,调用split方法重新分配 else { // 重新分配链表节点 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); // 清空链表尾部的next指针 if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; // 返回新的哈希表 }- 初始化变量
Node<K,V>[] oldTab = table;:获取当前的哈希表,此时table为null。int oldCap = (oldTab == null) ? 0 : oldTab.length;:旧的哈希表容量为0。int oldThr = threshold;:旧的阈值为0(默认情况下)。int newCap, newThr = 0;:声明新的哈希表容量和新的阈值。
- 处理旧哈希表容量
if (oldCap > 0):旧的哈希表容量为0,不执行此分支。else if (oldThr > 0):旧的阈值为0,不执行此分支。else:使用默认的初始容量和负载因子。负载因子DEFAULT_LOAD_FACTOR(0.75)newCap = DEFAULT_INITIAL_CAPACITY;:新的容量为默认初始容量(通常是16)。newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);:计算新的阈值(通常是12)。
- 计算新的阈值
if (newThr == 0):新的阈值已经计算为12,不执行此分支。threshold = newThr;:更新阈值为12。
- 创建新的哈希表
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];:创建新的哈希表,容量为16。table = newTab;:更新哈希表引用。
- 返回新的哈希表
return newTab;:返回新的哈希表。
-
计算索引位置
if ((p = tab[i = (n - 1) & hash]) == null):计算哈希值对应的索引位置i,并检查该位置是否为空。如果为空,则直接插入新节点。
-
处理链表头节点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))):检查链表头节点是否与要插入的键相同。如果相同,则将头节点赋值给e。else if (p instanceof TreeNode):如果头节点是树节点,则调用putTreeVal方法进行树化插入。
-
遍历链表
for (int binCount = 0; ; ++binCount):遍历链表,查找是否存在相同键的节点。if ((e = p.next) == null):如果链表末尾,则插入新节点。if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))):如果找到相同键的节点,则退出循环。
-
更新值
if (e != null):如果找到相同键的节点,则更新值,并返回旧值。
-
修改计数和扩容
++modCount:修改计数加1,用于支持迭代器的快速失败机制。if (++size > threshold):如果哈希表大小超过阈值,则进行扩容。
-
插入节点后的钩子方法
afterNodeInsertion(evict):插入节点后的钩子方法,用于支持子类的扩展。
(3)总结
-
HashSet 底层是 HashMap,第一次添加时,table 数组扩容到16,临界值(threshold)是16 * 加载因子(loadFactor)是0.75 = 12
-
如果 table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,新的临界值就是 32 * 0.75 = 24,以此类推
-
在 Java8 中,如果一条链表的元素个数到达
TREEIFY_THRESHOLD(默认值是8),并且 table 的大小 >=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树)。如果一条链表的元素个数到达了8,而此时,table 的大小为 16,继续添加元素到链表后面,链表长度会为9,同时 table 会进行扩容,大小为 16 * 2 = 32,继续添加元素到链表后面,链表长度变为 10,同时 table 数组会进行扩容,大小为 32 * 2 = 64。
在这种情况下,继续在这条链表后面添加元素,该链表会马上进行树化。
9 LinkedHashSet 分析与使用
-
LinkedHashSet 是 HashSet 的子类
-
LinkedHashSet 底层是一个 LinkedHashMap,底层维护了一个 数组 + 双向链表
创建一个 LinkedHashSet 对象:可以看到其源码创建的实际上是 LinkedHashMap
LinkedHashSet list = new LinkedHashSet();


-
LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
-
LinkedHashSet 不允许添加重复元素。
结论与源码分析:
- 在 LinkedHashSet 中维护了一个 hash 表和双向链表(LinkedHashSet 有head 和 tail)

-
每一个节点有 before 和 after 属性,这样可以形成双向链表。
在LinkedHashMap 中,使用的节点是 Entry,它继承了 HashMap 的 Node 节点:

- 在添加一个元素时,先求 hash 值,再求索引,确定该元素在 table 的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加【添加原则和 HashSet 一样】)
tail.next = newElement;
newElement.pre = tail;
tail = newElement;
- 这样的话,我们遍历 LinkedHashSet 也能确保插入顺序和遍历顺序一致
import java.util.LinkedHashSet;
public class LinkedHashSet_ {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add("jack");
linkedHashSet.add("xiong");
linkedHashSet.add("sing");
}
}

10 Map 接口
10.1 Map 接口实现类的特点
JDK 8 的 Map 的特点:
-
Map 与 Collection 并列存在。用于保存具有映射关系的数据:Key - Value
-
Map 中的 key 和 value 可以任何引用类型的数据,会封装到 HashMap$Node 对象中
-
Map 中的 key 不允许重复,原因和 HashSet 的原理一样。
-
Map 中的 value 可以重复。
-
Map 的 key 可以为 null,value 也可以为 null,注意 key 为 null ,只能有一个,value 为 null,可以多个。即,key 具有唯一性,不允许重复,value 则没有这个限制。
-
常用 String 类作为 Map 的 key。但实际上,key , value 接收类型都为 Object。
-
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value。
-
Map 存放数据的 key - value 示意图,一对 k - v 是放在一个 HashMap$Node 中的,因为 Node 实现了 Entry 接口。

也就是说,Map 在存放数据的时候,会将数据 Hash$Node 类型转为 Map.Entry,再将转化后的数据,存入EntrySet 集合中,EntrySet 集合存放的数据类型为 Map.Entry,但是实际存放的类型是 HashMap$Node,这是因为 HashMap$Node implement Map.Entry 。
为什么要这样操作呢?
这是因为 Map.Entry 提供了 getKey() 和 getValue() 方法,可以直接获取 key - value 的值。简单来说,就是为了方便我们进行数据的遍历和查找。
import java.util.HashMap;
import java.util.Map;
public class HashMap_ {
public static void main(String[] args) {
Map map = new HashMap();
map.put("no1","熊大");
map.put("no2","熊二");
}
}

Map 也提供了,KeySet 和 Values 内部类,可以用来单独的存放 key 值和 value 值。
//使用 keySet() 方法和 values() 方法
Set set1 = map.keySet();
Collection values = map.values();
10.2 Map 接口的常用方法
- put :添加
- remove :根据键删除映射关系
- get :根据键获取值(即根据 key,获取对应的 value)
- size :获取元素个数
- isEmpty :判断个数是否为 0
- clear :清除所有元素
- containsKey :查找键是否存在
通过代码进行讲解:
@SuppressWarnings({"all"})
public class MapMethod {
public static void main(String[] args) {
//演示map接口常用方法
Map map = new HashMap();
map.put("one", new Book("", 100));//OK
//需要注意的是,如果添加的值,key 已经存在了,
//但是 value 不同,此时会进行替换
//即用新的 key,value 值替换原来的
map.put("one", "熊大");//替换
map.put("two", "熊二");//OK
map.put("three", "光头强");//OK
map.put("four", null);//OK
map.put(null, "刘亦菲");//OK
map.put("five", "吉吉国王");//OK
map.put("xiong", "xiong 的小弟");
System.out.println("map=" + map);
//remove:根据键删除映射关系
map.remove(null);
System.out.println("map=" + map);
//get:根据键获取值
Object val = map.get("鹿晗");
System.out.println("val=" + val);
//size:获取元素个数
System.out.println("k-v=" + map.size());
//isEmpty:判断个数是否为0
System.out.println(map.isEmpty());//F
//clear:清除 k-v
//map.clear();
System.out.println("map=" + map);
//containsKey:查找键是否存在
System.out.println("结果=" + map.containsKey("xiong"));//T
}
}
class Book {
private String name;
private int num;
public Book(String name, int num) {
this.name = name;
this.num = num;
}
}
看结果:

10.3 Map 接口的遍历方法
- containsKey :查找键是否存在
- keySet :获取所有的键(key)
- entrySet :获取所有关系 k - z
- values :获取所有的值(value)
使用代码进行具体演示和讲解:
import javax.imageio.stream.IIOByteBuffer;
import java.util.*;
@SuppressWarnings({"all"})
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put("one", "熊大");
map.put("two", "熊二");
map.put("three", "光头强");
map.put("four", null);
//第一组: 先取出 所有的Key, 通过Key 取出对应的Value
Set keyset = map.keySet();
//1.使用增强for 循环
for (Object key : keyset) {
System.out.println(key + "-" + map.get(key));
}
//2.使用迭代器
Iterator iterator = keyset.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
//第二组:把所有的 values 取出
Collection values = map.values();
//可以使用 Collection 接口的遍历方法
//1.增强for 循环
for (Object value : values) {
//因为不能通过 value 查找 key ,所以只能输出 value
System.out.println(value);
}
//2.使用迭代器
while (iterator.hasNext()) {
Object value = iterator.next();
System.out.println(value);
}
//第三组:通过 EntrySet 来获取 k-v
Set entrySet = map.entrySet();
//1.增强for循环
for(Object entry: entrySet) {
Map.Entry entry1 = (Map.Entry) entry;
System.out.println(entry1.getKey() + "-" +
entry1.getValue());
}
//2.迭代器
while (iterator.hasNext()) {
Object entry = iterator.next();
Map.Entry entry1 = (Map.Entry) entry;
System.out.println(entry1.getKey() + "-" +
entry1.getValue());
}
}
}
11 HashMap 底层结构和源码解读
11.1 结论
- Map 接口的常用实现类:HashMap、Hashtable 和 Properties。
- HashMap 是 Map 接口使用频率最高的实现类。
- HashMap 是以 key - value 对的方式来存储数据(HashMap$Node 类型)
- key 不能重复,但是值可以重复,允许使用 null 键 和 null 值。
- 如果添加相同的 key ,则会覆盖原来的 key - value,等同于修改。(key 不会替换,value 会替换)
- 与 HashSet 一样,不保证映射的顺序,因为底层是以 hash 表的方式来存储的。(JDK 8 的 hashMap 底层 数组 + 链表 + 红黑树)
- HashMap 没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有
synchronized
11.2 源码分析
先分析下面的示意图:

- (k, v) 是一个 Node 实现了 Map.Entry<K, V> ,查看 HashMap 源码可以清楚的知道。
- jdk 7.0 的 HashMap 底层实现(数组 + 链表),jdk 8.0 底层(数组 + 链表 + 红黑树)
(1)结论
扩容机制和 HashSet 相同。
-
HashMap 底层维护了 Node 类型的数组 table,默认为 null。
-
当创建对象时,将加载因子(loadfactor)初始化为 0.75
-
当添加 key - value 时,通过 key 的哈希值得到在 table 的索引。然后判断该索引处是否有元素,如果没有元素直接添加;如果该索引处有元素,继续判断该元素的 key 准备加入的 key 是否相等。如果相等,则直接替换 value;如果不相等,则需要判断当前位置是树结构还是链表结构,然后做出相应的处理,如果添加时发现 table 表的容量不够,则需要进行扩容。
-
第一次添加,则需要扩容 table 的容量为 16,临界值(threshold)为 12 (16 * 0.75 得出)
-
以后再扩容,则会把 table 表的容量扩大为原来的2倍,临界值为原来的2倍,依次类推。
-
在 Java8 中,如果一条链表的元素个数到达
TREEIFY_THRESHOLD(默认值是8),并且 table 的大小 >=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化(红黑树)。如果一条链表的元素个数到达了8,而此时,table 的大小为 16,继续添加元素到链表后面,链表长度会为9,同时 table 会进行扩容,大小为 16 * 2 = 32,继续添加元素到链表后面,链表长度变为 10,同时 table 数组会进行扩容,大小为 32 * 2 = 64。
在这种情况下,继续在这条链表后面添加元素,该链表会马上进行树化。
(2)源码分析
以下面代码进行分析:
import java.util.HashMap;
public class HashMap_ {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
hashMap.put(1,1);
hashMap.put(1,2);
System.out.println(hashMap);
}
}
首先,我们进入 HashMap 的无参构造器:可以看到加载因子(loadfactor)初始化为 0.75

观察 debug 的控制台:可以看到 table 刚开始确实为 null

接下来,我们开始添加数据,进入put方法:

继续进入 hash() 方法:可以看到 哈希值计算的算法

接着进入putVal() 方法,进行第一次添加数据,原理和 HashSet 是一样的,这里就不再重复讲述了。
我们下面来重点关注,hashMap.put(1,1); hashMap.put(1,2); 中的第二条语句,value 值的替换,同样的我们直接进入,putVal() 方法:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 获取哈希表数组
if ((tab = table) == null || (n = tab.length) == 0)
// 如果哈希表为空或长度为0,初始化哈希表
n = (tab = resize()).length;
// 计算table表的索引
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果table表的该位置为空,创建新节点并放入该位置
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// table表的该位置不为空,检查第一个节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 如果第一个节点的键与要插入的键相同
e = p;
else if (p instanceof TreeNode)
// 如果是树化节点,调用 putTreeVal 方法处理
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 方法
afterNodeAccess(e);
// 返回旧值
return oldValue;
}
}
// 增加 modCount 和 size
++modCount;
if (++size > threshold)
// 如果 size 超过阈值,扩容
resize();
// 调用 afterNodeInsertion 方法
afterNodeInsertion(evict);
return null;
}
12 Hashtabel 的解读
12.1 基本介绍
- 存放的元素是键值对:即 K - V
- Hashtable 的键和值都不能为 null,否则会抛出 NullPointerException 异常。
- Hashtable 的使用方法基本和 HashMap 一样。
- Hashtable 是线程安全的(使用的方法基本上3都有
synchronized关键字),HashMap 是线程不安全的
12.2 简单的源码分析
(1)结论
- Hashtable 在底层维护了一个数组 table 存放的类型为 Hashtable$Entry,初始化大小为 11,临界值为(threshold)为 11 * 0.75 = 8,加载因子(loadfactor)也为0.75


简单的和 HashMap 比较一下:
- HashMap 维护的是 table 数组,存放的数据类型是,HashMap$Node
- Hashtable 维护的是 table 数组,存放的数据类型是 , Hashtable$Entry
需要注意的是,HashMap$Node 是 HashMap 的内部类,Hashtable$Entry 是 Hashtable 的内部类,table 数组是他们本类中独立定义的,不是同一个。
-
当 Hashtable 中的 table 数组里面的元素数量超过临界值(threshold)大小时,table 数组会进行扩容。
table 数组的初始化大小为 11,临界值为 11 * 0.75 = 8,当添加的元素数量到达8时,如果继续添加元素,table 数组将会进行扩容。
扩容机制为:
int newCapacity = (oldCapacity << 1) + 1:简单理解就是原来的数组容量 ✖ 2 + 1;比如说,第一次扩容 table 容量为 11,扩容之后的数组容量为 11 * 2 + 1 = 23,后面的以此类推即可。
(2)源码分析验证
使用以下代码进行分析:
import java.util.Hashtable;
public class Hashtable_ {
public static void main(String[] args) {
Hashtable hashtable = new Hashtable();
hashtable.put(null, 1);
hashtable.put(2,2);
}
}
首先,进入Hashtable 的无参构造器:可以看到初始化table数组大小为11,加载因子为0.75

接着,我们进入put() 方法中:

观察 put() 方法的源码,可以看到如果 value 值为空,会抛出异常;同样的,如果 key = null,在我们计算 int hash = key.hashCode(); 时,由于 key = null,同样会抛出空指针异常。
扩容机制的验证:
设计以下代码进行分析:
import java.util.Hashtable;
public class Hashtable_ {
public static void main(String[] args) {
Hashtable hashtable = new Hashtable();
hashtable.put(1,1);
hashtable.put(2,2);
hashtable.put(3,3);
hashtable.put(4,4);
hashtable.put(5,5);
hashtable.put(6,6);
hashtable.put(7,7);
hashtable.put(8,8);
hashtable.put("棐","木");
}
}
当执行到,hashtable.put("棐","木") ,我们查看 debug 的控制台:

可以看到此时,table 表中的元素已经到达了 8 个,如果继续添加,table 将会进行扩容,我们继续执行 hashtable.put("棐","木") 语句,进入 put() 方法:

执行之后,我们需要进入 addEntry() 方法:

可以看到,此时 if(count >= threshold) 为真,进入 if 语句,执行 rehash() 方法开始扩容,我们进入 rehash() 方法:
protected void rehash() {
// 获取当前哈希表的容量
int oldCapacity = table.length;
// 获取当前哈希表的数组引用
Entry<?,?>[] oldMap = table;
// 计算新的容量,新容量为旧容量的两倍加一
int newCapacity = (oldCapacity << 1) + 1;
// 检查新容量是否超过最大数组大小
if (newCapacity - MAX_ARRAY_SIZE > 0) {
// 如果旧容量已经达到最大数组大小,不再扩容
if (oldCapacity == MAX_ARRAY_SIZE)
// 保持当前容量,直接返回
return;
// 否则,将新容量设置为最大数组大小
newCapacity = MAX_ARRAY_SIZE;
}
// 创建新的哈希表数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
// 增加修改计数
modCount++;
// 重新计算阈值,阈值为新容量乘以负载因子,但不超过最大数组大小加一
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
// 将新的哈希表数组赋值给 table
table = newMap;
// 遍历旧哈希表的每个位置
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;
}
}
}
执行完成之后,table 数组的容量就扩大为了 23。

(3)HashMap 和 Hashtable 的比较
| 版本 | 线程安全(同步) | 效率 | 允许 null 键 null 值 | |
|---|---|---|---|---|
| HashMap | 1.2 | 不安全 | 高 | 可以 |
| Hashtable | 1.0 | 安全 | 较低 | 不可以 |
13 Properties 类的基本介绍
-
Properties 类继承自 Hashtable 类,并且实现了 Map 接口,也是使用一种键值对的形式来保存数据。
-
Properties 的使用特点和 Hashtable 类似。
-
Properties 还可以用于 从 xxx.properties 文件中,加载数据到 Properties 类对象,并进行读取和修改。
-
需要注意的是:在项目中,xxx.properties 文件通常作为配置文件,在 I/O 流中有所应用。
有兴趣了解的读者,可以先参考下面这篇文章:
public class Properties_ {
public static void main(String[] args) {
//Properties 继承 Hashtable
//通过 k-v 存放数据,key 和 value 不能为 null
Properties properties = new Properties();
//1.添加
//键值都不允许为 null
//properties.put(null, "abc");//抛出 空指针异常
//properties.put("abc", null); //抛出 空指针异常
properties.put("john", 100);//k-v
properties.put("lucy", 100);
properties.put("lic", 100);
properties.put("lic", 88);//如果有相同的 key , value 被替换
System.out.println("properties=" + properties);
//2.通过key 获取对应值
System.out.println(properties.get("lic"));//88
//3.删除
properties.remove("lic");
System.out.println("properties=" + properties);
//4.修改,利用替换的原理
properties.put("john", "约翰");
System.out.println("properties=" + properties);
}
}
14 TreeSet 的解读与分析

TreeSet 的最大特征就是可以进行排序,TreeSet 的底层实际上是TreeMap。TreeSet 内部使用一个 TreeMap 来存储元素,其中键是 TreeSet 的元素,值是一个固定的 PRESENT 对象。
TreeSet 本质上还是一个 Set,不能添加重复的 key 值。

(1)使用无参构造器
TreeSet 使用无参构造器 ,数据的存入顺序和取出顺序还是不一样的,即数据还是无序的。
import java.util.TreeSet;
public class TreeSet_ {
public static void main(String[] args) {
TreeSet treeSet = new TreeSet();
treeSet.add("xiong");
treeSet.add("fei");
treeSet.add("mu");
treeSet.add("csdn");
System.out.println(treeSet);
}
}
输出结果为:

TreeSet 的底层为 TreeMap ,我们进入它的构造器:

(2)使用 TreeSet(Comparator<E>) 构造器
使用这个构造器,可以传入一个我们自定义的比较器,根据需求构建排序规则。
使用以下代码进行分析:
import java.util.Comparator;
import java.util.TreeSet;
public class TreeSet_ {
public static void main(String[] args) {
//TreeSet treeSet = new TreeSet();
TreeSet treeSet = new TreeSet(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//使用compareTo 方法,让数据按字符串从小到大排序
return ((String)o1).compareTo((String) o2);
}
});
treeSet.add("xiong");
treeSet.add("fei");
treeSet.add("mu");
treeSet.add("csdn");
System.out.println(treeSet);
}
}
首先进入构造器TreeSet(Comparator<E>)方法中:

我们继续进入 new TreeMap<>(comparator) 这个对象的创建中:我们将会进入 TreeMap 源码中:

可以看到,我们传入的比较器被赋给了 TreeMap 的 comparator 。
我们继续,开始使用 add() 方法添加元素,进入 add() 方法:

继续进入put() 方法:put 方法是属于 TreeMap 的方法:因为在创建对象调用构造器进行初始化时,把创建的 new TreeMap<>(comparator) 对象赋给了 TreeSet 的 m,m.put() 时会通过动态绑定机制,调用 TreeMap 的 put 方法。
public V put(K key, V value) {
// 获取根节点
Entry<K,V> t = root;
if (t == null) {
// 如果根节点为空,进行类型检查(确保键不为null)
compare(key, key);
// 创建新的根节点
root = new Entry<>(key, value, null);
// 更新树的大小
size = 1;
// 增加修改计数
modCount++;
// 返回null,表示插入成功
return null;
}
int cmp;
Entry<K,V> parent;
// 分离比较器和可比较路径
Comparator<? super K> cpr = comparator;
if (cpr != null) {
// 使用比较器进行比较
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left; // 如果键小于当前节点的键,移动到左子树
else if (cmp > 0)
t = t.right; // 如果键大于当前节点的键,移动到右子树
else
return t.setValue(value); // 如果键相等,替换值并返回旧值
} while (t != null);
}
else {
// 键为null时抛出NullPointerException
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
// 使用Comparable接口进行比较
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left; // 如果键小于当前节点的键,移动到左子树
else if (cmp > 0)
t = t.right; // 如果键大于当前节点的键,移动到右子树
else
return t.setValue(value); // 如果键相等,替换值并返回旧值
} while (t != null);
}
// 创建新节点
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e; // 插入到父节点的左子树
else
parent.right = e; // 插入到父节点的右子树
// 插入后调整树的平衡
fixAfterInsertion(e);
// 更新树的大小
size++;
// 增加修改计数
modCount++;
// 返回null,表示插入成功
return null;
}
执行 treeSet.add("xiong") 时的逻辑
- 初始化:
Entry<K,V> t = root;:获取TreeMap的根节点。if (t == null) { ... }:检查根节点是否为空。如果是第一次插入,根节点为空。
- 根节点为空:
compare(key, key);:进行类型检查,确保键不为null。这里会调用Comparator的compare方法,但两个参数都是"xiong",所以不会抛出异常。root = new Entry<>(key, value, null);:创建新的根节点,键为"xiong",值为PRESENT,父节点为null。size = 1;:更新树的大小为1。modCount++;:增加修改计数。return null;:返回null,表示插入成功。
详细步骤
- 获取根节点:
Entry<K,V> t = root;:获取当前树的根节点。初始时,根节点为空。
- 根节点为空:
if (t == null) { ... }:根节点为空,说明这是第一次插入。compare(key, key);:调用Comparator的compare方法,确保键不为null。这里调用((String)o1).compareTo((String) o2),但两个参数都是"xiong",所以返回0。root = new Entry<>(key, value, null);:创建新的根节点,键为"xiong",值为PRESENT,父节点为null。size = 1;:更新树的大小为1。modCount++;:增加修改计数。return null;:返回null,表示插入成功。
执行 treeSet.add("fei") 的代码逻辑
初始状态
TreeSet 已经通过 treeSet.add("xiong") 插入了一个元素,此时 TreeMap 的结构如下:
- 根节点:
"xiong",值为PRESENT。
执行 treeSet.add("fei") 时
- 获取根节点:
Entry<K,V> t = root;:获取当前树的根节点。此时根节点是"xiong"。
- 根节点不为空:
Comparator<? super K> cpr = comparator;:获取比较器。if (cpr != null) { ... }:比较器不为空,使用比较器进行比较。do { ... } while (t != null);:遍历树,找到插入位置。parent = t;:保存当前节点的父节点,初始时为"xiong"。cmp = cpr.compare(key, t.key);:使用比较器比较键"fei"和"xiong"。((String)"fei").compareTo((String)"xiong")返回-1,因为"fei"小于"xiong"。
if (cmp < 0) t = t.left;:因为"fei"小于"xiong",移动到左子树。while (t != null);:此时t为null,退出循环。
- 插入新节点:
Entry<K,V> e = new Entry<>(key, value, parent);:创建新节点,键为"fei",值为PRESENT,父节点为"xiong"。if (cmp < 0) parent.left = e;:因为"fei"小于"xiong",将新节点插入到父节点的左子树。fixAfterInsertion(e);:插入后调整树的平衡。对于TreeMap,这通常涉及红黑树的调整,但在这个简单的插入中,可能不需要调整。size++;:更新树的大小为2。modCount++;:增加修改计数。return null;:返回null,表示插入成功。
最终状态
插入 "fei" 后,TreeMap 的结构如下:
- 根节点:
"xiong",值为PRESENT。 - 左子节点:
"fei",值为PRESENT。
需要注意的是:如果我们添加的元素通过我们自定义的比较器,比较之后是相等的,则该元素是不会被添加的。
15 TreeMap 的解读与分析

使用以下代码进行分析:
import java.util.Comparator;
import java.util.TreeMap;
public class TreeMap_ {
public static void main(String[] args) {
//使用默认的构造器,数据还是无序的
//TreeMap treeMap = new TreeMap();
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//使用compareTo 方法,让数据按字符串从小到大排序
return ((String)o1).compareTo((String) o2);
}
});
treeMap.put("xiong", "熊大");
treeMap.put("fei", "棐木");
treeMap.put("laoda", "牢大");
System.out.println(treeMap);
}
}
这里需要注意的是,TreeMap 的数据是以键值对的形式存入的。但是,我们传入的比较器,比较的仍然是 key 的值。也就是说,按 key 的值构建排序。
排序的方法和上面的 TreeSet 是一样的,都是通过 put 方法进行排序的,需要注意的是:如果我们添加的元素通过我们自定义的比较器,比较之后是相等的(比较的是 key 值),则该元素的 value 值会替换原来的 value 值。
treeMap.put("xiong", "熊大");
treeMap.put("fei", "棐木");
treeMap.put("xiong", "牢大");//牢大 会替换 熊大
16 在实际开发中如何选择合适的集合实现类?
在项目开发中,我们应该怎么样选择合适的集合实现类呢?
选择哪一种,我们首先需要明确业务的操作特点,然后根据这个特点去选择与之适配的集合实现类的特性。
-
先判断存储的类型(是一组对象还是一组键值对的形式)
- 单列
- 双列
-
一组对象(单列):Collection 接口
-
允许重复:List
- 增删操作多:LinkedList(底层维护了一个双向链表)
- 改查操作多:ArrayList(底层维护了一个 Object 类型的可变数组)
-
不允许重复:Set
- 无序:HashSet(底层是 HashMap,维护了一个哈希表(数组+链表+红黑树))
- 排序:TreeSet(内部使用一个
TreeMap来存储元素,可以通过构造器传入自定义的比较器,构建排序条件) - 插入和取出顺序一致:LinkedHashSet,底层维护了一个数组+双向链表
-
-
一组键值对(双列):Map 接口
- 键无序:HashMap(底层是:哈希表 jdk 7是数组+链表;jdk8 是数组+链表+红黑树)
- 键排序:TreeMap(可以通过构造器传入自定义的比较器,构建排序条件)
- 键插入和取出的顺序一致:LinkedHashMap
- 读取文件:Properties
17 Collections 工具类(这里只介绍使用较多的)
17.1 基本介绍
注意:前面提到的是 Collection 接口。
-
Collections 是一个操作 Set、List 和 Map 等集合的工具类。
-
Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作。
17.2 排序操作(均为 static 方法)
public class Collections_ {
public static void main(String[] args) {
//创建ArrayList 集合,用于测试.
List list = new ArrayList();
list.add("tom");
list.add("smith");
list.add("king");
list.add("milan");
list.add("tom");
}
}
(1)reverse(List) :反转 List 中元素的顺序

Collections.reverse(list);
(2)shuffle(List) :对 List 集合元素进行随机排序

Collections.shuffle(list);
(3)sort() :排序
-
sort(List) :根据元素的自然顺序对指定的 List 集合元素进行升序排序
-
sort(List, Comparator)::根据指定的 Comparator 产生的顺序对 List 集合元素进行排序

Collection.sort(list);

//我们希望按照字符串的长度大小排序
Collections.sort(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
//可以加入校验代码
return ((String) o2).length()- ((String) o1).length();
}
});
(4)swap(List, int, int) :将指定 List 集合中的 i 处元素和 j 处元素进行交换(索引从0开始)

Collections.swap(list, 0, 1);
17.3 查找、替换操作
(1)Object max(Collection) :根据元素的自然顺序,返回给定集合中的最大元素

Collection.max(list);
(2)Object max(Collection, Comparator) :根据 Comparator 指定的顺序返回给定集合中的最大元素

//比如,我们要返回长度最大的元素
Object maxObject = Collections.max(list, new Comparator() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).length()- ((String)o2).length();
}
});
(3)Object min(Collection) :根据元素的自然顺序,返回给定集合中的最小元素

(4)Object min(Collection, Comparator) :根据 Comparator 指定的顺序返回给定集合中的最小元素

(5)int frequency(Collection, Object) :返回指定集合中指定元素的出现次数

Collection.frequency(list, "tom");
(6)void copy(List dest, List src) :将 src 中的内容复制到 dest 中

(7)boolean replaceAll(List list, Object oldVal, Object newVal) :使用新值替换 List 对象的所有旧值

(8)如果希望了解更多,可以查阅官方文档。点击以下链接即可直达!
Collections - Java17中文文档 - API参考文档 - 全栈行动派
总结:在这篇文章中,我们对集合做了一个初步的源码分析,通过源码探究了Collection、Map 接口实现类的底层实现方式。看完这篇文章,并亲自动手实操,相信我们都能对集合有一个更加深刻的认识。
技术之路道阻且长,望你我各自努力。
598

被折叠的 条评论
为什么被折叠?



