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];
}
虽然这个代码比较简单, 但是实际上这里我们有一个部分是可以进行修改的, 就是关于这个异常的信息. 在上面的代码中, 我们也多次使用过这个信息, 但是很明显有两个问题:
- 这些文本理论上应该统一, 如果这样写, 一旦其中一个需要修改则需要手动修改全部的文本
- 每一次都要复制粘贴/手写非常的麻烦
为了解决这两个问题, 我们可以将其封装一下. 那么封装为什么东西比较好呢? 是一个常量还是方法呢?
这个是由我们的文本属性决定的, 可以看到我们的异常信息中有一个会变化的量, 就是下标, 因此这里封装为方法, 然后提供一个参数用于传入下标是比较合理的选择, 那么最终代码如下所示
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);}
}
输出结果如下
可以发现最后修改子列表也会修改的还是原列表的数据, 也就是说这个截取实际上只是截取了那一段的地址, 并没有真正的截取出一段数据组成一个新的列表