0498 0552 第十四章 集合
14.1 集合¶
14.1.1 数组¶
- 长度开始时必须指定,而且一旦指定,不能更改
- 保存的心须为同一类型的元素
- 使用数组进行增加/删除元素的示意代码-比较麻烦
写出 Person 数组扩容示意代码。
Person[] pers = newPerson[1]; //大小是 1
per[0] = new Person();
增加新的 Person 对象?
Person[]pers 2=newPerson[pers」ength+1];//新创建数组
for (){}//拷贝 pers 数组的元素到 pers 2
pers2[pers2.length-1] = newPerson();//添加新的对象
14.1.2 集合¶
- 可以动态保存任意多个对象(任意类型),使用比较方便!
- 提供了一系列方便的操作对象的方法:
add. remove. set. get等 - 使用集合添加,删除新元素的示意代码-简洁
14.2 集合的框架体系¶
classDiagram
class Object {
+toString() String
+hashCode() int
+equals(Object) boolean
}
class String {
+length() int
+charAt(int) char
+substring(int, int) String
}
class Collection~E~ {
<<interface>>
+add(E) boolean
+remove(Object) boolean
+size() int
}
class List~E~ {
<<interface>>
+get(int) E
+set(int, E) E
+add(int, E) void
}
class Set~E~ {
<<interface>>
}
class Map~K,V~ {
<<interface>>
+put(K, V) V
+get(Object) V
+remove(Object) V
}
class ArrayList~E~ {
+add(E) boolean
+get(int) E
+remove(int) E
}
class LinkedList~E~ {
+addFirst(E) void
+addLast(E) void
+getFirst() E
}
class HashSet~E~ {
+add(E) boolean
+remove(Object) boolean
}
class TreeSet~E~ {
+add(E) boolean
+first() E
+last() E
}
class HashMap~K,V~ {
+put(K, V) V
+get(Object) V
}
class TreeMap~K,V~ {
+put(K, V) V
+firstKey() K
+lastKey() K
}
Object <|.. String : extends
Collection <|.. List : extends
Collection <|.. Set : extends
List <|.. ArrayList : implements
List <|.. LinkedList : implements
Set <|.. HashSet : implements
Set <|.. TreeSet : implements
Map <|.. HashMap : implements
Map <|.. TreeMap : implements
![[14-1-1 单列集合.png]]
![[14-1-2 双列集合.png]]
14.3.2 Collection 接口遍历元素方式 1-使用 Iterator¶
Iterator称为迭代器,主要用于遍历Collection集合中的元素。- 所有实现了 Collection 接口的集合类都有一个
iterator方法,用以返回一个实现了 Iterator 接口的对象, 即可以返回一个迭代器。 - Iterator 仅用于遍历集合,Iterator 本身并不存放对
Iterator iterator =coll.iteratorO;//得到一个集合的迭代器
//hasNext():判断是否还有下一个元素while(iterator.hasNext()){
//next()作用:1.上的元素返回|下移.将下移以后集合位置|
System.out.print!n(iterator.next());
在调用 iterator.next() 方法之前必须要调用 iterator.hasNext() 进行检测。若不调用,且下一条记录无效,直接调用 it. next () 会抛出 NoSuchElementException 异常。
14.3.3 Collection 接口遍历对象方式 2-for 循环增强¶
增强 for 循环,可以代替 iteratoi 代器,特点:增强 for 就是简化版的 iterator, 本质一样。只能用于遍历集合或数组。
- 基本语法
for(Object object:col){System.out.println(object)}
14.4 List 接口和常用方法¶
14.4.1 List 接口基本介绍¶
List 接口是 Collection 接口的子接口
- List 集合类中元素有序(即添加顺序和取出顺序一致)、且可重复
- List 集合中的每个元素都有具对应的顺席索引,即支持索引。
- List 容器中的元素都对应一个整数型的序号记载具在容器中的位置,可以根据序号存取容器中的元素。
- 常用的有:ArrayList、LinkedList 和 Vector。
14.4.2 List 接口的常用方法¶
常用方法¶
- 添加元素
boolean add(E e)
将指定元素追加到列表末尾。- 参数:
e- 要添加的元素。 - 返回值:
true(通常成功,除非底层实现有限制)。 - 示例:
- 参数:
void add(int index, E element)
在指定索引位置插入元素,原有元素后移。- 参数:
index- 插入位置,element- 要插入的元素。 - 异常:
IndexOutOfBoundsException- 如果index超出范围。 - 示例:
- 参数:
-
boolean addAll(Collection<? extends E> c)
将指定集合中的所有元素追加到列表末尾。- 参数:
c- 要添加的集合。 - 返回值: 如果列表发生变化,返回
true。 - 示例:
- 参数:
-
获取和修改元素
E get(int index)
返回指定索引处的元素。- 参数:
index- 要获取的元素索引。 - 返回值: 索引处的元素。
- 异常:
IndexOutOfBoundsException- 如果index超出范围。 - 示例:
- 参数:
-
E set(int index, E element)
替换指定索引处的元素,并返回被替换的旧元素。- 参数:
index- 要替换的索引,element- 新元素。 - 返回值: 原索引处的元素。
- 异常:
IndexOutOfBoundsException- 如果index超出范围。 - 示例:
- 参数:
-
删除元素
E remove(int index)
删除指定索引处的元素,并返回被删除的元素。- 参数:
index- 要删除的元素索引。 - 返回值: 被删除的元素。
- 异常:
IndexOutOfBoundsException- 如果index超出范围。 - 示例:
- 参数:
-
boolean remove(Object o)
删除首次出现的指定元素(如果存在)。- 参数:
o- 要删除的元素。 - 返回值: 如果元素被移除,返回
true。 - 示例:
- 参数:
-
查询和搜索
int size()
返回列表中的元素数量。- 返回值: 列表大小。
- 示例:
boolean isEmpty()
检查列表是否为空。- 返回值: 如果列表为空,返回
true。 - 示例:
- 返回值: 如果列表为空,返回
boolean contains(Object o)
检查列表是否包含指定元素。- 参数:
o- 要检查的元素。 - 返回值: 如果包含,返回
true。 - 示例:
- 参数:
int indexOf(Object o)
返回首次出现的指定元素的索引,若不存在返回 -1。- 参数:
o- 要查找的元素。 - 返回值: 元素索引或 -1。
- 示例:
- 参数:
-
int lastIndexOf(Object o)
返回最后一次出现的指定元素的索引,若不存在返回 -1。- 参数:
o- 要查找的元素。 - 返回值: 元素索引或 -1。
- 示例:
- 参数:
-
子列表和批量操作
List<E> subList(int fromIndex, int toIndex)
返回指定范围的子列表(视图,非副本)。- 参数:
fromIndex- 起始索引(包含),toIndex- 结束索引(不包含)。 - 返回值: 子列表。
- 异常:
IndexOutOfBoundsException- 如果索引无效。 - 示例:
- 参数:
-
void clear()
清空列表中的所有元素。- 示例:
- 示例:
-
迭代和转换
Iterator<E> iterator()
返回一个迭代器,用于遍历列表元素。- 示例:
- 示例:
ListIterator<E> listIterator()
返回一个列表迭代器,支持双向遍历和修改。- 示例:
- 示例:
Object[] toArray()
将列表转换为对象数组。- 示例:
- 示例:
<T> T[] toArray(T[] a)
将列表转换为指定类型的数组。- 参数:
a- 目标数组类型。 - 示例:
- 参数:
特点和注意事项¶
- 有序性:
List维护元素的插入顺序,索引从 0 开始。 - 允许重复: 同一元素可多次出现在列表中。
- 线程安全:
ArrayList和LinkedList默认非线程安全,需使用Collections.synchronizedList或CopyOnWriteArrayList确保线程安全。 - 性能差异:
ArrayList: 适合随机访问(get/set),插入/删除较慢(尤其在列表开头)。LinkedList: 适合频繁插入/删除(尤其在列表两端),随机访问较慢。- 泛型:
List<E>支持泛型,确保类型安全。
示例代码¶
以下是一个综合示例,展示常用方法的使用:
import java.util.*;
public class ListDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// 添加
list.add("Java");
list.add(0, "Python");
list.addAll(Arrays.asList("C++", "Kotlin"));
System.out.println("列表: " + list); // [Python, Java, C++, Kotlin]
// 获取和修改
System.out.println("索引 1: " + list.get(1)); // Java
list.set(1, "Ruby");
System.out.println("修改后: " + list); // [Python, Ruby, C++, Kotlin]
// 删除
list.remove(2);
System.out.println("删除后: " + list); // [Python, Ruby, Kotlin]
// 查询
System.out.println("大小: " + list.size()); // 3
System.out.println("包含 Ruby: " + list.contains("Ruby")); // true
System.out.println("Ruby 索引: " + list.indexOf("Ruby")); // 1
// 子列表
List<String> sub = list.subList(0, 2);
System.out.println("子列表: " + sub); // [Python, Ruby]
}
}
如需更深入的讲解(例如特定实现类的性能分析)或更多示例,请进一步说明!
14.4.4 List 的三种遍历方式 ArrayList, LinkedList, Vector¶
-
方式一 :使用
iterator -
方式二:使用增强 for
for(Object o:col){ }
3) 方式三:使用普通 for
说明:使用 LinkedList 完成使用方式和 ArrayList —样14.5 ArrayList 底层结构和源码分析¶
14.5.1 ArrayList 的注意事项¶
permits all elements, including null , ArrayList可以加入 null, 并且多个- ArrayList 是由数组来实现数据存储的
- ArrayList 基本等同于 Vector ,除了ArrayList 是线程不安全(执行效率高)看源码. 在多线程情况下,不建议使用 ArrayList
14.5.2 ArrayList 的底层操作机制源码分析¶
-
ArrayList 中维护了一个 Object 类型的数组
elementData.transient Object[] elementData; //transient 表示瞬间,短暂的,装示该属性不会被序列化
-
当创建 ArrayList 对象时,如果使用的是无参构造器,则初始 elementData 容量为 0, 第 1 次添加,则扩容 elementData 为 10, 如需要再次扩容,则扩容 elementData 为 1.5 倍。
-
如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为 1.5 倍。
ArrayList 的无参构造器是其最常用的构造方式,源码简单但涉及后续操作的初始化逻辑。以下是详细分析:
1. 无参构造器源码¶
- 功能:初始化一个空的ArrayList,将底层数组 elementData 设置为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA。
- 关键字段:
- DEFAULTCAPACITY_EMPTY_ELEMENTDATA:定义为 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {},是一个共享的空数组常量,用于标识无参构造的初始状态。
- elementData:transient Object[] 类型,ArrayList 的底层存储数组,初始为空。
2. 初始化逻辑¶
- 延迟分配:无参构造器不立即分配容量为 10 的数组,而是将
elementData指向空数组DEFAULTCAPACITY_EMPTY_ELEMENTDATA。真正的数组分配在第一次添加元素时触发。 - 目的:这种延迟分配(lazy initialization)减少了内存浪费。如果
ArrayList仅声明但不使用,不会占用多余空间。 - 对比其他构造器:
- 指定容量构造(
ArrayList(int initialCapacity))直接分配指定大小的数组。 - 构造(
ArrayList(Collection c))根据输入初始化数组。
3. 第一次添加元素时的行为¶
无参构造器的真正初始化逻辑在调用 add(E e) 时体现。以下是相关源码:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查容量,size 初始为 0
elementData[size++] = e; // 存入元素,size 增 1
return true;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity); // 默认容量 10
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++; // 记录结构修改次数
if (minCapacity - elementData.length > 0) // 需要扩容
grow(minCapacity);
}
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 初始为 0
int newCapacity = oldCapacity + (oldCapacity >> 1); // 1.5 倍扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity; // 至少为 minCapacity
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity); // 复制到新数组
}
elementData 为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 时,calculateCapacity 返回 Math.max(DEFAULT_CAPACITY, minCapacity),其中 DEFAULT_CAPACITY = 10。因此,第一次添加元素时,数组容量扩容至 10。
- 扩容流程:
1. add(E e) 调用 ensureCapacityInternal(size + 1),size 初始为 0,minCapacity = 1。
2. calculateCapacity 检测到 elementData 是 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,返回 10。
3. grow(10) 创建容量为 10 的新数组,elementData 指向新数组。
4. 元素存储在 elementData[0],size 增至 1。
4. 关键点分析¶
- 内存效率:无参构造器通过延迟分配避免初始内存浪费,适合不确定元素数量的场景。
- 性能影响:第一次添加元素触发扩容,涉及数组分配和复制,略有性能开销。后续添加若不超容量则高效。
- 线程安全:无参构造器初始化不涉及线程竞争,但
ArrayList本身非线程安全,需外部同步。 - 序列化:
elementData为transient,序列化时仅保存有效元素(size个),配合writeObject和readObject优化存储。
5. 使用场景与注意事项¶
- 适用场景:无参构造器适合元素数量未知或较小的场景,如临时列表或动态增长的集合。
- 注意事项:
- 若预知列表会存储大量元素,建议使用
ArrayList(int initialCapacity)避免频繁扩容。 - 扩容机制(1.5 倍)可能导致内存碎片,需权衡初始容量。
- 空数组检查(如
isEmpty())基于size == 0,与elementData是否为空无关。
6. 源码总结¶
无参构造器通过将 elementData 设置为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA,实现延迟初始化。第一次添加元素时,触发容量为 10 的数组分配,后续扩容按 1.5 倍增长。这种设计平衡了内存效率和灵活性,但需注意扩容开销和线程安全问题。
14.6 Vector 底层结构和源码剖析¶
14.6.1 Vector 的基本介绍¶
-
Vector 类的定义说明
-
Vector 底层也是一个对象数组,
protected Object[] elementData; -
Vector 是线程同步的,即线程安全, Vector 类的操作方法带有
synchronized
public synchronized E get(int index) {
if (index >= elementcount)
throw new ArraylndexOutOfBoundsException(index);
return elementData(index);
}
- 在开发中,需要线程同步安全时,考虑使用
Vector
@SuppressWarnings({"all"})
public class Vector_ {
public static void main(String[] args) {
//无参构造器
//有参数的构造
Vector vector = new Vector(8);
for (int i = 0; i < 10; i++) {
vector.add(i)
}
vector.add(100);
System.out.println("vector=" + vector);
}
}
14.6.2 Vector 和 ArrayList 的比较¶
| 底层结构 | 版本 | 线程安全(同步)效率 | 扩容倍数 | |
|---|---|---|---|---|
| ArrayList | 可变数组 | jdk 1.2 | 不安全,效率高 | 如果有参构造 1.5 倍 如果是无参 1. 第一次 10 2. 让第二次开始扩 1.5 倍 |
| Vector | 可变数组 Object[] |
jdk 1.0 | 安全,效率高 | 如果是无参,默认 10 满后,就按 2 倍扩容 如果指定大小, 则每次直接扩容两倍 |
| 特性 | Vector (无参构造器) | ArrayList (无参构造器) |
|---|---|---|
| elementData 初始化 | 分配 new Object[10] |
设置为 DEFAULTCAPACITY_EMPTY_ELEMENTDATA(空数组) |
| 初始容量 | 10 | 0(延迟到第一次添加时分配 10) |
| elementCount/size | elementCount = 0 |
size = 0 |
| 扩容策略 | 默认翻倍(capacityIncrement = 0) |
1.5 倍增长 |
| 内存效率 | 立即分配 10,稍浪费内存 | 延迟分配,内存效率更高 |
| 线程安全 | 是(synchronized 方法) |
否 |
14.6.3 底层逻辑¶
Vector 无参构造器创建时的逻辑¶
Vector 是一个与 ArrayList 类似的动态数组实现,但它是线程安全的(通过 synchronized 实现),并且在设计上有一些历史遗留差异。以下是 Vector 无参构造器(new Vector<>())创建时的底层逻辑和变量状态。
1. 无参构造器源码¶
- 逻辑:无参构造器直接调用带参数的构造器Vector(int initialCapacity),传入默认初始容量 10。
- 有参构造器源码:
- 进一步调用:
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
Vector(int initialCapacity, int capacityIncrement),设置初始容量为 10,容量增量(capacityIncrement)为 0。
- 直接分配一个长度为 10 的 Object[] 数组给 elementData,与 ArrayList 的延迟初始化不同。
- 没有元素,因此 elementCount(类似 ArrayList 的 size)初始化为 0。
2. 相关变量及值大小¶
在 Vector 创建时(调用无参构造器后),以下是关键变量的状态:
elementData:- 类型:
protected Object[] - 值:
new Object[10] - 大小:10(长度为 10 的数组,元素全为
null)。 -
说明:
elementData是Vector存储元素的实际数组。创建时立即分配容量为 10 的数组,与ArrayList的空数组(DEFAULTCAPACITY_EMPTY_ELEMENTDATA)不同。 -
elementCount: - 类型:
protected int - 值:0
-
说明:
elementCount表示Vector中当前存储的元素个数。刚创建时没有元素,因此elementCount = 0(类似ArrayList的size)。 -
capacityIncrement: - 类型:
protected int - 值:0
-
说明:
capacityIncrement表示扩容时的容量增量。无参构造器设置为 0,意味着扩容时默认将容量翻倍(通常是当前容量的两倍)。 -
modCount: - 类型:
protected int(继承自AbstractList) - 值:0
-
说明:
modCount用于记录Vector的结构修改次数(如添加、删除元素)。创建时没有修改,因此值为 0。 -
最大容量:
Vector没有显式的MAX_ARRAY_SIZE常量,但受 JVM 数组长度限制(Integer.MAX_VALUE,约 2^31 - 1)。- 说明:创建时未直接使用,但在扩容时会检查数组长度是否超出 JVM 限制。
3. 创建时的逻辑总结¶
- 初始化行为:
- 无参构造器通过调用
Vector(10, 0),直接分配一个长度为 10 的Object[]数组给elementData。 elementCount初始化为 0,表示没有元素。capacityIncrement设置为 0,表示后续扩容按两倍增长(除非另行指定)。modCount保持默认值 0。- 内存分配:
- 与
ArrayList的延迟初始化不同,Vector在创建时立即分配容量为 10 的数组,内存开销稍大。 - 这种设计源于
Vector的历史(Java 1.0 遗留),当时更注重简单性和线程安全。 - 线程安全:
Vector的构造器操作是线程安全的(单线程初始化无竞争),且后续操作通过synchronized保证线程安全。- 后续影响:
- 由于创建时已有容量 10 的数组,第一次添加元素(
add(E e))通常不会触发扩容,除非添加超过 10 个元素。 - 扩容时,若
capacityIncrement > 0,新容量为当前容量 + capacityIncrement;否则,新容量为当前容量 * 2。
4. 关键点¶
- 为什么立即分配容量 10?
Vector是 Java 1.0 的遗留类,设计时未采用延迟初始化,优先简单性和可预测性。- 立即分配容量 10 确保创建后即可存储少量元素,适合早期 Java 应用的简单场景。
- 与有参构造器的对比:
- 有参构造器
Vector(int initialCapacity)或Vector(int initialCapacity, int capacityIncrement)允许自定义初始容量和扩容增量。 - 无参构造器是默认配置(容量 10,增量 0)。
- 性能:
- 创建时分配数组(
new Object[10])有轻微内存和时间开销,但通常可忽略。 - 线程安全导致后续操作(如
add)性能低于ArrayList,因每次操作需获取锁。 - 建议:
- 如果不需要线程安全,优先使用
ArrayList,性能更高且内存效率更好。 - 如果预知存储大量元素,建议使用
Vector(int initialCapacity)指定初始容量,避免扩容。
6. 值大小总结¶
在调用 new Vector<>() 后的瞬间:
- elementData = new Object[10](长度 10 的数组,元素为 null)
- elementCount = 0(元素个数)
- capacityIncrement = 0(扩容增量)
- modCount = 0(结构修改次数)
- 最大容量受 JVM 限制(Integer.MAX_VALUE)
7. 注意事项¶
- 序列化:
elementData是protected,非transient,创建时的数组会序列化,增加序列化开销(与ArrayList不同)。 - 历史遗留:
Vector因线程安全和早期设计,性能和灵活性不如ArrayList,现代开发中建议优先使用Collections.synchronizedList(new ArrayList<>())替代线程安全的Vector。 - 扩容:后续扩容逻辑(
ensureCapacityHelper)与ArrayList不同,Vector更简单(翻倍或增量),但因同步开销效率较低。
14.7 LinkedList 底层结构¶
14.7.1 LinkedList 的全面说明¶
LinkedList底层实现了双向链表双端队列特点- 可以添加任意元素(元素可以重复), 包括 null
- 线程不安全,没有实现同步
14.7.2 LinkedList 的底层操作机制¶
- LinkedList 底层维护了一个双向链表.
- LinkedList 中维护了两个属性
first和last分别指向首节点和尾节点 - 每个节点(Node 对象),里面又维护了
prev.next、item三个属性,其中通过 prev 指向前一个,通过 next 指向后一个节点。最终实现双向链表. - 所以 LinkedList 的元素的添加和删除,不是通过数组完成的,相对来说效率较高
classDiagram
class LinkedList {
-int size
-Node~E~ first
-Node~E~ last
-int modCount
}
class Node {
-E item
-Node~E~ next
-Node~E~ prev
}
LinkedList o--> "1" Node : first
LinkedList o--> "1" Node : last
Node o--> "1" Node : next
Node o--> "1" Node : prev
note for LinkedList "创建时:\nsize = 0\nfirst = null\nlast = null\nmodCount = 0"
note for Node "每个节点包含:\n- item: 元素值\n- next: 下一个节点\n- prev: 前一个节点"
graph TD
subgraph Empty LinkedList
L[LinkedList] -->|first| NULL1[null]
L -->|last| NULL2[null]
L -->|size| S[0]
L -->|modCount| M[0]
end
subgraph Non-Empty LinkedList
L2[LinkedList] -->|first| N1[Node1]
L2 -->|last| N3[Node3]
L2 -->|size| S2[3]
L2 -->|modCount| M2[0 or more]
N1 -->|item| I1[Element1]
N1 -->|next| N2[Node2]
N1 -->|prev| NULL3[null]
N2 -->|item| I2[Element2]
N2 -->|next| N3
N2 -->|prev| N1
N3 -->|item| I3[Element3]
N3 -->|next| NULL4[null]
N3 -->|prev| N2
end
14.7.3 LinkedList 的增删改查案例¶
@SuppressWarnings({"all"})
public class LinkedListCRUD {
public static void main(String[] args) {
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
linkedList.add(3);
System.out.println("linkedList=" + linkedList);
//演示一个删除结点的
linkedList.remove(); // 这里默认删除的是第一个结点
//linkedList.remove(2);
System.out.println("linkedList=" + linkedList);
//修改某个结点对象
linkedList.set(1, 999);
System.out.println("linkedList=" + linkedList);
//得到某个结点对象
//get(1) 是得到双向链表的第二个对象
Object o = linkedList.get(1);
System.out.println(o);//999
//因为 LinkedList 是 实现了 List 接口, 遍历方式
System.out.println("===LinkeList 遍历迭代器====");
Iterator iterator = linkedList.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
System.out.println("next=" + next);
}
System.out.println("===LinkeList 遍历增强 for====");
for (Object o1 : linkedList) {
System.out.println("o1=" + o1);
}
System.out.println("===LinkeList 遍历普通 for====");
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
/* 1. LinkedList linkedList = new LinkedList();
public LinkedList() {}
2. 这时 linkeList 的属性 first = null last = null
3. 执行 添加
public boolean add(E e) {
linkLast(e);
return true;
}
4.将新的结点,加入到双向链表的最后
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++;
}
*/
/*
4. 执行 removeFirst
public E remove() {
return removeFirst();
}
5. 执行
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
6. 执行 unlinkFirst, 将 f 指向的双向链表的第一个结点拿掉
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
*/
}
}
14.8 ArrayList 和 LinkedList 比较¶
14.8.1 ArrayList 和 LinkedList 的比较¶
| 底层结构 | 增删的效率 | 改查的效率 | |
|---|---|---|---|
| ArrayList | 可变数组 | 较低、数组扩容 | 较高 |
| LinkedList | 双向链表 | 较高、通过链表追加 | 较低 |
如何选择 ArrayList 和 LinkedList:
- 如果我们改查的操作多,选择 ArrayList
- 如果我们增删的操作多,选择 LinkedList
- 一般来说,在程序中,80%-90% 都是查询,因此大部分情况下会选择 ArrayList
- 在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是 ArrayList, 另外一介模块是 LinkedList, 也就是说,要根据业务来进行选择
14.9 Set 接口和常用方法¶
14.9.1 Set 接口基本介绍¶
- 无序(添加和取出的顺序不一致),没有索引 => 不能普通 for
- 不允许重复元素,所以最多包含一个 null
14.9.2 Set 接口的常用方法¶
和 List 接口一样, Set 接口也是 Collection 的子接口,因此,常用方法和 Collection 接口一样.
14.9.3 Set 接口的遍历方式¶
同 Collection 的遍历方式一样,因为 Set 接口是 Collection 接口的子接口。
- 可以使用迭代器
- 增强 for
- 不能使用索引的方式来获取 (有 getByIndex 的就可以使用,继承 List)
14.9.4 Set 接口的常用方法举例¶
public class SetMethod {
public static void main(String[] args) {
//老韩解读
//1. 以 Set 接口的实现类 HashSet 来讲解 Set 接口的方法
//2. set 接口的实现类的对象(Set 接口对象), 不能存放重复的元素, 可以添加一个 null
//3. set 接口对象存放数据是无序(即添加的顺序和取出的顺序不一致)
//4. 注意:取出的顺序的顺序虽然不是添加的顺序,但是他的固定.
Set set = new HashSet();
set.add("john");
set.add("lucy");
set.add("john");//重复
set.add("jack");
set.add("hsp");
set.add("mary");
set.add(null);//
set.add(null);//再次添加 null
for (int i = 0; i < 10; i ++) {
System.out.println("set=" + set);
}
//遍历
//方式 1: 使用迭代器
System.out.println("=====使用迭代器====");
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
set.remove(null);
//方式 2: 增强 for
System.out.println("=====增强 for====");
for (Object o : set) {
System.out.println("o=" + o);
}
//set 接口对象,不能通过索引来获取
}
}
14.10 Set 接口实现类-HashSet¶
14.10.1 HashSet 的全面说明¶
-
HashSet 实现了 Set 接口
-
HashSet 实际上是 HashMap ,看源码.
-
可以存放 null 值,但是只能有一个 null
-
HashSet 不保证元素是有序的,取决于 hash 后,再确定索引的结果.(即,不保证存放元素的顺序和取出顺序一致) 取出的顺序的顺序虽然不是添加的顺序,但是固定
5. 不能有重复元素 / 对象. 在前面 Set 接口使用已经讲¶
14.10.2 HashSet 案例说明¶
public class HashSet01 {
public static void main(String[] args) {
HashSet set = new HashSet();
//说明
//1. 在执行 add 方法后,会返回一个 boolean 值
//2. 如果添加成功,返回 true, 否则返回 false
//3. 可以通过 remove 指定删除哪个对象
System.out.println(set.add("john"));//T
System.out.println(set.add("lucy"));//T
System.out.println(set.add("john"));//F
System.out.println(set.add("jack"));//T
System.out.println(set.add("Rose"));//T
set.remove("john");
System.out.println("set=" + set);//3 个
set = new HashSet();
System.out.println("set=" + set);//0
//4 Hashset 不能添加相同的元素/数据?
set.add("lucy");//添加成功
set.add("lucy");//加入不了
set.add(new Dog("tom"));//OK
set.add(new Dog("tom"));//Ok
System.out.println("set=" + set);
//在加深一下. 非常经典的面试题.
//看源码,做分析, 先给小伙伴留一个坑,以后讲完源码,你就了然
//去看他的源码,即 add 到底发生了什么?=> 底层机制.
set.add(new String("hsp"));//ok
set.add(new String("hsp"));//加入不了.
System.out.println("set=" + set);
}
}
class Dog { //定义了 Dog 类
private String name;
public Dog(String name) {
this.name = name;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
'}';
}
}
14.10.3 HashSet 底层机制说明¶
分析 HashSet 底层是 HashMap, HashMap 底层是(数组+链表+红黑树) 分析 HashSet 的添加元素底层是如何实现
(hash();eqiials())
-
HashSet 底层是 HashMap
-
添加一个元素时,先得到 hash 值会转成索引值
-
找到存储数据表 table ,看这个索引位置是否已经存放的有元素如果没有,直接加入如果有,调用 equals 比较,如果相同,就放弃添加, 如果不相同,则添加到最后
- 在 Java 8 中, 如果一条链表的元素个数到达
TREEIFY THRESHOLD(默认是 8), 井且 table 的大小=MlN TREEIFY CAPACITY(默认 64), 就会独行树化(红黑树)
public class HashSetIncrement {
public static void main(String[] args) {
/*
HashSet 底层是 HashMap, 第一次添加时, table 数组扩容到 16,
临界值(threshold)是 16*加载因子(loadFactor)是 0.75 = 12
如果 table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,
新的临界值就是 32*0.75 = 24, 依次类推
*/
HashSet hashSet = new HashSet();
// for(int i = 1; i <= 100; i++) {
// hashSet.add(i);//1,2,3,4,5...100
// }
/*
在 Java8 中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ),
并且 table 的大小 >= MIN_TREEIFY_CAPACITY(默认 64),就会进行树化(红黑树),
否则仍然采用数组扩容机制
*/
// for(int i = 1; i <= 12; i++) {
// hashSet.add(new A(i));//
// }
/*
当我们向 hashset 增加一个元素, -> Node -> 加入 table , 就算是增加了一个 size++
*/
for (int i = 1; i <= 7; i++) { //在 table 的某一条链表上添加了 7 个 A 对象
hashSet.add(new A(i));//
}
for (int i = 1; i <= 7; i++) { //在 table 的另外一条链表上添加了 7 个 B 对象
hashSet.add(new B(i));//
}
}
}
class B {
private int n;
public B(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 200;
}
}
class A {
private int n;
public A(int n) {
this.n = n;
}
@Override
public int hashCode() {
return 100;
}
}
14.11 Set 接口实现类-LinkedHashSet¶
14.11.1 LinkedHashSet 的全面说明¶
- LinkedHashSet 是 HashSet 的子类
- LinkedHashSet 底层是一个 LinkedHashMap, 底层维护了一个数组+双向链表
- LinkedHashSet 根据元素的 hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序(图), 这使得元素看起来是以插入顺序保存的。
- LinkedHashSet 不允许添重复元素
@SuppressWarnings({"all"})
public class LinkedHashSetExercise {
public static void main(String[] args) {
LinkedHashSet linkedHashSet = new LinkedHashSet();
linkedHashSet.add(new Car("奥拓", 1000));//OK
linkedHashSet.add(new Car("奥迪", 300000));//OK
linkedHashSet.add(new Car("法拉利", 10000000));//OK
linkedHashSet.add(new Car("奥迪", 300000));//加入不了
linkedHashSet.add(new Car("保时捷", 70000000));//OK
linkedHashSet.add(new Car("奥迪", 300000));//加入不了
System.out.println("linkedHashSet=" + linkedHashSet);
}
}
/**
* Car 类(属性:name,price), 如果 name 和 price 一样,
* 则认为是相同元素,就不能添加。 5min
*/
class Car {
private String name;
private double price;
public Car(String name, double price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
@Override
public String toString() {
return "\nCar{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
//重写 equals 方法 和 hashCode
//当 name 和 price 相同时, 就返回相同的 hashCode 值, equals 返回 t
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Car car = (Car) o;
return Double.compare(car.price, price) == 0 &&
Objects.equals(name, car.name);
}
@Override
public int hashCode() {
return Objects.hash(name, price);
}
}
14.12 Map 接口和常用方法¶
14.12.1 Map 简介¶
-
Map 与 Collection 并列存在。用于保存具有映射关系的数据:
Key-Value -
Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
-
Map 中的 key 不允许重复,原因和 HashSet 一样,前面分析过源码.
-
Map 中的 vaule 可以重复
-
Map 的 key 可以为 null, value 也可以为 null ,注意 key 为 null, 只能有一个,value 为 null ,可以多个.
-
常用 String 类作为 Map 的 key
-
key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value
-
Map 存放数据的 key-vaEe 示意图, 一对 k-v 是放在一个 HashMap$Node 中的,有因为 Node 实现了 Entry 接口,有些书上也说一对 k-v 就是一个 Entry
14.12.2 Map 接口常用方法¶
@SuppressWarnings({"all"})
public class MapMethod {
public static void main(String[] args) {
//演示 map 接口常用方法
Map map = new HashMap();
map.put("邓超", new Book("", 100));//OK
map.put("邓超", "孙俪");//替换-> 一会分析源码
map.put("王宝强", "马蓉");//OK
map.put("宋喆", "马蓉");//OK
map.put("刘令博", null);//OK
map.put(null, "刘亦菲");//OK
map.put("鹿晗", "关晓彤");//OK
map.put("hsp", "hsp 的老婆");
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("hsp"));//T
}
}
class Book {
private String name;
private int num;
public Book(String name, int num) {
this.name = name;
this.num = num;
}
}
14.12.3 Map 接口遍历方法¶
- containsKey:查找键是否存在
- keyset:获取所有的键
- entrySet:获取所有关系k-v
- values:获取所有的值
@SuppressWarnings({"all"})
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put("邓超", "孙俪");
map.put("王宝强", "马蓉");
map.put("宋喆", "马蓉");
map.put("刘令博", null);
map.put(null, "刘亦菲");
map.put("鹿晗", "关晓彤");
//第一组: 先取出 所有的 Key , 通过 Key 取出对应的 Value
Set keyset = map.keySet();
//(1) 增强 for
System.out.println("-----第一种方式-------");
for (Object key : keyset) {
System.out.println(key + "-" + map.get(key));
}
//(2) 迭代器
System.out.println("----第二种方式--------");
Iterator iterator = keyset.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key + "-" + map.get(key));
}
//第二组: 把所有的 values 取出
Collection values = map.values();
//这里可以使用所有的 Collections 使用的遍历方法
//(1) 增强 for
System.out.println("---取出所有的 value 增强 for----");
for (Object value : values) {
System.out.println(value);
}
//(2) 迭代器
System.out.println("---取出所有的 value 迭代器----");
Iterator iterator2 = values.iterator();
while (iterator2.hasNext()) {
Object value = iterator2.next();
System.out.println(value);
}
//第三组: 通过 EntrySet 来获取 k-v
Set entrySet = map.entrySet();// EntrySet<Map.Entry<K,V>>
//(1) 增强 for
System.out.println("----使用 EntrySet 的 for 增强(第 3 种)----");
for (Object entry : entrySet) {
//将 entry 转成 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
//(2) 迭代器
System.out.println("----使用 EntrySet 的 迭代器(第 4 种)----");
Iterator iterator3 = entrySet.iterator();
while (iterator3.hasNext()) {
Object entry = iterator3.next();
//System.out.println(next.getClass());//HashMap$Node -实现-> Map.Entry (getKey,getValue)
//向下转型 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}
}
}
14.13 Map 接口实现类-HashMap¶
14.13.1 HashMap 小结¶
-
Map接口的常用实现类:HashMap、Hashtable和 Properties。
-
HashMap是 Map 接口使用频率最高的实现类。
-
HashMap 是以key-val 对的方式来存储数据(HashMaprNode类型)
-
key 不能重复,但是值可以重复,允许使用null键和null值。
-
如果添加相同的key ,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)
-
与 HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的.(jdk8的hashMap 底层数组+链表+红黑树)
-
HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized
14.13.2 HashMap 底层机制及源码剖析¶
在 HashMap 中,EntrySet 和 table 之间的关系是 逻辑上关联但物理上间接 的。
table 的定义¶
table 是 HashMap 的核心数据结构,定义为一个 Node<K,V>[] 数组,用于存储键值对。每个数组元素(称为“桶”)可能包含:
- 单个 Node(实现 Map.Entry<K,V> 的对象,包含 hash、key、value 和 next 字段)。
- 链表(通过 Node 的 next 字段连接多个节点,处理哈希冲突)。
- 红黑树(当链表过长时,Node 转换为 TreeNode)。
- 作用:
table是HashMap的底层存储结构,负责实际保存所有键值对。 - 存储方式:通过键的哈希值计算索引
(table.length - 1) & hash,将键值对存储到对应桶中。
EntrySet 的定义¶
EntrySet 是 HashMap 的一个内部类(HashMap$EntrySet),实现了 Set<Map.Entry<K,V>> 接口,表示 HashMap 中所有键值对的集合。
private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
// 其他方法如 size(), contains(), remove() 等
}
- 作用:
EntrySet提供了一种逻辑视图,允许用户通过map.entrySet()访问和遍历HashMap中的所有键值对。 - 实现:
EntrySet本身不存储数据,而是通过迭代器动态访问table中的Node或TreeNode。
EntrySet 和 table 的关系¶
EntrySet 和 table 的关系可以总结为以下几点:
- 逻辑视图与物理存储:
table是HashMap的物理存储结构,直接存储所有Node或TreeNode(键值对)。EntrySet是HashMap的逻辑视图,通过map.entrySet()返回一个Set<Map.Entry<K,V>>,其内容是对table中所有键值对的引用。-
EntrySet的每个元素(Map.Entry)实际上是table中的Node或TreeNode。 -
动态访问:
EntrySet不独立存储键值对,而是通过迭代器(EntryIterator)遍历table数组及其链表或红黑树,动态生成Map.Entry对象。-
例如,调用
entrySet().iterator()时,EntryIterator会:- 遍历
table的每个桶(table[i])。 - 如果桶包含链表或红黑树,依次访问每个
Node或TreeNode。 - 将每个节点作为
Map.Entry返回给用户。
- 遍历
-
操作同步:
- 对
EntrySet的操作(如通过迭代器删除元素)会直接反映到table上,因为EntrySet的迭代器直接操作table中的节点。 -
例如,调用
entrySet().remove(entry)会从table的对应桶中删除相应的Node或TreeNode。 -
代码示例:
输出:import java.util.HashMap; import java.util.Map; import java.util.Set; public class EntrySetTableDemo { public static void main(String[] args) { HashMap<String, Integer> map = new HashMap<>(); map.put("苹果", 10); map.put("香蕉", 20); // 获取 EntrySet Set<Map.Entry<String, Integer>> entrySet = map.entrySet(); System.out.println("EntrySet: " + entrySet); // 通过 EntrySet 修改值 for (Map.Entry<String, Integer> entry : entrySet) { entry.setValue(entry.getValue() + 100); } System.out.println("修改后的 Map: " + map); } } -
entrySet返回的Set包含table中的Node对象(作为Map.Entry)。 - 修改
entry.setValue()直接更改table中对应Node的value字段。
EntrySet 的迭代过程¶
EntrySet 的迭代器(HashMap$EntryIterator)的工作原理如下:
- 初始化:从 table[0] 开始,记录当前桶索引和当前节点。
- 遍历:
- 如果当前桶不为空,访问桶中的 Node 或 TreeNode(通过 next 字段遍历链表,或按红黑树顺序遍历)。
- 移动到下一个非空桶,重复上述过程。
- 返回:将每个 Node 或 TreeNode 作为 Map.Entry 返回,供用户访问 getKey() 和 getValue()。
class EntryIterator extends HashIterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
nextNode()方法遍历table,确保EntrySet能访问所有键值对。
关键点总结¶
- 物理与逻辑:
table是HashMap的物理存储,保存Node或TreeNode。EntrySet是逻辑视图,通过引用table中的节点提供键值对集合。- 依赖关系:
EntrySet依赖table,其内容是对table中节点的动态引用。EntrySet本身不存储数据,所有操作(如遍历、删除、修改)最终作用于table。- 效率:
EntrySet提供高效遍历方式,直接访问Node的键和值,避免通过key重新计算哈希查找值的开销。- 一致性:
EntrySet的修改(如setValue或remove)直接更新table,确保数据一致。
常见问题¶
- EntrySet 是否复制了 table 的数据?
-
没有,
EntrySet只是table的视图,迭代器直接访问table中的Node或TreeNode。 -
为什么使用 EntrySet 而不是直接遍历 table?
table是内部实现细节,用户无法直接访问。-
EntrySet提供标准化的Set<Map.Entry>接口,符合 Java 集合框架,方便遍历和操作。 -
EntrySet 和 table 的修改如何同步?
EntrySet的修改(如通过迭代器删除)会直接更新table的节点。- 反之,
HashMap的修改(如put、remove)会更新table,并通过modCount确保EntrySet迭代器感知结构变化(否则抛出ConcurrentModificationException)。