前言

在之前的文章中我们提到过ArrayList,ArrayList可以说是每一个学java的人使用最多最熟练的集合了,但是知其然不知其所以然。关于ArrayList的具体实现,一些基本的都也知道,譬如数组实现,线程不安全等等,但是更加具体的就很少去了解了,例如:初始化的长度,扩容等。

本篇主要通过一些对源码的分析,讲解几个ArrayList常见的方法,以及和Vector的区别。

ArrayList

定义

public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList实际上是一个动态数组,容量可以动态的增长,其继承了AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。

RandomAccess接口,标记接口,表明List提供了随机访问功能,也就是通过下标获取元素对象的功能。

1.RandomAccess接口,标记接口,表明List提供了随机访问功能,也就是通过下标获取元素对象的功能。之所以是标记接口,是该类本来就具有某项能力,使用接口对其进行标签化,便于其他的类对其进行识别(instanceof)。
2.ArrayList数组实现,本身就有通过下标随机访问任意元素的功能。那么需要细节上注意的就是随机下标访问和顺序下标访问(LinkedList)的不同了。也就是为什么LinkedList最好不要使用循环遍历,而是用迭代器遍历的原因。
3.实现RandomAccess同时意味着一些算法可以通过类型判断进行一些针对性优化,例子有Collections的shuffle方法,源代码就不粘贴了,简单说就是,如果实现RandomAccess接口就下标遍历,反之迭代器遍历

实现了Cloneable, java.io.Serializable意味着可以被克隆和序列化。

初始化

在使用中我们经常需要new出来各种泛型的ArrayList,那么在初始化过程是怎样的呢?

如下一行代码,创建一个ArrayList对象

List<Person> list = new ArrayList<>();

我们来看源码,是如何初始化的,找到构造方法

//无参构造方法
public ArrayList() {super();this.elementData = EMPTY_ELEMENTDATA;
}

看到这些代码的时候,我也是不解的,elementData和EMPTY_ELEMENTDATA是什么啊?但是很明显EMPTY_ELEMENTDATA是一个常量,追本溯源我们去看一下成员属性。

//如果是无参构造方法创建对象的话,ArrayList的初始化长度为10,这是一个静态常量
private static final int DEFAULT_CAPACITY = 10;//在这里可以看到我们不解的EMPTY_ELEMENTDATA实际上是一个空的对象数组private static final Object[] EMPTY_ELEMENTDATA = {};//保存ArrayList数据的对象数组缓冲区 空的ArrayList的elementData = EMPTY_ELEMENTDATA 这就是为什么说ArrayList底层是数组实现的了。elementData的初始容量为10,大小会根据ArrayList容量的增长而动态的增长。private transient Object[] elementData;
//集合的长度private int size;

执行完构造方法,如下图

可以说ArrayList的作者真的是很贴心,连缓存都处理好了,多次new出来的新对象,都指向同一个引用。

添加方法add

add(E e)

 /*** Appends the specified element to the end of this list.*/
//增加元素到集合的最后
public boolean add(E e) {ensureCapacityInternal(size + 1);  // Increments modCount!!//因为++运算符的特点 先使用后运算  这里实际上是//elementData[size] = e//size+1elementData[size++] = e;return true;
}

先不管ensureCapacityInternal的话,这个方法就是将一个元素增加到数组的size++位置上。

再说回ensureCapacityInternal,它是用来扩容的,准确说是用来进行扩容检查的。下面我们来看一下整个扩容的过程

//初始长度是10,size默认值0,假定添加的是第一个元素,那么minCapacity=1 这是最小容量 如果小于这个容量就会报错
//如果底层数组就是默认数组,那么选一个大的值,就是10
private void ensureCapacityInternal(int minCapacity) {if (elementData == EMPTY_ELEMENTDATA) {minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);}//调用另一个方法ensureExplicitCapacityensureExplicitCapacity(minCapacity);}private void ensureExplicitCapacity(int minCapacity) {//记录修改的次数modCount++;// overflow-conscious code//如果传过来的值大于初始长度 执行grow方法(参数为传过来的值)扩容if (minCapacity - elementData.length > 0)grow(minCapacity);}
//真正的扩容private void grow(int minCapacity) {// overflow-conscious codeint oldCapacity = elementData.length;//新的容量是在原有的容量基础上+50% 右移一位就是二分之一int newCapacity = oldCapacity + (oldCapacity >> 1);//如果新容量小于最小容量,按照最小容量进行扩容if (newCapacity - minCapacity < 0)newCapacity = minCapacity;if (newCapacity - MAX_ARRAY_SIZE > 0)newCapacity = hugeCapacity(minCapacity);// minCapacity is usually close to size, so this is a win://这里是重点 调用工具类Arrays的copyOf扩容elementData = Arrays.copyOf(elementData, newCapacity);}//Arrays
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {T[] copy = ((Object)newType == (Object)Object[].class)? (T[]) new Object[newLength]: (T[]) Array.newInstance(newType.getComponentType(), newLength);System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));return copy;
}

