【Java 数据结构】ArrayList类介绍

ArrayList类介绍

  • 初识List接口
  • ArrayList类
    • ArrayList类是什么
    • 顺序表的模拟实现
      • 初始化
      • 增加元素
      • 删除元素
      • 查找元素
      • 修改元素
    • ArrayList类使用
      • 构造方法
      • ArrayList源码阅读
      • 常用方法及其注意事项

初识List接口

List 是集合框架中的一个接口, 它的里面包含了一些方法, 例如add(), remove(), get()等等这些方法. 根据名字我们也可以看出来这些方法似乎是用来进行添加, 删除, 查询这样的基本操作.

实际上 List接口里面包含的也就是一些基本的增删查改以及遍历的功能. 但是此时就有了一个疑问: 这个 List 是一个接口, 它的这些方法能用来干什么呢, 对谁来进行增删改呢?

结合我们对于接口的认识, 接口是用于描述行为的, 用于描述一个类可以干什么. 那么结合这一点我们就可以推测, 集合类中应该是有一些类实现了这个 List 接口, 然后这个 List 接口就是用于描述这些类的基本行为的. 那么接下来我们就要开始了解一下这些具体的类, 主要就是了解其中的 ArrayList类和 LinkedList类

ArrayList类

ArrayList类是什么

ArrayList类是集合框架中继承于 List 接口的一个集合类, 它的底层是一个顺序表, 顺序表是数据结构中的一个定义, 大体定义如下

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构, 一般情况下采用数组存储, 在数组上完成数据的增删查改. 其擅长对于元素的随机访问

这里需要注意的是, 这个随机访问和大部分人印象里面的 “随机” 大概率不是同一个概念的. 对于大多数人来说, 随机是指的类似于抽奖的那种随机. 而这里指的随机访问则是, 提供一个具体的位置, 然后去访问对应位置的元素.

对于顺序表, 我们就可以简单理解为他就是一个数组, 然后 ArrayList类则是由一个数组成员和一系列方法组成的一个类. 其中数组用于存储数据, 方法用于对这个数组进行增删查改.

顺序表的模拟实现

看了上面的介绍,有些人可能还是对于顺序表这个概念有些陌生, 但由于为了能够更好的了解 ArrayList类, 对其底层的存储结构具有初步的理解是必不可少的, 因此我们这里就通过手动的实现一个简单的顺序表, 从而更好的理解这个数据结构.

初始化

首先我们要创建一个类, 用于模拟实现顺序表, 我们这里就取名为MyArrayList. 同时赋予其最基本的两个成员, 分别是用于存储元素的数组和一个用于标识有效数据个数的标志, 如下所示

public class MyArrayList {// 存储数据的数组private int[] array;// 存储数据的个数private int size;
}

可以看到我们这里创建了一个 int 类型的数组, 用于存储数据, 这里使用 int 类型是为了简化, 实际场景中可能使用泛型来容纳各种类型. 另外创建了一个标识 size 用于标明有效数据的个数.

此时可能有人要问了: 为什么要这个 size 标志, 我直接array.length获取数组长度不就是长度了吗?

实际上在顺序表中, 数组的大小和有效元素的个数并不一定是匹配的. 有可能我的数组大小是 10, 但是我里面只存储了一个有效元素, 那么此时很明显通过array.length的这种方式去返回数据就是有问题的.

那么此时就引出了另一个问题: 那么我可不可以插入一个数据就令数组的大小 + 1呢? 这样不就可以直接通过数组长度来获取有效元素个数了吗?

实际上这样确实解决了有效元素个数和数组长度不匹配的问题, 但是随之迎来的新问题就是, 这样做的效率是非常低的. 我们如果要对数组扩容, 就需要新开辟一个空间, 然后将原来的数组复制过去, 那么如果我进行一次插入就要进行一次这样的开辟空间和复制数组的操作, 对于一些数据量大并且频繁插入的情境下, 很明显就是非常低效的.

那么还有一个问题就是: 可不可以通过遍历一次顺序表, 然后查看其中数据是否有效的方式来获取有效元素的个数呢?

实际上这个也是可以的, 但是依旧是对于长度较长的顺序表不友好, 遍历是一个较为耗时的操作, 不如直接返回一个有效数据数来的直接. 另外在本例中还会产生的一个问题, 我们如何去检测这个数据是有效的还是无效的呢? 如果是检测默认的 0 的话, 那如果存储进去的数据就是 0 怎么办呢?

