JAVA容器之ArrayList

一、ArrayList的整体结构

1、重要属性

//Default initial capacity 初始化容量
private static final int DEFAULT_CAPACITY = 10;
//空实例
private static final Object[] EMPTY_ELEMENTDATA = {};
//初始化时的实例
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//当前List容器
transient Object[] elementData;
//The size of the ArrayList (the number of elements it contains).
//容器元素的数量,也是元素的下标
private int size;
//结构被修改的次数(AbstractList 中的属性)
protected transient int modCount = 0;

transient:在JAVA中一个对象只要实现了Serilizable接口,这个对象就可以被序列化,被序列化的对象可以在网络中传输,也可以存在磁盘中,然后通过反序列化可以获取原数据。但是对于一些敏感的数据,如银行卡密码、用户登录名、密码等数据我们不希望在网络中传播,也不希望存储在磁盘中。对于这样的数据可以使用transient修饰。简单说当在需要序列化的属性前添加关键字transient后,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

2、数据结构

Objecte类型的数组

3、特点

  1. 有序
  2. 允许null元素
  3. 允许重复
  4. size、isEmpty、get、set、add 等方法时间复杂度都是 O (1);
  5. 非迭代器遍历修改时容器大小时会抛出ConcurrentModificationException
  6. 非线程安全,可以List list = Collections.synchronizedList(new ArrayList(...));转线程安全

二、源码

1、构造函数

 //1、指定容量大小的构造器
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                    initialCapacity);
        }
    }
    //2、无参构造器
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    //3、传入一个集合的构造器
    public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elementData.getClass() != Object[].class)
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
  1. 对于第一个构造函数当initialCapacity=0时,数组被初始化位空数组,数组的初始大小是在第一次add的时候赋值10
  2. 对于第二个构造函数,数组被初始化为默认类型的空数组,数组的初始大小是在第一次add的时候赋值10
  3. c.toArray might (incorrectly) not return Object[] (see 6260652) 这是JDK中的一个BUG编号; c.toArray();得到的数组不一定就是Object类型的数组 ,所以在这里做了一个if()判断,然后通过Arrays.copyOf(elementData, size, Object[].class);将其转化为Object类型的数组。例如以下情况

List<String> list = Arrays.asList("abc");可以知道返回的实际类型是java.util.Arrays$ArrayList,而不是ArrayList。我们调用Object[] objArray = list.toArray();返回是String[]数组,所以我们不能将Object对象,放到objArray数组中。

2、添加元素与扩容

添加元素:

public boolean add(E e) {
    //保证数组的容量可以添加当前元素
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    //在当前size上放入元素,并将size+1(在ArrayList中并没有成员变量index,他是直接使用size做下标)
    elementData[size++] = e;
    return true;
}

扩容:

private static int calculateCapacity(Object[] elementData, int minCapacity) {
    //如果初始化的数组是默认的空数组(无参构造函数初始化)就返回默认容量10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        return Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    return minCapacity;
}

private void ensureExplicitCapacity(int minCapacity) {
    //数组被修改记录+1
    modCount++;
    // 如果数组的添加一个元素的实际容量 > 当前数组的容量,就扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
//数组的扩容
private void grow(int minCapacity) {
    // 当前数组的容量
    int oldCapacity = elementData.length;
    //新的容量=老的容量+老容量的一半(右位移一位就是除以2向下取整)
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //新的容量 < 我们期望的容量,就采用期望的容量
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //新的容量  >  MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8) ,就采用Integer.MAX_VALUE
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    //进行扩容
    elementData = Arrays.copyOf(elementData, newCapacity);
}

数组的copy:数组扩容的本质就是大数组与原来小的数组进行copy

//数组copy,该方法是JDK提供给我的本地方法,
//他是浅copy,非线程安全,底层是对内存的直接复制,所以相比于for循环,效率高出很多 
System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); 
Object src : 原数组
int srcPos :  从原数组的起始位置开始
Object dest : 目标数组
int destPos : 目标数组的开始起始位置
int length  : 要copy的数组的长度
public static native void arraycopy(Object src,  int  srcPos, Object dest, int destPos,int length);