add(int index, E element)

添加到指定的位置

public void add(int index, E element) {//判断索引是否越界,如果越界就会抛出下标越界异常rangeCheckForAdd(index);
//扩容检查ensureCapacityInternal(size + 1);  // Increments modCount!!//将指定下标空出 具体作法就是index及其后的所有元素后移一位System.arraycopy(elementData, index, elementData, index + 1,size - index);//将要添加元素赋值到空出来的指定下标处elementData[index] = element;//长度加1size++;
}
//判断是否出现下标是否越界
private void rangeCheckForAdd(int index) {//如果下标超过了集合的尺寸 或者 小于0就是越界  if (index > size || index < 0)throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}

remove(int index)

ArrayList支持两种删除元素的方式

  1. remove(int index) 按照下标删除 常用
  2. remove(Object o) 按照元素删除 会删除和参数匹配的第一个元素

下面我们看一下ArrayList的实现

 /**移除list中指定位置的元素* Removes the element at the specified position in this list.所有后续元素左移 下标减1* Shifts any subsequent elements to the left (subtracts one from their* indices).*参数是要移除元素的下标* @param index the index of the element to be removed返回值是被移除的元素* @return the element that was removed from the list* @throws IndexOutOfBoundsException {@inheritDoc}*/
public E remove(int index) {//下标越界检查rangeCheck(index);
//修改次数统计modCount++;//获取这个下标上的值E oldValue = elementData(index);
//计算出需要移动的元素个数 (因为需要将后续元素左移一位 此处计算出来的是后续元素的位数)int numMoved = size - index - 1;//如果这个值大于0 说明后续有元素需要左移  是0说明被移除的对象就是最后一位元素if (numMoved > 0)//索引index只有的所有元素左移一位  覆盖掉index位置上的元素System.arraycopy(elementData, index+1, elementData, index,numMoved);// 将最后一个元素赋值为null  这样就可以被gc回收了elementData[--size] = null; // clear to let GC do its work
//返回index位置上的元素return oldValue;
}//移除特定元素
public boolean remove(Object o) {//如果元素是null 遍历数组移除第一个nullif (o == null) {for (int index = 0; index < size; index++)if (elementData[index] == null) {//遍历找到第一个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; // clear to let GC do its work
}

ArrayList总结

  1. 底层数组实现,使用默认构造方法初始化出来的容量是10
  2. 扩容的长度是在原长度基础上加二分之一
  3. 实现了RandomAccess接口,底层又是数组,get读取元素性能很好
  4. 线程不安全,所有的方法均不是同步方法也没有加锁,因此多线程下慎用
  5. 顺序添加很方便
  6. 删除和插入需要复制数组 性能很差(可以使用LinkindList)

为什么ArrayList的elementData是用transient修饰的?

transient修饰的属性意味着不会被序列化,也就是说在序列化ArrayList的时候,不序列化elementData。

为什么要这么做呢?

  1. elementData不总是满的,每次都序列化,会浪费时间和空间
  2. 重写了writeObject 保证序列化的时候虽然不序列化全部 但是有的元素都序列化

所以说不是不序列化 而是不全部序列化。

private  writeObject(java.io.ObjectOutputStream s)throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();// Write out array lengths.writeInt(elementData.length);// Write out all elements in the proper order.
for (int i=0; i<size; i++)s.writeObject(elementData[i]);if (modCount != expectedModCount) {throw new ConcurrentModificationException();}
}

ArrayList和Vector的区别

标准答案

  1. ArrayList是线程不安全的,Vector是线程安全的
  2. 扩容时候ArrayList扩0.5倍,Vector扩1倍

那么问题来了

ArrayList有没有办法线程安全?

Collections工具类有一个synchronizedList方法

可以把list变为线程安全的集合,但是意义不大,因为可以使用Vector

Vector为什么是线程安全的?

老实讲,抛开多线程 它俩区别没多大,但是多线程下就不一样了,那么Vector是如何实现线程安全的,我们来看几个关键方法

public synchronized boolean add(E e) {modCount++;ensureCapacityHelper(elementCount + 1);elementData[elementCount++] = e;return true;
}public synchronized E remove(int index) {modCount++;if (index >= elementCount)throw new ArrayIndexOutOfBoundsException(index);E oldValue = elementData(index);int numMoved = elementCount - index - 1;if (numMoved > 0)System.arraycopy(elementData, index+1, elementData, index,numMoved);elementData[--elementCount] = null; // Let gc do its workreturn oldValue;
}

就代码实现上来说,和ArrayList并没有很多逻辑上的区别,但是在Vector的关键方法都使用了synchronized修饰

