Java集合框架主要包括两种类型的容器,一种是集合,存储一个元素集合(Collection),另一种是图(Map),存储键/值对映射
容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
Map接口和Collection是所有集合框架的父接口
List集合
1、List接口
List是一个有序的Collection,用户可以通过索引访问元素,并且允许重复元素。
特点:
有序(插入顺序)
允许重复元素
可以通过索引访问元素
主要方法:
E get(int index) - 获取指定位置的元素
E set(int index, E element) - 替换指定位置的元素
void add(int index, E element) - 在指定位置插入元素
E remove(int index) - 移除指定位置的元素
int indexOf(Object o) - 返回指定元素第一次出现的索引
int lastIndexOf(Object o) - 返回指定元素最后一次出现的索引
List<E> subList(int fromIndex, int toIndex) - 返回指定范围内的子列表
1、ArrayList
ArrayList 是 Java 中 List 接口的一个实现类,属于集合框架中的一个重要类。它是基于动态数组实现的,可以存储任意类型的元素(包括 null),并且可以自动调整数组大小。ArrayList 提供了对元素的顺序访问、添加、删除等操作。
主要特点:
1、动态数组:
ArrayList 是通过动态数组来实现的,因此它可以自动扩容。每当添加的元素超过当前数组的容量时,ArrayList 会自动扩展数组的大小。
2、顺序存储:
元素是按照插入顺序存储的,支持通过索引来访问和操作元素。可以使用 get(index) 方法获取指定索引位置的元素。
3、支持重复元素:
ArrayList 允许存储重复的元素,也就是说,列表中可以包含多个相同的元素。
4、允许 null 元素:
ArrayList 允许元素为 null,这与某些其他类型的集合(如 HashSet)不同。
5、有序集合:
ArrayList 是有序的,这意味着元素的插入顺序会被保留。通过索引可以访问任何元素。
6、动态扩容:
ArrayList 的容量不是固定的。当元素个数超过当前容量时,它会自动扩容,通常会扩展为原来容量的 1.5 倍。
你也可以在创建 ArrayList 时,通过 ensureCapacity 方法预设其容量,以减少扩容次数,从而提高性能。
7、线程不安全:
ArrayList 是 非线程安全 的,如果多个线程同时访问一个 ArrayList 并且至少有一个线程修改了它的结构(即增加或删除元素),那么就会导致数据不一致。
如果需要在多线程环境中使用 ArrayList,可以使用 Collections.synchronizedList 方法来将其包装为线程安全的集合。
8、查询性能:
在访问元素时,ArrayList 具有 常数时间 复杂度(O(1)),即通过索引快速访问元素。
9、插入和删除性能:
ArrayList 在末尾插入元素的性能较好(平均时间复杂度为 O(1))。
在中间或开头插入或删除元素的性能较差,因为需要移动后续元素,因此时间复杂度是 O(n)。
10、迭代器:
ArrayList 提供的迭代器是快速失败的,如果在迭代过程中结构发生修改(例如添加或删除元素),将会抛出 ConcurrentModificationException 异常。
底层数据结构
注意:transient:这个关键字表示该字段在序列化时会被忽略。序列化是将对象转换为字节流的过程,在这个过程中,elementData 字段不会被序列化。这是因为 elementData 可能在不同的环境下有不同的实际值,因此不需要被持久化到磁盘。
构造方法
ArrayList数组初始化长度
/**
* 通过反射验证ArrayList初始化长度为0
*/
public class ListDemoA {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
ArrayList<Object> list = new ArrayList<>();
// 获取 ArrayList 类的 elementData 字段
Field field = ArrayList.class.getDeclaredField("elementData");
// 使字段可访问
field.setAccessible(true);
// 获取 elementData 数组
Object[] elementData = (Object[]) field.get(list);
// 输出当前的容量,即数组的长度
System.out.println("默认容量是: " + elementData.length);
}
}
运行结果:
当存储一个值的时候
/**
* @author liuwei
* @version JDK 8
* @className ListDemoB
* @description 验证ArrayList存储一个值后的长度
*/
public class ListDemoB {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
ArrayList<Object> list = new ArrayList<>();
list.add("Hello");
System.out.println("ArrayList元素个数为"+list.size());
// 获取 ArrayList 类的 elementData 字段
Field field = ArrayList.class.getDeclaredField("elementData");
// 使字段可访问
field.setAccessible(true);
// 获取 elementData 数组
Object[] elementData = (Object[]) field.get(list);
// 输出当前的容量,即数组的长度
System.out.println("默认容量是: " + elementData.length);
}
}
运行结果:
自动扩容
每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
/**
* ArrayList 扩容测试
*/
public class ArrayListResizeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 创建一个空的 ArrayList
ArrayList<Object> list = new ArrayList<>();
// 初始容量检查
System.out.println("初始容量: " + getCapacity(list));
// 向 ArrayList 添加元素并检查扩容
for (int i = 0; i < 30; i++) {
list.add(i);
System.out.println("添加第 " + (i + 1) + " 个元素后容量: " + getCapacity(list));
}
}
// 通过反射获取 ArrayList 的容量
private static int getCapacity(ArrayList<Object> list) throws NoSuchFieldException, IllegalAccessException {
// 获取 elementData 字段
Field field = ArrayList.class.getDeclaredField("elementData");
field.setAccessible(true);
// 获取 elementData 数组
Object[] elementData = (Object[]) field.get(list);
// 返回当前容量(即数组长度)
return elementData.length;
}
}
Arraylist常用方法:
方法名 |
说明 |
示例 |
|
添加元素到末尾 |
|
|
在指定位置插入元素 |
|
|
获取指定位置元素 |
|
|
修改指定位置的元素 |
|
|
删除指定位置元素 |
|
|
删除首次出现的指定元素 |
|
|
返回列表长度 |
|
|
判断列表是否为空 |
|
|
是否包含某元素 |
|
|
清空所有元素 |
|
|
返回第一次出现的位置 |
|
|
返回最后一次出现的位置 |
|
|
转换成数组 |
|
|
返回迭代器 |
|
|
Lambda遍历 |
|
|
获取子列表(左闭右开) |
|
|
扩容容量(手动) |
|
Arraylist遍历(7种方式)
/**
* @description: ArrayList 遍历方式全集(7种)
* @author: liuwei
*/
public class ArrayListTraversalDemo {
public static void main(String[] args) {
// 初始化 ArrayList
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Python");
list.add("C++");
list.add("Go");
System.out.println("=== 1. for 普通索引遍历 ===");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
System.out.println("\n=== 2. 增强 for-each 遍历 ===");
for (String lang : list) {
System.out.println(lang);
}
System.out.println("\n=== 3. Iterator 遍历 ===");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println("\n=== 4. ListIterator 正向遍历 ===");
ListIterator<String> listIterator = list.listIterator();
while (listIterator.hasNext()) {
System.out.println(listIterator.next());
}
System.out.println("\n=== 5. ListIterator 反向遍历 ===");
while (listIterator.hasPrevious()) {
System.out.println(listIterator.previous());
}
System.out.println("\n=== 6. forEach 方法 (Lambda表达式) ===");
list.forEach(item -> System.out.println(item));
System.out.println("\n=== 7. Java 8 Stream 遍历 ===");
list.stream().forEach(System.out::println);
}
}
2、LinkedList
LinkedList 是 Java 中的一个集合类,位于 java.util 包中,它实现了 List 接口,并且是基于链表实现的。与 ArrayList 基于数组存储元素不同,LinkedList 使用节点(Node)来存储元素,每个节点包含数据和指向前后节点的指针。
特点:
1、基于链表实现:
LinkedList 使用链表结构(双向链表)来存储元素,每个元素(节点)包含两个部分:数据部分和两个指针部分(指向前一个节点和后一个节点)。
由于采用链表结构,LinkedList 没有固定的大小,因此在添加或删除元素时,它不会发生数组大小的变化,而是通过修改指针来进行操作。
2、动态扩展:
LinkedList 不像 ArrayList 那样需要重新分配数组空间。当元素被插入或删除时,链表的结构通过修改指针来完成扩展或收缩。
3、访问速度:
由于 LinkedList 是基于链表结构的,它在随机访问元素时性能较差。访问某个元素需要从头或尾部逐一遍历节点,时间复杂度为 O(n)。
对比之下,ArrayList 在随机访问时具有 O(1) 的时间复杂度,因为它是基于数组实现的。
4、插入和删除:
在链表中,插入和删除元素的时间复杂度通常是 O(1),尤其是在头部或尾部插入时,因为不需要移动其他元素,只需要改变指针。
对比 ArrayList,当在中间插入或删除元素时,ArrayList 需要移动后续元素,因此时间复杂度为 O(n)。
5、内存使用:
由于每个节点不仅保存数据,还需要额外存储两个指针(前驱和后继指针),因此 LinkedList 在内存上的开销较大。
ArrayList 仅需要存储元素本身,因此内存占用较小。
6、双向链表:
LinkedList 是一个双向链表,即每个节点都有指向前后节点的指针。因此,LinkedList 可以从头到尾遍历,也可以从尾到头遍历。
7、线程安全:
LinkedList 是非线程安全的。如果多个线程同时操作同一个 LinkedList,需要手动同步。
8、适用场景:
频繁插入和删除元素的场景:如果需要在中间或两端频繁插入或删除元素,LinkedList 的性能优于 ArrayList。
不需要频繁随机访问的场景:对于需要顺序遍历或在两端操作的场景,LinkedList 也是合适的。
9、主要实现:
LinkedList 实现了 List、Queue 和 Deque 接口,因此它既可以作为列表使用,也可以作为队列或双端队列来使用。
构造方法
LinkedList常用方法:
方法名 |
说明 |
示例 |
|
添加元素到末尾 |
|
|
添加元素到开头 |
|
|
添加元素到末尾(等同于 add) |
|
|
在指定位置插入元素 |
|
|
获取指定位置的元素 |
|
|
获取第一个元素 |
|
|
获取最后一个元素 |
|
|
替换指定位置元素 |
|
|
移除第一个元素(等同于 |
|
|
移除第一个元素 |
|
|
移除最后一个元素 |
|
|
移除指定位置的元素 |
|
|
移除指定元素(第一个匹配项) |
|
|
查看第一个元素但不移除 |
|
|
查看第一个元素但不移除 |
|
|
查看最后一个元素但不移除 |
|
|
获取并移除第一个元素 |
|
|
获取并移除第一个元素 |
|
|
获取并移除最后一个元素 |
|
|
添加元素到队尾 |
|
|
添加元素到队头 |
|
|
添加元素到队尾 |
|
|
判断是否包含某元素 |
|
|
返回元素第一次出现位置 |
|
|
返回元素最后一次出现位置 |
|
|
清空所有元素 |
|
|
返回集合大小 |
|