因此我们这里最终就选择采用 size 记录的方法来记录元素的有效个数.

确认了基本的成员变量, 接下来就是书写构造方法, 我们这里就提供两个构造方法. 首先第一个无参构造方法用于创建大小为 10 的顺序表, 如下所示

// 定义一个常量用于说明默认大小
private static final int DEFAULT_CAPACITY = 10;// 无参构造方法
public MyArrayList() {this.array = new int[DEFAULT_CAPACITY];this.size = 0;
}

此时可能有人要问了: 为什么你这里要用一个常量来表示这个默认大小? 我直接写new int[10]又有什么区别呢?

实际上, 这种定义常量去声明一些基本值的做法是非常常见的, 其核心的目的就是增强代码的可维护性. 试想一个场景, 假设我有好几个方法都用到了这个默认容量, 那如果是我全部写成10, 一旦我想要修改这个基本容量, 我就需要全部一个一个的改. 即便编译器提供了查找替换功能, 但是如果代码量一大, 就很难保证不会误伤到其他的代码. 而假如我使用了一个常量来表示这个容量, 那么我就只需要改常量的这个位置即可. 因此直接定义成一个常量, 是非常不错的选择.

接下来是第二个构造方法, 其有一个参数用于代表容量

// 有参构造方法, 参数表示容量
public MyArrayList(int capacity) {this.array = new int[capacity];this.size = 0;
}

接下来我们再实现一些基本的方法, 首先是一个用于获取容量的方法, 这个方法还是非常简单的, 直接返回 size 即可

// 获取元素个数
public int size(){return this.size;
}

然后是一个打印顺序表的方法, 这个方法主要是方便我们去查看顺序表的具体变化, 这里需要注意的是, 不能通过直接遍历整个数组的方式去进行打印, 因为数组大小不一定等于有效数据的个数

// 打印顺序表
public void display(){for (int i = 0; i < this.size; i++){System.out.print(this.array[i] + " ");}
}

此时基本的准备工作都已经完成, 可以开始书写一些操作的代码了.

增加元素

首先这里实现一个尾插的增加元素方法, 实际上就是在最后一个有效元素的后一个位置插入一个元素. 此时自然就产生了一个问题, 如何找到最后一个有效元素? 如果我们上面选择使用了一个标志位来表示, 那么很明显, 最后一个有效元素的下标就是size - 1, 那么我们既然是要将元素插入到最后一个有效元素的后面, 自然就是选择下标为size位置的. 此时我们就可以写出尾插元素的核心逻辑

// 添加元素
public void add(int data){// 插入元素this.array[this.size] = data;// 有效元素个数+1, 这一步可以和上一步合并this.size++;
}

但是此时肯定是有问题的, 如果容量不够的话, 此时就会直接越界, 因此我们在进行增加操作之前, 要先检查一下容量是否够用, 随后给出提示或者扩容, 我们这里就直接实现一个扩容版本

// 尾部添加元素
public void add(int data){// 判断容量是否已满, 已满则进行扩容if (this.size == this.array.length){increaseCapacity();}// 插入元素, 同时使得size++this.array[this.size++] = data;
}private void increaseCapacity() {// 获取原来的容量int oldCapacity = this.array.length;// 先创建一个新的数组, 容量为原来的1.5倍int[] newArray = new int[oldCapacity + (oldCapacity >> 1)];// 将老数组的元素复制到新数组中for(int i = 0; i < oldCapacity; i++){newArray[i] = this.array[i];}// 将新数组赋值给this.arraythis.array = newArray;
}

可以看到, 实际上逻辑也是比较简单的, 就是新建一个容量更大的数组, 然后将原来的元素复制过去, 最后把新数组的引用赋值给this.array即可. 其中比较特殊的就是这个容量的计算, 主要就是这个(oldCapacity >> 1), 实际上这个就等效于 (oldCapacity / 2), 只不过位移操作相较于直接进行除法运算是更加高效的, 因此我们这里选择了这种方法.

同时, 这里将扩容操作封装为一个方法的目的是为了能够给后面一个指定位置插入的方法一同使用, 从而实现代码的复用.

总而言之, 这里的增加操作就是先查看容量是否足够,如果足够就直接插入元素, 如果容量不足就扩容为原来容量的1.5倍, 随后在进行插入.

当然, 这里的扩容我们也可以采用Arrays.copyOf()方法来实现

