ArrayList 是 java 集合框架中比较常用的数据结构了,继承自 AbstractList,实现了 List 接口。
底层基于数组实现容量大小动态变化,允许 null 的存在,同时还实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化的。
1)ArrayList 其内部是以动态数组的形式来存储数据的,你可能会疑惑:数组不是定长的吗?
这里的动态数组不是意味着去改变原有内部生成的数组的长度、而是保留原有数组的引用、将其指向新生成的数组对象、这样会造成数组的长度可变的假象;
2)ArrayList 具有数组的特性、通过索引可以支持随机访问;所以通过随机访问ArrayList中的元素效率非常高、但是执行插入、删除时效率比较低,具体原因后面有分析;
3)ArrayList 实现了AbstractList抽象类、List接口,所以其兼具了 AbstractList 和 List的功能、前面我们知道 AbstractList 内部已经实现了获取 Iterator 和 ListIterator 的方法、所以ArrayList只需关心对数组操作的方法的实现;
4)ArrayList 实现了 RandomAccess 接口,此接口只有声明、没有方法体,表示 ArrayList 支持随机访问;
5)ArrayList 实现了 Cloneable 接口、此接口只有声明、没有方法体,表示 ArrayList 支持克隆;
6)ArrayList实现了 Serializable 接口、此接口只有声明、没有方法体、表示 ArrayList 支持序列化,即可以将 ArrayList 以流的形式通过 ObjectInputStream/ObjectOutputStream 来 写/读。
ArrayList部分源码如下:
public class ArrayList extends AbstractList
implements List, RandomAccess, Cloneable, java.io.Serializable{
private static final int DEFAULT_CAPACITY = 10;
private static final Object[] EMPTY_ELEMENTDATA = {};
private transient Object[] elementData;
private int size;
//...省略部分代码
}
如上代码为 ArrayList 的主要属性:
DEFAULT_CAPACITY:默认容量,即为初始值大小
EMPTY_ELEMENTDATA:共享的空数组,用于初始化空实例
elementData:ArrayList内部结构,是一个Objec [ ]类型的数组
size:数组长度大小
如下为ArrayList的构造方法:
1.public ArrayList(int initialCapacity)
2.public ArrayList()
3.public ArrayList(Collection c){
elementData = c.toArray();
size = elementData.length;
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
}
1)构造方法1,表示接受指定地容量值,初始化创建数组,建议在可估算数组大小时,创建ArrayList可指定;
2)构造方法2,是默认的构造方法,它将创建一个空数组;
3)构造方法3,接收一个Collection的实体,将该Collection实体转换为ArrayList对象。
主干流程
【1】添加指定元素代码如下
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
可以看到实际上只有3行代码,其流程主要如下:
1.扩容 (这里就解释了在上面介绍中提出的问题):
主要源码如下:
private void ensureCapacityInternal(int minCapacity) {
if (elementData == EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(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) {
// overflow-conscious code
int oldCapacity = elementData.length;
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:
elementData = Arrays.copyOf(elementData, newCapacity);
}
1)第一个方法的逻辑为:判断是不是第一次添加元素,若为第一次,则设置初始化大小为默认的值10,否则使用传入的参数;
2)第二个方法的逻辑为:若长度大于数组长度,则扩容;
3)第三个方法的逻辑为:
扩容的大小为3/2倍原数组长度
若值newCapacity比传入值minCapacity还要小,则使用传入minCapacity,若newCapacity比设定的最大数组容量大,则使用最大整数值
实际扩容,使用了Arrays.copyof(elementData, newCapacity) (此处有两个问题 1.为啥扩容是原来的3/2倍原数组的长度? 2.调用Arrays.copyOf(elementData, newCapacity)方法具体做了什么操作? )
2.赋值:将添加的值放置到size++的位置上
3.返回:返回true
【2】添加指定元素到指定的位置上代码如下:
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++;
}
其流程为:
1.校验下标:调用rangeCheckForAdd方法进行下标校验,不正确则会抛出IndexOutOfBoundsException异常
2.扩容:详见上部分中做的介绍
3.移动数据:将数据index后面的数据,都向后移动
4.赋值:将加入的值放置到index位置中
5.长度增加:长度增加
1.问题描述
在使用 ArrayList 比较常见的一个问题就是在遍历ArrayList的时候调用remove()方法进行元素的删除操作,从而得到意想不到的结果,本人在开发过程中也遇到过这样的问题,所以在这里提出了,希望能够帮助到大家。
2.实例及分析
如下代码中,在遍历List时,调用了remove方法,删除元素a
//arrayList中的值为 [a,a,c,a,a]
for (int i = 0; i < arrayList.size(); i++) {
if (arrayList.get(i) == "a") {
arrayList.remove(i);
}
}
System.out.println(arrayList);
这段代码看似解决了删除列表中所有的a元素,但是删除后得出List的结果为[a, c, a];为什么这种方式没有达到想要的效果,其实仔细分析后会发现,在调用 remove() 方法时 List 的长度会发生变化而且元素的位置会发生移动,从而在遍历时list实际上是变化的,
例如当 i=0时,此时 list 中的元素为[a,a,c,a,a],
但当 i=1时,此时 List 中的元素为 [a,c,a,a] 元素 的位置发生了移动从而导致在遍历的过程中不能达到删除的效果
3.解决方案
通过上述的分析可以看出,出现问题的原因是元素的位置发生了移动,从而导致异常的结果;
方案一、逆向遍历 List 删除,代码如下:
for (int i = arrayList.size()-1; i >=0 ; i--) {
if (arrayList.get(i) == "a") {
arrayList.remove(i);
}
}
System.out.println(arrayList);
这种做法可行主要是因为 remove()方法 删除 index 处的元素时,是将 index+1 到 size-1 索引处的元素前移,而逆向遍历可以避免元素位置的移动
方案二、使用迭代器中的 remove 方法,迭代器具体参考 Iterator 详解,主要代码如下(这种方式比较推荐)
Iterator ite = arrayList.listIterator();
while (ite.hasNext()){
if(ite.next() == "a")
ite.remove();
}
System.out.println(arrayList);
自己手写一个ArrayList,代码如下:
public class MyArrayList implements Iterable {
private T[] theItems;
private int theSize;
private static final int DEAULT_CAPACITY=10;
public MyArrayList(){
theSize=0;
ensureCapacity(DEAULT_CAPACITY);
}
public void add(T data){
if(size()==theItems.length){
ensureCapacity(size()*2+1);
}
theItems[size()]=data;
theSize++;
}
public void add(int index,T data){
if(size()==theItems.length){
ensureCapacity(size()*2+1);
}
for(int i=theSize;i>index;i--){
theItems[i]=theItems[i-1];
}
theItems[index]=data;
theSize++;
}
public T get(int index){
if(index<0|index>=size()){
throw new IndexOutOfBoundsException("index error");
}
return theItems[index];
}
public T remove(int index){
T removeData=get(index);
for(int i=index;inewCapacity){
return;
}
T[] old=theItems;
theItems= (T[]) new Object[newCapacity];
for(int i=0;i iterator() {
return null;
}
@Override
public void forEach(Consumer action) {
}
@Override
public Spliterator spliterator() {
return null;
}
}
1)ArrayList 是基于数组实现的,它的内存储元素的数组为 elementData;
elementData的声明为:transient Object[] elementData;
2)ArrayList 中 EMPTYELEMENTDATA 和 DEFAULTCAPACITYEMPTY_ELEMENTDATA 的使用;这两个常量使用场景不同。前者是用在用户通过 ArrayList(int initialCapacity) 该构造方法直接指定初始容量为0时,后者是用户直接使用无参构造创建ArrayList时;
3)ArrayList 默认容量为10。调用无参构造新建一个ArrayList时,它的elementData = DEFAULTCAPACITYEMPTYELEMENTDATA, 当第一次使用 add() 添加元素时,ArrayList的容量会为 10;
4)ArrayList 的扩容计算为 newCapacity = oldCapacity + (oldCapacity >> 1); 扩容并非是无限制的,有内存限制、虚拟机限制;
5)ArrayList的toArray() 方法和 subList() 方法,在源数据和子数据之间的区别;
6)注意扩容方法 ensureCapacityInternal();ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量;
当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用 Arrays.copyof() 方法将元素拷贝到新的数组;
从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则不建议使用。
转载自微信公众号< Java技术前线>--《面试题:Java容器之ArrayList全解析》