Java集合框架概述

集合是什么?

在Java中,集合是一种用于存储和操作一组对象的容器,定义在java.util包下。

有数组为什么还要有集合?

在Java中,数组(Array)是一种固定长度、连续的数据结构,用于保存多个相同类型的对象或数据。数组(Array)在创建时长度就固定,无法动态改变长度,如果需要改变数组的长度,需要通过数组拷贝的方式操作,不太方便。

1
int[] array = {1, 2, 3, 4, 5};

为了解决数组长度固定和操作不便的问题,Java提供了一些集合类用于存储、操作和处理数据集合,这些集合类属于Java集合框架(Collections Framework),相比数组,集合具有以下优势:

  • 动态改变长度:集合的长度是动态可变的,可以根据需要自动扩展或缩小。
  • 提供丰富的操作方法:集合类提供了许多方便的方法来添加、删除、查找、遍历等操作,简化了对集合的操作。
  • 支持泛型:集合类支持泛型,可以在编译时强制类型检查,提高代码的安全性和可读性。
  • 提供迭代器:集合类提供了迭代器(Iterator)来遍历集合元素,可以方便地进行循环操作。
  • 内置算法和排序:集合类内置了各种算法和排序方法,可以方便地对集合元素进行排序、查找和比较等操作。

集合体系结构

在 Java 集合框架中,有两个基本的根接口,分别是 Collection 单列集合接口和 Map 双列集合接口。

单列集合Collection体系

Collection 接口是存储单列集合对象的根接口,其中包括 ListSetQueue 接口

  • List接口:单列有序集合,存储有序可重复的对象,对象按插入顺序有序排列,元素可重复。常用的实现类有ArrayList、LinkedList、Vector
  • Set接口:单列无序集合,存储无序不可重复的对象,对象按一定规则无序排列,元素不可重复。常用的实现类有HashSet、LinkedHashSet、TreeSet
  • Queue接口:队列,存储有序可重复的对象,对象按照先进先出(FIFO)的原则进行操作。常用的实现类有:ArrayDeque、ArrayBlockingQueue、PriorityQueue

双列集合Map体系

Map接口:双列集合,存储键值对映射,键和值可以是任意类型的对象,可以根据键(key)来获取对应的值(value)。常用的实现类有HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

Collection接口(单列集合)

Collection接口简介

(1)java.util.Collection 接口是 List、Set 和 Queue 接口的父接口,该接口里定义的方法 既可用于操作 Set 集合,也可用于操作 List 和 Queue 集合

(2)JDK不提供此接口的任何直接实现,而是提供更具体的子接口(Set和List)实现

(3)在 Java5 之前,Java 集合会丢失容器中所有对象的数据类型,把所有对象都 当成 Object 类型处理;从 JDK 5.0 后增加了泛型以后,Java 集合可以记住容 器中对象的数据类型。

Collection接口方法

添加操作

方法名作用
add()添加单个数据,结果返回布尔值
addALl(Collection<? extends E>)批量添加
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void test() {
// add(Object obj):添加单个数据,结果返回布尔值
Collection arrayList = new ArrayList();
arrayList.add("AA");
arrayList.add(123);
System.out.println(arrayList);// [AA, 123]

// addAll():批量添加
Collection newArrayList = new ArrayList();
newArrayList.addAll(arrayList);
System.out.println(newArrayList);// [AA, 123]
}

删除操作

方法名作用
remove()删除单个数据,结果返回布尔值
removeALL(Collection<?>)批量删除,取差集,,从当前集合中移除另一集合中相同的元素
retainAll(Collection<?>)批量删除,取交集
removeIF(Predicate<? super E>)条件删除,参数是过滤器,判断元素是否要删除
clear()清空集合
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class Demo {
@Test
public void test1() {
// add(Object obj):添加单个数据,结果返回布尔值
Collection arrayList = new ArrayList();
arrayList.add("AA");
arrayList.add(123);
System.out.println(arrayList);// [AA, 123]

// remove():删除单个数据,结果返回布尔值
arrayList.remove(123);// 删除123
System.out.println(arrayList);// [AA]

// removeIF(Predicate<? super E>):条件删除,参数是过滤器,判断元素是否要删除
arrayList.removeIf(test -> test.equals("AA"));// 删除AA
System.out.println(arrayList);// []

// clear():清空集合
arrayList.clear();// []
}

@Test
public void test2() {
Collection arrayList1 = new ArrayList();
arrayList1.add("AA");
arrayList1.add(123);
Collection arrayList2 = new ArrayList();
arrayList2.add("BB");
arrayList2.add(123);
// removeAll(Collection coll):取差集,从当前集合中移除另一集合中相同的元素
arrayList1.removeAll(arrayList2);
System.out.println(arrayList1);// [AA]
}

@Test
public void test3() {
Collection arrayList1 = new ArrayList();
arrayList1.add("AA");
arrayList1.add(123);
Collection arrayList2 = new ArrayList();
arrayList2.add("BB");
arrayList2.add(123);
// retainAll(Collection c):取交集,获取当前集合和另一集合的交集,并返回给当前集合
arrayList1.retainAll(arrayList2);
System.out.println(arrayList1);// [123]
}
}

查询操作

方法名作用
size()返回此集合中有效的元素数。
isEmpty()是否是空集合,如果集合中不包含元素,则返回 true 。
contains(Object o)是否包含元素,如果此集合包含指定的元素,则返回true。
containsAll(Collection<?>)如果此集合包含指定集合中的所有元素,则返回true。
equals(Object obj)判断集合元素是否相等
hashCode()返回当前对象的哈希值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Demo {
@Test
public void test() {
Collection arrayList = new ArrayList();
arrayList.add("AA");
Collection newArrayList = new ArrayList();
newArrayList.add("AA");

// size():获取添加的元素个数
System.out.println(arrayList.size());// 1

// isEmpty():判断集合是否为空
System.out.println(arrayList.isEmpty());// false

// contains(Object o):是否包含元素,如果此集合包含指定的元素,则返回true
System.out.println(arrayList.contains("AA"));// true

// containsAll(Collection<?>):如果此集合包含指定集合中的所有元素,则返回true。
System.out.println(arrayList.containsAll(newArrayList));// true

// equals(Object obj):判断集合元素是否相等
System.out.println(arrayList.equals(newArrayList));// true

// hashCode():返回当前对象的哈希值
System.out.println(arrayList.hashCode());// 2111
}
}

转换操作

方法名作用
toArray()集合 —> 数组,正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo {
@Test
public void test() {
Collection arrayList = new ArrayList();
arrayList.add("AA");
arrayList.add("BB");
arrayList.add("CC");
System.out.println(arrayList);// [AA, BB, CC]

// toArray():集合 —> 数组,正确的顺序返回一个包含此列表中所有元素的数组(从第一个到最后一个元素)
Object[] arr = arrayList.toArray();
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}

// 扩展:数组 —> 集合,调用Arrays类的静态方法asList()创建集合
String[] array = {"AA", "BB", "CC"};
Collection<String> asList = Arrays.asList(array);
System.out.println(asList);// [AA, BB, CC]
}
}