private void increaseCapacity() {// 获取原来的容量int oldCapacity = this.array.length;// 计算新容量int newCapacity = oldCapacity + (oldCapacity >> 1);// 使用Arrays.copyOf()方法来进行扩容this.array = Arrays.copyOf(this.array, newCapacity);
}

下面是一个书写在Main类中用于测试的代码, 由于我们设定的默认容量为10, 因此如果所有元素都正常插入, 那么则说明添加操作是可以正常运行的.

public class Main {public static void main(String[] args) {MyArrayList myArrayList = new MyArrayList();for (int i = 0; i < 20; i++) {myArrayList.add(i);}myArrayList.display();}
}

下面就是实现一个指定位置插入的方法, 首先根据顺序表的定义, 它是连续依次存储元素的, 因此如果想要指定位置插入元素的话, 第一步就是需要去检验这个位置是否合法

// 添加元素到指定位置
public void add(int index, int data){// 判断index是否合法if (index < 0 || index > this.size){throw new IndexOutOfBoundsException("下标 " + index + " 超出长度 " + this.size);}
}

随后就是将指定位置后面的元素全部后移, 同时将元素放入指定位置, 下面是一个例子, 假设想在下标为 4 的位置(存储数据为 5 的位置)插入一个 20, 那么就要将下标为 4 及其后方的所有数字后移一位, 然后再将 20 放入下标为 4 的位置

// 添加元素到指定位置
public void add(int index, int data){// 判断index是否合法if (index < 0 || index > this.size){throw new IndexOutOfBoundsException("下标 " + index + " 超出长度 " + this.size);}// 判断容量是否已满, 已满则进行扩容if (this.size == this.array.length){increaseCapacity();}// 插入元素for (int i = this.size - 1; i >= index; i--){this.array[i + 1] = this.array[i];}this.array[index] = data;this.size++;
}

这里我们可以注意到, 我们将所有数字后移一位是从最后一个数据开始一个一个的往后移动的, 那么能否从前面的位置一个一个的往后移动呢?

下面是一个测试代码, 我们来看看如果是从前面开始会发生什么

// 添加元素到指定位置
public void addTest(int index, int data){// 判断index是否合法if (index < 0 || index > this.size){throw new IndexOutOfBoundsException("下标 " + index + " 超出长度 " + this.size);}// 判断容量是否已满, 已满则进行扩容if (this.size == this.array.length){increaseCapacity();}// 从前往后进行移动for (int i = index + 1; i <= this.size; i++){this.array[i] = this.array[i - 1];}this.array[index] = data;this.size++;
}public static void main(String[] args) {MyArrayList myArrayList = new MyArrayList();for (int i = 0; i < 10; i++) {myArrayList.add(i);}myArrayList.display();// 测试插入方法myArrayList.addTest(3, 100);myArrayList.display();
}

最后运行结果如下

可以看到, 这个结果很明显出错了. 我们可以看一下如果使用正常的add()方法的结果, 如下图所示

那么为什么会发生这样的事情呢? 我们这里通过上面的例子来看.

在这个例子中, 如果我们采用从前往后的移动元素, 那么此时可以看到, 这个 5 就会覆盖掉我们的数据 6

然后后面的所有的数字, 就会一次一次的被这个复制过去的 5 覆盖掉, 最后全部变成 5

而当我们采用先移动最后一个数字的方法时, 就会事先将后面的元素复制一份, 此时即使前面的元素会盖掉数字也无所谓了. 下面可以看到, 我们这里先将 9 移动到后面去

后面 8 移动过来虽然会覆盖前面这个 9, 但是由于这个 9 已经是无效的了, 那么就可以随意覆盖掉了


在上面实现了这个指定位置插入后, 有人可能就有问题了: 在我们的第一个add()方法中, 实现了一个默认插入到结尾位置的添加方法, 那么我们是否可以将其修改为调用这个指定位置插入的add()方法, 从而实现代码的复用呢?

答案是当然可以, 下面就是修改后的代码

// 尾部添加元素
public void add(int data){// 调用添加元素到指定位置的方法, 实现代码复用add(this.size, data);
}

当然, 实际上这些代码并没有非常的复杂, 同时这样还增加了一些多余的判断开销, 是否要修改为这种实现方法可以自己选择

删除元素

接下来就是实现删除元素的方法, 我们先实现一个删除指定下标元素的方法. 实际上就是将指定下标后面的元素覆盖过来即可, 如下图所示

