Java集合类
- 一、集合
- [二、 Collection集合与Iterator迭代器](#二、 Collection集合与Iterator迭代器)
- 三、单列集合List
-
- [3.1 List接口](#3.1 List接口)
-
- [3.1.1 结构的特点](#3.1.1 结构的特点)
- [3.1.2 常用的方法](#3.1.2 常用的方法)
- [3.1.3 遍历](#3.1.3 遍历)
- [3.2 实现类](#3.2 实现类)
-
- [3.2.1 ArrayList](#3.2.1 ArrayList)
- [3.2.2 LinkedList](#3.2.2 LinkedList)
- [3.3 对比](#3.3 对比)
- 四、单列集合Set
-
- [4.1 Set接口](#4.1 Set接口)
-
- [4.1.1 常用方法](#4.1.1 常用方法)
- [4.1.2 遍历方式](#4.1.2 遍历方式)
- [4.2 实现类](#4.2 实现类)
-
- [4.2.1 HashSet](#4.2.1 HashSet)
-
- [HashSet 底层实现总结](#HashSet 底层实现总结)
-
- [1. 哈希表的基本特性](#1. 哈希表的基本特性)
- [2. HashSet 的底层实现](#2. HashSet 的底层实现)
- [3. 哈希值和碰撞](#3. 哈希值和碰撞)
- [4. 关键方法](#4. 关键方法)
- [5. 常见问题解答](#5. 常见问题解答)
- 总结
- [4.2.2 LinkedHashSet](#4.2.2 LinkedHashSet)
- [4.2.3 TreeSet](#4.2.3 TreeSet)
- [4.2.4 红黑树的特点和结构描述](#4.2.4 红黑树的特点和结构描述)
- 五、双列集合Map
-
- [5.1 Map接口](#5.1 Map接口)
-
- [5.1.1 特点](#5.1.1 特点)
- [5.1.2 常见API方法](#5.1.2 常见API方法)
- [5.1.3 遍历](#5.1.3 遍历)
-
- [1. 使用 `keySet()` 方法](#1. 使用
keySet()
方法) - [2. 使用 `entrySet()` 方法](#2. 使用
entrySet()
方法) - [3. 使用 Lambda 表达式和 `forEach()` 方法](#3. 使用 Lambda 表达式和
forEach()
方法)
- [1. 使用 `keySet()` 方法](#1. 使用
- [5.2 HashMap -- 效率最高](#5.2 HashMap -- 效率最高)
- [5.3 LinkedHashMap -- 保证存取顺序](#5.3 LinkedHashMap -- 保证存取顺序)
- [5.4 TreeMap -- 进行排序](#5.4 TreeMap -- 进行排序)
一、集合
蓝色框的是实现类,其他是其集合框架的接口。最下的是迭代器
1.1 集合和数组之间的对比
都是用于存储数据的容器
- 大小
数组:数组是固定大小的。一旦创建数组,其大小就不能改变。
集合:集合是动态大小的。根据需要,可以增加或减少元素。
java
int[] array = new int[10]; // 长度为 10 的数组
ArrayList<Integer> list = new ArrayList<>(); // 动态大小的列表
list.add(1);
list.add(2);
- 类型
数组:数组可以存储基本数据类型和对象。它们是类型特定的。
集合:集合只能存储对象。不能直接存储基本数据类型,但可以使用其包装类。
java
int[] intArray = {1, 2, 3}; // 存储整数
String[] strArray = {"A", "B", "C"}; // 存储字符串
ArrayList<Integer> intList = new ArrayList<>(); // 存储整数对象
- 操作
数组:对数组的操作通常涉及索引操作。添加和删除元素相对复杂。
集合:集合提供了许多便捷的方法来添加、删除、和操作元素。
java
int[] array = {1, 2, 3};
int element = array[1]; // 访问元素
array[1] = 5; // 修改元素
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 添加元素
list.remove(0); // 删除元素
int element = list.get(0); // 访问元素
- 性能
数组:数组在访问和修改元素时性能较高,因为它们是连续的内存块。
集合 :集合的性能取决于具体实现。ArrayList
在随机访问时性能较高,但在插入和删除中间元素时性能较低。
java
int element = array[5]; // 直接访问,时间复杂度为 O(1)
int element = list.get(5); // 访问元素,时间复杂度为 O(1)
list.add(0, 1); // 插入元素,时间复杂度为 O(n)
- 灵活性
数组:数组适合在大小已知且固定的情况下使用
集合:集合更灵活,适合处理大小动态变化的数据集合。
- 泛型支持
数组:不支持泛型,但可以使用特定类型的数组。
集合:集合支持泛型,提供类型安全的集合操作。
java
Integer[] intArray = new Integer[10];
ArrayList<String> list = new ArrayList<>();
list.add("A");
1.2 集合框架的核心接口
-
Collection接口:这是所有集合的根接口,但它并不直接提供实现。Java集合框架中的大多数集合类都实现了这个接口。常见的子接口包括List、Set和Queue、。
-
List接口:一个有序的集合,允许重复元素。实现类包括ArrayList、LinkedList等。
元素是有序的、可重复的、有索引的
-
Set接口:一个不允许重复元素的集合。实现类包括HashSet、TreeSet等。
元素是无序的、唯一的、无索引的
-
Queue接口:一种用于按特定顺序(通常是FIFO,先进先出)处理元素的集合。实现类包括LinkedList、PriorityQueue等。
-
-
Map接口:键值对集合,键唯一,值可以重复。实现类包括HashMap、TreeMap等。
-
**迭代器(Iterator)**是一个用于遍历集合中元素的接口,提供了以下基本操作:
hasNext()
:判断是否还有下一个元素。next()
:返回下一个元素。remove()
:移除迭代器返回的最后一个元素(可选操作)。
1.3 集合框架中的实现类
单列集合
-
ArrayList
:动态数组,支持快速随机访问。适合于查找操作频繁的场景。适用场景:查找操作频繁,插入和删除操作较少的场景。
-
LinkedList
:双向链表,支持快速插入和删除操作。适合于频繁插入和删除的动作快,但随机访问性能较差。适用场景:频繁插入和删除操作的场景
-
HashSet
:基于哈希表的实现,不保证元素的顺序,适合于快速查找和删除操作。适用场景:需要快速查找和不关心元素顺序的场景
-
TreeSet
:基于红黑树的实现,元素有序,适合于需要有序元素的场景。适用场景:需要有序元素的场景。
双列集合
-
HashMap
:基于哈希表的实现,支持快速插入和查找操作。键值对无序。适用场景:需要快速查找且不关心顺序的场景。
-
TreeMap
:基于红黑树的实现,键值对有序。适合于需要按键排序的场景适用场景:需要按键排序的场景。
-
LinkedHashMap
:基于哈希表和链表的实现,维护了键值对的插入顺序。适合于需要保持插入顺序的场景。适用场景:需要维护插入顺序或访问顺序的场景.
1.4 集合框架的特点
- 接口和实现的分离:集合框架通过接口(如List、Set、Map)定义操作集合的标准方法,不同的实现类提供具体的实现。
- 支持泛型:集合框架中的类都是泛型类,可以在编译时检查类型安全,减少运行时错误。
- 丰富的操作方法:集合框架提供了丰富的操作方法,如添加、删除、遍历、排序等,简化了开发过程。
- 高效的算法实现:集合框架中的类大多采用高效的算法和数据结构,如哈希表、红黑树、链表等,保证了操作的高效性。
二、 Collection集合与Iterator迭代器
2.1 Collection的概述
Collection 是单例集合的顶层接口,可为所有的单列集合提供继承使用。
Collection
是一个接口,不能直接创建对象。只能创建其实现类的对象.例如ArrayList
2.1.1 常用方法
增加元素的方法
方法名 | 说明 | 注意点 |
---|---|---|
boolean add(E e) | 向集合中添加一个元素 | 若操作的集合不允许 添加重复元素如 Set ,添加相同元素会返回 false 。 |
boolean addAll(Collection<? extends E> c) | 向集合中添加另一个集合中的所有元素 |
修改元素的方法
Collection
接口本身不直接提供修改元素的方法,但通过删除旧元素并添加新元素,可以实现修改的效果。
删除元素的方法
方法名 | 说明 | 注意点 |
---|---|---|
void clear() | 移除集合中的所有元素 | 操作不可逆 |
boolean remove(Object o) | 从集合中删除指定的元素 | 如果集合中有多个相同元素,只删除第一个匹配的;不存在的元素,返回 false 。 |
boolean removeAll(Collection<?> c) | 从集合中删除包含在指定集合中的所有元素 | 如果传入的集合包含不在原集合中的元素,原集合不变 |
boolean retainAll(Collection<?> c) | 仅保留集合中包含在指定集合中的元素 | 此操作会改变集合,保留指定集合中的元素,删除其他元素 |
查询元素的方法
方法名 | 说明 | 注意点 |
---|---|---|
boolean contains(Object o) | 判断集合中是否包含指定的元素 | 依赖于元素的 equals 方法 |
boolean containsAll(Collection<?> c) | 判断集合中是否包含指定集合中的所有元素 | 依赖于集合中元素的 equals 方法 |
boolean isEmpty() | 判断集合是否为空,则返回true | |
int size() | 返回集合中元素的数量 |
public boolean contains(Object o)
:判断是否包含指定元素,其底层以equals方法进行判断是否合法。
所以,集合中存储的自定义对象,也想通过contains方法来判断是否合法,在javabean中,一定要重写equals的方法
如果存的是自定义对象,没有重写equals方法,那么默认使用object类中的equals方法进行判断,而object类中equals方法,依赖地址值进行判断。
遍历集合的方法
方法名 | 说明 | 注意点 |
---|---|---|
Iterator iterator() | 返回集合中元素的迭代器,用于遍历集合 | 使用迭代器时,不要修改集合(避免 ConcurrentModificationException ) |
Object[] toArray() | 返回包含集合中所有元素的数组 | 数组的运行时类型是 Object[] |
T[] toArray(T[] a) | 返回包含集合中所有元素的数组,数组的运行时类型由传入的数组参数决定 | 如果传入的数组大小不足,方法会创建一个新数组 |
示例代码
java
import java.util.*;
public class CollectionExample {
public static void main(String[] args) {
// 创建一个ArrayList,ArrayList实现了Collection接口
Collection<String> collection = new ArrayList<>();
// 增加元素
collection.add("Apple");
collection.add("Banana");
collection.add("Cherry");
// 查询元素
System.out.println("是否包含Apple: " + collection.contains("Apple"));
System.out.println("collection集合的大小: " + collection.size());
// 遍历集合
for (String fruit : collection) {
System.out.println(fruit);
}
// 删除元素
collection.remove("Banana");
System.out.println("移除Banana后,collection集合的大小 : " + collection.size());
// 清空集合
collection.clear();
System.out.println("Collection集合是否为空: " + collection.isEmpty());
}
}
2.2 集合遍历
2.2.1 Iterator迭代器
遍历的时候,不能用集合的方式进行增加减少.
最终循环结束,指针会指向没有元素的位置,即二次遍历的时,只能再次获取一个新的迭代器对象。
方法名 | 说明 |
---|---|
hasNext() |
判断是否还有下一个元素,如果有返回 true ,否则返回 false 。 |
next() |
返回迭代的下一个元素。如果没有下一个元素,则抛出 NoSuchElementException 异常。 |
remove() |
从迭代器指向的集合中移除调用 next() 方法返回的最后一个元素。在每次调用 next() 之前只能调用一次此方法,否则抛出 IllegalStateException 异常。 |
java
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
public class IteratorExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
Iterator<String> it = list.iterator(); // 创建了指针
while(it.hasNext()) {
String str = it.next();
System.out.println(str);
}
}
}
注意事项:
- 如果调用
next
方法,但集合中没有元素,会抛出NoSuchElementException
异常。 - 迭代完毕,指针不会复位(即二次遍历时需要重新获取一个新的迭代器对象)。
- 循环中只能用一次
next
方法(每次调用next
方法会移动指针,多次调用可能导致NoSuchElementException
异常)。 - 遍历过程中,不能使用集合的方法进行增加或减少元素,否则会抛出
ConcurrentModificationException
异常。如果需要删除元素,应使用迭代器的方式删除。
2.2.2 增强for循环
格式
java
for(元素类型 变量名 : 集合或者数组) {
// 操作
}
增强for语句中的变量,不会改变集合中原本的数据(只是赋值给了一个第三方变量)
java
import java.util.List;
import java.util.ArrayList;
public class EnhancedForExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
//增强for语句中的变量,不会改变集合中原本的数据
for(String str : list) {
System.out.println(str);
}
}
}
2.2.3 Lambda表达式(JDK 8引入)遍历
格式:
集合.forEach(元素类型 变量名 -> {
// 操作
});
java
import java.util.List;
import java.util.ArrayList;
public class LambdaExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
list.forEach(str -> {
System.out.println(str);
});
}
}
- Lambda表达式中的变量,不会改变集合中原本的数据(只是赋值给了一个第三方变量)。
- 不能使用
final
修饰,因为Lambda表达式中的变量不是局部变量,而是集合中的元素。
三、单列集合List
3.1 List接口
List
接口是Java集合框架中的一个重要接口,继承自Collection
接口。它表示一个有序的集合,允许重复的元素,并且提供了基于索引的访问方式。
3.1.1 结构的特点
-
有序:存储和取出的元素顺序一致
-
可重复:存储的元素可以重复
-
有索引:可以通过索引操作元素
3.1.2 常用的方法
方法名 | 说明 |
---|---|
void add(E e) |
增:将指定的元素追加到此列表的末尾。 |
boolean add(int index, E e) |
增:在此列表中的指定位置插入指定的元素。 |
E remove(int index) |
删:移除此列表中指定位置上的元素。 |
boolean remove(Object o) |
删:移除此列表中首次出现的指定元素(如果存在)。 |
E get(int index) |
查:返回此列表中指定位置上的元素。 |
E set(int index, E e) |
改:用指定的元素替代此列表中指定位置上的元素。 |
int indexOf(Object o) |
查:返回此列表中第一次出现的指定元素的索引,如果列表不包含此元素,则返回-1。 |
int lastIndexOf(Object o) |
查:返回此列表中最后一次出现的指定元素的索引,如果列表不包含此元素,则返回-1。 |
List<E> subList(int fromIndex, int toIndex) |
查:返回此列表中指定的fromIndex (包括)和toIndex (不包括)之间的部分视图。 |
void clear() |
删:移除此列表中的所有元素。 |
int size() |
查:返回此列表中的元素数。 |
boolean isEmpty() |
查:如果此列表不包含元素,则返回true 。 |
boolean contains(Object o) |
查:如果此列表包含指定的元素,则返回true 。 |
Java中的List
只能存储对象类型的元素,所以在使用remove(Object o)
方法时,
基本数据类型(如int
)需要先进行装箱(自动装箱或手动装箱),转成对应的对象类型(如Integer
)。
3.1.3 遍历
列表迭代器遍历(可在遍历过程中添加元素)
java
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
String element = listIterator.next();
if ("b".equals(element)) {
listIterator.add("d"); // 在元素 "b" 之后添加元素 "d"
}
}
System.out.println(list); // 输出: [a, b, d, c]
-
迭代器遍历:适合在遍历过程中删除元素,可避免并发修改异常。
如果需要在遍历时修改元素,使用迭代器遍历可能更安全,因为它可以确保在修改操作中不会发生并发修改异常。
-
增强for循环遍历:仅用于遍历,不进行修改操作(只读)。
不能修改集合结构,否则会引发
ConcurrentModificationException
。 -
普通for循环遍历:适合在遍历过程中直接访问操作。
- 操作索引,需注意删除时的集合结构变化。
在遍历过程中需要删除元素,需要注意避免对集合进行结构修改。需要使用适当的同步机制。
-
Lambda表达式:用于简单遍历,不进行修改操作。如果需要删除元素,请使用迭代器。
不能直接修改集合,本身不提供线程安全保障,需要外部同步机制保证线程安全。
-
列表迭代器遍历:适合在遍历过程中添加元素,通过列表迭代器进行操作。
非常适合需要动态修改列表的场景。
ListIterator
允许双向遍历,可以使用next()
和previous()
方法在列表中前后移动。
3.2 实现类
3.2.1 ArrayList
ArrayList
的底层原理是基于数组的动态数组,可以动态地增加和减少元素。当添加元素时,如果数组容量不足,ArrayList
会自动扩容,扩容后的数组容量通常是原来的 1.5 倍。删除元素时,后面的元素会被向前移动以填补被删除元素的位置。
ArrayList
的查找操作的时间复杂度是 O(1),因为可以直接通过索引访问元素。插入操作在末尾时也是 O(1),但在中间位置插入时需要移动后续元素,因此时间复杂度是 O(n)。删除操作的时间复杂度是 O(n),因为需要将后续元素向前移动。
总体来说,
ArrayList
适用于频繁查找和在末尾添加元素的场景,而在频繁的插入和删除操作(特别是在中间或开头位置)场景下效率不如其他数据结构(如LinkedList
)。
- 初始化和底层数组
- 当你使用无参构造函数创建一个
ArrayList
时,它会创建一个默认长度为 0 的底层数组elementData
。这个数组在初始化时不分配实际存储空间。 size
变量用于记录当前ArrayList
中的元素个数以及下一个元素应该插入的位置。
- 当你使用无参构造函数创建一个
- 第一次添加元素
- 当你第一次向
ArrayList
中添加元素时,底层会创建一个新的长度为 10 的数组来存储这些元素。这个长度为 10 的数组是ArrayList
的初始容量。如果之前没有分配空间,添加元素后才会创建并分配新的数组。
- 当你第一次向
- 自动扩容
ArrayList
在添加元素时,如果当前数组的容量不足以存储新的元素,它会自动扩容。扩容通常是将数组容量增加到原来的 1.5 倍(即原数组容量 × 1.5)。这种扩容策略使得在大量元素添加时,ArrayList
能够动态调整大小,以适应新的元素需求。
- 批量添加元素
- 当批量添加多个元素时,如果扩容的 1.5 倍容量仍然不足以容纳所有新元素,
ArrayList
会创建一个足够大的新数组来满足实际需求。这个新数组的大小将基于实际需要来计算,而不是简单地依赖于 1.5 倍的扩容策略。
- 当批量添加多个元素时,如果扩容的 1.5 倍容量仍然不足以容纳所有新元素,
方法名 | 说明 |
---|---|
add(E e) |
添加一个元素 e 到 ArrayList 中,调用内部方法处理实际添加逻辑。 |
add(e, elementData, size) |
实际执行添加元素的操作,检查数组是否满,如果满则调用扩容方法。 |
grow(int minCapacity) |
扩容方法,计算新数组的容量,并分配新数组以容纳更多元素。 |
newLength(int oldLength, int minGrowth, int prefGrowth) |
计算新的数组长度,以便根据实际需要增加容量。返回新数组的长度。 |
这些方法共同作用,确保 ArrayList
在添加单个或多个元素时能够正确地管理其底层数组的大小和内容。
属性名 | 说明 |
---|---|
elementData |
底层数组,用于存储 ArrayList 的元素。初始时为空,随着元素的添加而扩容。 |
size |
当前 ArrayList 中实际存储的元素个数。增加或删除元素时更新。 |
modCount |
记录 ArrayList 结构修改的次数,用于检测并发修改情况。 |
DEFAULT_CAPACITY |
ArrayList 的默认初始容量,通常为 10。当初次创建 ArrayList 时使用。 |
DEFAULTCAPACITY_EMPTY_ELEMENTDATA |
elementData 的默认空数组,用于初始化 ArrayList 时的占位。 |
SOFT_MAX_ARRAY_LENGTH |
预设的最大数组长度限制,通常非常大,用于防止数组长度超过某个阈值。 |
3.2.2 LinkedList
LinkedList
适合用于需要频繁插入和删除操作的场景,因为其双向链表结构可以高效地完成这些操作。然而,由于需要遍历链表来查找元素,因此查询操作的效率相对较低。
javaLinkedList<String> list = new LinkedList<>();
(1)底层数据结构
-
双向链表
LinkedList
的底层是一个双向链表。每个节点包含以下三个部分:- 数据域:存储节点的实际数据。
- 前驱指针:指向链表中的前一个节点。
- 后继指针:指向链表中的后一个节点。
这种双向链表结构允许在链表的任意位置快速地进行插入和删除操作,因为每个节点都知道其前驱和后继节点,从而可以在不需要遍历整个链表的情况下进行操作。
-
LinkedList
双向链表的数据结构图,其中包含多个节点以及它们之间的data
、next
和previous
关系。[data1] <-> [data2] <-> [data3]
每个节点包含的数据结构如下:
data
: 节点存储的数据next
: 指向下一个节点previous
: 指向上一个节点
具体的结构图如下:
plaintext
+-------+ +-------+ +-------+
null <--- | | <--> | | <--> | | ---> null
| data1 | | data2 | | data3 |
| | | | | |
| next | ----> | next | ----> | next |
| prev | <---- | prev | <---- | prev |
+-------+ +-------+ +-------+
- 第一个节点 (
data1
):
data
: 存储的数据为data1
next
: 指向下一个节点 (data2
)previous
: 指向null
,因为它是链表的头节点
- 第二个节点 (
data2
):
data
: 存储的数据为data2
next
: 指向下一个节点 (data3
)previous
: 指向上一个节点 (data1
)
- 第三个节点 (
data3
):
data
: 存储的数据为data3
next
: 指向null
,因为它是链表的尾节点previous
: 指向上一个节点 (data2
)通过这种双向链表结构,每个节点都可以通过
next
指针访问下一个节点,通过previous
指针访问上一个节点,从而实现高效的插入和删除操作。
(2)性能特点
-
查询操作较慢:由于链表的查询操作需要从头节点或尾节点开始逐步遍历链表,以找到指定的位置或元素,因此查询操作的时间复杂度为 O(n),其中 n 是链表的长度。
-
增删操作较快:由于链表的插入和删除操作只需调整指针(前驱和后继指针),不需要移动其他元素,因此这些操作的时间复杂度通常为 O(1),前提是已经获取到插入或删除位置的节点。
(3)常用方法
方法名 | 说明 |
---|---|
void addFirst(E e) |
在链表的开头插入指定的元素 e 。 |
void addLast(E e) |
在链表的末尾插入指定的元素 e 。 |
E removeFirst() |
移除并返回链表的第一个元素。 |
E removeLast() |
移除并返回链表的最后一个元素。 |
E pollLast() |
移除并返回链表的最后一个元素,如果链表为空则返回 null 。 |
E getFirst() |
返回链表的第一个元素,但不移除它。 |
E getLast() |
返回链表的最后一个元素,但不移除它。 |
E peekFirst() |
返回链表的第一个元素,但不移除它。如果链表为空则返回 null 。 |
E peekLast() |
返回链表的最后一个元素,但不移除它。如果链表为空则返回 null 。 |
ListIterator<E> listIterator(int index) |
返回一个从指定位置开始的列表迭代器。 |
Iterator<E> descendingIterator() |
返回一个反向迭代器,用于从链表的末尾向开头遍历。 |
(4)添加元素的机制
链表作为一个有序集合,每个对象的位置固然重要。,add方法会时期添加到链表的尾部。
-
LinkedList
使用双向链表结构实现,添加元素时通过linkLast
方法将新节点连接到链表的末尾。 -
每次添加元素,都需要更新链表的
last
指针和可能的first
指针(如果链表为空时)。
add
方法的实现:
java
public boolean add(E e) {
linkLast(e);
return true;
}
linkLast
方法的实现:
java
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
节点类 Node
的实现:
java
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Node
类定义了双向链表的节点结构,包括item
存储元素,next
指向下一个节点,prev
指向前一个节点。- 构造方法
Node(Node<E> prev, E element, Node<E> next)
初始化节点的数据和指针。
3.3 对比
在Java中,ArrayList
和 LinkedList
是两种常见的集合实现,它们各有特点,适用于不同的应用场景。
ArrayList
优点:
- 随机访问和更新高效:通过索引可以快速访问和更新元素,时间复杂度为 O(1)。
- 顺序添加和删除尾部元素高效:在末尾添加元素的时间复杂度为 O(1)。
- 内存占用相对紧凑:使用连续的内存块存储元素,适合存储大量基本数据类型或对象。
缺点:
- 插入和删除中间元素效率低:在中间或开头添加或删除元素时,需要移动后续元素,时间复杂度为 O(n)。
- 可能产生内存碎片:在存储大对象或大量元素时,可能会产生较多的内存碎片。
适用场景:
- 需要频繁随机访问和更新元素的场景。
- 对内存使用有一定要求,不希望出现大量的内存碎片。
- 大部分操作集中在末尾添加或删除元素时。
LinkedList
优点:
- 插入和删除操作高效:在任何位置插入和删除元素的时间复杂度为 O(1),特别是在开头或中间操作更为明显。
- 不会产生内存碎片:每个元素只需额外存储前后节点的引用。
缺点:
- 随机访问性能差:访问特定索引位置的元素需要 O(n) 的时间复杂度。
- 占用更多内存:每个元素需要额外的引用空间,可能占用更多内存。
适用场景:
- 需要频繁在集合开头或中间进行插入和删除操作的场景。
- 对顺序访问性能要求不高,或者可能会进行大量的插入和删除操作。
- 需要构建特定的数据结构,如队列或双端队列。
综合选择
- 选择
ArrayList
的典型场景包括需要高效随机访问、大量末尾添加或删除操作、以及对内存使用效率要求较高的情况。 - 选择
LinkedList
的典型场景包括需要频繁中间或开头插入、删除操作、不需要随机访问元素、或者需要构建特定的数据结构时。
四、单列集合Set
4.1 Set接口
Set 是一个接口,继承自 Collection,不允许存储重复的元素。常见的 Set
实现包括 HashSet
、LinkedHashSet
和 TreeSet
。
4.1.1 常用方法
方法名 | 说明 |
---|---|
add(E e) |
添加元素 e 到集合中,如果元素已存在,则不添加。 |
remove(Object o) |
从集合中移除元素 o 。 |
contains(Object o) |
检查集合中是否包含元素 o 。 |
size() |
返回集合中的元素个数。 |
isEmpty() |
检查集合是否为空。 |
clear() |
清空集合中的所有元素。 |
iterator() |
返回一个用于遍历集合元素的迭代器。 |
4.1.2 遍历方式
(1)迭代器
(2)增强for循环
(3)Lambda表达式(JDK8.0以后)
java
Set<String> set = new java.util.HashSet<>();
//添加元素第一次可以,多次不可
boolean r = set.add("a");
boolean r1 = set.add("a");
System.out.println(r); //true
System.out.println(r1); //false
set.add("b");
set.add("c");
set.add("e");
//System.out.println(set);
/* // 迭代器遍历
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}*/
/* //增强for循环
for (String s : set) {
System.out.println(s);
}*/
/* //Lambda表达式
set.forEach(s -> System.out.println(s));*/
4.2 实现类
4.2.1 HashSet
无序 不重复 无索引
- 特点:基于哈希表实现,元素没有特定的顺序。
- 实现原理:使用哈希表存储元素,通过计算哈希值快速定位元素的位置。
- 使用场景:适用于需要快速查找和不在意元素顺序的场景。
HashSet 底层实现总结
1. 哈希表的基本特性
- 哈希表:是一种用于高效存储和检索数据的结构。它通过将数据映射到固定大小的数组索引来实现高效的增、删、查操作。
2. HashSet 的底层实现
JDK 8 之前:
-
组成:哈希表由数组和链表组成。数组用于存储链表的头部指针,链表用于存储在同一个数组索引处发生哈希碰撞的元素。
-
插入过程:
-
初始化 :创建一个默认长度为16、加载因子为0.75的数组(
table
)。javaHashSet<String> hm = new HashSet<>();
-
计算索引:根据元素的哈希值计算索引位置。
javaint index = hash(key) % table.length;
或
javaint index = (table.length - 1) & hashValue;
-
判断位置 :如果当前位置为
null
,直接存入元素。 -
处理碰撞 :如果当前位置已经有元素(链表),调用
equals
方法比较元素:- 如果
equals
比较返回true
,不存入元素(已存在)。 - 如果
equals
比较返回false
,在链表末尾添加新元素。
- 如果
-
链表存储:新元素挂在链表的尾部,老元素保持在链表中。
-
JDK 8 之后:
- 组成:哈希表由数组、链表和红黑树组成。当链表长度超过8且数组长度达到64时,链表会被转换为红黑树以提高性能。
- 插入过程 :
- 与JDK 7类似,但在链表长度超过8并且数组长度大于等于64时,链表转换为红黑树,以提高查找和插入的效率。
3. 哈希值和碰撞
-
哈希值:
- 通过
hashCode
方法计算得到的int
类型的整数。 hashCode
方法定义在Object
类中,所有对象都可以调用。默认实现基于对象的内存地址。- 通常会重写
hashCode
方法,利用对象的内部属性计算哈希值,以提高哈希表的性能。
- 通过
-
哈希碰撞:
- 特点 :即不同对象计算出的哈希值可能相同,这种情况称为哈希碰撞。虽然
hashCode
方法可以设计为尽量减少碰撞,但完全避免碰撞是不可能的。 - 解决:通过链表或红黑树解决碰撞。
- 特点 :即不同对象计算出的哈希值可能相同,这种情况称为哈希碰撞。虽然
4. 关键方法
hashCode
和equals
:- 在
HashSet
中,如果存储的是自定义对象,必须重写hashCode
和equals
方法。这样,哈希值可以根据对象的属性值计算,比较时也基于属性值。 - 如果未重写这两个方法,
HashSet
默认使用Object
类的方法,比较的是对象的内存地址。
- 在
5. 常见问题解答
- HashSet 为什么存和取的顺序不一样?
-
回答 :
HashSet
的存储顺序和取出顺序不同是因为其底层是基于哈希表的,元素的存储位置由哈希值决定,不保证顺序。遍历时,数组的每个位置会被遍历,位置上可能有链表或红黑树,遍历顺序取决于哈希值的分布和链表/树的结构。
- HashSet 为什么没有索引?
-
回答 :
HashSet
使用数组和链表(及红黑树)来存储数据,不使用索引。数组的索引用来确定链表或树的位置,而链表或树内部结构中的元素没有固定索引位置。
由于哈希碰撞的存在,同一个索引可能对应多个元素。
-
HashSet 为什么没有重复元素?
-
回答 :
HashSet
使用元素的哈希值(由hashCode
方法计算)和equals
方法来确保唯一性。如果两个元素的哈希值相同,
HashSet
会通过equals
方法进一步检查它们是否相等。只有当
equals
返回false
时,新元素才会被加入集合。
-
总结
- 底层结构 :
HashSet
使用哈希表存储数据,JDK 8 前后分别使用数组+链表、数组+链表+红黑树的组合。 - 哈希值 :由
hashCode
方法计算,决定元素在哈希表中的位置。 - 碰撞处理:通过链表或红黑树处理哈希碰撞。
- 自定义对象 :需要重写
hashCode
和equals
方法,以确保哈希表的正确性和性能。 - 存取顺序 :
HashSet
不保证存取顺序,因为哈希表的内部结构和碰撞处理机制不保持元素的插入顺序。
4.2.2 LinkedHashSet
有序 不重复 无索引
-
特点:基于哈希表和双向链表实现,维护元素的插入顺序。
-
实现原理 :在
HashSet
的基础上,增加了一个双向链表,记录元素的插入顺序。 -
使用场景:适用于需要快速查找并且希望保持元素插入顺序的场景。
特点
- 有序性 :
LinkedHashSet
继承自HashSet
,但是在内部通过双链表维护元素的插入顺序,因此可以保证存储和迭代时的顺序一致。 - 不重复 :与
HashSet
相同,LinkedHashSet
不允许存储重复的元素。 - 无索引 :
LinkedHashSet
内部仍然是基于哈希表实现的,没有像传统索引那样直接访问特定位置的机制。
原理
- 底层数据结构:仍然是基于哈希表,但是每个存储桶(bucket)除了存储链表(或红黑树)用于处理哈希冲突外,还维护一个双链表。这个双链表按照元素的插入顺序排列。
- 存储顺序保证 :当元素被添加到
LinkedHashSet
中时,首先检查元素是否已经存在,如果不存在,则将其添加到哈希表中的适当位置,并同时将其插入双链表的尾部。 - 迭代顺序 :通过双链表,
LinkedHashSet
可以按照插入顺序进行迭代,因此可以保证迭代时的顺序与存储时的顺序一致。
java
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("Apple");
linkedHashSet.add("Banana");
linkedHashSet.add("Cherry");
// 迭代时的顺序与存储时的顺序一致
for (String fruit : linkedHashSet) {
System.out.println(fruit);
}
总结
LinkedHashSet
继承自 HashSet
,在保持 HashSet
不重复和哈希表快速访问的基础上,通过双链表来实现有序性,保证了存储和迭代的顺序一致。这使得 LinkedHashSet
在需要保持插入顺序的场景下非常有用,同时仍然具备哈希表的高效性能特点。
4.2.3 TreeSet
可排序 不重复 无索引
- 特点:基于红黑树实现,元素按自然顺序或指定的比较器顺序排序。
- 实现原理:使用红黑树(自平衡二叉搜索树)存储元素。
- 使用场景:适用于需要有序存储和快速查找的场景。
特点
-
不重复性 :
TreeSet
不允许存储重复的元素,确保集合中的每个元素都是唯一的。 -
无索引 :与列表不同,
TreeSet
的存储方式基于红黑树,没有直接的索引访问方式。元素的顺序依赖于其在树中的位置。 -
可排序性 :
TreeSet
中的元素按照默认规则进行排序。对于数值类型(如整数、浮点数),默认按照从小到大的顺序排序;对于字符和字符串类型,按照 ASCII 码表中的数字升序排序。
底层实现
- 数据结构 :
TreeSet
的底层是基于红黑树(Red-Black Tree)实现的。红黑树是一种自平衡的二叉查找树,能够保证增删改查操作的时间复杂度为 O(log n),因此TreeSet
在大多数操作上有较好的性能表现。
java
TreeSet<Integer> ts = new TreeSet<>();
//`TreeSet` 会自动按照元素的默认规则(从小到大)对插入的元素进行排序
ts.add(12);
ts.add(32);
ts.add(-3);
ts.add(-114);
System.out.println(ts); // 输出结果将会是 [-114, -3, 12, 32]
比较方式
-
默认排序 / 自然排序:
- 如果元素类型实现了
Comparable
接口,并且重写了compareTo
方法,TreeSet
将使用该方法来确定元素的顺序,称为自然排序。 - 例如,
Integer
类型已经实现了Comparable
接口,因此可以直接用作TreeSet
的元素。
- 如果元素类型实现了
-
比较器排序:
- 如果元素类型没有实现
Comparable
接口,或者需要以不同于自然顺序的方式排序,可以通过传递一个Comparator
对象给TreeSet
的构造方法来指定比较规则。 - 比较器排序通过
Comparator
接口的compare
方法来实现自定义的排序逻辑。
- 如果元素类型没有实现
总结
TreeSet
是一个适用于需要有序且不允许重复元素的集合。它利用红黑树数据结构实现了高效的增删改查操作,并且可以通过自然排序或者比较器排序来满足不同的排序需求。
4.2.4 红黑树的特点和结构描述
- 节点结构:
- 每个节点包含的信息通常有:存储的值(元素值)、指向左子节点的指针、指向右子节点的指针、父节点指针和一个颜色标记(红色或黑色)。
-
颜色规则:
- 每个节点要么是红色,要么是黑色。
- 根节点必须是黑色。
- 所有叶子节点(NIL节点,即空节点)是黑色。
- 红色节点的子节点必须是黑色(即不存在两个相连的红色节点)。
- 从任一节点到其每个叶子节点的路径包含相同数量的黑色节点(黑色平衡)。
-
平衡操作:
- 当插入或删除节点时,需要进行调整以确保满足红黑树的所有规则。主要的调整操作包括:旋转(左旋或右旋)和变色(改变节点的颜色)。
-
查找和插入操作:
- 查找操作和普通的二叉查找树类似,但由于红黑树的平衡性,查找的时间复杂度为 O(log n)。
- 插入操作可能需要进行旋转和变色操作,以保持红黑树的平衡性。
红黑树大致结构:
8 (B)
/ \
4(R) 12 (R)
/ \ / \
2(B) 6(B) 10(B) 14(B)
/ \ / \ / \ / \
1(R) 3(R) 5(R) 7(R) 9(R) 11(R) 13(R) 15(R)
下面是其NIL
五、双列集合Map
双列集合Map是一种用于存储键值对的集合。每个元素由一个键(Key)和一个值(Value)组成,键和值之间是一对一的关系。
5.1 Map接口
5.1.1 特点
-
双列集合中的元素都是键值对,每个元素都是key-value(键值对)的形式存储的
-
双列集合中的键是唯一的,值是可重复的
-
双列集合中的键和值的数据类型可以相同,也可以不同
-
键+值这个整体我们称之为"键值对"或者"键值对对象",在Java中叫做"Entry对象"
5.1.2 常见API方法
方法名 | 说明 |
---|---|
V put(K key, V value) |
将指定的值与此映射中的指定键关联(可选操作)。 |
V remove(Object key) |
从此映射中移除指定键的映射(如果存在)(可选操作)。 |
void clear() |
从此映射中移除所有映射(可选操作)。 |
boolean containsKey(Object key) |
如果此映射包含指定键的映射关系,则返回 true 。 |
boolean containsValue(Object value) |
如果此映射将一个或多个键映射到指定值,则返回 true 。 |
boolean isEmpty() |
如果此映射未包含键-值映射关系,则返回 true 。 |
int size() |
返回此映射中的键-值映射关系数。 |
5.1.3 遍历
1. 使用 keySet()
方法
获取所有的键,然后通过每个键获取对应的值。
java
import java.util.HashMap;
import java.util.Map;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Apple", 1);
map.put("Banana", 2);
map.put("Orange", 3);
// 使用 keySet() 方法遍历
for (String key : map.keySet()) {
Integer value = map.get(key);
System.out.println(key + ": " + value);
}
}
}
2. 使用 entrySet()
方法
适用于需要同时访问键和值的场景,通常性能更优。
java
import java.util.HashMap;
import java.util.Map;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Apple", 1);
map.put("Banana", 2);
map.put("Orange", 3);
// 使用 entrySet() 方法遍历
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
System.out.println(key + ": " + value);
}
}
}
3. 使用 Lambda 表达式和 forEach()
方法
提供了更简洁的现代化编程风格。
java
import java.util.HashMap;
import java.util.Map;
public class MapTraversalExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("Apple", 1);
map.put("Banana", 2);
map.put("Orange", 3);
// 使用 Lambda 表达式和 forEach() 方法遍历
map.forEach((key, value) -> System.out.println(key + ": " + value));
}
}
5.2 HashMap -- 效率最高
HashMap 的特点
-
实现类:
HashMap
是Map
接口的一个具体实现类,用于存储键值对(key-value pairs)。
-
无序、不重复、无索引:
- 无序 :
HashMap
中的键值对是无序的,元素的位置是由哈希值决定的。 - 不重复 :
HashMap
的键是唯一的,如果插入了重复的键,则新值会替换掉旧值。 - 无索引 :
HashMap
不支持基于索引的访问,它是基于键的哈希值来存取元素的。
- 无序 :
-
底层结构:
- 哈希表 :
HashMap
使用哈希表作为底层数据结构,通过计算键的哈希值来决定存储位置。哈希表是基于数组和链表(或树)的实现。 - HashSet 关系 :
HashSet
也是基于哈希表实现的,但它只存储键,没有值。底层原理和HashMap
一样,只是HashSet
使用HashMap
的一个实例作为底层存储。
- 哈希表 :
工作原理
-
哈希表结构:
HashMap
底层使用一个数组(桶)来存储链表或树节点。每个桶代表一个哈希值范围,当插入键值对时,HashMap
计算键的哈希值,并根据哈希值确定桶的位置。- 如果不同的键具有相同的哈希值,它们会被存储在同一个桶中,这种情况称为哈希冲突。哈希冲突通过链表或红黑树解决。
-
哈希值和唯一性:
- 依赖
hashCode
和equals
方法 :HashMap
使用键的hashCode
方法计算哈希值,并使用equals
方法判断两个键是否相等。因此,键的哈希值和相等性非常重要。 - 自定义对象 :如果键是自定义对象,必须重写
hashCode
和equals
方法,以确保正确的键唯一性和哈希冲突解决。
- 依赖
-
自定义对象:
- 键 :如果自定义对象作为键,需要重写
hashCode
和equals
方法,以保证HashMap
能正确处理哈希值和相等性。 - 值 :如果自定义对象作为值,则不需要重写
hashCode
和equals
方法,HashMap
只使用键的哈希值和相等性来存取元素。
- 键 :如果自定义对象作为键,需要重写
总结
HashMap
是基于哈希表的实现,提供了高效的插入、删除和查找操作。- 键的唯一性由
hashCode
和equals
方法保证。 - 自定义对象作为键时,必须重写
hashCode
和equals
方法,以确保HashMap
正确处理键值对。 - 自定义对象作为值时,不需要重写这些方法,哈希操作仅在键上进行。
5.3 LinkedHashMap -- 保证存取顺序
java
public class test {
public static void main(String[] args) {
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("1", "a");
map.put("2", "b");
map.put("3", "c");
map.put("4", "d");
map.put("4", "x");
System.out.println(map);//{1=a, 2=b, 3=c, 4=x}
}
}
5.4 TreeMap -- 进行排序
特点
-
底层数据结构:
TreeMap
是基于红黑树的实现。红黑树是一种自平衡的二叉搜索树,能够提供对键的有序存储。TreeSet
也是基于红黑树的集合类,用于存储唯一的元素,提供有序性。
-
由键决定的特性:
- 不重复 :
TreeMap
中的键是唯一的,不允许有重复的键。 - 无索引 :
TreeMap
不支持索引访问,元素通过键进行访问。 - 可排序 :
TreeMap
对键进行排序,默认情况下按照键的自然顺序(即从小到大)排序。
- 不重复 :
-
排序规则:
- 默认按键的自然顺序排序,这要求键的类实现了
Comparable
接口。 - 也可以在创建
TreeMap
实例时提供一个Comparator
对象,以自定义排序规则。
- 默认按键的自然顺序排序,这要求键的类实现了
统计字符
java
public static void main(String[] args) {
String s = "abaecebabcdcded";
TreeMap<Character,Integer> map = new TreeMap<>();
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if(map.containsKey(c)){
map.put(c,map.get(c)+1);
}else {
map.put(c,1);
}
}
System.out.println(map);
}
实现排序规则
1. 使用 Comparable
接口
如果你的键是自定义对象,并且希望按自然顺序排序,你需要让自定义对象实现 Comparable
接口,并重写 compareTo
方法。
java
import java.util.Map;
import java.util.TreeMap;
class Person implements Comparable<Person> {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public int compareTo(Person other) {
// 按年龄排序(从小到大)
return Integer.compare(this.age, other.age);
}
@Override
public String toString() {
return name + ": " + age;
}
}
public class TreeMapExample {
public static void main(String[] args) {
TreeMap<Person, String> treeMap = new TreeMap<>();
treeMap.put(new Person("Alice", 30), "Engineer");
treeMap.put(new Person("Bob", 25), "Doctor");
treeMap.put(new Person("Charlie", 35), "Teacher");
// 按年龄排序输出
for (Map.Entry<Person, String> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + " - " + entry.getValue());
}
}
}
2. 使用 Comparator
接口
如果你需要自定义排序规则,可以在创建 TreeMap
时传递一个 Comparator
对象。
java
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + ": " + age;
}
}
public class TreeMapExample {
public static void main(String[] args) {
// 使用Comparator按名字字母顺序排序
Comparator<Person> nameComparator = (p1, p2) -> p1.name.compareTo(p2.name);
TreeMap<Person, String> treeMap = new TreeMap<>(nameComparator);
treeMap.put(new Person("Alice", 30), "Engineer");
treeMap.put(new Person("Bob", 25), "Doctor");
treeMap.put(new Person("Charlie", 35), "Teacher");
// 按名字字母顺序排序输出
for (Map.Entry<Person, String> entry : treeMap.entrySet()) {
System.out.println(entry.getKey() + " - " + entry.getValue());
}
}
}
总结
TreeMap
使用红黑树结构来维护键的有序性。- 默认情况下,
TreeMap
按照键的自然顺序进行排序,这要求键的类实现了Comparable
接口。 - 可以通过传递
Comparator
对象在创建TreeMap
时指定自定义的排序规则。 TreeSet
与TreeMap
类似,底层也是红黑树,适用于需要排序的集合。