一:List
1.1 什么是List
在集合框架中,List是一个接口,继承自Collection。
Collection也是一个接口,该接口中规范了后序容器中常用的一些方法,具体如下所示:
Iterable也是一个接口,Iterable接口表示实现该接口的类是可以逐个元素进行遍历的,
站在数据结构的角度来看,List就是一个线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删改查以及变量等操作。
List中常用的方法如下所示:
方法 | 解释 |
---|---|
boolean add(E e) | 尾插 e |
void add(int index, E element) | 将 e 插入到 index 位置 |
boolean addAll(Collection<? extends E> c) | 尾插 c 中的元素 |
E remove(int index) | 删除 index 位置元素 |
boolean remove(Object o) | 删除遇到的第一个 o |
E get(int index) | 获取下标 index 位置元素 |
E set(int index, E element ) | 将下标 index 位置元素设置为 element |
void clear() | 清空 |
boolean contains(Object o) | 判断 o 是否在线性表中 |
int indexOf(Object o) | 返回第一个 o 所在下标 |
int lastIndexOf(Object o) | 返回最后一个 o 的下标 |
List < E> subList(int fromIndex, int toIndex) | 截取部分 list |
1.2 List的使用
注意:List是个接口,并不能直接用来实例化。
如果要使用,必须去实例化List的实现类。在集合框架中,ArrayList和LinkedList都实现了List接口。
使用实例如下:
java
List<String> arrayList = new ArrayList<>(); // 使用ArrayList实现
List<Integer> linkedList = new LinkedList<>(); // 使用LinkedList实现
//添加元素
arrayList.add("Apple");
arrayList.add("Banana");
arrayList.add("Orange");
//添加元素
linkedList.add(10);
linkedList.add(20);
linkedList.add(30);
我们通过向上转型,分别调用了ArrayList和LinkedList中的add重写方法,这两个方法在ArrayList和LinkedList中都有自己的实现方式
二: ArrayList的使用
2.1 什么是线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
比如说:
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
2.2ArrayList简介
【说明】
- ArrayList是以泛型方式实现的,使用时必须要先实例化,并指定一下泛型参数
- ArrayList实现了RandomAccess接口,表明ArrayList支持随机访问
- ArrayList实现了Cloneable接口,表明ArrayList是可以clone的
- ArrayList实现了Serializable接口,表明ArrayList是支持序列化的
- 和Vector不同,ArrayList不是线程安全的,在单线程下可以使用,在多线程中可以选择Vector或者
CopyOnWriteArrayList - ArrayList底层是一段连续的空间,并且可以动态扩容,是一个动态类型的顺序表
2.3 ArrayList的使用
方法 | 解释 |
---|---|
ArrayList() | 无参构造 |
ArrayList(Collection<? extends E> c) | 利用其他 Collection 构建 ArrayList |
ArrayList(int initialCapacity) | 指定顺序表初始容量 |
ArrayList(Collection<? extends E> c)
是一个构造函数,它接受一个实现了 Collection
接口的对象 c
。这个构造函数会将 c
中的所有元素添加到新创建的 ArrayList
中。
通常情况下,ArrayList
是按照元素的添加顺序来存储的,而 ArrayList(Collection<? extends E> c)
可以方便地将另一个集合的元素全部添加到当前的 ArrayList
中。
例如,假设有一个 List
对象 list
,可以使用如下代码创建一个新的 ArrayList
,并将 list
中的所有元素添加到新的 ArrayList
中:
java
ArrayList<String> arrayList = new ArrayList<>(list);
在上述代码中,ArrayList<String> arrayList
是一个新创建的 ArrayList
对象,它包含了 list
中的所有元素。
注意:ArrayList(Collection<? extends E> c)
的泛型参数要与 ArrayList
的泛型参数类型一致或构成子类父类的关系,即它们需要是相同类型或者是其子类型。
2.4 ArrayList的常用方法
方法 | 解释 |
---|---|
boolean add(E e) | 尾插 e |
void add(int index, E element) | 将 e 插入到 index 位置 |
boolean addAll(Collection<? extends E> c) | 尾插 c 中的元素 |
E remove(int index) | 删除 index 位置元素 |
boolean remove(Object o) | 删除遇到的第一个 o |
E get(int index) | 获取下标 index 位置元素 |
E set(int index, E element) | 将下标 index 位置元素设置为element |
void clear() | 清空 |
boolean contains(Object o) | 判断 o 是否在线性表中 |
int indexOf(Object o) | 返回第一个 o 所在下标 |
int lastIndexOf(Object o) | 返回最后一个 o 的下标 |
List< E > subList(int fromIndex, int toIndex) | 截取部分 list |
使用实例:
java
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("JavaSE");
list.add("JavaWeb");
list.add("JavaEE");
list.add("JVM");
list.add("测试课程");
System.out.println(list);
// 获取list中有效元素个数
System.out.println(list.size());
// 获取和设置index位置上的元素,注意index必须介于[0, size)间
System.out.println(list.get(1));
list.set(1, "JavaWEB");
System.out.println(list.get(1));
// 在list的index位置插入指定元素,index及后续的元素统一往后搬移一个位置
list.add(1, "Java数据结构");
System.out.println(list);
// 删除指定元素,找到了就删除,该元素之后的元素统一往前搬移一个位置
list.remove("JVM");
System.out.println(list);
// 删除list中index位置上的元素,注意index不要超过list中有效元素个数,否则会抛出下标越界异常
list.remove(list.size()-1);
System.out.println(list);
// 检测list中是否包含指定元素,包含返回true,否则返回false
if(list.contains("测试课程")){
list.add("测试课程");
}
// 查找指定元素第一次出现的位置:indexOf从前往后找,lastIndexOf从后往前找
list.add("JavaSE");
System.out.println(list.indexOf("JavaSE"));
System.out.println(list.lastIndexOf("JavaSE"));
// 使用list中[0, 4)之间的元素构成一个新的SubList返回,但是和ArrayList共用一个elementData数组
List<String> ret = list.subList(0, 4);
System.out.println(ret);
list.clear();
System.out.println(list.size());
}
2.5 ArrayList的遍历
ArrayList 可以使用三方方式遍历:for循环+下标、foreach、使用迭代器
java
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
// 使用下标+for遍历
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
// 借助foreach遍历
for (Integer integer : list) {
System.out.print(integer + " ");
}
System.out.println();
//迭代器遍历
Iterator<Integer> it = list.listIterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
}
下面我们讲解一下使用迭代器遍历的方式
迭代器是用于遍历集合(如列表、数组等)中的元素的一种接口。它提供了一种统一的方式来访问和处理集合中的元素,而不需要暴露集合的内部实现细节。
迭代器有以下几个主要的方法:
hasNext()
: 检查迭代器中是否还有下一个元素,如果有则返回true
,否则返回false
。next()
: 返回迭代器的下一个元素,并将迭代器的位置向后移动。remove()
: 从迭代器指向的集合中移除迭代器返回的最后一个元素。
使用迭代器,你可以按顺序访问集合中的每一个元素,而不需要知道集合的内部结构或者索引。这样可以提供更灵活和独立的遍历方式。
在这段代码中,Iterator<Integer> it = list.listIterator();
这行代码的作用是创建一个整数类型的迭代器 it
,并将其初始化为列表的迭代器。这样我们就可以使用 it
来遍历并访问列表中的元素。
java
while(it.hasNext()){
System.out.print(it.next() + " ");
}
在上述示例中,it.hasNext()
检查迭代器中是否还有下一个元素,如果有,则执行循环内部代码。it.next()
返回迭代器的下一个元素,并将迭代器的位置向后移动。你可以在循环内部处理当前元素,例如打印出来。
迭代器是一次性的,即只能从头到尾进行一次遍历。如果需要重新遍历集合,你需要重新创建一个新的迭代器。
注意:
- ArrayList最长使用的遍历方式是:for循环+下标 以及 foreach
- 迭代器是设计模式的一种,后序容器接触多了再给大家铺垫
2.6ArrayList的扩容机制
ArrayList是一个动态类型的顺序表,当我们向 Java 的 ArrayList 中添加元素时,ArrayList 会自动扩容,以容纳更多的元素。ArrayList 的扩容机制如下:
-
初始容量:每个 ArrayList 对象都有一个初始容量。在创建 ArrayList 对象时,如果没有指定初始容量大小,则默认为10。这意味着初始时 ArrayList 可以容纳最多10个元素。
-
操作次数:当我们向 ArrayList 中添加新元素时,ArrayList 会根据当前存储的元素数量和内部数组的长度进行判断。每次添加元素的操作都会增加 ArrayList 的操作次数。
-
扩容策略:当 ArrayList 的操作次数达到其内部数组长度时,就会触发扩容操作。ArrayList 的默认扩容因子是1.5倍,即每次扩容后的容量为当前容量的1.5倍。
-
数组拷贝:扩容时,ArrayList 会创建一个新的数组,将原有元素拷贝到新数组中。这个过程涉及到数组的创建和数据的拷贝,所以在扩容操作时可能会对性能产生一定的影响。
需要注意的是,扩容是一个相对耗时的操作,因为它涉及到数据的拷贝和存储空间的重新分配。因此,在知道需要存储大量元素的情况下,为 ArrayList 显式指定一个初始容量,可以减少扩容操作的次数,提高性能。
ArrayList 的扩容策略是在底层数组容量不足时,将当前容量乘以一个扩容因子来进行扩容。默认情况下,ArrayList 的扩容因子是 1.5 倍。
注意:ArrayList 的扩容因子是可调的,可以通过构造函数来指定。我们可以使用 ArrayList(int initialCapacity, float loadFactor)
构造函数来同时指定初始容量和扩容因子。
三:模拟实现一个ArrayList
3.1方法框架
java
public class SeqList {
private int[] array;
private int size;
// 默认构造方法
SeqList(){ }
// 将顺序表的底层容量设置为initcapacity
SeqList(int initcapacity){ }
// 新增元素,默认在数组最后新增
public void add(int data) { }
// 在 pos 位置新增元素
public void add(int pos, int data) { }
// 判定是否包含某个元素
public boolean contains(int toFind) { return true; }
// 查找某个元素对应的位置
public int indexOf(int toFind) { return -1; }
// 获取 pos 位置的元素
public int get(int pos) { return -1; }
// 给 pos 位置的元素设为 value
public void set(int pos, int value) { }
//删除第一次出现的关键字key
public void remove(int toRemove) { }
// 获取顺序表长度
public int size() { return 0; }
// 清空顺序表
public void clear() { }
// 打印顺序表,注意:该方法并不是顺序表中的方法,为了方便看测试结果给出的
public void display() { }
}
3.2 模拟实现ArrayList
java
class MyArrayList {
private int[] array;
private int size;
// 默认构造方法
public MyArrayList() {
// 初始化底层数组,初始容量为10
array = new int[10];
// 初始大小为0
size = 0;
}
// 将顺序表的底层容量设置为initCapacity
public MyArrayList(int initCapacity) {
if (initCapacity < 0) {
throw new IllegalArgumentException("容量不能小于0");
}
// 初始化底层数组,初始容量为initCapacity
array = new int[initCapacity];
// 初始大小为0
size = 0;
}
// 在数组最后新增元素
public void add(int data) {
// 如果数组已满,则扩容
if (size == array.length) {
resize();
}
// 在数组末尾新增元素
array[size] = data;
// 更新大小
size++;
}
// 在指定位置新增元素
public void add(int pos, int data) {
// 检查位置是否合法
if (pos < 0 || pos > size) {
throw new IndexOutOfBoundsException("索引超出范围");
}
// 如果数组已满,则扩容
if (size == array.length) {
resize();
}
// 将指定位置及其后面的元素后移一位
for (int i = size - 1; i >= pos; i--) {
array[i + 1] = array[i];
}
// 在指定位置插入元素
array[pos] = data;
// 更新大小
size++;
}
// 判定是否包含某个元素
public boolean contains(int toFind) {
// 遍历数组查找元素
for (int i = 0; i < size; i++) {
if (array[i] == toFind) {
return true;
}
}
return false;
}
// 查找某个元素对应的位置
public int indexOf(int toFind) {
// 遍历数组查找元素
for (int i = 0; i < size; i++) {
if (array[i] == toFind) {
return i;
}
}
// 未找到元素,返回-1
return -1;
}
// 获取指定位置的元素
public int get(int pos) {
// 检查位置是否合法
if (pos < 0 || pos >= size) {
throw new IndexOutOfBoundsException("索引超出范围");
}
// 返回指定位置的元素
return array[pos];
}
// 设置指定位置的元素
public void set(int pos, int value) {
// 检查位置是否合法
if (pos < 0 || pos >= size) {
throw new IndexOutOfBoundsException("索引超出范围");
}
// 更新指定位置的元素
array[pos] = value;
}
// 删除第一次出现的指定元素
public void remove(int toRemove) {
// 遍历数组查找要删除的元素
for (int i = 0; i < size; i++) {
if (array[i] == toRemove) {
// 将要删除元素后面的元素前移一位
for (int j = i + 1; j < size; j++) {
array[j - 1] = array[j];
}
// 更新大小
size--;
return;
}
}
}
// 获取顺序表长度
public int size() {
return size;
}
// 清空顺序表
public void clear() {
// 将数组清空
for (int i = 0; i < size; i++) {
array[i] = 0;
}
// 大小置为0
size = 0;
}
// 扩容数组
private void resize() {
// 创建新的数组,长度为当前容量的两倍
int[] newArray = new int[array.length * 2];
// 将原数组中的元素复制到新数组中
for (int i = 0; i < size; i++) {
newArray[i] = array[i];
}
// 更新底层数组
array = newArray;
}
// 打印顺序表
public void display() {
// 遍历数组打印元素
for (int i = 0; i < size; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
}
这是一个自定义的MyArrayList
类,模拟实现了一个动态数组。以下是每个功能的解释:
- 构造方法:使用默认构造方法创建一个初始容量为10的数组,并将大小初始化为0。使用带有参数的构造方法可以设置初始容量。
add
方法:在数组最后新增元素,如果数组已满,则扩容。add
方法(重载):在指定位置新增元素,如果数组已满,则扩容。为了将指定位置的元素后移,我们需要从后往前遍历数组。contains
方法:遍历数组查找是否包含某个元素,如果找到则返回true
,否则返回false
。indexOf
方法:遍历数组查找某个元素对应的位置,如果找到则返回元素的索引,否则返回-1。get
方法:获取指定位置的元素,如果位置合法则返回元素,否则抛出IndexOutOfBoundsException
异常。set
方法:设置指定位置的元素,如果位置合法则更新元素值,否则抛出IndexOutOfBoundsException
异常。remove
方法:删除第一次出现的指定元素,将后面的元素前移一位,并更新大小。size
方法:获取顺序表的长度,即元素的个数。clear
方法:清空顺序表,将数组
3.3 测试功能
接着我们再写一个测试类,用于测试功能:
java
public class Main {
public static void main(String[] args) {
MyArrayList list = new MyArrayList();
// 测试默认构造方法
System.out.println("测试默认构造方法:");
System.out.println("顺序表是否为空:" + (list.size() == 0));
System.out.println("顺序表的大小:" + list.size());
list.display();
// 测试 add 方法
System.out.println("\n测试 add 方法:");
list.add(1);
list.add(2);
list.add(3);
list.display();
// 测试 add(pos, data) 方法
System.out.println("\n测试 add(pos, data) 方法:");
list.add(1, 5);
list.display();
// 测试 contains 方法
System.out.println("\n测试 contains 方法:");
System.out.println("顺序表中是否包含元素 2:" + list.contains(2));
System.out.println("顺序表中是否包含元素 4:" + list.contains(4));
// 测试 indexOf 方法
System.out.println("\n测试 indexOf 方法:");
System.out.println("元素 2 在顺序表中的位置:" + list.indexOf(2));
System.out.println("元素 4 在顺序表中的位置:" + list.indexOf(4));
// 测试 get 方法
System.out.println("\n测试 get 方法:");
System.out.println("位置 2 上的元素:" + list.get(2));
// 测试 set 方法
System.out.println("\n测试 set 方法:");
list.set(1, 4);
list.display();
// 测试 remove 方法
System.out.println("\n测试 remove 方法:");
list.remove(2);
list.display();
// 测试 size 方法
System.out.println("\n测试 size 方法:");
System.out.println("顺序表的大小:" + list.size());
// 测试 clear 方法
System.out.println("\n测试 clear 方法:");
list.clear();
list.display();
System.out.println("顺序表是否为空:" + (list.size() == 0));
}
}
-
运行结果如下:
测试默认构造方法: 顺序表是否为空:true 顺序表的大小:0 测试 add 方法: 1 2 3 测试 add(pos, data) 方法: 1 5 2 3 测试 contains 方法: 顺序表中是否包含元素 2:true 顺序表中是否包含元素 4:false 测试 indexOf 方法: 元素 2 在顺序表中的位置:2 元素 4 在顺序表中的位置:-1 测试 get 方法: 位置 2 上的元素:2 测试 set 方法: 1 4 2 3 测试 remove 方法: 1 4 3 测试 size 方法: 顺序表的大小:3 测试 clear 方法: 顺序表是否为空:true
可见输出都符合我们的预期,说明我们模拟实现的ArrayList在功能上是完好的,并且没有什么缺陷。
四:ArrayList的问题及思考
- ArrayList底层使用连续的空间,任意位置插入或删除元素时,需要将该位置后序元素整体往前或者往后搬移,故时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继
续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
所以应该使用 ArrayList 的场景包括:
- 需要频繁进行随机访问的场景。
不应该使用 ArrayList 的场景包括:
-
需要频繁进行插入和删除操作,并对性能要求较高的场景。
-
列表中的元素数量比较少,可能造成内存浪费的场景。