遍历操作

方法名作用
iterator()返回Iterator迭代器接口的示例,用于遍历集合元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class Demo {
Collection<String> asList = Arrays.asList("AA", "BB", "CC");
// iterator():返回Iterator接口的示例,用于遍历集合元素
Iterator iterator = asList.iterator();

@Test
public void test1() {
// 方式一:使用next()使指针下移,将下移以后集合位置上的元素返回
System.out.println(iterator.next());// AA
System.out.println(iterator.next());// BB
System.out.println(iterator.next());// BB
}

@Test
public void test2() {
// 方式二:使用for循环+next()
for (int i = 0; i < asList.size(); i++) {
System.out.println(iterator.next());
}
}

@Test
public void test3() {
// 方式三:使用hasNext()判断是否还有下一个元素,然后使用next()指针下移,将下移以后集合位置上的元素返回
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}

@Test
public void test4() {
// 使用增强for循环foreach,内部仍然调用了迭代器
// 语法:for(集合元素的类型 局部变量:集合对象){}
for (Object obj : asList) {
System.out.println(obj);
}
}
}

其他操作

方法名作用
stream()返回一个顺序Stream与此集合作为其来源。
parallelStream()返回可能并行的Stream与此集合作为其来源。 该方法允许返回顺序流。
1
2
3
4
5
6
7
8
9
10
public class Demo {
@Test
public void test() {
Collection<String> asList = Arrays.asList("AA", "BB", "CC");
List<String> collect1 = asList.stream().collect(Collectors.toList());
System.out.println(collect1);// [AA, BB, CC]
List<String> collect2 = asList.parallelStream().collect(Collectors.toList());
System.out.println(collect2);// [AA, BB, CC]
}
}

List接口(有序集合)

List接口简介

单列有序集合,存储有序可重复的对象,对象按插入顺序有序排列,元素可重复。常用的实现类有ArrayList、LinkedList、Vector

名称底层数据结构特点线程安全性
ArrayList数组查询快(支持随机访问),增删慢不安全
LinkedList双向链表查询慢,增删快(支持快速插入和删除)不安全
Vector数组查询快,增删慢,线程安全安全

List接口实现类ArrayList

(1)ArrayList 是List接口的典型实现类、主要实现类。本质上ArrayList是对象引用的一个“变长”数组

(2)ArrayList是一个有顺序的容器,底层是一个数组,不过它是会进行动态扩容

(3)ArrayList的JDK1.8之前与之后的实现区别?

JDK版本简介
JDK1.7ArrayList像饿汉式,直接创建一个初始容量为10的数组,缺点就是如果没使用就浪费空间。
JDK1.8ArrayList像懒汉式,一开始创建一个长度为0的数组,当添加第一个元素时在创建一个始容量为10的数组

成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

// 用于版本控制的序列化ID。
private static final long serialVersionUID = 8683452581122892189L;

// 默认的初始容量大小,如果在构造函数中未指定初始容量,默认使用该值。
private static final int DEFAULT_CAPACITY = 10;

// 空数组对象,用于标识初始容量为0时的情况。
private static final Object[] EMPTY_ELEMENTDATA = {};

// 默认空数组对象,用于标识初始容量未指定时的情况。
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 存储元素的数组,用于保存实际的元素数据。由transient关键字修饰,表示不会被序列化。
transient Object[] elementData;

// 当前ArrayList中元素的数量
private int size;
}

构造方法

构造方法简介
ArrayList()构造一个空容器,底层的数组长度默认为10
ArrayList(int initialCapacity)构造一个初始长度为initialCapacity大小的容器,也就是底层的数组长度为initialCapacity
ArrayList(Collection<? extends E> c)构造一个内容为入参容器c的有序的容器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

/**
* 构造一个具有指定初始容量的空 ArrayList。
*
* @param initialCapacity 初始容量
* @throws IllegalArgumentException 如果初始容量小于0,则抛出IllegalArgumentException异常
*/
public ArrayList(int initialCapacity) {
// 如果初始容量大于0
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];// 初始化元素数组
} else if (initialCapacity == 0) {
// 如果初始容量为0,空数组对象
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 如果初始容量小于0,抛出异常
throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity);
}
}

/**
* 构造一个空的 ArrayList,其初始容量为10。
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

/**
* 构造一个包含指定集合元素的ArrayList。
*
* @param c 指定的集合
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray(); // 将集合转换为数组
if ((size = elementData.length) != 0) {
// 元素数量不为0时,进一步处理
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class); // 复制数组到Object[]类型
} else {
// 元素数量为0时,使用空数组
this.elementData = EMPTY_ELEMENTDATA;
}
}
}

常用方法

方法作用
add(E e)添加元素,向集合中添加元素
add(int index, E element)向集合指定位置后面添加一个元素
addAll(Collection<? extends E> c)向集合中添加另外一个集合的元素
addAll(int index, Collection<? extends E> c)向集合指定位置后面添加另外一个集合的全部元素
set(int index,E element)覆盖指定位置的元素
remove(int index)删除指定位置的元素
get(int index)获取指定位置的元素
indexOf(Object o)获取指定位置的索引
iterator()获取迭代器
size()获取集合大小
isEmpty()判断集合是否为空
clear()清空集合
stream()为集合创建流
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

/**
* 将指定元素添加到ArrayList的末尾。
*
* @param e 要添加的元素
* @return 如果添加成功,则返回true
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 确保容量足够
elementData[size++] = e; // 将元素添加到数组末尾
return true;
}

/**
* 确保ArrayList内部容量不小于指定的最小容量。
*
* @param minCapacity 指定的最小容量
*/
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

/**
* 根据当前数组元素和指定的最小容量计算出新的容量。
*
* @param elementData 当前的数组元素
* @param minCapacity 指定的最小容量
* @return 新的容量
*/
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

/**
* 根据具体的最小容量确保ArrayList的容量。
*
* @param minCapacity 具体的最小容量
*/
private void ensureExplicitCapacity(int minCapacity) {
modCount++;

// 如果需要的最小容量超过当前数组的长度,则进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

/**
* 对ArrayList进行扩容。
*
* @param minCapacity 新的最小容量
*/
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 当前数组的容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 扩容后的新容量,为原容量的1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; // 如果新容量小于最小容量,则直接使用最小容量
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity); // 超过最大容量时,调用hugeCapacity方法获得新容量

elementData = Arrays.copyOf(elementData, newCapacity); // 复制数组并更新数组引用为新的容量数组
}
}

