Java集合之常用List

ArrayList

ArrayList : 基于数组实现的非线程安全的集合。查询元素快,插入,删除中间元素慢。

查询数据快,是因为数组可以通过下标直接找到元素。

写数据慢有两个原因:一是数组复制过程需要时间,二是扩容需要实例化新数组也需要时间。

ArrayList在执行查询操作时:
第一步:先判断下标是否越界。
第二步:然后在直接通过下标从数组中返回元素。

ArrayList在执行顺序添加操作时:
第一步:通过扩容机制判断原数组是否还有空间,若没有则重新实例化一个空间更大的新数组,把旧数组的数据拷贝到新数组中。
第二步:在新数组的最后一位元素添加值。

ArrayList在执行中间插入操作时:
第一步:先判断下标是否越界。
第二步:扩容。
第三步:若插入的下标为i,则通过复制数组的方式将i后面的所有元素,往后移一位。
第四步:新数据替换下标为i的旧元素。
删除也是一样:只是数组往前移了一位,最后一个元素设置为null,等待JVM垃圾回收。

扩容机制

在JDK1.7当中,当第一个元素添加时,ensureCapacityInternal()方法会计算ArrayList的扩容大小,默认为10;

其中grow()方法最为重要,如果需要扩容,那么扩容后的大小是原来的1.5倍,实际上最终调用了Arrays.copyOf()方法得以实现;

//添加元素e
public boolean add(E e) {
    ensureCapacityInternal(size + 1);
    //将对应角标下的元素赋值为e:
    elementData[size++] = e;
    return true;
}
//得到最小扩容量
private void ensureCapacityInternal(int minCapacity) {
    //如果此时ArrayList是空数组,则将最小扩容大小设置为10:
    if (elementData == EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    //判断是否需要扩容:
    ensureExplicitCapacity(minCapacity);
}
//判断是否需要扩容
private void ensureExplicitCapacity(int minCapacity) {
    //操作数+1
    modCount++;
    //判断最小扩容容量-数组大小是否大于0:
    if (minCapacity - elementData.length > 0)
        //扩容:
        grow(minCapacity);
}
//ArrayList动态扩容的核心方法:
private void grow(int minCapacity) {
    //获取现有数组大小:
    int oldCapacity = elementData.length;
    //位运算,得到新的数组容量大小,为原有的1.5倍:
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    //如果新扩容的大小依旧小于传入的容量值,那么将传入的值设为新容器大小:
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;

    //如果新容器大小,大于ArrayList最大长度:
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        //计算出最大容量值:
        newCapacity = hugeCapacity(minCapacity);
    //数组复制:
    elementData = Arrays.copyOf(elementData, newCapacity);
}
//计算ArrayList最大容量:
private static int hugeCapacity(int minCapacity) {
    if (minCapacity < 0)
        throw new OutOfMemoryError();
    //如果新的容量大于MAX_ARRAY_SIZE。将会调用hugeCapacity将int的最大值赋给newCapacity:
    return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
}

JDk1.6版本时,扩容之后容量为 1.5 倍+1

JDk1.6版本以后扩容1.5倍

若扩容的长度太大,会造成大量的闲置空间;若扩容的长度太小,会造成频发的扩容(数组复制),效率更低。

System.arraycopy() 和 Arrays.copyOf()方法

两者联系和区别

联系:

看两者源代码可以发现 copyOf() 内部实际调用了 System.arraycopy() 方法

区别:

arraycopy() 需要目标数组,将原数组拷贝到你自己定义的数组里或者原数组,而且可以选择拷贝的起点和长度以及放入新数组中的位置 copyOf() 是系统自动在内部新建一个数组,并返回该数组

ensureCapacity方法

在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数,因为每次扩容都是创建一个新的对象数组,然后将元素挪至新数组中。相当耗费空间和时间

    /**
     * 如有必要,增加此 ArrayList 实例的容量,以确保它至少可以容纳由minimum capacity参数指定的元素数。
     * @param   minCapacity   所需的最小容量
     */
    public void ensureCapacity(int minCapacity) {
        int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)? 
            //如果不是默认空数组,则将下限设为0
            0
            //如果ArrayList是通过无参构造且尚未添加元素的实例,则
            : DEFAULT_CAPACITY;
        //也就是说该方法只有一个特例,就是当该实例是通过无参构造且尚未添加元素时,此时扩容应该至少扩张为
        //DEFAULT_CAPACITY,否则不予处理,在第一次add时扩容为DEFAULT_CAPACITY(回顾之前的解释)。
        //其他情况下,只要大于0就开始扩容程序。
        if (minCapacity > minExpand) {
            ensureExplicitCapacity(minCapacity);
        }
    }

LinkedList

基于双向链表实现的非线程安全的集合。查询元素慢,插入,删除中间元素快。

JDK1.7之前的版本是环形链表,而到了JDK1.7以后进行了优化,变成了直线型链表结构

不能像数组一样随机访问,必须是每个元素依次遍历直到找到元素为止。其结构的特殊性导致它查询数据慢。

LinkedList在执行查询操作时:
第一步:先判断元素是靠近头部,还是靠近尾部。
第二步:若靠近头部,则从头部开始依次查询判断。和ArrayList的elementData(index)相比当然是慢了很多。

LinkedList在插入元素的思路:
第一步:判断插入元素的位置是链表的尾部,还是中间。
第二步:若在链表尾部添加元素,直接将尾节点的下一个指针指向新增节点。
第三步:若在链表中间添加元素,先判断插入的位置是否为首节点,是则将首节点的上一个指针指向新增节点。否则先获取当前节点的上一个节点(简称A),并将A节点的下一个指针指向新增节点,然后新增节点的下一个指针指向当前节点。


Vector

Vector的底层是基于数组实现的线程安全的集合。线程同步(方法被synchronized修饰),性能比ArrayList差,默认大小也是10。

主要特点:查询快,增删慢  , 线程安全,但是效率低。

实现原理:

创建对象与ArrayList类似,但有一点不同,它可以设置扩容是容量增长大小。

根据Vector的三个构造器就可以很明了的理解 new Vector(); 与 new Vector(10);与 new Vector(10,0); 三个是等同的。

1.无参构造器
 public Vector() {
        this(10);
    }

2.传一个参数(容量大小) 容量大小即底层数组大小
public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }

3.传两个参数(容量大小,容量修正) 容量修正即扩容时的增加量
public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }

扩容机制:

jdk1.8的扩容算法:newCapacity = oldCapacity + ( ( capacityIncrement > 0 ) ? capacityIncrement : oldCapacity );

jdk1.6的扩容算法:newCapacity = ( capacityIncrement > 0 ) ? ( oldCapacity + capacityIncrement ) : (  oldCapacity  * 2 );

参数介绍:capacityIncrement 是容量修正(即容量新增大小),没有设置,默认为0    ,newCapacity 是扩容后的容量大小,oldCapacity 是扩容前的大小

一观察,就会发现1.6与1.8的写法变化不大,但是仔细一分析,就会发现jdk1.6中有使用乘法运算,即 oldCapacity  * 2。 在jdk1.8中换成了加法运算,这是因为乘法的效率是低于加法的,这应该算法的优化。

猜你喜欢

转载自www.cnblogs.com/ywblogs/p/11978164.html