数据结构之ArrayList

Posted gitzzp

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构之ArrayList相关的知识,希望对你有一定的参考价值。

ArrayList

ArrayList详解

ArrayList概念

​ ArrayList其实是一个数组,数组是一种线性表数据结构,使用一组连续的内存空间来存储同一种数据类型;特点:

​ 1:增删慢,每次删除元素都要更改数组长度,拷贝和移动元素位置;(为了保证数据的连续性)

​ 2:查询快,可以根据地址+索引的方式快速获取对应位置上的元素;

​ 内存寻址:计算机在分配内存的时候,会给每个内存单元都分配了一个地址,然后通过地址来访问数据;比如new int[5]; 计算机会给数组分配一块连续的内存空间,并且得到这个内存空间的起始位置,比如从0-4;,所以如果要通过地址来访问数据的时候,计算机有一个寻址公式来到得到其地址;

ArrayList类

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

Serializable 标记性接口

不实现这个类的接口将不会使用任何状态序列化或反序列化,可序列化类的所有子类型都是可以序列化的;

而且这个序列化的接口是没有方法或者字段的,仅仅是标识可串行序列化的语义;

那么序列化有什么作用呢?

序列化:将对象得数据写入到文件(写对象)

反序列化:将对象数据从文件中读取出来

不使用就会无法写入数据,写入到文件中;

Cloneable 标记性接口

克隆就是可以创建一份新的完全一样的数据拷贝,如果不实现这个接口,克隆的时候会出问题;CloneNotSupprotedException

源码分析:clone方法其实调用的native层的clone,(super.Clone)的时候,调用的是object对象的clone方法;

拷贝分为以下两种方式:

浅拷贝:基本数据类型可以达到复制,引用数据类型则不可以;引用类型只会拷贝对象的地址,所以当原对象发生改变的时候,拷贝的对象也会发生改变。

使用浅拷贝的时候,该类需要实现Cloneable接口,并且实现clone()方法;

深拷贝:主类的基本数据类型和引用数据类型都可以进行拷贝,但需要注意的是,主类中的所有引用类型数据需要重写clone方法;然后在主类中修改clone方法,clone的时候依次去调用引用类的clone方法。

RandomAccess 标记性接口

该接口主要由List实现,用来表示支持快速随机访问;这个接口的目的是为了让一些通用的算法可以更改行为;比如随机访问列表或者顺序访问列表的时候能够提供更好的性能;

for(int i=0;i=list.size();i++)
	list.get(i) // 这种方式就是典型的随机访问列表,因为我们可以根据i来指定我们需要访问具体的某个索引上的值;

随机访问列表的速度要比顺序访问的速度要快

Iterator<String> it = list.iterator();
while (it.hasNext())
    it.next();// 这种方式就是典型的顺序访问,因为它的访问顺序是一个接着一个;

RandomAccess接口测试

AbstractList 抽象类

该类提供了List接口的骨架实现,以最小化实现由“随机存取”数据存储(如阵列)支持的此接口所需的工作量,对于顺序访问数据(例如链表列表),应该使用AbstractSequentialList优先;

要实现一个不可修改的列表,开发者只需要拓展这个类并提供get(int)和size()方法实现;

要实现可修改的列表,开发者必须覆盖set(int,E)方法(否则将抛出UnsupportedOprationException),如果列表可变大小,开发者必须覆盖add(int,E)和remove(int)方法。

根据Collection接口规范中的建议,开发者通常情况下应该提供一个无参以及集合构造函数;

而且程序员不必提供迭代器实现,迭代器和列表迭代器由此类实现;

(以上皆来自JDK官方对于AbstractList定义)

通俗的解释:AbstractList针对List集合做了一些通用的定义,如果你要自定义List,该List的行为与AbstractList中的功能重合,则不必自己实现,然后根据自己的需求去重写AbstractList其他的方法;

ArrayList源码解析

构造函数

public ArrayList()  // 无参构造
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
        // 当我们调用ArrayList时,会创建一个空数组(Object类型)