实现原理

  1. ArrayList底层是用 Object 类型的动态数组来存储元素。
  2. 当创建ArrayList集合时,ArrayList默认创建一个容量为0的空数组。
  3. 当第一次向 ArrayList 中添加元素时,会将空数组初始化为容量为10的数组。
  4. 后续向 ArrayList 中添加元素时,会先检查当前数组是否已满,如果数组未满,则直接将元素添加到数组的末尾。
  5. 如果数组已满,会触发扩容机制创建一个新数组(新数组的容量是旧集合容量的1.5倍),然后将旧数组中的元素拷贝到新数组中。
  6. 如果扩容时数组容量是偶数,那么新容量就是旧容量的1.5倍;
  7. 如果扩容时数组容量是奇数,会先将数组容量右移一位,然后再乘以1.5,得到新容量。
  8. 虽然扩容过程中数组的容量会逐渐增大,但并不会无限增大,不能超过MAX_ARRAY_SIZE(约为2^31-1)

List接口实现类LinkedList

(1)对于频繁的插入或删除元素的操作,建议使用LinkedList类,效率较高

(2)ArrayList与LinkedList的区别

对比ArrayLIstLinkedList
数据结构数组链表
查询速度
增删速度
内存空间
应用场景查询较多增删较多

(3)LinkedList的常用方法