同时, 也不要忘记了检查下标的合理性, 下面是实现代码

// 删除指定下标元素, 返回被删除的元素
public int removeIndex(int index){// 判断index是否合法if (index < 0 || index > this.size){throw new IndexOutOfBoundsException("下标 " + index + " 超出长度 " + this.size);}// 保存元素int ret = this.array[index];// 删除元素for (int i = index; i < this.size - 1; i++){this.array[i] = this.array[i + 1];}// 返回元素, 大小减少this.size--;return ret;
}

根据代码我们可以看到, 这里是从前往后一个一个覆盖的方法实现整体的移动的. 和上面的在中间位置插入的类似, 如果我们这里是从后往前的话, 也会产生类似的问题. 由于问题十分相似, 这里就不再阐述了, 可以自行实现一个测试代码查看效果, 并且借助画图/调试的方法来查看原因.

这里可能有人要问了: 那我们这样会残留一个元素在最后面没有删掉, 那那个元素就不用管了吗?

实际上如果我们是正常的去使用这个顺序表的话, 由于残留的那个元素已经失去了索引, 我们是无法访问到那个元素的, 此时就可以看作是删除掉了, 这种删除也可以看作是一种逻辑删除, 也就是没有实际上的删除掉元素, 而是删除掉用于访问这个元素的索引, 令其无法被访问, 使其成为一个无效的元素.

并且由于其被视作为是一个无效的元素, 那么在后续写入元素的时候, 也就会直接覆盖掉这个元素, 也就是说这个元素实际上就和后面的那些默认值 0 一样, 都是无所谓的元素了.

但是如果我们这里实现的是一个装载对象的顺序表, 那么此时我们就可以将这个对象置空, 从而让 JVM 将这个对象回收掉, 不要占据空间.(了解即可)


接下来就是实现一个删除指定元素的方法, 那么此时可能有人就要问了: 你删除指定元素, 那假如有多个一样的元素怎么办呢?

我们这里就默认删除第一个出现的元素, 比如我们的顺序表中存储了1 2 3 4 5 6 1 2 3 4 5 6, 我们要删除元素 2, 那么我们就会删除第一个 2, 使得顺序表变为1 3 4 5 6 1 2 3 4 5 6

这个的实现也是非常简单的, 直接遍历找到第一个元素, 然后执行和上面一样的删除操作即可