3、删除元素

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            //找到第一个null元素的下标
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            //找到第一个非null元素的下标
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}
private void fastRemove(int index) {
    //数组被修改记录+1
    modCount++;
    //要移动元素的个数
    int numMoved = size - index - 1;
    if (numMoved > 0)
        //将移除的位置补齐(在这里采用的是数组copy的方式)
        System.arraycopy(elementData, index+1, elementData, index,numMoved);
    //移除一个元素后将释放占用的空间   
    elementData[--size] = null; // clear to let GC do its work
}

删除数据的核心点:当移除一个元素后,为了保证数组的连续性,并且释放调没用的空间,他需要把空出来的位置填补上;简单数说就是被删除元素后面的元素依次向前移动一位。
ArrayList采用的方式:被删除元素前面不动,把其之后的元素copy到目标数组中,copy的起点是被删除元素的下标,copy长度是其之后元素个数

如图数组我们要删除C元素:System.arraycopy(Object src,  int  srcPos, Object dest, int destPos, int length);

  1. index=2
  2. srcPos=index+1=2+1=3;
  3. destPos=index
  4. numMoved=6-2-1=3
  5. length=numMoved=3
  6. src=elementData
  7. dest=elementData 

4、迭代器

ArrayList 有自己的迭代器 

 private class Itr implements Iterator<E>

核心成员:

int cursor;       // 下一个元素的索引
int lastRet = -1; // 最后一个元素的索引,如果没有返回-1
int expectedModCount = modCount;//数组被修改记录(数组版本)

迭代的核心方法:

//判断是否存在下一个元素
public boolean hasNext() {
    //下一个元素的索引如果与size(元素个数)相等,表示没有元素可迭代
    return cursor != size;
}

//取出元素
@SuppressWarnings("unchecked")
public E next() {
    //检查数组是否被修改 ,版本不一致 ConcurrentModificationException
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    //下一个元素的索引(下次迭代的位置)    
    cursor = i + 1;
    //返回档次的元素
    return (E) elementData[lastRet = i];
}

迭代器中删除元素:

public void remove() {
    //检查元素是不是已经被删除了
    if (lastRet < 0)
        throw new IllegalStateException();
    //检查数组是否被修改    
    checkForComodification();
    try {
        //移除元素
        ArrayList.this.remove(lastRet);
        //移除后将cursor还原;当前元素已经删除了,并且被后续元素补齐了,所以cursor不变
        cursor = lastRet;
        //删除元素设为-1,主要目的就是防止重复删除
        lastRet = -1;
        //将检查版本的属性版本号给补齐,避免下次检察的不一致
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

看了源码后我想大家明白了为什么在循环中删除元素要使用迭代器,而不是普通的循环了

迭代其删除元素:做了这两件事

  1. //移除后将cursor还原;当前元素已经删除了,并且被后续元素补齐了,所以cursor不变
  2. cursor = lastRet;
  3. //将检查版本的属性版本号给补齐,避免下次检察的不一致
  4. expectedModCount = modCount;

普通循环中删除元素:直接操作的是数组,会存在遍历丢失

三、总结

1、时间复杂度:

  • add(E e):添加元素到末尾,时间复杂度为O(1)
  • add(int index, E element):添加元素到指定位置,时间复杂度为O(n)
  • get(int index):获取指定索引位置的元素,时间复杂度为O(1)
  • remove(int index):删除指定索引位置的元素,时间复杂度为O(n)
  • remove(Object o):删除指定元素的元素,时间复杂度为O(n)

2、线程安全:

  1. ArrayList 作为共享变量时,会存在线程安全问题,如果ArrayList 是方法内的局部变量,是没有线程安全的问题的
  2. ArrayList 存在线程安全问题的本质是因为 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,并且这些变量的类型并非是volatile的,也不是原子操作。
  3. 类注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList 锁的粒度是整个方法,实现了线程安全,但是性能降低,具体实现源码:
    public void add(int index, E element) {
        synchronized (mutex) {
            list.add(index, element);
        }
    }

     

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值