public ArrayList(int initialCapacity) // 有参构造
        if (initialCapacity > 0) 
            this.elementData = new Object[initialCapacity];  //创建你传进来大小的数组
         else if (initialCapacity == 0) 
            this.elementData = EMPTY_ELEMENTDATA; // 如果传进来的是0,则默认是个空数组
         else 
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        
    
public ArrayList(Collection<? extends E> c) 
    elementData = c.toArray();   // 将集合转成数组 (toArray方法本质上是创建了一个新的数组,新数组的长度一定和集合的size一样)
    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.  // 将空数组赋值给elementData
        this.elementData = EMPTY_ELEMENTDATA;
    

// 知识点:c.toArray()调用的其实也是Arrays.copyOf方法,而这个方法的底层逻辑都是调用
// System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));
// original:源数组  0:源数组起始位置 copy:目标数组,0:目的数组的起始位置;Math.min(original.length, newLength):要复制的数组元素数量;
// 另外这种方式的拷贝属于浅拷贝;

add(E e)

add方法基本的源码调用阶段如下:

public boolean add(E e) 
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        // 扩容之后,将值放在size++的索引上
        elementData[size++] = e;
        return true;
    

    private void ensureCapacityInternal(int minCapacity) 
        // 判断elementData是否等于空数组
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) 
        // 如果相等,最小的capacity就等于传进来的参数与默认的(10)中最大的数;相当于如果你添加的是第一个数,那么这个时候要需要扩容
        // 扩容的时候相当于加10;
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        
        ensureExplicitCapacity(minCapacity);
    
    
    private void ensureExplicitCapacity(int minCapacity) 
        modCount++;  //实际修改数组的次数
        // 假设第一次调用这个方法,minCapacity会等于系统给的默认值10,你的elementData.length为0,必然走扩容;
        // 当你第二次调用的时候,minCapacity等于数组的实际数据长度(比如你添加了2次,那么这个数为2),而elementData.length等于第一次的10,所以不会走扩		// 容数组;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);  // 扩容数组
    
    
    private void grow(int minCapacity) 
        // 将初始化之后的elementData的长度赋值给oldCapacity
        int oldCapacity = elementData.length;
        // 新数组长度等于老数组长度加上老数组右移1位;这边扩容相当于是原容量的1.5倍;
        int newCapacity = oldCapacity + (oldCapacity >> 1); // >> 右移几位可以简单的认为是除以2的几次幂; << 左移可简单认为乘以2的几次幂;
        if (newCapacity - minCapacity < 0)//如果新数组的长度减去传进来的长度(10)小于0,就说明这次想要的扩容没意义;
            newCapacity = minCapacity;//新数组的长度就等于传进来的(10);
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);//如果新数组的长度-最大的值还大于0,就给它一个Integer里面最大的值;
        // 然后进行拷贝
        elementData = Arrays.copyOf(elementData, newCapacity);
    

add(int index, E element)

public void add(int index, E element) 
        if (index > size || index < 0) // 判断插入数据的时候是否越界
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
            // 跟之前走的方法一样;
        ensureCapacityInternal(size + 1);  
        // original:源数组  0:源数组起始位置 copy:目标数组,0:目的数组的起始位置;Math.min(original.length, newLength):要复制的数组元素数量;
        // 从elementData进行拷贝;从传入的索引开始(比如1);拷贝到原来的数组;拷贝到位置加1的地方;拷贝的长度为总元素的长度减去传进来的索引位置;
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        // 将添加进来的元素放到指定的索引上
        elementData[index] = element;
        size++;
    

addAll(Collection<? extends E> c)

public boolean addAll(Collection<? extends E> c) 
//将传进来的集合转成数组
    Object[] a = c.toArray();
    // 得到数组的长度
    int numNew = a.length;
    // 走一下是否扩容的代码
    ensureCapacityInternal(size + numNew);  // Increments modCount
    //拷贝
    System.arraycopy(a, 0, elementData, size, numNew);
    size += numNew;
    // 返回是否拷贝成功,如果你传进来的数组长度为0,这里返回的是false;
    return numNew != 0;

addAll(int index, Collection<? extends E> c)