方法作用
getFirst返回此列表中的第一个元素。
getLast返回此列表中的最后一个元素。
removeFirst从此列表中删除并返回第一个元素。
removeLast从此列表中删除并返回最后一个元素。
add将指定的元素追加到此列表的末尾。
addFirst在该列表开头插入指定的元素。
size返回此列表中的元素数。
clear从列表中删除所有元素。 此呼叫返回后,列表将为空。
contains如果此列表包含指定的元素,则返回true`
listIterator从列表中的指定位置开始,返回此列表中元素的列表迭代器(按适当的顺序)。
straem为集合创建流

List接口实现类Vector

(1)Vector是一个古老的集合,JDK1.0就有了。大多数操作与ArrayList相同,区别之处在于Vector是线程安全的

(2)在各种list中,最好把ArrayList作为缺省选择。当插入、删除频繁时,使用LinkedList。Vector总是比ArrayList慢,所以尽量避免使用

Set接口(无序集合)

Set接口简介

单列无序集合,存储无序不可重复的对象,对象按一定规则无序排列,元素不可重复。常用的实现类有HashSet、LinkedHashSet、TreeSet

名称底层数据结构特点线程安全性
HashSet哈希表(数组+链表/红黑树)不允许重复元素,不保证元素顺序不安全
LinkedHashSet哈希表(数组+双向链表)不允许重复元素,但使用双向链表维护元素插入顺序不安全
TreeSet红黑树不允许重复元素,元素可以自然顺序或指定Comparator排序不安全

Set接口实现类HashSet

(1)HashSet是Set接口的典型实现,大多数时候使用Set集合时都是用这个实现类,HashSet底层:数组+链表的结构

(2)HashSet按Hash算法来存储集合中的元素,因此具有很好的存取、查找、删除性能

(3)HashSet具有的特点:不能保证元素的排列顺序、HashSet不是线程安全的、集合元素可以是null

(4)HashSet集合判断两个元素相等的标准:两个对象通过hashCode()方法比较相等,并且两个对象的equals()方法返回值也相同

(5)对于存放在Set容器中的对象,对应的类一定要重写equals()和hashCode(Object obj)方法,以实现对象想等原则。即:“相等的对象必须具有相等的散列码”

成员变量

1
2
3
4
5
6
7
8
9
10
11
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {

// 序列化版本号
static final long serialVersionUID = -5024744406713321676L;

// 内部使用HashMap作为存储容器,利用HashMap实现HashSet的底层存储。由transient关键字修饰,表示不会被序列化。
private transient HashMap<E,Object> map;

// HashMap的value所对应的固定对象,在HashMap中,value是一个固定的常量对象,用来占位
private static final Object PRESENT = new Object();
}

构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {

/**
* 默认构造函数,初始化一个空的HashSet。
*/
public HashSet() {
map = new HashMap<>();
}

/**
* 根据给定Collection初始化HashSet。
*
* @param c 给定的Collection
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size() / .75f) + 1, 16));
addAll(c);
}

/**
* 指定初始容量和加载因子的构造函数。
*
* @param initialCapacity 初始容量
* @param loadFactor 加载因子
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}

/**
* 指定初始容量的构造函数。
*
* @param initialCapacity 初始容量
*/
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}

/**
* 构造一个LinkedHashMap的HashSet。
*
* @param initialCapacity 初始容量
* @param loadFactor 加载因子
* @param dummy 占位变量
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
}

常见方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
public class HashSet<E> extends AbstractSet<E> implements Set<E>, Cloneable, java.io.Serializable {

/**
* 默认构造函数,初始化一个空的HashSet。
*/
public HashSet() {
map = new HashMap<>();
}

/**
* 根据给定Collection初始化HashSet。
*
* @param c 给定的Collection
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size() / .75f) + 1, 16));
addAll(c);
}

/**
* 指定初始容量和加载因子的构造函数。
*
* @param initialCapacity 初始容量
* @param loadFactor 加载因子
*/
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}

/**
* 指定初始容量的构造函数。
*
* @param initialCapacity 初始容量
*/
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}

/**
* 构造一个LinkedHashMap的HashSet。
*
* @param initialCapacity 初始容量
* @param loadFactor 加载因子
* @param dummy 占位变量
*/
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

/**
* 返回一个迭代器,用于遍历HashSet中的元素。
*
* @return HashSet的迭代器
*/
public Iterator<E> iterator() {
return map.keySet().iterator();
}

/**
* 返回HashSet的大小,即元素的个数。
*
* @return HashSet的大小
*/
public int size() {
return map.size();
}

/**
* 判断HashSet是否为空。
*
* @return 如果HashSet为空,则返回true;否则返回false
*/
public boolean isEmpty() {
return map.isEmpty();
}

/**
* 判断HashSet是否包含指定元素。
*
* @param o 要查找的元素
* @return 如果HashSet包含指定元素,则返回true;否则返回false
*/
public boolean contains(Object o) {
return map.containsKey(o);
}

/**
* 向HashSet中添加指定元素。
*
* @param e 要添加的元素
* @return 如果HashSet中不存在该元素且成功添加,则返回true;否则返回false
*/
public boolean add(E e) {
return map.put(e, PRESENT) == null;
}

/**
* 从HashSet中移除指定元素。
*
* @param o 要移除的元素
* @return 如果HashSet中存在该元素且成功移除,则返回true;否则返回false
*/
public boolean remove(Object o) {
return map.remove(o) == PRESENT;
}

/**
* 清空HashSet中的所有元素。
*/
public void clear() {
map.clear();
}

/**
* 克隆HashSet。
*
* @return 克隆后的HashSet对象
*/
@SuppressWarnings("unchecked")
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}

/**
* 将HashSet序列化到输出流中。
*
* @param s 输出流
* @throws java.io.IOException 如果发生IO异常
*/
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
s.defaultWriteObject();
s.writeInt(map.capacity());
s.writeFloat(map.loadFactor());
s.writeInt(map.size());
for (E e : map.keySet())
s.writeObject(e);
}

/**
* 从输入流中反序列化HashSet。
*
* @param s 输入流
* @throws java.io.IOException 如果发生IO异常
* @throws ClassNotFoundException 如果找不到类定义
* @throws InvalidObjectException 如果对象是无效的
*/
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
int capacity = s.readInt();
if (capacity < 0) {
throw new InvalidObjectException("Illegal capacity: " + capacity);
}

float loadFactor = s.readFloat();
if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new InvalidObjectException("Illegal load factor: " + loadFactor);
}

int size = s.readInt();
if (size < 0) {
throw new InvalidObjectException("Illegal size: " + size);
}
capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f), HashMap.MAXIMUM_CAPACITY);
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));
map = (((HashSet<?>) this) instanceof LinkedHashSet ?
new LinkedHashMap<E, Object>(capacity, loadFactor) :
new HashMap<E, Object>(capacity, loadFactor));

for (int i = 0; i < size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

/**
* 返回一个Spliterator,用于遍历HashSet中的元素。
*
* @return HashSet的Spliterator
*/
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<E, Object>(map, 0, -1, 0, 0);
}
}

实现原理

  • HashSet 的父类接口是Set集合,以Hash表数据结构存储数据
  • HashSet的底层是基于HashMap实现,它使用 HashMap 的 key 存储元素,并将 value 设置为一个固定的 Object 对象来占位。
  • 因为 HashMap 不能存储重复的 key,所以 HashSet 不能存放重复元素
  • HashSet 的元素是无序的,并且允许存储Null元素,只能有存储一个Null元素
  • HashSet 没有提供 get 方法,但可以通过 Iterator 迭代器来遍历HashSet中的元素。

Set接口实现类LinkedHashSet

(1)LinkedHashSet是HashSet的子类,插入性能略低于HashSet,但在迭代访问Set里的全部元素时有很好的性能

(2)LinkedHashSet根据元素的hashCode来决定元素的存储位置,但它同时使用双向链表维护元素的次序,这使得元素看起来是以插入顺序保存的。

(4)LinkedHashSet不允许集合元素重复,对于频繁的遍历操作,LinkedHashSet效率高于HashSet

Set接口实现类TreeSet

(1)Tree是SortedSet接口的实现类,TreeSet可以确保集合元素除余排序状态

(2)Tree底层使用红黑树结构存储数据

(3)TreeSet和TreeMap采用红黑树的存储结构。特点:有序,查询速度比List快

(4)Tree两种排序方法:自然排序和定制排序。默认情况下,Tree采用自然排序

Query接口(队列)

Query接口简介

队列,存储有序可重复的对象,对象按照先进先出(FIFO)的原则进行操作。常用的实现类有:ArrayDeque、ArrayBlockingQueue、PriorityQueue

名称底层数据结构特点线程安全性
ArrayDeque数组双端队列,支持在队头和队尾进行元素操作不安全
ArrayBlockingQueue数组有界阻塞队列,支持并发操作具有固定容量限制安全
PriorityQueue堆(数组/树)优先级队列,按照元素的优先级进行排序不安全

Map接口(双列集合)

Map接口简介

双列集合,存储键值对映射,键和值可以是任意类型的对象,可以根据键(key)来获取对应的值(value)。常用的实现类有HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

名称底层数据结构特点线程安全性
HashMap哈希表(数组+链表/红黑树)存储键值对映射,通过键的哈希值快速查找对应的值不安全
LinkedHashMap哈希表(数组+双向链表)存储键值对映射,在HashMap的基础上使用双向链表维护键值对的插入顺序不安全
TreeMap红黑树存储键值对映射,元素可以按照键自然顺序或指定Comparator排序不安全
Hashtable哈希表(数组+链表)存储键值对映射,内部使用synchronized关键字实现同步安全
Properties哈希表(数组+链表)存储键值对映射,继承自Hashtable类,主要用于处理属性文件安全

HashMap

(1)HashMap是 Map 接口使用频率最高的实现类,HashMap无序但增删改查快、key不可以重复、可以为null、哈希值为0

成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

// 用于版本控制的序列化ID。
private static final long serialVersionUID = 362498820763181265L;

// 默认初始容量。HashMap默认的初始容量为16,即1左移4位。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大容量。HashMap的最大容量为2的30次方。
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认加载因子。加载因子用于决定何时对HashMap进行扩容,默认值为0.75。当HashMap中的元素数量超过容量乘以加载因子时,就会触发扩容操作。
static final float DEFAULT_LOAD_FACTOR = 0.75f;

// 链表转红黑树阈值。当HashMap中的链表长度超过8时,会将链表转换为红黑树。
static final int TREEIFY_THRESHOLD = 8;

// 红黑树转链表阈值。当HashMap中的红黑树节点数量小于等于6时,会将红黑树转换为链表。
static final int UNTREEIFY_THRESHOLD = 6;

// 红黑树最小容量。当HashMap进行扩容时,如果容量小于该值,不会将红黑树转换为链表,而是直接扩容。
static final int MIN_TREEIFY_CAPACITY = 64;

// 存储键值对的数组,用于实现HashMap的底层数据结构。transient关键字表示该字段不参与序列化。
transient Node<K,V>[] table;

// 存储HashMap中的键值对的集合视图。transient关键字表示该字段不参与序列化。
transient Set<Map.Entry<K,V>> entrySet;

// HashMap中键值对的数量。
transient int size;

// HashMap结构修改的次数。用于实现fail-fast机制,以确保在迭代过程中对HashMap的修改不会导致并发异常。
transient int modCount;

// 扩容的阈值,当HashMap中的键值对数量超过此值时触发扩容操作。
int threshold;

// 加载因子,用于控制HashMap的扩容行为。加载因子越大,触发扩容的键值对数量就越小,空间利用率就越低,但查找效率较高;加载因子越小,触发扩容的键值对数量就越大,空间利用率就越高,但查找效率较低。
final float loadFactor;

}

构造方法

方法作用
HashMap()构造一个空的 HashMap ,默认初始容量(16)和默认负载系数(0.75)。
HashMap(int initialCapacity)构造一个空的 HashMap具有指定的初始容量和默认负载因子(0.75)
HashMap(int initialCapacity, float loadFactor)构造一个空的HashMap具有指定的初始容量和负载因子。
HashMap(Map<? extends K,? extends V> m)HashMap(int initialCapacity, float loadFactor)构造一个新的 HashMap与指定的相同的映射 Map`
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

/**
* 构造函数:使用指定的初始容量和加载因子创建一个HashMap实例。
*
* @param initialCapacity 初始容量
* @param loadFactor 加载因子
* @throws IllegalArgumentException 如果初始容量为负数或加载因子小于等于0或为NaN时抛出异常
*/
public HashMap(int initialCapacity, float loadFactor) {
// 校验初始容量是否合法
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 如果初始容量超过最大容量,则将初始容量设置为最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 校验加载因子是否合法
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
// 设置加载因子和阈值
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}

/**
* 构造函数:使用指定的初始容量和默认加载因子创建一个HashMap实例。
*
* @param initialCapacity 初始容量
*/
public HashMap(int initialCapacity) {
// 调用有初始容量和默认加载因子的构造函数
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
* 构造函数:使用默认的初始容量和加载因子创建一个HashMap实例。
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 其他字段使用默认值
}