arraylist扩容是创建新数组吗 java_Java集合干货——ArrayList源码分析相关推荐

  1. arraylist扩容是创建新数组吗 java_Java 集合,你肯定也会被问到这些

    文章收录在 GitHub JavaKeeper ,N线互联网开发必备技能兵器谱 作为一位小菜 "一面面试官",面试过程中,我肯定会问 Java 集合的内容,同时作为求职者,也肯定会 ...

  2. arraylist扩容是创建新数组吗 java_Java ArrayList扩容问题实例详解

    本文研究的主要是Java ArrayList扩容问题实例详解的相关内容,具体介绍如下. 首先我们需要知道ArrayList里面的实质的其实是一个Object类型的数组,ArrayList的扩容问题其实 ...

  3. arraylist扩容是创建新数组吗 java_Java 基础数据结构分析

    java -version java version "13.0.2" 2020-01-14 Java(TM) SE Runtime Environment (build 13.0 ...

  4. arraylist扩容是创建新数组吗 java_Java编程之数组扩容

    一.背景 数组在实际的系统开发中用的越来越少了,我们只有在阅读某些开源项目时才会看到数组的使用.在Java中,数组与List.Set.Map等集合类相比,后者使用起来方便,但是在基本数据类型处理方面, ...

  5. arraylist扩容是创建新数组吗 java_arraylist扩容机制要怎么实现?arraylist怎么扩容...

    ArrayList大家都知道了吧,这是一个动态数组.以java语言来说,数组是定长的,在被创建之后就不能被加长或缩短了,因此,了解它的扩容机制对使用它尤为重要.下面,我们就一起来看看它的扩容机制是怎么 ...

  6. Android 12 新APP启动画面(SplashScreen API)简介源码分析

    以往的启动画面 默认情况下刚启动APP时会显示一会白色背景 如果把这个启动背景设置为null,则一闪而过的白色会变成黑色 如果把启动Activity设置为背景透明[< item name=&qu ...

  7. activity 点击后传递数据给fragment_Fragment 新特性 : Fragment Result API 使用以及源码分析

    原标题: Android Fragments: Fragment Result 原文地址: https://proandroiddev.com/android-fragments-fragment-r ...

  8. hashmap 扩容是元素还是数组_曹工说JDK源码(1)--ConcurrentHashMap,扩容前大家同在一个哈希桶,为啥扩容后,你去新数组的高位,我只能去低位?...

    如何计算,一对key/value应该放在哪个哈希桶 大家都知道,hashmap底层是数组+链表(不讨论红黑树的情况),其中,这个数组,我们一般叫做哈希桶,大家如果去看jdk的源码,会发现里面有一些变量 ...

  9. setitimer 创建两个定时器_UE4 Timer(定时器)相关源码分析

    前言 文章属于旧有文章搬运, 之前在csdn上面 2019.11.6修改 Timer是定时器, 用途: 延时执行事件 周期性执行事件 参考官方文档Using Timers Timer可以... Tim ...

最新文章

  1. 【模板】RMQ问题—st表实现
  2. 利用U盘进行软件加密(转)
  3. 在Linux上编写并运行Python文件
  4. MybatisPlus实现自动填充
  5. python 枚举相等判断_python中is和==的区别
  6. redis rdb aof区别_理解Redis的持久化机制:RDB和AOF
  7. JavaEE基础(03):Http请求详解,握手挥手流程简介
  8. java的URLEncoder.encode() 疑问与解惑
  9. ansible最大并发_通过这7种方法来最大程度地提高Ansible技能
  10. java 学习案例之英汉字典
  11. vb代码转换为java_有人可以解释如何使用JACOB将VB代码转换为Java吗?
  12. 【读书笔记】100个Switf必备tips
  13. MySQL 游标(CURSOR)
  14. 炒币之止损止盈控制的艺术、投资入门
  15. 批量导出docker images 批量导入docker镜像
  16. 学好Python能做什么 python就业方向有哪些
  17. 如何用防火墙禁止某个软件联网
  18. Java岗大厂面试百日冲刺 - 日积月累,每日三题【Day22】—— 并发编程2
  19. 一键中文数据增强包 ; NLP数据增强、bert数据增强、EDA:pip install nlpcda
  20. 通信原理 | 波段的划分

热门文章

  1. 想了解Xtrabackup备份原理和常见问题分析,看这篇就够了
  2. 实践解析丨Rust 内置 trait:PartialEq 和 Eq
  3. 使用Keil5构建GD32450i-EVAL工程
  4. 设计模式的C语言应用-外观模式-第八章
  5. php二分法 冒泡 快速排序,PHP 常见算法【冒泡排序, 快速排序, 插入排序, 取舍排序, 二分法查找, .】...
  6. Android doc |Getting Started|部分 部分译文 --Building Your First App
  7. Python操作HDF5存储数据方法总结
  8. 【Paper-Attack】Exploratory Adversarial Attacks on Graph Neural Networks
  9. c++ 设置类型别名
  10. latex中erro:extra alignment tab has changed to\cr但列数没错的解决方法