// 删除指定元素, 默认删除第一个
public boolean removeData(int data){// 默认删除第一个出现的元素int index = -1;// 遍历查找for (int i = 0; i < this.size; i++){if (this.array[i] == data){index = i;break;}}// 没有出现, 返回falseif (index == -1){return false;}// 出现, 删除元素for (int i = index; i < this.size - 1; i++){this.array[i] = this.array[i + 1];}this.size--;return true;}

此时我们可以发现, 在这两个删除方法中, 都使用到了这个将整体元素左移的方法. 那么此时我们就可以选择将这个操作封装起来, 从而实现代码的复用, 下面是封装的方法

// 将区间内的元素整体左移
private void leftShift(int start, int end){// 遍历, 将元素依次左移for (int i = start; i < end; i++){this.array[i] = this.array[i + 1];}
}

然后就是修改上面的删除元素的代码, 实际上就是将那个 for循环改为调用这个方法

// 删除指定下标元素, 返回被删除的元素
public int removeIndex(int index){// 判断index是否合法if (index < 0 || index > this.size){throw new IndexOutOfBoundsException("下标 " + index + " 超出长度 " + this.size);}// 保存元素int ret = this.array[index];// 删除元素, 将index及后面的元素依次左移leftShift(index, this.size - 1);// 返回元素, 大小减少this.size--;return ret;
}// 删除指定元素, 默认删除第一个
public boolean removeData(int data){// 默认删除第一个出现的元素int index = -1;// 遍历查找for (int i = 0; i < this.size; i++){if (this.array[i] == data){index = i;break;}}// 没有出现, 返回falseif (index == -1){return false;}// 出现, 删除元素, 将index及后面的元素依次左移leftShift(index, this.size - 1);this.size--;return true;}

查找元素

查找元素, 首先我们先实现一个查找对应下标元素的方法, 这个方法实际上非常简单, 直接检查一下下标, 然后返回对应元素即可, 下面是代码

public int get(int index){// 检验index是否合法if (index < 0 || index >= this.size){throw new IndexOutOfBoundsException("下标 " + index + " 超出长度 " + this.size);}return this.array[index];
}

虽然这个代码比较简单, 但是实际上这里我们有一个部分是可以进行修改的, 就是关于这个异常的信息. 在上面的代码中, 我们也多次使用过这个信息, 但是很明显有两个问题:

  1. 这些文本理论上应该统一, 如果这样写, 一旦其中一个需要修改则需要手动修改全部的文本
  2. 每一次都要复制粘贴/手写非常的麻烦

为了解决这两个问题, 我们可以将其封装一下. 那么封装为什么东西比较好呢? 是一个常量还是方法呢?

这个是由我们的文本属性决定的, 可以看到我们的异常信息中有一个会变化的量, 就是下标, 因此这里封装为方法, 然后提供一个参数用于传入下标是比较合理的选择, 那么最终代码如下所示

private String outOfBoundsMsg(int index) {return "下标: " + index + ", 长度: " + this.size;
}

随后修改涉及到这个异常信息的部分即可, 例如我们这里的get()方法

public int get(int index){// 检验index是否合法if (index < 0 || index >= this.size){throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}return this.array[index];
}

接下来就是根据元素查找下标的方法, 这里和删除元素中的查找一样, 默认查找第一次出现的元素, 但是我们这里不能返回 boolean类型, 因为如果找到了需要返回 int类型的下标, 因此这里如果没有找到是返回 -1 的, 因为 -1 在 Java 中是不合法的下标, 经常用于表示类似于这样的情况

最终实现代码如下

public int indexOf(int data){// 默认删除第一个出现的元素, 将下标标识先设置为-1int index = -1;// 遍历查找for (int i = 0; i < this.size; i++){if (this.array[i] == data){index = i;break;}}return index;
}

上面也说了, 这个就和删除中的查找元素部分一样, 因此我们可以将那里的代码修改为调用这个方法, 修改后如下所示

public boolean removeData(int data){// 获取元素下标int index = indexOf(data);// 没有出现, 返回falseif (index == -1){return false;}// 出现, 删除元素, 将index及后面的元素依次左移leftShift(index, this.size - 1);this.size--;return true;
}

修改元素

修改元素, 实际上就是将对应下标的元素修改为传入的元素, 简单的检查一下下标, 然后进行修改即可, 最后可以返回一下原先的元素. 代码如下

public int set(int index, int data){if (index < 0 || index >= this.size){throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}int old = this.array[index];this.array[index] = data;return old;
}

这样我们大体的增删查改方法就书写完毕了, 实际上本身这些方法并不难, 由于中间我们进行了一些复用思路的讲解, 因此前面涉及到的讲解比较多. 后面由于基本上都是讲过的, 因此也就没有那么多的东西了. 下面我们就可以正式的了解一下 ArrayList类了

ArrayList类使用

构造方法

要了解一个类, 首先就要看它的构造方法, ArrayList的构造方法如下

方法签名作用
ArrayList()初始化一个ArrayList
ArrayList(int initialCapacity)根据提供的int类型构造对应大小的ArrayList
ArrayList(Collection<? extends E> c)根据提供的集合类型构造ArrayList

可以看到, 前面的两个方法我们在上面的模拟实现中是模拟过的, 因此还是比较好理解的, 因此我们这里就只讲解最后一个.

首先这里是一个通配符? extends E, 其中E是 ArrayList类的泛型参数名, 也就是说这个? extends E指的是E本身或者其子类. 而这个Collection类则是集合框架中的顶层接口, 它可以存储各种集合类对象的引用.

结合上面的解释, 我们就可以大致理解, 这个构造方法是接收一个集合类, 将里面的元素构造为一个ArrayList. 同时这个集合类里面存储的类型必须是这个ArrayList类本身或者是子类.

例如我现在有一个LinkedList<Integer>(LinkedList类实现了Collection接口), 我就可以将其传入到一个ArrayList<Number>()构造方法中, 如下所示

public class Main {public static void main(String[] args) {LinkedList<Integer> integers = new LinkedList<>();ArrayList<Number> numbers = new ArrayList<>(integers);}
}

下面也是一个例子

class Son extends Father{}class Father{
}public class Main {public static void main(String[] args) {// ArrayList就是属于集合框架的, 也是集合类的子类ArrayList<Son> sons = new ArrayList<>();// 使用存储类型为Son的构造一个存储类型是Father的ArrayListArrayList<Father> fathers = new ArrayList<>(collection);}
}

ArrayList源码阅读

接下来我们进入ArrayList的源码, 来看看其是如何进行默认的初始化的.

一进去这个默认的构造方法, 就可以看到非常简单的代码, 如下所示

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
/**
* Constructs an empty list with an initial capacity of ten.
*/
public ArrayList() {this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

此时我们可以看到, 它就是给这个存储元素的数组 elementData 赋予了一个空的数组, 同时上面的注释说容量为10. 那这里似乎和我们上面的模拟实现不太一样, 这里并没有提供任何的空间, 它为什么说默认容量是10? 我们又是如何插入元素的呢?

此时我们自然就需要去到add()方法来一探究竟了

public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!elementData[size++] = e;return true;
}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++;
}

可以看到上面的两个add()方法中在添加元素前, 都有一个共同的操作, 就是ensureCapacityInternal(size + 1), 翻译过来就是确保内部容量, 其参数代表的是当前的元素个数 + 1, 我们就可以猜测这个方法是用于保证当前的内部容量是否足够这个插入操作的. 大概率这个方法就是上面问题的答案, 我们就可以进去一看

进入代码后, 主要涉及到的就是这三个方法, 如下所示

// 此时传入的minCapacity是上面的size + 1, 也就是 完成操作需要的最小容量
private void ensureCapacityInternal(int minCapacity) {ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}private static int calculateCapacity(Object[] elementData, int minCapacity) {// 查看elementData是否是初始的空列表// 如果是就返回 完成操作需要的最小容量 和 默认容量 中的最大值// 从这里就可以看出默认容量是 DEFAULT_CAPACITY, 也就是 10if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {return Math.max(DEFAULT_CAPACITY, minCapacity);}// 如果不是初识的空列表, 那么就返回传进来的 操作所需最小容量return minCapacity;
}// 此时的minCapacity和上面的不同, 代表的是计算过后得出的最小需求容量
private void ensureExplicitCapacity(int minCapacity) {// 这个数据用于记录列表的修改次数, 主要用于检测线程安全问题, 我们这里不用关心modCount++;// 如果计算出的最小需求容量超过了数组长度, 那么就需要扩容if (minCapacity - elementData.length > 0)grow(minCapacity);
}

从上面我们就可以看出, 这个方法主要就是用于计算出需要的最小容量, 同时查看是否需要扩容, 从而保证内部容量足够进行插入操作. 并且如果是第一次操作, 则其计算出的容量会是默认的 10

接下来我们就继续深入, 来看看这个扩容方法是如何操作的. 其主要涉及到的就是如下两个方法

// 此时传入的参数是上面计算出的期望最小容量
private void grow(int minCapacity) {// 首先先获取到老容量int oldCapacity = elementData.length;// 新容量 = 1.5*老容量// 这种计算方法在模拟实现中已经了解过了int newCapacity = oldCapacity + (oldCapacity >> 1);// 查看根据老容量计算出的新容量是否够用, 不够用则将新容量变为期望的最小容量if (newCapacity - minCapacity < 0)newCapacity = minCapacity;// 查看容量是否超过了最大数组容量, 如果超过了则调用hugeCapacity()方法if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// 扩容elementData = Arrays.copyOf(elementData, newCapacity);
}private static int hugeCapacity(int minCapacity) {// 检测minCapacity是否溢出if (minCapacity < 0) throw new OutOfMemoryError();// 如果需要的最小容量真正大于了MAX_ARRAY_SIZE, 那么就返回 Integer.MAX_VALUE// 反之返回 MAX_ARRAY_SIZEreturn (minCapacity > MAX_ARRAY_SIZE) ?Integer.MAX_VALUE :MAX_ARRAY_SIZE;
}

其中扩容方法主要就是进一步的去计算要扩容的新容量, 随后进行扩容. 同时我们也可以看出其扩容, 默认也是扩容为老容量的1.5倍的

常用方法及其注意事项

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)截取下标从from到to部分的list

下面介绍一些使用这些方法时候的注意事项


下面这两个方法在某些情况使用的时候容易弄混

返回值, 方法名, 参数说明
E remove(int index)删除 index 位置元素
boolean remove(Object o)删除遇到的第一个 o

例如我存储的是一个Integer类型, 代码如下

public class Main {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);list.remove(1);System.out.println(list);}
}