/**
* 构造函数:使用指定的Map创建一个HashMap实例。
*
* @param m 要将键值对添加到HashMap中的Map
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 使用默认加载因子
putMapEntries(m, false); // 将传入的Map中的键值对加入HashMap中
}
}

静态内部类Node

静态内部类Node表示HashMap中每个键值对的节点,包含了键、值以及与下一个节点的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 哈希码,用于确定节点在数组中的位置
final K key; // 键
V value; // 值
Node<K,V> next; // 下一个节点的引用

/**
* 创建一个新的节点
*
* @param hash 节点的哈希码
* @param key 节点的键
* @param value 节点的值
* @param next 下一个节点的引用
*/
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

/**
* 获取节点的键
*
* @return 节点的键
*/
public final K getKey() {
return key;
}

/**
* 获取节点的值
*
* @return 节点的值
*/
public final V getValue() {
return value;
}

/**
* 返回节点的字符串表示形式,格式为 "key=value"
*
* @return 节点的字符串表示形式
*/
public final String toString() {
return key + "=" + value;
}

/**
* 计算节点的哈希码
*
* @return 节点的哈希码
*/
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

/**
* 设置节点的新值,并返回旧值
*
* @param newValue 新的节点值
* @return 旧的节点值
*/
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

/**
* 比较节点是否与给定对象相等
*
* @param o 要比较的对象
* @return 如果节点与给定对象相等,返回true,否则返回false
*/
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
}

常用的方法

方法名作用
size返回此地图中键值映射的数量。
isEmpty如果此地图不包含键值映射,则返回 true 。
get返回到指定键所映射的值,或null如果此映射包含该键的映射。
put将指定的值与此映射中的指定键相关联。 如果地图先前包含了该键的映射,则替换旧值。
remove从该地图中删除指定键的映射(如果存在)。
clear从这张地图中删除所有的映射。 此呼叫返回后,地图将为空。
containsKey如果此映射包含指定键的映射,则返回 true 。
keySet返回此地图中包含的键的Set视图。 该集合由地图支持,因此对地图的更改将反映在集合中,反之亦然。 如果在集合中的迭代正在进行中修改映射(除了通过迭代器自己的remov操作),迭代的结果是未定义的。 该组支持元件移除,即从映射中相应的映射,经由Iterator.remove,Set.remove,removeAll,retainAll和clear操作。 它不支持add或addAll操作。

HashMap中的put()方法

  1. 首先,put()方法接收一个键和一个值作为参数。
  2. 传入的键会通过hashCode()方法获取键的哈希值,用于确定键值对在HashMap内部数组中的位置。
  3. 如果该位置还没有任何元素,则直接将键值对存储在该位置。
  4. 如果该位置已经有元素存在(发生了哈希冲突),则遍历该位置上的链表或红黑树,找到具有相同键的节点。
  5. 如果找到具有相同键的节点,则更新该节点的值为新的值,并返回旧的值。
  6. 如果未找到具有相同键的节点,则将新的键值对添加到链表或红黑树的末尾,根据需要转换链表为红黑树。
  7. 如果插入后,链表长度超过了阈值(默认为8),则将链表转换为红黑树,以提高查找效率。
  8. 如果红黑树的节点数量达到了树化阈值(默认为6),则进行树化操作,将红黑树转换为一个更高效的结构。
  9. 如果HashMap的元素数量超过扩容阈值,将进行扩容操作,重新计算键值对的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

/**
* 将指定的键值对插入到HashMap中。
*
* @param key 要插入的键
* @param value 要插入的值
* @return 插入前该键对应的旧值,如果没有旧值则返回null
*/
public V put(K key, V value) {
// 调用putVal()方法插入键值对,并返回旧值
return putVal(hash(key), key, value, false, true);
}

/**
* 实际插入键值对的方法。
*
* @param hash 键的哈希码
* @param key 要插入的键
* @param value 要插入的值
* @param onlyIfAbsent 为true时,只有当键不存在时才插入
* @param evict 为false时,不执行删除操作
* @return 插入前该键对应的旧值,如果没有旧值则返回null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; // 声明一个Node类型的数组tab,用于存储HashMap的元素
Node<K,V> p; // 当前节点
int n, i; // n表示数组长度,i表示计算出的槽位索引

// 如果table为空或长度为0,则进行扩容操作
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;

// 计算hash对应的槽位索引,并判断该位置是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
// 如果为空则直接在该位置新建一个节点并插入
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e;
K k;
// 如果槽位上的第一个节点与当前要插入的节点key相等,则直接更新该节点的value值
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果槽位上的第一个节点是TreeNodes,则调用TreeNodes的putTreeVal()方法插入或更新节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 否则,遍历链表查找是否存在相同的key节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 没有找到相同key的节点,则在链表末尾插入新节点
p.next = newNode(hash, key, value, null);
// 如果链表长度超过TREEIFY_THRESHOLD阈值,则将链表转化为红黑树以提高查找效率
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// 如果找到相同key的节点,则退出循环
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果e不为空,则表示已经存在相同key的节点
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 如果onlyIfAbsent为false或旧值oldValue为空,则更新节点的值为新值value
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 调用afterNodeAccess()方法,用于维护访问顺序等特定操作
afterNodeAccess(e);
return oldValue;
}
}
// 更新modCount计数器,并检查是否需要进行扩容操作
++modCount;
if (++size > threshold)
resize();
// 调用afterNodeInsertion()方法,用于维护插入顺序等特定操作
afterNodeInsertion(evict);
return null;
}
}

HashMap中的get()方法

  1. 根据传入的键,调用hashCode()方法获取键的哈希值
  2. 根据哈希值,计算出在HashMap内部数组中的索引位置,在确定的索引位置上查找元素
  3. 如果该索引位置没有元素,则返回null,表示未找到对应的值。
  4. 如果该索引位置有元素,首先检查该位置上的元素是否与传入的键相等(使用equals()方法进行比较)。
  5. 如果传入的键与该位置上的键相等,说明找到了对应的值,HashMap会返回该键对应的值。
  6. 如果传入的键与该位置上的键不相等,可能存在哈希冲突,即该位置上的链表或红黑树中可能存在具有相同键但不同值的节点。
  7. 存在哈希冲突HashMap会遍历链表或红黑树,查找具有相同键的节点,如果找到则返回该节点的值。
  8. 如果遍历完链表或红黑树仍未找到具有相同键的节点,则返回null,表示未找到对应的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

/**
* 根据指定键获取对应的值。
*
* @param key 要获取值的键
* @return 与键对应的值,如果键不存在则返回null
*/
public V get(Object key) {
Node<K,V> e; // 定义节点e用于保存获取到的节点
// 调用getNode()方法获取与传入key对应的节点,并将节点的值返回,如果节点为null则返回null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
* 根据哈希值和键获取节点。
*
* @param hash 键的哈希值
* @param key 要获取节点的键
* @return 与键对应的节点,如果节点不存在则返回null
*/
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; // 定义数组tab用于保存HashMap的内部数组
Node<K,V> first, e; // 定义节点first、e用于遍历链表或红黑树
int n; // 数组的长度
K k; // 保存节点的键

// 如果数组tab不为null,且数组长度大于0,并且索引位置上存在节点
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {

// 检查第一个节点是否匹配
if (first.hash == hash && // 哈希值相等
((k = first.key) == key || (key != null && key.equals(k)))) {
return first; // 返回第一个节点
}

// 如果第一个节点不匹配,且存在后续节点
if ((e = first.next) != null) {
// 如果第一个节点是红黑树节点,则调用红黑树的getTreeNode()方法获取节点
if (first instanceof TreeNode) {
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
}
// 否则遍历链表查找匹配的节点
do {
// 如果节点的哈希值和键与传入的哈希值和键匹配,则返回节点
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
return e;
}
} while ((e = e.next) != null); // 移动到下一个节点进行判断
}
}
return null; // 未找到匹配的节点,返回null
}
}

