List接口
- List集合类中元素有序(即添加和取出顺序一致),且可重复
- List集合中的每个元素都有其对应的顺序索引,即支持索引;
- List容器中的元素都对应一个整数型的序号记其在容器中的位置,可以根据序号存取容器中的元素
- List常用接口有 ArrayList、LinkedList、Vector
// 创建 List 集合
List<String> fruits = new ArrayList<>();
// 添加元素
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("橘子");
fruits.add("香蕉"); // 可重复添加元素
// 显示 List 中的所有元素
System.out.println("List中的元素(按添加顺序): " + fruits); // [苹果, 香蕉, 橘子, 香蕉]
// 使用索引访问元素
System.out.println("第一个元素是: " + fruits.get(0)); // 苹果
System.out.println("第三个元素是: " + fruits.get(2)); // 橘子
// 修改元素
fruits.set(1, "葡萄"); // 将第二个元素(索引为1)修改为 "葡萄"
System.out.println("修改后的List: " + fruits); // [苹果, 葡萄, 橘子, 香蕉]
// 删除元素
fruits.remove(3); // 删除第四个元素(索引为3)
System.out.println("删除元素后的List: " + fruits); // [苹果, 葡萄, 橘子]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 1. List 接口的常用方法
方法 | 描述 |
---|---|
add(int index, Object ele) | 在索引 index 处添加元素 ele |
addAll(int index, Collection eles) | 从索引 index 开始,添加 eles 集合的所有元素 |
get(int index) | 获取索引 index 处的元素 |
indexOf(Object obj) | 查找 obj 的首次出现位置 |
lastIndexOf(Object obj) | 查找 obj 的最后出现位置 |
remove(int index) | 移除索引 index 处的元素 |
set(int index, Object ele) | 将索引 index 处的元素替换为 ele |
subList(int fromIndex, int toIndex) | 获取从 fromIndex 到 toIndex 的子列表 |
更多方法可以自行JDK API在线查询 (opens new window) 或 下载中文版JavaAPI帮助文档【免费下载】 (opens new window)
import java.util.ArrayList;
import java.util.List;
public class ListMethodsDemo {
public static void main(String[] args) {
// 创建一个ArrayList,并使用List接口引用它
List<String> list = new ArrayList<>();
// 1. 在列表末尾添加元素
list.add("开心的你");
// 在列表的开始位置(索引0)添加元素
list.add(0, "帅气的我");
System.out.println("添加元素后的列表: " + list);
// 输出: [帅气的我, 开心的你]
// 2. 从指定位置开始添加另一个集合中的所有元素
List<String> list1 = new ArrayList<>();
list1.add("Jack");
list1.add("Tom");
list1.add("Marry");
list.addAll(1, list1);
System.out.println("在索引1处添加另一个集合后的列表: " + list);
// 输出: [帅气的我, Jack, Tom, Marry, 开心的你]
// 3. 获取指定位置的元素
System.out.println("列表中索引0处的元素: " + list.get(0));
// 输出: 帅气的我
// 4. 查找元素首次出现的位置
System.out.println("‘开心的你’首次出现的位置: " + list.indexOf("开心的你"));
// 输出: 4
// 5. 查找元素最后一次出现的位置
list.add("Jack"); // 添加一个重复元素
System.out.println("‘Jack’最后一次出现的位置: " + list.lastIndexOf("Jack"));
// 输出: 5
// 6. 移除指定位置的元素
System.out.println("移除索引5处的元素: " + list.remove(5));
System.out.println("移除元素后的列表: " + list);
// 输出: [帅气的我, Jack, Tom, Marry, 开心的你]
// 7. 替换指定位置的元素
list.set(1, "!!!");
System.out.println("将索引1处的元素替换后的列表: " + list);
// 输出: [帅气的我, !!!, Tom, Marry, 开心的你]
// 8. 获取子列表
System.out.println("索引2到4之间的子列表: " + list.subList(2, 4));
// 输出: [Tom, Marry]
// 注意: subList方法返回的范围是左闭右开,即包括fromIndex,不包括toIndex
}
}
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
# 2. List的五种遍历方式
import java.util.*;
public class ListFor {
public static void main(String[] args) {
// List的实现接口子类ArrayList,LinkedList,Vector
List<String> list = new Vector<>();
list.add("熊大");
list.add("熊二");
list.add("光头强");
System.out.println("迭代器遍历:");
// 迭代器iterator遍历
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()){
String next = iterator.next();
System.out.println(next);
}
System.out.println("增强for遍历:");
// 增强for遍历
for (String o : list) {
System.out.println(o);
}
System.out.println("普通for遍历:");
// 普通for遍历
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println("Lambda表达式遍历:");
// Lambda表达式遍历
list.forEach(item -> System.out.println(item));
// 或者使用方法引用
System.out.println("Lambda方法引用遍历:");
list.forEach(System.out::println);
}
}
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
# 3. List 排序练习
import java.util.ArrayList;
import java.util.List;
public class BookSortDemo {
public static void main(String[] args) {
// 创建一个 List 来存储 Book 对象
List<Book> books = new ArrayList<>();
books.add(new Book("红楼梦", 36.4f, "曹雪芹"));
books.add(new Book("水浒传", 19.9f, "施耐庵"));
books.add(new Book("西游记", 28.8f, "吴承恩"));
// 打印排序前的书籍列表
System.out.println("排序前:");
for(Book book : books){
System.out.println(book);
}
// 对书籍列表进行排序(按价格从低到高)
sortBooksByPrice(books);
// 打印排序后的书籍列表
System.out.println("---- 排序后 ----");
for(Book book : books){
System.out.println(book);
}
}
// 通过冒泡排序算法对书籍列表按价格进行排序
public static void sortBooksByPrice(List<Book> books){
for (int i = 0; i < books.size(); i++) {
for (int j = 0; j < books.size() - 1 - i; j++) {
Book currentBook = books.get(j);
Book nextBook = books.get(j + 1);
// 如果当前书籍的价格大于下一本书籍的价格,则交换位置
if(currentBook.getPrice() > nextBook.getPrice()){
books.set(j, nextBook);
books.set(j + 1, currentBook);
}
}
}
}
}
// 定义 Book 类
class Book{
private String name;
private float price;
private String author;
public Book(String name, float price, String author) {
this.name = name;
this.price = price;
this.author = author;
}
public float getPrice() {
return price;
}
@Override
public String toString() {
return "书名:" + name + " 价格:" + price + " 作者:" + author;
}
}
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
# 4. ArrayList 使用注意事项
- 允许存放任何元素,包括空元素null
ArrayList list = new ArrayList();
list.add(null);
list.add("OK");
list.add(null);
System.out.println(list); //[null,OK,null]
2
3
4
5
- ArrayList 是由数组来实现数据存储的;
- ArrayList基本等同于 Vector ,除了 ArrayList是线程不安全的,但执行效率高,在多线程的情况下不建议用ArrayList;
# 5. ArrayList 底层结构
ArrayList底层使用Object数组实现;当数组容量不够时,创建一个新数组,这个新数组的容量是原数组的1.5倍,然后将原数组中的元素复制到这个新数组中。建议使用时,直接在构造方法中指定数组大小。避免扩容,影响效率。
- JDK7:当使用无参构造创建列表时,底层默认创建长度是10的Object数组。
- JDK8:当是用无参构造创建列表时,底层并不会指定数组的容量,第一次添加容量时才默认创建长度为10的数组
总结
- 当使用无参的构造方法创建ArrayList,容量默认值10在第一次插入的时候才会初始化初始容量,而不是在创建的时候就初始化容量。
- 在插入时,会检测是否真正需要扩容,如果需要,调用grow扩容。
- 初步预估按照原容量的1.5倍扩容
- 如果用户所需大小超过预估的1.5倍,则按照用户所需大小扩容。
- 真正扩容之前检测是否能扩容成功,防止太大导致扩容失败。
- 使用copyOf进行扩容 。
# 6. ArrayList 在多线程环境中的线程安全问题
ArrayLis
在 Java 中是非线程安全的,这意味着在多线程环境下,如果没有适当的同步措施,同时对 ArrayList 进行操作可能会导致不可预期的行为
- 并发修改异常(ConcurrentModificationException):
- 当多个线程同时尝试修改
ArrayList
时,可能会抛出此异常。例如,一个线程正在遍历ArrayList
,而另一个线程同时修改了它(添加、删除或修改元素)。
- 当多个线程同时尝试修改
- 数据不一致性:
- 在没有适当同步的情况下,一个线程对
ArrayList
所做的修改可能不会立即(或根本不会)对其他线程可见。这可能导致不同线程看到ArrayList
的不同状态。
- 在没有适当同步的情况下,一个线程对
- 数组扩容问题:
- 当两个或更多线程同时添加元素,触发
ArrayList
扩容时,可能会出现数据丢失或覆盖的问题。因为内部数组的大小需要根据添加的数据量动态调整,而这个过程涉及到数组复制和数据迁移,如果同时发生,可能导致问题。
- 当两个或更多线程同时添加元素,触发
- 索引混乱:
- 如果多个线程同时对
ArrayList
进行添加或删除操作,可能会导致元素的索引位置出现混乱,因为每个添加或删除操作都可能影响数组中元素的索引。
- 如果多个线程同时对
为了在多线程环境中安全地使用类似
ArrayList
的数据结构,建议使用线程安全的替代品,如Vector
或Collections.synchronizedList(new ArrayList<...>())
。另一个选择是使用java.util.concurrent
包中的并发集合,例如CopyOnWriteArrayList
。在实践中,还可以通过在关键操作周围添加同步块来手动管理线程安全。
# 7. Vector 底层结构
Vector:线程安全的,里面的方法基本都加了synchornized,因此效率很慢;底层使用Object数组存储;当使用无参构造创建时,JDK7和JDK8都默认创建一个长度为10的数组;当容量不够时会按照原始容量的2倍进行扩容。
- Vector 底层也是一个对象数组,protected Object[ ] elementData;
- Vector 是线程同步的,即线程安全,Vector类的操作方法带有synchronized
- 在开发中,需要线程同步安全时,考虑使用Vector
# 8. Stack的使用
什么是栈?(Stack)
栈是一种数据结构,是操作受限制的线性表(先进后出),它只能在固定的一端进行插入和删除操作,进行数据插入和删除的一端为栈顶,另一端为栈底,按存储方式分为顺序栈(更好实现)和链式栈。
压栈: 栈的插入操作叫做进栈/压栈/入栈。入数据在栈顶。
出栈: 栈的删除操作叫做出栈。出数据也在栈顶。
栈的使用 (参考博客 (opens new window))
方法 | 功能说明 |
---|---|
Stack() | 构造一个空栈 |
E push(E e) | 将e入栈并返回e |
E pop() | 将栈顶元素出栈并返回 |
E peek() | 获取栈顶元素 |
int size() | 获取栈中有效元素的个数 |
boolean empty() | 检测栈是否为空 |
栈的使用建议使用下面两种方式,不建议使用Stack
●顺序栈:Deque接口+实现类ArrayDeque
// 如果您的栈操作主要是添加和移除元素,且元素数量相对固定或变化不大,那么 ArrayDeque 更合适。
Deque<Integer> deque = new ArrayDeque<>();//顺序栈
2
●链式栈:Deque接口+实现类LinkedList
// 如果您需要除了栈操作以外的其他链表操作,或者栈中元素数量非常大且频繁变化,LinkedList 更合适。
Deque<Integer> deque1 = new LinkedList<>();//链式栈
2
Deque
接口(双端队列)拥有Stack
(栈)中的方法,是因为Deque
被设计为能够在两端进行元素的插入和移除操作,这使得它非常适合表现栈的行为。
在栈结构中,元素按照后进先出(LIFO)的顺序被处理,这意味着只有最后一个被添加的元素可以被移除。这种行为可以通过 Deque
的以下方法来实现:
push(E e)
: 在Deque
中,这个方法将元素添加到队列的前端,与栈的push
操作相同。pop()
: 在Deque
中,这个方法移除并返回队列前端的元素,模拟了栈的pop
操作。peek()
: 这个方法返回队列前端的元素,但不移除它,与栈的行为一致。
因此,Deque
接口自然成为实现栈结构的理想选择,同时提供了比传统 Stack
类更丰富和灵活的操作。实际上,Java 官方文档中也推荐使用 Deque
来代替 Stack
类,因为 Stack
类是基于 Vector
实现的,而 Vector
在现代 Java 版本中通常不被推荐使用,主要是因为它是线程同步的,这会在非多线程环境下带来不必要的性能开销。而 ArrayDeque
和 LinkedList
实现了 Deque
接口,提供了更高效、更灵活的栈操作。
# 9. LinkedList 的使用
LinkedList
是一个实现了List
和Deque
接口的双向链表,因此它提供了列表的功能,也能作为栈或队列使用。这些方法使LinkedList
成为一种灵活的数据结构,适合于各种不同的使用场景。
方法名称 | 功能描述 |
---|---|
add(E e) | 在链表尾部添加指定的元素 |
add(int index, E element) | 在链表的指定位置插入指定的元素 |
addFirst(E e) | 在链表头部插入指定的元素 |
addLast(E e) | 在链表尾部插入指定的元素(与 add(E e) 相同) |
remove() | 移除并返回链表头部的元素 |
remove(int index) | 移除链表中指定位置的元素 |
remove(Object o) | 移除链表中首次出现的指定元素 |
removeFirst() | 移除并返回链表的第一个元素 |
removeLast() | 移除并返回链表的最后一个元素 |
get(int index) | 返回链表中指定位置的元素 |
getFirst() | 返回链表的第一个元素 |
getLast() | 返回链表的最后一个元素 |
set(int index, E element) | 替换链表中指定位置的元素 |
size() | 返回链表中的元素个数 |
clear() | 清空整个链表 |
contains(Object o) | 判断链表是否包含指定的元素 |
isEmpty() | 判断链表是否为空 |
LinkedList的增删改查案例:
import java.util.Iterator;
import java.util.LinkedList;
public class LinkListCRUD {
public static void main(String[] args) {
// 创建一个 LinkedList 实例
LinkedList<Integer> linkedList = new LinkedList<>();
// 增加元素
linkedList.add(1); // 添加一个元素,此时它既是头节点也是尾节点
linkedList.add(2); // 添加第二个元素,first 仍然指向第一个节点,但 last 更新为新节点
linkedList.add(3); // 继续添加元素
System.out.println("增加元素后: " + linkedList); // 显示列表中的元素
// 删除元素
linkedList.remove(); // 默认删除第一个元素
System.out.println("删除元素后: " + linkedList); // 显示删除元素后的列表
// 修改元素
linkedList.set(1, 999); // 修改第二个元素为 999
System.out.println("修改元素后: " + linkedList); // 显示修改元素后的列表
// 查询元素
int element = linkedList.get(1); // 获取链表中第二个位置的元素
System.out.println("查询到的元素: " + element); // 输出查询到的元素
// 遍历 LinkedList
System.out.println("LinkedList 遍历:");
// 使用迭代器遍历
Iterator<Integer> iterator = linkedList.iterator();
while (iterator.hasNext()) {
int nextElement = iterator.next();
System.out.println(nextElement);
}
// 使用增强 for 循环遍历
System.out.println("使用增强 for 循环遍历:");
for (Integer num : linkedList) {
System.out.println(num);
}
// 使用普通 for 循环遍历
System.out.println("使用普通 for 循环遍历:");
for (int i = 0; i < linkedList.size(); i++) {
System.out.println(linkedList.get(i));
}
}
}
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
可以自行debug看一下调用方法的实现
# 10. LinkedList 底层结构
- LinkedList 实现了双向链表和双端队列的特点
- 可以添加任意元素(元素可以重复),包括null;
- 线程不安全,没有实现同步
- LinkedList底层维护了一个双向链表;
- LinkedList中维护了两个属性first和last分别指向 首节点 和 尾节点;
- 每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终完成双向链表;
- 所以 LinkedList的元素的添加和删除不是通过数组完成的,相对来说效率较高;
双向链表的模拟:(更多详情可点击此处 (opens new window))
public class TestLinkedList {
public static void main(String[] args) {
// 创建链表的节点
Node jack = new Node("Jack");
Node tom = new Node("Tom");
Node marry = new Node("Marry");
// 将节点连接起来形成双向链表
jack.next = tom; // Jack 的下一个是 Tom
tom.prev = jack; // Tom 的前一个是 Jack
tom.next = marry; // Tom 的下一个是 Marry
marry.prev = tom; // Marry 的前一个是 Tom
Node first = jack; // 双向链表的头节点是 Jack
Node last = marry; // 双向链表的尾节点是 Marry
// 从头到尾遍历链表
System.out.println("---- 从头到尾遍历 ----");
traverseFromHead(first);
// 从尾到头遍历链表
System.out.println("---- 从尾到头遍历 ----");
traverseFromTail(last);
// 在 Tom 和 Marry 之间插入一个新节点 Smith
Node smith = new Node("Smith");
insertNode(tom, smith); // 在 Tom 后面插入 Smith
// 再次从头到尾遍历链表
System.out.println("---- 插入 Smith 后从头到尾遍历 ----");
traverseFromHead(first);
}
// 遍历链表:从头到尾
public static void traverseFromHead(Node node) {
while (node != null) {
System.out.println(node);
node = node.next; // 移动到下一个节点
}
}
// 遍历链表:从尾到头
public static void traverseFromTail(Node node) {
while (node != null) {
System.out.println(node);
node = node.prev; // 移动到前一个节点
}
}
// 在链表中插入新节点
public static void insertNode(Node currentNode, Node newNode) {
// 将新节点链接到当前节点和下一个节点
newNode.next = currentNode.next;
newNode.prev = currentNode;
// 如果当前节点的下一个节点不为空,则设置其前一个节点为新节点
if (currentNode.next != null) {
currentNode.next.prev = newNode;
}
currentNode.next = newNode; // 将当前节点的下一个节点设置为新节点
}
}
// 定义一个 Node 类,表示双向链表的节点
class Node {
public Object data; // 节点存储的数据
public Node next; // 指向下一个节点
public Node prev; // 指向前一个节点
public Node(Object data) {
this.data = data; // 初始化节点数据
}
@Override
public String toString() {
return "Node: " + data; // 输出节点信息
}
}
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
# 11. ArrayList 和 LinkedList 比较
由于ArrayList在任意位置插入或删除元素的时候,需要将后续元素整体往前或者往后移动,时间复杂度为O(n),效率比较低,因此ArrayList不适合在插入删除较多的场景下使用,而这些问题链表都可以解决。
不同点 | ArrayList | LinkedList |
---|---|---|
存储空间上 | 物理上连续 | 逻辑上连续,物理上不一定连续 |
随机访问 | 支持O(1) | 不支持O(N) |
头插 | 需要移动元素,效率低O(N) | 只需要修改引用的指向O(1) |
插入 | 空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 频繁访问+随机存取 | 任意位置插入+频繁删除 |
如何选择 ArrayList 和 LinkedList :
- 如果改查的操作较多,选择 ArrayList;
- 如果增删的操作较多,选择 LinkedList;
- 一般程序中,80%-90%都是查询,因此大部分会使用ArrayList;
- 在项目中,灵活选择,可以一个模块用LinkedList,一个模块用ArrayList;
多线程的情况还是考虑 Vector ,因为它是线程安全的