运行后结果如下

此时会发现, 如果我们只提供一个数字, 则默认提供的参数是 int类型的, 也就会被识别为index. 如果我们希望移除的是元素的话, 我们就需要去new Integer()

public class Main {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);list.remove(new Integer(1));System.out.println(list);}
}

运行后结果如下

此时可能有人问了: 为什么这里明明使用的是 ArrayList类, 但是你却是使用一个 List 来接受呢? 我这里能不能用ArrayList来接收呢?

实际上两个使用方法在此处并没有什么区别, 只不过 Java 的代码风格就是趋向于去使用向上转型的写法的, 但是如果是使用其他的语言, 则不一定提倡向上转型的这种写法. 这里的区别就类似于 C/C++ 习惯写代码块时把左括号写下面, 而 Java 习惯将其写语句后同一行. 例如下面这样的写法

C/C++

int test()
{
}

Java

int test(){
}

具体应该怎么写我们具体情况具体分析, 这里我们并没有什么讲究因此两者都可以自由使用.


下面这个方法也有一些需要注意的点

返回值, 方法名, 参数说明
List<E> subList(int fromIndex, int toIndex)截取下标从from到to部分的list

我们可以看一下下面的代码的输出结果

public class Main {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);// 修改子列表List<Integer> list2 = list.subList(1, 3);list2.set(0, 123);// 输出原列表System.out.println(list);}
}