HashMap中的resize()方法

  1. 初始化数组容量:在创建HashMap对象时,会根据指定的初始容量(或者默认值)创建一个初始数组。这个初始容量通常是2的幂,以提高哈希算法的效率。
  2. 添加元素并触发扩容:每当向HashMap中添加元素时,都会检查当前元素数量是否已经达到了扩容阈值,即当前容量与负载因子(默认为0.75)的乘积。如果达到了阈值,就会触发扩容操作。
  3. 创建新数组:触发扩容后,HashMap会创建一个更大的新数组,通常大小是原始容量的两倍。这样可以提供更多的槽位用于存储元素,减少哈希冲突的概率,从而提高性能。
  4. 重新哈希化元素:接下来,HashMap会遍历原数组中的每个元素,并重新计算它们的哈希值和在新数组中的位置。元素的哈希值通过与新数组大小减一的位与操作,得到在新数组中的索引位置。然后将元素放置到新数组的相应位置上。
  5. 处理冲突:如果多个元素计算得到相同的新索引位置,就会发生哈希冲突。HashMap使用链表或红黑树来解决冲突,将相同索引位置上的元素以链表或红黑树的方式存储起来,并保持它们在新数组中的相对顺序。
  6. 更新引用:最后,HashMap会将内部的数组引用指向新的数组,以便后续的操作可以使用新的数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {

/**
* 扩容数组并重新哈希化元素
*/
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(大于0说明数组已经初始化)
if (oldCap > 0) {
// 如果旧数组容量已达到最大容量,无需扩容,直接返回旧数组
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;// 如果是,将扩容阈值直接设置为int类型的最大数值并直接返回
return oldTab;
}

// 如果旧数组容量小于最大容量,并且大于等于默认初始容量,则将新容量翻倍,新阈值也翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // 阈值翻倍
}
// 如果旧数组容量为0,但阈值大于0,表示使用的是默认值,则设置新容量为默认初始容量,新阈值为默认初始容量乘以负载因子
else if (oldThr > 0) {
newCap = oldThr;
}
// 如果旧数组容量和阈值都为0,表示没有进行初始化操作,则将新容量设置为默认初始容量,新阈值为新容量乘以负载因子
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; // 更新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;

// 如果旧数组该位置是红黑树节点,则执行split方法拆分红黑树节点,并将拆分后的节点放入新数组相应位置
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

// 如果旧数组该位置是链表节点,则按照节点哈希值是否为0进行分组,
// 将哈希值为0的节点放入新数组相同位置,将哈希值非0的节点放入新数组位置为原索引位置加上旧数组容量的位置
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);

if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}

if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}

return newTab; // 返回新的数组
}

}

LinkedHashMap

(1)LinkedHashMap有序、key唯一可为null

(2)LinkedHashMap在HashMap存储结构的基础上,使用了一对双向链表来记录添加 元素的顺序

(3)LInkedHashMap中常用的方法

方法作用
put(K,V)添加元素
get(Object)获取指定键的元素
containsKey(Object)查询集合中是否包含指定键
remove(Object)删除指定键的元素
keyset()以set集合的形式返回所有值
size()获取集合大小
isEmpty()判断集合是否为空
clear()清空集合

TreeMap

(1)TreeMap的key值不可为null且唯一

(2)keymap的常用方法

方法作用
put(K,V)添加元素
get(Object)获取指定键的元素
containsKey(Object)查询集合中是否包含指定键
remove(Object)删除指定键的元素
keyset()以set集合的形式返回所有值
size()获取集合大小
isEmpty()判断集合是否为空
clear()清空集合
descendingMap倒序遍历

Hashtable

(1)Hashtable是个古老的 Map 实现类,JDK1.0就提供了

(2)不同于HashMap, Hashtable是线程安全的

Properties

(1)Properties 类是 Hashtable 的子类,该对象用于处理属性(配置)文件

(2)由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型

(3)存取数据时,建议使用setProperty(String key,String value)方法和 getProperty(String key)方法

(4)使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1、创建配置文件test.properties
uesr=wen
password=123456
2、读取配置文件内容
// 使用Properties类,读写配置文件
Properties properties = new Properties();

// 使用字节流,获取配置文件(配置文件名后缀为.properties)
FileInputStream fileInputStream = new FileInputStream("src/test.properties");

// load():逐行读取properties配置文件,分隔成两个字符串key和value,将他们放进Properties对象中
properties.load(fileInputStream);

// get():获取指定key对应的value
Object username = properties.get("username");
Object password = properties.get("password");
System.out.println(username);
System.out.println(password);

Iterator迭代器接口

迭代器简介

(1)迭代器是一种通用的遍历集合,取出集合中元素的方式,Java提供了Iterator迭代器接口,主要用于遍历 Collection 集合中的元素,但Iterator 本身不提供承装对象的能力。

(2)Iterator 本身不提供承装对象的能力,而是提供了一个iterator()方法,用以返回一个实现了 Iterator接口的对象,如果需要创建 Iterator 对象,则必须有一个被迭代的集合

(3)Collection接口继承了java.lang.Iterable接口,所有实现了Collection接口的集合类都有一个iterator()方法,集合对象每次调用iterator()方法都得到一个全新的迭代器对象,默认游标都在集合的第一个元素之前

常用方法

