Java集合框架笔记--List系列集合基本使用方法,ArrayList,LinkedList和迭代器底层源码
集合体系架构
Collection接口
-
单列集合的顶层接口
-
所有方法被List和Set系列集合共享
List系列集合
-
特点:有序(存和取的顺序)、可重复、有索引
-
实现类:
-
ArrayList
-
LinkedList
-
Vector
-
Set系列集合
-
特点:无序(存和取的顺序)、不重复、无索引
-
实现类:
-
HashSet → LinkedHashSet
-
TreeSet
-
Map
-
双列集合
Collection常用方法
方法 | 描述 |
---|---|
boolean add(E e) | 添加元素 |
void clear() | 清空集合 |
boolean remove(Object o) | 删除指定元素 |
boolean contains(Object o) | 判断是否包含指定元素 |
boolean isEmpty() | 判断集合是否为空 |
int size() | 获取集合长度 |
Collection<String> list = new ArrayList<>();//多态
list.add("aaa");
list.add("bbb");
list.add("ccc");
System.out.println(list); // [aaa, bbb, ccc]
list.clear(); // 清空集合
// 删除元素
System.out.println(list.remove("aaa")); // true
System.out.println(list); // [bbb, ccc]
// 判断元素是否包含
boolean result = list.contains("bbb");
System.out.println(result); // true
// 判断集合是否为空
System.out.println(list.isEmpty()); // false
// 获取集合长度
System.out.println(list.size()); // 2
注意:
-
Collection是一个接口,不能直接创建对象
-
使用多态创建对象:
Collection<String> list = new ArrayList<>();
-
添加元素细节:
-
List系列集合:方法永远返回true,因为允许重复
-
Set系列集合:
-
如果添加元素不存在,返回true,表示添加成功
-
如果添加元素已存在,返回false,表示添加失败
-
-
集合遍历方式
1. 迭代器遍历
Collection<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
迭代器常用方法:
-
boolean hasNext()
:判断当前位置是否有元素 -
E next()
:获取当前位置的元素,并将迭代器移动到下一个位置
注意事项:
-
NoSuchElementException
:迭代器没有更多元素时调用next()方法会抛出此异常 -
迭代器遍历完毕后,指针不会复位,需要重新获取迭代器
-
循环中只能使用一次next()方法,多次使用需用变量保存返回值
-
迭代器遍历时,不能用集合的方法进行增删元素,否则会抛出
ConcurrentModificationException
2. 增强for循环
Collection<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
for (String s : list) {
System.out.println(s);
}
特点:
-
底层就是Iterator迭代器
-
所有单列集合和数组都可以使用
-
修改循环变量不会改变集合中原本的数据
3. Lambda表达式遍历
Collection<String> list = new ArrayList<>();
list.add("aaa");
list.add("bbb");
list.add("ccc");
// 匿名内部类方式
list.forEach(new Consumer<String>() {
@Override
public void accept(String string) {
System.out.println(string);
}
});
// Lambda表达式方式
list.forEach(string -> System.out.println(string));
List系列集合独有方法
方法 | 描述 |
---|---|
void add(int index, E element) | 在指定位置添加元素 |
E remove(int index) | 删除指定索引位置的元素,并返回被删除的元素 |
E get(int index) | 获取指定索引位置的元素 |
E set(int index, E element) | 修改指定索引位置的元素,并返回被修改的元素 |
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
list.add("java");
// add方法
list.add(1, "world111");
System.out.println(list); // [hello, world111, world, java]
// remove方法
String remove = list.remove(1);
System.out.println(remove); // world111
// get方法
String get = list.get(1);
System.out.println(get); // world
// set方法
String set = list.set(1, "world222");
System.out.println(set); // world
System.out.println(list); // [hello, world222, java]
注意:
-
List系列集合有两个删除方法:
-
直接删除元素:
remove(Object o)
-
通过索引删除:
remove(int index)
-
-
方法重载时,优先调用实参与形参类型一致的方法
列表迭代器遍历
还有一个普通的for循环遍历,加上迭代器,增强for循环,lambda一共是五种遍历方式。
List<String> list = new ArrayList<>();
list.add("hello");
list.add("world");
list.add("java");
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
String next = listIterator.next();
System.out.println(next);
}
// 额外方法
// listIterator.add():在遍历过程中添加元素
// previous():返回列表中的前一个元素
// hasPrevious():判断列表中是否有前一个元素
ArrayList集合底层原理
核心特点
-
底层是数组结构
-
利用空参创建的集合,在底层创建一个默认长度为0的数组
-
添加第一个元素时,底层创建一个长度为10的数组
-
存满时,会扩容1.5倍
-
如果一次添加多个元素,1.5倍还放不下,则新创建数组的长度以实际为准
源码分析
//1.利用空参创建的集合 elementData DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {} size = 0
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//2.添加元素 调用add
public boolean add(E e) {
modCount++;
add(e, elementData, size);//要添加的元素,数组名字,当前集合的长度/现在元素应存入的位置,
// //size代表下一个新元素应该被放入的位置(索引)
return true;
}
//3.调用add(e, elementData, size);
//elementData.length是容量,size表示当前元素数量 判断数组容量和当前元素数量是否相等,满足扩容条件
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
//4.调用grow()扩容
private Object[] grow() {
return grow(size + 1); //size + 1 代表的是添加一个新元素所需的最小容量。
// grow() 方法需要确保扩容后的数组至少能容纳 size + 1 个元素,这是它进行扩容计算的目标和依据
}
//5.调用grow(size + 1);
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA /* { } */) {
int newCapacity = ArraysSupport.newLength(oldCapacity, /* 老容量 */
minCapacity - oldCapacity, /*理论上至少要新增的容量*/
oldCapacity >> 1 /*默认新增的容量大小 /2 0.5 */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
//DEFAULT_CAPACITY = 10;
return elementData = new Object[Math.max(DEFAULT_CAPACITY /* 10 */, minCapacity)];// 第一个加入的数据小于10,则创建一个长度为10的数组
}
}
//6.newLength()
public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
//新数组真正的长度
int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) { /*SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8*/
return prefLength; //返回新数组的长度
} else {
return hugeLength(oldLength, minGrowth);//处理极端情况下的容量计算,在可能的情况下尽量满足容量需求,在无法满足时提供清晰的错误信息(OutOfMemoryError)
}
}
//7.Arrays.copyOf();
//elementData = Arrays.copyOf(elementData, newCapacity);
//1.根据新容量创建新的数组,2.把原数组的元素复制到新数组中,3.把新数组赋给elementData,并返回新数组
//8.回到add(e, elementData, size);
//elementData = 返回的新数组
//执行elementData[s] = e; size = s + 1;
//9.回到add() return true;
关键点:
-
SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8
,为JVM数组头信息预留空间 -
hugeLength()
方法处理极端情况下的容量计算,确保不会创建超过JVM支持的数组大小 -
第一次添加元素时,会创建一个长度为10的数组(如果所需容量小于10)
集合中的"空位置"问题
-
ArrayList:底层是数组,但空位置只存在于数组末尾,迭代器只会遍历到size-1
-
LinkedList:每个元素都是节点,节点之间通过指针连接,不存在空位置
-
HashSet/TreeSet:基于哈希表或树,可能有空桶,但迭代器会跳过这些空桶
-
集合允许存储null值,但这不同于"空位置"
-
迭代器设计为只遍历集合中实际存在的元素,不会访问任何"空位置"
-
当从集合中删除元素时,集合会调整其内部结构,确保不会留下"空位"
并发修改异常(ConcurrentModificationException)
原因
迭代器的"快速失败(fail-fast)"机制在单线程内检测到了意料之外的修改
机制原理
-
创建迭代器时,迭代器内部记录当前集合的修改次数(modCount)
-
集合本身也有一个变量记录修改次数
-
每次对集合进行结构性修改时,modCount值增加
-
迭代器在每次操作前检查modCount是否与记录值一致
-
如果不一致,抛出ConcurrentModificationException
解决方案
-
在迭代过程中,如果要删除元素,必须使用迭代器自身的remove()方法
-
使用Java 8+的removeIf()方法
-
在多线程环境下,使用并发集合类如CopyOnWriteArrayList
LinkedList 集合
特点
-
底层数据结构是双向链表
-
查询慢,增删快
-
如果操作的是首尾元素,速度极快
-
提供了很多直接操作首尾元素的特有 API
特有 API 方法
方法 | 描述 |
---|---|
void addFirst(E e) | 在列表开头添加一个元素 |
void addLast(E e) | 将指定元素追加到列表末尾 |
E getFirst() | 返回列表的第一个元素 |
E getLast() | 返回列表的最后一个元素 |
E removeFirst() | 删除并返回列表的第一个元素 |
E removeLast() | 删除并返回列表的末尾元素 |
LinkedList 底层原理
1. 创建空链表(无参构造)
// LinkedList 成员变量
transient int size = 0; // 链表大小
transient Node<E> first; // 头节点
transient Node<E> last; // 尾节点
// 无参构造函数
public LinkedList() {
// 初始化空链表
}
2. 添加元素
//1.创建一个空链表,无参构造
/**
* transient int size = 0;
* transient LinkedList.Node<E> first;
* transient LinkedList.Node<E> last;
*/
public LinkedList() {
}
//2.添加元素 调用add(E e);
public boolean add(E e) {
linkLast(e);
return true;
}
//3.调用linkLast(E e);
//尾插法
void linkLast(E e) {
final LinkedList.Node<E> l = last;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null); //创建一个新节点
last = newNode; //链表的头节点中的last 指向新节点
if (l == null)
first = newNode; //链表为空,新节点作为头节点
else
l.next = newNode; //链表不为空,新节点作为尾节点
size++; //链表的长度加1
modCount++; //链表结构改变次数加1(防止多线程篡改)
}
//4.add(E e); return true
3. 节点结构
// 双向链表节点定义
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;
}
}
迭代器底层原理
迭代器创建
// 获取迭代器
public Iterator<E> iterator() {
return new Itr(); // 创建内部类Itr的实例
}
// 每次调用iterator()方法都会创建一个新的迭代器对象
迭代器内部类实现
// 迭代器内部类
private class Itr implements Iterator<E> {
int cursor; // 光标,表示迭代器指针,默认指向0索引
int lastRet = -1; // 上一次操作的索引
int expectedModCount = modCount; // 记录创建迭代器时的修改次数
// 构造函数
Itr() {
}
// 判断是否还有元素
public boolean hasNext() {
return cursor != size; // 光标不等于集合大小表示还有元素
}
// 获取下一个元素
public E next() {
checkForComodification(); // 检查是否发生并发修改
int i = cursor; // 记录当前指针位置
if (i >= size) {
throw new NoSuchElementException();
}
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
cursor = i + 1; // 移动指针到下一个位置
return (E) elementData[lastRet = i]; // 返回当前元素并记录上一次操作索引
}
// 检查并发修改
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
// 删除当前元素
public void remove() {
if (lastRet < 0) {
throw new IllegalStateException();
}
checkForComodification();
try {
ArrayList.this.remove(lastRet); // 调用集合的remove方法
cursor = lastRet; // 调整光标位置
lastRet = -1; // 重置上一次操作索引
expectedModCount = modCount; // 同步修改次数
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
关键机制解释
1. 快速失败 (Fail-Fast) 机制
// 检查并发修改的核心方法
final void checkForComodification() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
-
modCount
:集合实际的修改次数,每次结构性修改都会增加 -
expectedModCount
:迭代器期望的修改次数,在迭代器创建时记录 -
当两者不相等时,说明集合在迭代过程中被意外修改,抛出异常
2. 迭代器删除操作的特殊处理
迭代器的 remove()
方法做了特殊处理:
-
调用集合的
remove()
方法删除元素 -
调整光标位置 (
cursor = lastRet
) -
重置上一次操作索引 (
lastRet = -1
) -
同步修改次数 (
expectedModCount = modCount
)
这一步同步操作是关键,它确保了迭代器知道集合已经被修改,并且更新了自己的期望值,从而避免了 ConcurrentModificationException
。
总结对比
LinkedList vs ArrayList
特性 | LinkedList | ArrayList |
---|---|---|
底层结构 | 双向链表 | 动态数组 |
查询效率 | 慢 (O(n)) | 快 (O(1)) |
增删效率 | 快 (O(1)) | 慢 (O(n)) |
内存占用 | 较高 (每个元素需要额外指针) | 较低 (连续内存) |
适用场景 | 频繁增删操作 | 频繁查询操作 |
迭代器使用注意事项
-
并发修改异常:迭代过程中不要直接使用集合的增删方法
-
多次获取迭代器:每次调用
iterator()
都会返回一个新的迭代器对象 -
迭代器删除:使用迭代器自身的
remove()
方法可以安全删除元素 -
单向遍历:基本迭代器只能向前遍历,ListIterator 支持双向遍历
-
快速失败机制:这是为了保护集合在迭代过程中的一致性