输出结果如下

可以发现最后修改子列表也会修改的还是原列表的数据, 也就是说这个截取实际上只是截取了那一段的地址, 并没有真正的截取出一段数据组成一个新的列表

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/49579.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

记录下 java.lang.UnsatisfiedLinkError 错误

java.lang.UnsatisfiedLinkError 错误 java.lang.UnsatisfiedLinkError 错误解决方式一解决方式二 前因&#xff1a;在我的项目中调用到设备&#xff0c;使用厂家提供的sdk中有dll文件调用&#xff0c;在项目中引用dll文件提示了异常&#xff0c;奇怪的是如果把dll直接copy放在…

企业微信获客助手广告平台深度回传/双回传设置教程参考

很多商家在使用【转化宝】进行推广时只采用了单回传&#xff0c;其实很多情况下单回传即可满足推广模型优化需求&#xff1b;但是最近很多专业化广告运营的代投或运营都开始采用双回传&#xff0c;【转化宝】支持抖音巨量引擎、百度营销广告、快手广告、腾讯广告等均支出深度优…

前端自动化测试(一):揭秘自动化测试秘诀

目录 [TOC](目录)前言自动化测试 VS 手动测试测试分类何为单元测试单元测试的优缺点优点缺点 测试案例测试代码 测试函数的封装实现 expect 方法实现 test 函数结语 正文开始 &#xff0c; 如果觉得文章对您有帮助&#xff0c;请帮我三连订阅&#xff0c;谢谢&#x1f496;&…

使用MariaDB数据库管理系统

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 一、数据库管理系统 数据库是指按照某些特定结构来存储数据资料的数据仓库&#xff1b; 数据库管理系统是一种能够对数据库中存放的数据进行建立、修…

操作系统——笔记(1)

操作系统是管理计算机硬件资源&#xff0c;控制其他程序运行并为用户提供交互操作界面的系统软件的集合&#xff0c;控制和管理着整个计算机系统的硬件和软件资源&#xff0c;是最基本的系统软件。 常见的操作系统&#xff1a;ios、windows、Linux。 计算机系统的结构层次&am…

css气泡背景特效

css气泡背景特效https://www.bootstrapmb.com/item/14879 要创建一个CSS气泡背景特效&#xff0c;你可以使用CSS的伪元素&#xff08;:before 和 :after&#xff09;、border-radius 属性来创建圆形或椭圆形的“气泡”&#xff0c;以及background 和 animation 属性来设置背景…

Dify中接入GPT-4o mini模型

GPT-4o mini模型自己承认是基于GPT-3.5架构的模型&#xff0c;有图有真相&#xff1a; 一.GPT-4o mini官网简介 GPT-4o mini&#xff08;“o"代表"omni”&#xff09;是小型型号类别中最先进的型号&#xff0c;也是OpenAI迄今为止最便宜的型号。它是多模态的&#x…

hadoop大数据安全管理:ldap、keberos、ranger

hadoop大数据中认证一般用keberos&#xff0c;授权用ranger&#xff0c;kerberos和Ldap组件共同组成整个集群的安全鉴权体系&#xff0c;Ldap负责用户数据存储。 kerberos Kerberos 是一种网络认证协议&#xff0c;用于在不安全的网络中以安全的方式对用户和服务进行身份验证。…