方法简介
iterator()返回迭代器对象,用于集合遍历,集合每次调用iterator()方法都得到一个全新的迭代器对象
hasNext()判断是否还有下一个元素
next()①指针下移 ②将下移以后集合位置上的元素返回
remove()内部定义了remove(),可以在遍历的时候,删除集合中的元素,此方法不同于集合中直接调用的remove()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class IteratorTest {
@Test
public void test1() {
Collection arrayList = new ArrayList();
arrayList.add("AA");
arrayList.add("BB");
arrayList.add("CC");

// iterator():返回Iterator接口的示例,用于遍历集合元素
Iterator iterator = arrayList.iterator();
//方式一:
// System.out.println(iterator.next());
// System.out.println(iterator.next());
// System.out.println(iterator.next());

//方式二:不推荐
for (int i = 0; i < arrayList.size(); i++) {
System.out.println(iterator.next());
}

//方式三:推荐
//hasNext():判断是否还有下一个元素
//next():①指针下移 ②将下移以后集合位置上的元素返回
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}

@Test
public void test2() {
Collection arrayList = new ArrayList();
arrayList.add("AA");
arrayList.add("BB");
arrayList.add("CC");

// iterator():返回Iterator接口的示例,用于遍历集合元素
Iterator iterator = arrayList.iterator();

// remove():删除集合中的CC,不同于集合中的方法remove()
while (iterator.hasNext()) {
Object obj = iterator.next();
if ("CC".equals(obj)) {
iterator.remove();
}
}

// 遍历集合
Iterator i = arrayList.iterator();
while (i.hasNext()) {
System.out.println(i.next());
}
}
}

排序相关接口

Java排序方案说明

在Java中,对象通常使用==和!=来判断对象是否相等或不相等,但在开发场景中,有时需要对多个对象进行排序或比较大小,Java提供了两种解决方案:

  • 实现Comparable接口:用于定义对象的自然排序规则。
  • 实现Comparator接口:用于定义对象的定制排序规则。

Comparable接口(自然排序)

Comparable接口是Java中定义的一个接口,用于定义对象之间的自然排序规则。对于自定义类来说,如果需要进行排序,可以让自定义类实现Comparable接口并重写compareTo方法,在其中指定如何进行排序。对于String类和常见的包装类,内部已经实现了Comparable接口,并提供了默认的排序规则。

Comparator接口(定制排序)

Comparator接口是Java中定义的一个接口,用于支持对象之间的定制排序规则。相对于实现Comparable接口,使用Comparator接口可以在不修改原始类的情况下定义不同的比较规则。使用场景如下

  • 当元素的类型没有实现Comparable接口,且不能方便地修改代码时;
  • 当元素的类型已经实现了Comparable接口,但该实现的排序规则不适合当前的操作时。

Comparable接口与Comparator接口的区别

ComparableComparator是Java中用于排序的两个接口,它们有以下区别:

区别一:实现方式不同

  • Comparable接口:在被比较的类本身实现,比较当前对象与另一个对象的大小关系,使对象自身具备比较的能力
  • Comparator接口:不在被比较的类本身实现,在外部单独定义比较器,比较两个独立的对象之间的大小关系

区别二:使用范围不同

  • Comparable接口:只能对实现了Comparable接口的对象进行排序
  • Comparator接口:可以对任意的对象进行比较和排序,不管对象有没有实现Comparable接口都可以进行排序

区别三:排序方式不同

  • Comparable接口:只能利用默认的比较规则实现自然排序
  • Comparator接口:可以根据需要定义不同的比较规则进行排序

排序案例

使Goods对象按价格从低到高排序

创建Goods类实现Comparable接口重写compareTo(T o)方法,返回值有以下三种情况:

  • 返回正整数:比较者大于被比较者,当前对象(this)大于形参对象(obj)
  • 返回负整数:比较者小于被比较者,当前对象(this)小于形参对象(obj)
  • 返回零:比较者等于被比较者,当前对象(this)等于形参对象(obj)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Data
@NoArgsConstructor
@AllArgsConstructor
class Goods implements Comparable {
private String name;
private double price;

@Override
public int compareTo(Object o) {
if (o instanceof Goods) {
Goods goods = (Goods) o;
// 自定义比较规则,按照价格从低到高进行排序
if (this.price > goods.price) {
return 1; // 当前对象的价格大于传入对象的价格,返回正数
} else if (this.price < goods.price) {
return -1; // 当前对象的价格小于传入对象的价格,返回负数
} else {
// 当价格相等时,按照商品名称的字母顺序进行比较,使用负数表示当前商品名称在字母顺序中应该排在传入对象之前
return -this.name.compareTo(goods.name);
}
}
// 抛出异常,表示输入的数据类型不是Goods类
throw new RuntimeException("输入的数据类型不一致");
}

}

调用Collections.sort或Arrays.sort等方法,对对象进行排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {
public static void main(String[] args) {
Goods goods1 = new Goods("苹果", 2.5);
Goods goods2 = new Goods("香蕉", 1.8);
Goods goods3 = new Goods("橙子", 3.2);
Goods[] goods = new Goods[]{goods1,goods2,goods3};// 创建数组
List<Goods> goodsList = new ArrayList<>();// 创建集合
goodsList.add(goods1);
goodsList.add(goods2);
goodsList.add(goods3);

// 数组排序
System.out.println("未排序:"+Arrays.toString(goods));// 未排序:[Goods(name=苹果, price=2.5), Goods(name=香蕉, price=1.8), Goods(name=橙子, price=3.2)]
Arrays.sort(goods);// 对数组进行自然排序
System.out.println("已排序:"+Arrays.toString(goods));// 已排序:[Goods(name=香蕉, price=1.8), Goods(name=苹果, price=2.5), Goods(name=橙子, price=3.2)]

// 集合排序
System.out.println("未排序:" + goodsList);// 未排序:[Goods(name=苹果, price=2.5), Goods(name=香蕉, price=1.8), Goods(name=橙子, price=3.2)]
Collections.sort(goodsList);// 对集合进行自然排序
System.out.println("已排序:" + goodsList);// 已排序:[Goods(name=香蕉, price=1.8), Goods(name=苹果, price=2.5), Goods(name=橙子, price=3.2)]
}
}

使Goods对象按价格从高到低排序

创建Goods类,不实现Comparable接口

1
2
3
4
5
6
7
@Data
@NoArgsConstructor
@AllArgsConstructor
class Goods {
private String name;
private double price;
}

