接上次博客:基于数组实现的顺序表(SeqList)_di-Dora的博客-CSDN博客
目录
2、ArrayList(int initialCapacity)
我们上次实现了一个自己的顺序表,那我们每次用到顺序表,难道都要重新写一个顺序表吗?
当然不是啦!
Java已经帮我们实现好了,它就是 ArrayList !而且,它的功能很强大,比起我们自己实现的那个只可以保存整型数据的 MySeqList 方便太多了!
接下来我们就一起来学习一下 ArrayList 。
ArrayList
是Java编程语言中的一个类,位于java.util包中。它是一个动态数组(Dynamic Array),可以根据需要自动调整大小。它是以泛型方法实现的,使用时必须要先实例化。ArrayList可以存储任意类型的对象,并提供了一系列方法来操作和管理存储的数据。
ArrayList的特点包括:
1、动态大小:ArrayList内部使用数组来存储元素,但与普通的数组不同,ArrayList的大小是可以动态调整的。当需要添加或删除元素时,ArrayList会自动调整内部数组的大小,以容纳新的元素或减少空间浪费。
2、对象存储:ArrayList可以存储任意类型的对象,包括基本类型的包装类(如Integer、Double等)。它提供了一种集中管理对象的方式,方便对数据进行统一的操作和处理。
3、索引访问:ArrayList中的元素可以通过索引进行访问。索引从0开始,最后一个元素的索引为size() - 1。可以使用get()方法通过索引获取元素,也可以使用set()方法修改指定位置的元素。
4、动态增删:ArrayList提供了多种方法用于添加和删除元素。可以使用add()方法在末尾添加元素,使用remove()方法删除指定位置的元素。此外,还提供了其他一些方法,如add(index, element)用于在指定位置插入元素,remove(element)用于删除指定元素等。
5、自动装箱/拆箱:ArrayList可以自动进行基本类型和包装类之间的转换。当添加基本类型值时,会自动将其转换为对应的包装类对象;当获取元素时,会自动将包装类对象转换为基本类型值。
使用ArrayList时,需要在使用前导入java.util包,并创建一个ArrayList的实例。例如:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
// 创建一个ArrayList实例,用于存储整数
ArrayList<Integer> numbers = new ArrayList<>();
// 添加元素
numbers.add(10);
numbers.add(20);
numbers.add(30);
// 获取元素
int firstNumber = numbers.get(0);
System.out.println("第一个元素:" + firstNumber);
// 修改元素
numbers.set(1, 25);
System.out.println("修改后的第二个元素:" + numbers.get(1));
// 删除元素
numbers.remove(2);
System.out.println("删除后的ArrayList:" + numbers);
}
}
总之,ArrayList是一种非常常用的数据结构,可以方便地管理和操作动态大小的数据集合。
ArrayList()的构造方法
ArrayList提供了三个常用的构造方法,用于创建ArrayList实例。这些构造方法包括:
1、ArrayList()
这是最常见的构造方法,它创建一个空的ArrayList实例,初始容量为10(订好了的,我们的DEFAULT_CAPACITY)。
当向ArrayList添加元素时,如果容量不足,ArrayList会自动进行扩容,以容纳更多的元素。
ArrayList<Integer> numbers = new ArrayList<>();
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
// 创建一个空的ArrayList实例
ArrayList<String> list = new ArrayList<>();
// 添加元素
list.add("Apple");
list.add("Banana");
list.add("Orange");
// 输出元素
System.out.println(list); // 输出: [Apple, Banana, Orange]
}
}
2、ArrayList(int initialCapacity)
这个构造方法允许你指定初始容量。它创建一个空的ArrayList实例,并为内部数组分配指定的初始容量。当需要添加更多元素时,ArrayList会自动进行扩容。
ArrayList<String> names = new ArrayList<>(20);
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
// 创建一个初始容量为5的ArrayList实例
ArrayList<Integer> numbers = new ArrayList<>(5);
// 添加元素
numbers.add(10);
numbers.add(20);
numbers.add(30);
numbers.add(40);
numbers.add(50);
// 输出元素
System.out.println(numbers); // 输出: [10, 20, 30, 40, 50]
}
}
当然,你也可以使用向上转型:
//可以调用的方法更多
ArrayList<Integer> list1 =new ArrayList<>();
//实现了向上转型
List<Integer> list2 =new ArrayList<>();
3、ArrayList(Collection<? extends E> c)
这个构造方法接受一个集合(Collection)作为参数,并创建一个包含集合中元素的ArrayList实例。注意,这里的能够引用的集合都必须保证是 extends E 的(一定是E的子类,或者是E本身)。‘ ? ’ 是通配符。
public static void main3(String[] args) {
ArrayList<Integer> list1 = new ArrayList<>(15);
ArrayList<Integer> list2 = new ArrayList<>();
list2.add(10);
list2.add(0,100);
System.out.println(list2);
//说明我们可以这样:LinkedList<>也实现了List接口
List<Integer> list233 = new LinkedList<>();
list233.add(1);
list233.add(2);
list233.add(3);
List<Integer> list3 = new ArrayList<>(list233);
list3.add(4);//默认是放到数组最后一个位置
System.out.println(list3); //[1,2,3,4]
}
新创建的ArrayList的元素顺序与集合中的元素顺序相同。
ArrayList<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
import java.util.ArrayList;
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
// 创建一个包含集合元素的ArrayList实例
ArrayList<String> names = new ArrayList<>(Arrays.asList("Alice", "Bob", "Charlie"));
// 输出元素
System.out.println(names); // 输出: [Alice, Bob, Charlie]
}
}
这些构造方法提供了不同的初始化方式,我们可以根据需求选择适合的构造方法可以方便地创建ArrayList实例并初始化元素。
public static void main(String[] args) {
// ArrayList创建,推荐写法
// 构造一个空的列表
List<Integer> list1 = new ArrayList<>();
// 构造一个具有10个容量的列表
List<Integer> list2 = new ArrayList<>(10);
list2.add(1); //默认是放到数组的最后一个位置
list2.add(2);
list2.add(3);
// list2.add("hello"); // 编译失败,List<Integer>已经限定了,list2中只能存储整形元素
// list3构造好之后,与list中的元素一致
ArrayList<Integer> list3 = new ArrayList<>(list2);
// 避免省略类型,否则:任意类型的元素都可以存放,使用时将是一场灾难
List list4 = new ArrayList();
list4.add("112");
list4.add(100);
}
需要注意的是,ArrayList是可变长度的,即它的底层是一段连续的空间,并且它可以根据需要自动扩容和缩减容量,是一个动态类型的顺序表,因此在使用ArrayList时无需担心容量的问题。
ArrayList的扩容
ArrayList是一个动态类型的顺序表,即:在插入元素的过程中会自动扩容。
我们根据下面这段代码来了解一下ArrayList扩容的机制:
ArrayList<Integer> list1 =new ArrayList<>();
我们跳转过去看看ArrayList:
现在给它添加元素,思考,能放进元素吗?:
ArrayList<Integer> list1 =new ArrayList<>();
list1.add(1);
放进去了! 没有分配内存,但是可以往里面放元素?
我们来看看它的add:
当我们调用不带参数的构造方法的时候,默认在第一次 add 的时候才会分配大小为10的内存。
扩容是按照 1.5 倍的方式进行的。
最大容量就是整数的最大值:
扩容过程的总结:
1. 检测是否真正需要扩容,如果是调用grow准备扩容
2. 预估需要库容的大小 初步预估按照1.5倍大小扩容 如果用户所需大小超过预估1.5倍大小,则按照用户所需大小扩容 真正扩容之前检测是否能扩容成功,防止太大导致扩容失败
3. 使用copyOf进行扩容
以下是扩容涉及到的全部源码,都整合在这里了:
Object[] elementData; // 存放元素的空间
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; // 默认空间
private static final int DEFAULT_CAPACITY = 10; // 默认容量大小
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
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);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// 获取旧空间大小
int oldCapacity = elementData.length;
// 预计按照1.5倍方式扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果用户需要扩容大小 超过 原空间1.5倍,按照用户所需大小扩容
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果需要扩容大小超过MAX_ARRAY_SIZE,重新计算容量大小
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用copyOf扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
// 如果minCapacity小于0,抛出OutOfMemoryError异常
if (minCapacity < 0)
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;
}
ArrayList 的方法
其实 ArrayList 的各种方法的实现和我们之前自己写的那个顺序表中的方法的实现相差不大,我把源码拷贝过来,你可以对比着看一看:
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
//判断 o 是否在线性表中
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
//返回第一个 o 所在下标
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
//返回最后一个 o 所在下标
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public Object clone() {
try {
ArrayList<?> v = (ArrayList<?>) super.clone();
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
//获取下标 index 位置元素
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
//将下标 index 位置元素设置为 element
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
//尾插 e
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
//将 e 插入到 index 位置
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
//删除 index 位置元素
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 把最后一个位置置为空
return oldValue;
}
//删除遇到的第一个 o
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // 把最后位置置为空
}
//清空
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null; // 注意,这里就和我们之前提到过的将引用置为null一样了
size = 0;
}
//尾插 c 中的元素
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//截取部分list
List<E> subList(int fromIndex, int toIndex);
当然,这里面也有一些需要注意的点:
1、遇到重载方法怎么办?
以 remove 为例:
//删除 index 位置元素
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
return oldValue;
}
//删除遇到的第一个 o
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
我们在这么写的时候:
list.remove(1);
它会默认删除的是 1 下标的数据。可是如果我们要删的是 1 这个数字本身,怎么办?
public boolean remove(Object o)
传进去的是一个 Object 的引用类型,我们只有这样写:
list.remove(new Integer(1));
所以我们在面对重载方法的时候,要记得把类型也传进去,除非通过参数可以直接区分你想要的具体方法。
2、subList——截取一段list。
public class Test {
public static void main(String[] args) {
List<Integer> list2 =new ArrayList<>();
list2.add(1);
list2.add(2);
list2.add(3);
list2.add(4);
list2.add(5);
list2.add(6);
list2.add(7);
//返回一个list接口
List<Integer> list4=list2.subList(1,3); //[1,3)
//把list4的0下标更新为666
list4.set(0,666);
System.out.println(list4);
System.out.println("-------------------");
System.out.println(list2);
}
}
打印一下看看结果:
所以这里是把下标地址直接给了 list 4,而不是拷贝了字符串。
3、你有没有发现,我们可以直接就打印出 ArrayList 来看?
一般情况下,能够直接通过sout输出引用指向对象当中的内容的时候,此时一定重写了toString方法。
CTRL+F ,找找看:
等等,为啥没有啊?
ArrayList 是继承类的,父类呢?写没写?
还是没有,那继续向上寻找: 找到了!我们来看看:
这里面的第一行的:
Iterator<E> it = iterator();
就是我们一会儿会详细讲解的迭代器,它的作用是遍历我们当前的集合。
我们先来了解一下 ArrayList () 的遍历方式。
ArrayList()的遍历
ArrayList 是 Java 中的一个常用集合类,它提供了多种遍历方法。
以下是一些常见的遍历方式的具体示例:
1、使用 for 循环遍历:
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
for (int i = 0; i < list.size(); i++) {
String element = list.get(i);
System.out.println(element);
}
该方法使用索引访问 ArrayList 的每个元素,并通过 list.get(i) 方法获取元素的值。
2、使用增强型 for-each 循环遍历:
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
for (String element : list) {
System.out.println(element);
}
这种方式更简洁,直接将集合中的每个元素赋值给循环变量 element,然后对其进行处理。
3、使用迭代器(Iterator)遍历:
我们可以通过:ALT+回车----->导入包
import java.util.Iterator;
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
迭代器提供了一种在遍历过程中删除元素的方式,通过 iterator.hasNext() 判断是否还有下一个元素,然后使用 iterator.next() 获取当前元素的值。
Iterator 接口的一个常见子类是 ListIterator。
ListIterator 继承自 Iterator 接口,并且提供了更多的功能,特别是在对列表进行双向遍历时非常有用。除了具有 Iterator 接口中的方法外,ListIterator 还增加了一些额外的方法,比如 previous()、hasPrevious()、add() 和 set()。
我们也可以使用 ListIterator 遍历 ArrayList :
ArrayList<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Orange");
ListIterator<String> iterator = list.listIterator();
// 正向遍历
while (iterator.hasNext()) {
String element = iterator.next();
System.out.println(element);
}
// 反向遍历
//在调用 list.listIterator() 方法时,可以选择传递一个整数参数来指定起始位置的索引值。
ListIterator<String> iterator2 = list.listIterator(list.size());
while (iterator2.hasPrevious()) {
String element = iterator2.previous();
System.out.println(element);
}
我们可以通过 list.listIterator() 方法获取 ListIterator 对象,然后可以使用 iterator.next() 和 iterator.previous() 方法分别进行正向和反向遍历。
需要注意的是,当使用 ListIterator 进行反向遍历时,需要在创建 ListIterator 对象时指定一个起始位置。
在 list.listIterator(list.size()) 中,我们将 list.size() 作为参数传递给 listIterator() 方法,这样就会创建一个从列表末尾开始的 ListIterator 对象,从而实现反向遍历。
请注意,反向遍历时索引的范围是 [0, list.size()],即起始位置的索引值为列表的大小,结束位置的索引值为 0。
还有,ListIterator 是 List 接口的一个特定迭代器,只能用于 List 类型的集合,而不能用于其他类型的集合,如 Set 或 Map。
用ArrayList实现杨辉三角:
给定一个非负整数 numRows
,生成「杨辉三角」的前 numRows
行。
在「杨辉三角」中,每个数是它左上方和右上方的数的和。
这里是力扣的链接:118. 杨辉三角 - 力扣(Leetcode)
在写这个题之前,我们要先来了解一个东西:
List<Integer> ret = new ArrayList<>();
ret.add(1);
//像一个二维数组
List<List<Integer>> ret2 = new ArrayList<>();
ret2.add(new ArrayList<>());
ret2.get(0).add(1);
import java.util.ArrayList;
import java.util.List;
public class YangHuiTriangle {
public static List<List<Integer>> generate(int numRows) {
List<List<Integer>> triangle = new ArrayList<>();
if (numRows == 0) {
return triangle;
}
// 第一行的特殊处理
List<Integer> firstRow = new ArrayList<>();
firstRow.add(1);
triangle.add(firstRow);
// 从第二行开始生成杨辉三角
for (int i = 1; i < numRows; i++) {
List<Integer> row = new ArrayList<>();
List<Integer> prevRow = triangle.get(i - 1);
// 每一行的第一个元素为1
row.add(1);
// 根据上一行的元素计算当前行的元素
for (int j = 1; j < i; j++) {
int num = prevRow.get(j - 1) + prevRow.get(j);
row.add(num);
}
// 每一行的最后一个元素为1
row.add(1);
triangle.add(row);
}
return triangle;
}
public static void main(String[] args) {
int numRows = 5;
List<List<Integer>> triangle = generate(numRows);
// 打印生成的杨辉三角
for (List<Integer> row : triangle) {
System.out.println(row);
}
}
ArrayList 的优缺点
ArrayList 的优点和缺点如下:
优点:
1、随机访问:ArrayList 可以通过索引快速访问列表中的元素,因为它内部使用数组来存储元素。这使得在给定索引的情况下,访问元素的时间复杂度为 O(1)。
2、快速遍历:ArrayList 实现了 RandomAccess 接口,因此可以使用普通的 for 循环进行快速遍历。这对于需要遍历列表的操作非常方便。
3、数据存储密集:相比链表,ArrayList 的元素在内存中是连续存储的,这样可以更好地利用 CPU 缓存,提高访问效率。
缺点:
1、扩容可能造成空间浪费:当 ArrayList 的元素数量超过当前容量时,会触发扩容操作,通常会创建一个更大的数组并将元素复制到新数组中。如果预估不准确,可能会导致一些空间的浪费。
我们之前提到过扩容了,在 Java 中,ArrayList 是动态数组的实现,它的内部使用数组来存储元素。当元素数量超过当前数组容量时,ArrayList 会自动进行扩容操作,通常是创建一个更大的数组并将元素复制到新数组中。这意味着 ArrayList 并不是随用随分配,而是根据需要进行动态扩容,以适应元素的增加。这样做的目的是为了保证在大部分情况下都能提供较好的性能,避免频繁的扩容和移动元素。
2、添加和删除元素的效率低:当需要在 ArrayList 中间插入或删除元素时,需要移动后续元素来填补空缺或收缩数组,这导致插入和删除操作的时间复杂度为 O(n)。如果需要频繁进行这些操作,使用 ArrayList 的性能可能不佳。
所以顺序表适合静态的数据进行查找和更新,不适合用来插入和删除数据。
那这个时候我们用什么东西来实现动态的数据处理比较好呢?——依赖链表来做(我们稍后会学到的)。
链表适合插入和删除操作:
相对于 ArrayList,链表(LinkedList)确实在插入和删除元素方面具有更高的效率,因为它不需要移动其他元素。链表通过在节点之间建立引用来连接元素,这样插入和删除只需要修改引用的指向,且它的时间复杂度为 O(1)。而 ArrayList 在插入和删除元素时,需要移动后面的元素,时间复杂度变成O(n)。
综上所述,ArrayList 适合于随机访问和频繁的查找和更新操作,但不适合频繁的插入和删除操作。链表更适合需要频繁插入和删除元素的场景,但随机访问的效率较低。
我们还是需要根据实际的需求和使用场景来选择合适的数据结构,以获得最佳的性能和效率。
我们再来看一个面试题:
一个CVTE的面试笔试题:
str1:"Welcome to CVTE!";
str2:"come"
描述:删除第一个字符串当中出现的所有第二个字符串中的字符!
输出结果:Wl t vt
要求用ArrayList 实现
public static List<Character> func(String str1,String str2) { //1、遍历str1这个字符串看当中 是不是存在str2中的字符 List<Character> list = new ArrayList<>(); for (int i = 0; i < str1.length(); i++) { char ch = str1.charAt(i); // contains 需要一个字符串! if(!str2.contains(ch+"")) { list.add(ch); } } return list; } public static void main(String[] args) { String str1 = "welcome to cvte"; String str2 = "come"; List<Character> ret = func(str1,str2); for(char ch : ret){ System.out.print(ch); } }