【MySQL】Ubuntu22.04 安装 MySQL8 数据库详解

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《C》 《Linux》《MySQL》《Qt》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 一、安装目录 1.1 更新软件源 sheepAron:/root$ sudo apt update1.2 安装mysql_ser…

Spring之Spring Bean的生命周期

Spring Bean的生命周期 通过BeanDefinition获取bean的定义信息调用构造函数实例化beanBean的依赖注入处理Aware接口&#xff08;BeanNameAware、BeanFactoryAware、ApplicationContextAware&#xff09;Bean的后置处理器BeanPostProcessor-前置初始化方法&#xff08;Initiali…

UE4 自动换行——按排序关键字1.2.3.

要自动换行的字符串举例&#xff1a;“有效节点为:1.demo-worker-02 2.demo-worker-01 3.demo-master-01” 1.获取相邻两位字符串&#xff0c;组合后与关键字比较 2.当两位字符串与关键字相等&#xff0c;附加一次换行 3.其他例如 1)2)3)、(1)(2)(3)、<1><2><…

springboot配置文件如何读取pom.xml的值

比如想读取profile.active的值&#xff0c;默认属性为pro 在maven中加入以下插件&#xff1a; <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>3.2.0</version>&l…

goenv丝滑控制多版本go

安装 先装下goenv brew install goenv去 ~/.bash_profile 添加一下 export GOENV_ROOT"$HOME/.goenv" export PATH"$GOENV_ROOT/bin:$PATH" eval "$(goenv init -)"执行一下让配置生效 source ~/.bash_profile插一嘴&#xff0c;如果之前是在…

保持形态真实性的大脑生成建模| 文献速递-基于人工智能(AI base)的医学影像研究与疾病诊断

Title 题目 Realistic morphology-preserving generative modelling of the brain 保持形态真实性的大脑生成建模 01 文献速递介绍 医学影像研究通常受到数据稀缺和可用性的限制。治理、隐私问题和获取成本都限制了医学影像数据的访问&#xff0c;加上深度学习算法对数据的…

(十九)原生js案例之h5地里位置信息与高德地图的初使用

h5 地里位置信息 1. 获取当前位置信息 window.onload function () {const oBtn document.querySelector("#btn");const oBox document.querySelector("#box");oBtn.onclick function () {window.navigator.geolocation.getCurrentPosition(function (…

SQL每日一题:游戏玩法分析 I

题干 活动表 Activity&#xff1a; --------------------- | Column Name | Type | --------------------- | player_id | int | | device_id | int | | event_date | date | | games_played | int | --------------------- 在 SQL 中&#xff0c;表的主键是 (player_id, eve…

49 IRF 经典案例

49 IRF 经典案例 一 H3C-FWW-RF 脚本 <IRF-MATER-FW1000>display current-configuration #version 7.1.064, Alpha 7164 #sysname IRF-MATER-FW1000 # context Admin id 1 #telnet server enable #irf mac-address persistent timerirf auto-update enableundo irf l…

案例研究|柯尼卡美能达软件开发(大连)有限公司基于DataEase构筑内部数据可视化体系

柯尼卡美能达软件开发&#xff08;大连&#xff09;有限公司于2007年5月25日注册成立。公司以“洞悉在工作的人们真实情况&#xff0c;探寻他们的愿望&#xff0c;持续提供使人们更加幸福的服务”为使命&#xff0c;致力于系统品质测试服务、软件开发服务、IT安全服务、高级BPO…

2024最新手机软件APP下载排行网站源码 软件下载站PHP源码

源码介绍 这是一款简洁蓝色的手机软件下载应用排行、平台和最新发布网站源码&#xff0c;主要包括主页、APP列表页、APP详情介绍页、新闻资讯列表、新闻详情页、关于我们等模块页面。 软件下载站PHP网站源码&#xff0c;简单的部署上线&#xff0c;访问首页安装程序&#xff…

小程序收银视频介绍

千呼新零售2.0系统是零售行业连锁店一体化收银系统&#xff0c;包括线下收银线上商城连锁店管理ERP管理商品管理供应商管理会员营销等功能为一体&#xff0c;线上线下数据全部打通。 适用于商超、便利店、水果、生鲜、母婴、服装、零食、百货、宠物等连锁店使用。 详细介绍请…