在进行排序时,可以通过调用Collections.sort或Arrays.sort等方法,并传入相应的Comparator对象来指定比较规则,这样就能根据特定需求对对象进行定制排序。Comparator接口定义了compare方法,该方法接收两个对象作为参数,并返回一个整数值来表示比较结果,整数值有以下含义:

  • 返回正整数:表示第一个对象(o1)大于第二个对象(o2)。
  • 返回负整数:表示第一个对象(o1)小于第二个对象(o2)。
  • 返回零:表示第一个对象(o1)等于第二个对象(o2)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class Main {
public static void main(String[] args) {
Goods goods1 = new Goods("苹果", 2.5);
Goods goods2 = new Goods("香蕉", 1.8);
Goods goods3 = new Goods("橙子", 3.2);
Goods[] goods = new Goods[]{goods1, goods2, goods3};// 创建数组
List<Goods> goodsList = new ArrayList<>();// 创建集合
goodsList.add(goods1);
goodsList.add(goods2);
goodsList.add(goods3);

// 数组排序
System.out.println("未排序:" + Arrays.toString(goods));// 未排序:[Goods(name=苹果, price=2.5), Goods(name=香蕉, price=1.8), Goods(name=橙子, price=3.2)]
// 使用Comparator接口实现自定义排序规则来对数组进行排序
Arrays.sort(goods, new Comparator() {
/**
* 自定义比较方法,按照价格从高到低进行排序
* @param o1 第一个对象
* @param o2 第二个对象
* @return 返回负数表示第一个对象排在前面,返回正数表示第二个对象排在前面
*/
@Override
public int compare(Object o1, Object o2) {
Goods g1 = (Goods) o1;
Goods g2 = (Goods) o2;
// 自定义比较规则,按照价格从高到低进行排序
if (g1.getPrice() > g2.getPrice()) {
return -1;
} else if (g1.getPrice() < g2.getPrice()) {
return 1;
} else {
// 如果价格相同,则按照商品名称的字典顺序进行排序
return g1.getName().compareTo(g2.getName());
}
}
});
System.out.println("已排序:" + Arrays.toString(goods));// 已排序:[Goods(name=橙子, price=3.2), Goods(name=苹果, price=2.5), Goods(name=香蕉, price=1.8)]

// 集合排序
System.out.println("未排序:" + goodsList);// 未排序:[Goods(name=苹果, price=2.5), Goods(name=香蕉, price=1.8), Goods(name=橙子, price=3.2)]
// 使用Comparator接口实现自定义排序规则来对集合进行排序
Collections.sort(goodsList, new Comparator() {
/**
* 自定义比较方法,按照价格从高到低进行排序
* @param o1 第一个对象
* @param o2 第二个对象
* @return 返回负数表示第一个对象排在前面,返回正数表示第二个对象排在前面
*/
@Override
public int compare(Object o1, Object o2) {
Goods g1 = (Goods) o1;
Goods g2 = (Goods) o2;
// 自定义比较规则,按照价格从高到低进行排序
if (g1.getPrice() > g2.getPrice()) {
return -1;//
} else if (g1.getPrice() < g2.getPrice()) {
return 1;//
} else {
// 如果价格相同,则按照商品名称的字典顺序进行排序
return g1.getName().compareTo(g2.getName());
}
}
});
System.out.println("已排序:" + goodsList);// 已排序:[Goods(name=橙子, price=3.2), Goods(name=苹果, price=2.5), Goods(name=香蕉, price=1.8)]
}
}

序列化与反序列化

什么是序列化与反序列化?

在 Java 中,序列化和反序列化是一种用于对象与字节流互相转换,以便存储或传输的机制。

  • 序列化:对象—>字节序列
  • 反序列化:字节序列—>对象

什么情况下需要序列化与反序列化?

当 Java 对象需要在网络传输或持久化存储到文件中时,需要序列化

  • 网络传输:当需要将 Java 对象在网络上进行传输时,可以使用序列化将对象转换为字节流,然后通过网络发送给接收方。例如,在客户端和服务器之间进行通信时,可以将请求或响应对象序列化并通过网络传输。这样,对象的信息就可以在不同的计算机或进程之间传递,实现分布式系统的协作。
  • 持久化存储:当需要将 Java 对象保存到磁盘或数据库中以供以后使用时,可以使用序列化将对象写入文件或数据库字段。序列化后的对象可以被存储,稍后可以从存储位置读取并恢复为原始对象。这种持久化存储方式适用于需要长期保留对象状态的情况,比如保存用户配置、缓存数据、日志等。

序列化与反序列化有什么前提?

在 Java 中,让一个对象可序列化与反序列化,需要满足以下要求:

  1. 实现 Serializable 接口:将要序列化的类实现 Serializable 接口,Serializable 接口是 Java 提供的一个标记接口,接口中没有任何方法定义,用于标识一个类可以被序列化和反序列化。
  2. 提供 private static final long serialVersionUID 常量:这个常量用于表示类的版本号,它在反序列化时用于判断对象和字节流之间的兼容性。如果没有显式地定义该字段,Java 编译器会自动生成一个版本号。
  3. 内部所有属性也必须是可序列化的:将类的内部所有属性都标记为可序列化,这意味着它们要么是基本数据类型(如 intboolean 等),要么是实现了 Serializable 接口的类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Data
@NoArgsConstructor
@AllArgsConstructor
class Person implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private transient int age;
}
class SerializationExample {
public static void main(String[] args) {
// 创建一个Person对象
Person person = new Person(1L, "Alice", 25);

// 将对象序列化到文件
serializeObject(person, "person.ser");

// 从文件中反序列化对象
Person deserializedPerson = (Person) deserializeObject("person.ser");

// 打印反序列化后的对象信息
System.out.println("名字: " + deserializedPerson.getName());
System.out.println("年龄: " + deserializedPerson.getAge());
}

// 序列化对象到文件
private static void serializeObject(Object obj, String filename) {
try {
FileOutputStream fileOut = new FileOutputStream(filename);
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(obj);
out.close();
fileOut.close();
System.out.println("序列化的对象并保存到:" + filename);
} catch (IOException e) {
e.printStackTrace();
}
}

// 从文件中反序列化对象
private static Object deserializeObject(String filename) {
try {
FileInputStream fileIn = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(fileIn);
Object obj = in.readObject();
in.close();
fileIn.close();
System.out.println("反序列化的对象: " + filename);
return obj;
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
return null;
}
}
}

如果某些数据不想序列化与反序列化怎么办?

方式一:使用transient关键字

在Java中,transient是一个关键字,用来修饰类的成员变量。当一个变量被声明为transient时,它表示该变量不参与对象的序列化过程,将被跳过,不会被序列化,通常用于一些敏感信息或者不需要被序列化和传输的数据

1
2
3
4
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name; // 使用transient关键字标识name字段不能被序列化与反序列化
}

方式二:自定义序列化方式

你可以实现Serializable接口,并在其中自定义序列化和反序列化过程,通过编写writeObject()和readObject()方法来控制序列化和反序列化的行为,在这些方法中可以决定哪些数据要被序列化,哪些数据要被忽略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
@NoArgsConstructor
@AllArgsConstructor
class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;

// 自定义序列化方法
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // 默认的序列化操作
out.writeObject(name.toUpperCase()); // 将 name 字段以大写形式写入输出流
}

// 自定义反序列化方法
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // 默认的反序列化操作
name = ((String) in.readObject()).toLowerCase(); // 从输入流中读取字符串,并将其转换为小写形式
}
}

方式三:使用Externalizable接口自定义序列化方式

Externalizable接口是Java提供的一个用于实现自定义序列化的接口,相比Serializable接口更加灵活,使用步骤如下

  1. 在类上实现Externalizable接口,并实现writeExternal()readExternal()方法。
  2. writeExternal()方法中定义需要序列化的成员变量。
  3. readExternal()方法中定义如何从流中恢复成员变量的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
@NoArgsConstructor
@AllArgsConstructor
class Person implements Externalizable {
private String name;

@Override
public void writeExternal(ObjectOutput out) throws IOException {
// 自定义序列化过程,在这里定义哪些成员变量需要序列化
out.writeObject(name);// // 将name字段手动写入到输出流中
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
// 自定义反序列化过程,在这里恢复成员变量的值
name = (String) in.readObject();// // 从输入流中手动读取name字段,并赋值给对象的相应字段
}
}