• Java集合:ArrayList


    前言

    这个分类中,将会写写Java中的集合。集合是Java中非常重要而且基础的内容,因为任何数据必不可少的就是该数据是如何存储的,集合的作用就是以一定的方式组织、存储数据。这里写的集合,一部分是比较常见的、一部分是不常用但是我个人平时见到过的,一些比较相似的集合(比如HashMap和Hashtable)就只讲一个,突出它们之间的区别即可。

    最后,要指出一点,对于集合,我认为关注的点主要有四点:

    1、是否允许空

    2、是否允许重复数据

    3、是否有序,有序的意思是读取数据的顺序和存放数据的顺序是否一致

    4、是否线程安全

    ArrayList

    ArrayList是最常见以及每个Java开发者最熟悉的集合类了,顾名思义,ArrayList就是一个以数组形式实现的集合,以一张表格来看一下ArrayList里面有哪些基本的元素:

    元素 作用
    private transient Object[] elementData; ArrayList是基于数组的一个实现,elementData就是底层的数组
    private int size; ArrayList里面元素的个数,这里要注意一下,size是按照调用add、remove方法的次数进行自增或者自减的,所以add了一个null进入ArrayList,size也会加1 

      

    四个关注点在ArrayList上的答案

    关注点 结论
    是否允许空
    是否允许重复数据
    是否有序
    是否线程安全

    添加元素

    1 public boolean add(E e) {
    2    ensureCapacityInternal(size + 1);  // Increments modCount!!
    3    elementData[size++] = e;           // 往数组中添加元素
    4    return true;
    5 }

     判断是否需要扩容

     扩容的方法,若初始化时没有指定capacity,那么在首次插入数据的时候,就需要进行扩容。

    // minCapacity指的是:插入一个元素之后,整个elementData的长度
    private
    void ensureCapacityInternal(int minCapacity) { ensureExplicitCapacity(calculateCapacity(elementData, minCapacity)); }

    calculateCapacity()方法计算当前数组的在添加数据后的长度

    若在构造ArrayList时未指定capacity,那么就会创建一个空的数组,待第一次插入数据的时候再进行扩容。

    /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    在首次插入数据的时候,minCapacity=1(需要插入一个元素),小于DAFAULT_CAPACITY,那么就返回DAFAULT_CAPACITY

    在 ensureExplicitCapacity() 方法中,是需要扩容的。

    1 private static int calculateCapacity(Object[] elementData, int minCapacity) {
    2     // 如果当前的数组为空,返回默认的Capacity和minCapacity中的最小值
    3     // DEFAULT_CAPACITY为10
    4     if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
    5         return Math.max(DEFAULT_CAPACITY, minCapacity);
    6     }
    7     // 如果不为空,返回minCapacity
    8     return minCapacity;
    9 }

     ensureExplicitCapacity()方法用于判断是否需要扩容

     1 private void ensureExplicitCapacity(int minCapacity) {
     2 
     3     // 修改的次数+1
     4     modCount++;
     5 
     6     // 如果minCapacity大于当前数组的长度
     7     // 需要扩容
     8     if (minCapacity - elementData.length > 0)
     9         grow(minCapacity);
    10 }

    扩容

    /**
        扩容方法
        @param minCapacity 指插入数据需要的最小数组长度
    */
    private void grow(int minCapacity) {
        
        // 获取当前数组的大小
        int oldCapacity = elementData.length;
        // 新数组大小 = 旧数组大小的1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 如果newCapacity仍然不满足,直接minCapacity赋给它
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
            
        // 如果newCapacity特别大
        // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
        // 返回Integer.MAX_VALUE或者MAX_ARRAY_SIZE;
        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);
    }

       Arrays.copyOf()方法,进行复制

    /**
     * @param original 原始数组
     * @param newLength 新数组的长度
     * @param newType 新数组的数据类型
     * @return 返回旧数组的拷贝
     
     */
    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        // 新建一个数组
        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;
    }

    删除元素

    在ArrayList的移除方法中有两个重载方法,一是remove(Object o),二是remove(int index)

    (1) remove(Object o) 方法

    public boolean remove(Object o) {
        // 删除一个null元素
        if (o == null) {
            for (int index = 0; index < size; index++)
                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;
    }
    
    private void fastRemove(int index) {
        modCount++;
        // 需要移动的元素个数
        int numMoved = size - index - 1;
        if (numMoved > 0)
            // 把index元素后面的元素往前都往前挪一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        // 把元素组的最后一个位删除,让gc回收
        elementData[--size] = null; // clear to let GC do its work
    }

    (2)remove(int index) 方法

    public E remove(int index) {
    
        // 检查下标范围
        rangeCheck(index);
    
        modCount++;
        
        // 获取该元素
        E oldValue = elementData(index);
    
        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
    
        // 返回已删除的元素
        return oldValue;
    }

    ArrayList的优缺点

    从上面的几个过程总结一下ArrayList的优缺点。ArrayList的优点如下:

    1、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快

    2、ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已

    不过ArrayList的缺点也十分明显:

    1、删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

    2、插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能

    因此,ArrayList比较适合顺序添加、随机访问的场景

    ArrayList和Vector的区别

    ArrayList是线程非安全的,这很明显,因为ArrayList中所有的方法都不是同步的,在并发下一定会出现线程安全问题。那么我们想要使用ArrayList并且让它线程安全怎么办?一个方法是用Collections.synchronizedList方法把你的ArrayList变成一个线程安全的List

    另一个方法就是Vector,它是ArrayList的线程安全版本,其实现90%和ArrayList都完全一样,区别在于:

    1、Vector是线程安全的,ArrayList是线程非安全的

    2、Vector可以指定增长因子,如果该增长因子指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小*2,源代码是这样的:

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

    最后一个问题,我们看一下ArrayList中的数组,是这么定义的:

    transient Object[] elementData;

    为什么elementData是使用transient修饰的呢?关于这个问题,说说我的看法。我们看一下ArrayList的定义:

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

    看到ArrayList实现了Serializable接口,这意味着ArrayList是可以被序列化的,用transient修饰elementData意味着我不希望elementData数组被序列化。这是为什么?因为序列化ArrayList的时候,ArrayList里面的elementData未必是满的,比方说elementData有10的大小,但是我只用了其中的3个,那么是否有必要序列化整个elementData呢?显然没有这个必要,因此ArrayList中重写了writeObject方法:

    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        
        // 先序列化ArrayList中非transient元素
        s.defaultWriteObject();
    
        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);
    
        // 把当前数组中存在元素的位置的元素序列化
        // 其他未插入数据的位置就不需要序列化了
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
    
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

    每次序列化的时候调用这个方法,先调用defaultWriteObject()方法序列化ArrayList中的非transient元素,elementData不去序列化它,然后遍历elementData,只序列化那些有的元素,这样:

    1、加快了序列化的速度

    2、减小了序列化之后的文件大小

    不失为一种聪明的做法,如果以后开发过程中有遇到这种情况,也是值得学习、借鉴的一种思路。

  • 相关阅读:
    Apache部署Django项目
    Docker
    常用算法
    Go之基本数据类型
    Go之流程控制
    Go基本使用
    Go安装与Goland破解永久版
    Linux
    详解java中的byte类型
    Linux统计文本中某个字符串出现的次数
  • 原文地址:https://www.cnblogs.com/yn-huang/p/10682817.html
Copyright © 2020-2023  润新知