一、集合大家族
从图中,我们可以看出集合家族是非常庞大的,它几乎支撑着我们开发过程中所有的数据存储和数据操作功能,可想而知其重要性。
而我们今天要了解的ArrayList就是属于集合框架中的 List 体系中,它实现了 List 接口而 List又继承了 Collection 接口从而使ArrayList的功能更加丰富。
二、ArrayList前奏
在使用之前我们先来看看它的类继承和实现结构图:
- 实线是继承关系
- 虚线是实现关系
那么我们就先来看看它的顶层父接口 Collection
的相关方法:
可以看出它定义了一些集合中最基本的抽象方法,其子类可以根据自身的需求定制化的实现,下面我们来简单的分析一下这些方法功能:
添加功能
boolean add(E e); // 添加一个元素 boolean addAll(Collection<? extends E> c); // 添加一个集合的元素
删除功能
boolean remove(Object o); // 删除一个指定元素 boolean removeAll(Collection<?> c); // 移除一个集合的元素,只要一个元素被移除,就返回true void clear(); // 删除所有元素
判断功能
boolean contains(Object o); // 判断集合中是否存在对于元素 boolean containsAll(Collection<?> c); // 判断集合中是否包含指定的集合元素,只有包含所有的元素,才叫包含 boolean isEmpty(); // 判断集合是否为空
获取功能
Iterator<E> iterator(); // 迭代器
长度功能
int size(); // 元素个数
交集功能
boolean retainAll(Collection<?> c); // 移除此 Collection 中未包含在指定Collection中的所有元素 // 集合 A 和集合 B 做交集,最终的结果保存在集合 A 返回值表示的是A是否发生过变化
接下来就是List
这个接口的介绍了,我们先来看图了解一下其中的定义的抽象方法:
我们可以看到,它的方法比Collection接口的方法多了一点,具体如下:
boolean equals(Object o); // 比较两个元素的值是否相等 E get(int index); // 根据对应的下边获取值 int hashCode(); // 获取对象的哈希码 int indexOf(Object o); // 获取第一个查找的对象下标 int lastIndexOf(Object o); // 获取最后一个查找对象的下标 List<E> subList(int fromIndex, int toIndex); // 获取从 fromIndex下标到toIndex下标结尾的范围内元素 Object[] toArray(); // 将对应的list类型数据变成数组类型数据
经过那么多的铺垫,我们正式来认识认识ArrayList
吧!
三、ArrayList使用
我们先要有个概念就是,ArrayList
底层就是一个数组,只不过它能动态扩容而已。
// 存储元素的数组,transient的作用为序列化时不序列化它修饰的实属性 transient Object[] elementData;
上才艺
@Test public void demoTest01(){ // 创建一个ArrayList对象,不带初始容量 List<String> stringList = new ArrayList<>(); // 创建一个ArrayList对象,传入初始容量 List<String> stringListSize = new ArrayList<>(10); // 向list中添加元素 stringList.add("J3 - 白起"); stringList.add("关注他,认识这个 18 岁的年轻小伙"); stringListSize.add("^v^"); stringListSize.add("记得关注!"); stringListSize.add("记得点赞!"); // 设置对应下标的元素 stringList.set(0,"白起"); // 获取对应下标元素 System.out.println(stringList.get(0)); // 移除对应下标元素 stringListSize.remove(2); // 输出stringList的元素个数 System.out.println(stringList.size()); // 输出list中的所有元素 System.out.println(stringList.toString()); System.out.println(stringListSize.toString()); }
上面我们使用了ArrayList的一些基本方法,来完成了一个简单的案例。
对于这些如果我们就只认识到这种地步的话那就只是一个非常低级的API调用工程师了,我们不能仅仅的局限于此。都知道使用起来方便,但去进一步的了解每个方法的内部才是我们学习的一个目标,所以我们下面就去瞅瞅这些基本方法在ArrayList中是如何实现的(简单的瞅瞅)。
四、方法解析
4.1 构造方法
从刚刚我写的案例来看,构造方法可以分带参和不带参两种具体如下:
// 创建一个ArrayList对象,不带初始容量 List<String> stringList = new ArrayList<>(); // 创建一个ArrayList对象,传入初始容量 List<String> stringListSize = new ArrayList<>(10);
那我们点进去看看这两个构造器方法
// 带参构造,initialCapacity:初始数组容量 public ArrayList(int initialCapacity) { // 如果初始容量大于 0 直接创建对应容量的数组 if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { // 如果初始容量为 0 直接赋一个空的数组 this.elementData = EMPTY_ELEMENTDATA; } else { // 初始容量不合规,报错 throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } // 不带参构造 public ArrayList() { // 直接赋值空数组 this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; }
4.2 add方法
add⽅法可以说是ArrayList⽐较重要的⽅法了,在这里我们介绍两个常用的:
// 向list中直接添加元素 public boolean add(E e); // 向list中的指定下标添加元素 public void add(int index, E element)
4.2.1 add(E e)
// 向list中添加元素 public boolean add(E e) { // 判断list再添加一个元素时,是否需要扩容 ensureCapacityInternal(size + 1); // Increments modCount!! // 在数组后面直接赋值 elementData[size++] = e; // 返回添加成功 return true; }
通过代码可以看出这个方法向list中添加元素分为两步:
判断是否需要扩容,如果是就是进行相应的扩容操作
插入元素
这个判断是否需要扩容的方法内容较多,就不带大家进去看了,知道它的功能就行,如果你们想要了解的话可以自己动手进去看看😀。
4.2.2 add(int index, E element)
// 向list中的指定下标添加元素 public void add(int index, E element) { // 检查下标是否合理 rangeCheckForAdd(index); // 判断list再添加一个元素时,是否需要扩容 ensureCapacityInternal(size + 1); // Increments modCount!! // 将index下标以后的元素往后移动 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 给index的下标的位置赋值 elementData[index] = element; // list的元素个数加一 size++; }
方法步骤:
- 检查下标是否合理
- 判断是否需要扩容,如果是就是进行相应的扩容操作
- 将对应下标后面的元素后移
- 在指定下标上赋值
- 元素个数加一
在这里又涉及到了一个方法System.arraycopy()它的底层是 native 修饰的(非Java所写),所以在这里我就不过多的介绍它了。
4.3 get方法
// 根据下标获取元素值 public E get(int index) { // 检查下标是否大于list中存储的元素个数 rangeCheck(index); // 返回对应下标的值 return elementData(index); }
方法步骤:
- 检查下标
- 返回对应的值
这个方法是不是很简单那呢!我觉得是,我们想简单一点,对应下标有值就返回给我们,没有就抛错(😝)
4.4 set方法
// 将对应下标的位置设置成指定值 public E set(int index, E element) { // 检查下标是否大于list中存储的元素个数 rangeCheck(index); // 获取下标的原始值 E oldValue = elementData(index); // 给对应下标设置新值 elementData[index] = element; // 返回对应下标的原始值 return oldValue; }
方法步骤:
- 检查下标
- 获取下标下的原始值
- 给对应下标设置新值
- 返回原始值
没有什么可以细讲的了,检查的方法前面都已经说过了,我们往下看吧!
4.5 remove方法
移除方法我们同样的介绍聊个一个是根据下标移除,一个是根据元素移除
// 移除对应下标的元素 public E remove(int index); // 移除对应的元素值 public boolean remove(Object o);
4.5.1 remove(int index)
// 移除对应下标的元素 public E remove(int index) { // 检查下标是否大于list中存储的元素个数 rangeCheck(index); // 修改次数加一。modCount描述List被修改过的次数 modCount++; // 获取下标的值 E oldValue = elementData(index); // 计算开始往前移动的下标 int numMoved = size - index - 1; if (numMoved > 0) // 开始将元素从对应的位置往前移动 System.arraycopy(elementData, index+1, elementData, index, numMoved); // 将数组的最后一个元素置为null,方便回收 elementData[--size] = null; // clear to let GC do its work // 返回被移除的值 return oldValue; }
方法步骤:
- 检查⻆标
- 获取旧的值
- 计算移动下标并移动
- 将最后的位置置为null,方便回收
- 返回被移除的值
4.5.2 remove(Object o)
// 移除对应的元素值 public boolean remove(Object o) { // 移除的值为null if (o == null) { // 遍历数组,移除null for (int index = 0; index < size; index++) // 如果对应位置为nukk 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; }
方法步骤:
- 判断移除的是null值还是非null值
- 遍历元素数组
- 判断数组中的值是否与移除的值相等
- 是则移除并结束遍历返回,否者继续遍历,直到退出
这里我们可以发现根据对象移除只会移除第一个与移除对象相同的值,如果数组中有多个相同的值,只会移除一个。
五、总结
经过上面的介绍我们已经了解了ArrayList的一些基本的知识点了,那么我来简单的概括一下上面所讲的内容:
- ArrayList是Collection集合框架中List体系的一员
- 底层基于数组实现,可动态扩容并且移除元素后容量不会减少
- 有带参和无参两种构造方法
- 在添加和删除元素的时候都要移动元素
- 存放的值可以重复,可以存放null值
- 没有做同步限制,多线程不安全
上面的内容有几个点没有进行深入的讲解,比如初始化后的容量问题,初始化空数组问题,数组扩容问题,元素移动问题等这些打算后期再出一篇进行讲解。
好了,今天的内容到这里就结束了,关注我,我们下期见
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^