public boolean addAll(int index, Collection<? extends E> c) 
// 校验索引
    if (index > size || index < 0)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
	
    Object[] a = c.toArray();
    int numNew = a.length;
    ensureCapacityInternal(size + numNew);  // Increments modCount

    // 要移动元素的个数  (目标源的长度减去传进来的索引)
    int numMoved = size - index;
    if (numMoved > 0)
    	// 使用arrayCopy进行移动
        System.arraycopy(elementData, index, elementData, index + numNew,
                         numMoved);
	
    // 真正将数据源(刚传进来的数组)添加到目的
    System.arraycopy(a, 0, elementData, index, numNew);
    size += numNew;
    return numNew != 0;

E set(int index, E element)

public E set(int index, E element) 
    if (index >= size) //校验
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
	// 取出
    E oldValue = (E) elementData[index];
    // 替换
    elementData[index] = element;
    return oldValue;

E get(int index)

public E get(int index) 
    if (index >= size)
        throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
	// 直接获取
    return (E) elementData[index];

Iterator iterator()

public Iterator<E> iterator() 
	// 创建一个对象
    return new Itr();

private class Itr implements Iterator<E> 
        
        protected int limit = ArrayList.this.size;

        int cursor;       // 游标 指向下一个应该被返回的索引 默认值为0
        int lastRet = -1; // 前一个记录 默认值-1
        // 将实际修改次数赋值给期望修改次数
        int expectedModCount = modCount;

        public boolean hasNext() 
        // 如果当前游标小于limit(list的长度)则说明还有;
            return cursor < limit;
        
		public E next() 
            if (modCount != expectedModCount)//如果实际修改的次数不等于预期修改的次数;错误叫:并发修改异常
                throw new ConcurrentModificationException();
            // 将游标赋值给i
            int i = cursor;
            if (i >= limit) //大于则抛出异常
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;//获取arraylist中的elementData数组
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;//游标+1
            return (E) elementData[lastRet = i];//从数组中取出元素并返回
        
    

list.remove(Object o)

public boolean remove(Object o) 
    if (o == null) // 判断是否为null,为null就删除
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) 
                fastRemove(index);
                return true;
            
     else 
        // 处理不为null的情况
        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);
        // 将删除的元素置为null,让GC回收 --size 等于size-1
        elementData[--size] = null; // clear to let GC do its work

iterator.remove()

public void remove() 
    if (lastRet < 0)
        throw new IllegalStateException();
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    try 
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        // 迭代器的删除方法主要是
        expectedModCount = modCount;
        limit--;
     catch (IndexOutOfBoundsException ex) 
        throw new ConcurrentModificationException();
    

clear()

public void clear() 
    modCount++;
		
    // clear to let GC do its work
    for (int i = 0; i < size; i++)
        elementData[i] = null;
	// 全部置为nul,让GC好回收,size直接赋值为0
    size = 0;

contains(Object o)

public int indexOf(Object o) 
    if (o == null) 
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
     else 
    // 也是循环对比数据
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    
    return -1;

isEmpty()

public boolean isEmpty() 
    return size == 0;

问题:

ArrayList频繁扩容导致性能低下如何优化?

答:原因:主要是因为每次都是1.5倍的扩容,第一次默认是10;所以如果数据够大,扩容的次数就会增加;可以通过创建ArrayList的时候直接指定数组长度;

优化删除方式,数组在删除数据的时候会进行数据的迁移,如果频繁删除,可以前期先使用某种方式记录下来,然后集中在一起进行删除;

System.arraycopy是属于深拷贝还是浅拷贝?

答:浅拷贝

如果拷贝的是一个二维数组,拷贝之后修改新的数组是否会影响之前的数据?(数组中的数据都是基本类型)

答:会影响,因为数组是引用类型,拷贝底层是浅拷贝,所以只会复制引用;

ArrayList是否线程安全?

答:线程不安全,如果多个线程要进行访问,可使用同步关键字;也可以使用Vector来替代List,它内部实现了线程安全;但Vector的速度相对来说比较慢;

以上是关于数据结构之ArrayList的主要内容,如果未能解决你的问题,请参考以下文章

Java之ArrayList类(集合)

数据结构线性表之顺序表ArrayList

JAVA容器之ArrayList集合详解

集合类 collection接口 ArrayList

Java基础系列--集合之ArrayList

对java中arraylist深入理解