2.Java容器-List详解

       本文主要介绍List的三个子类:ArrayList,LinkedList和Vector;

ArrayList详解

概述

       ArrayList的底层实现就是一个数组,但有扩容的功能,能够实现动态地增长;它是顺序容器,即元素存放的顺序和放入的顺序相同,并且允许放入null元素;

       需要注意的是,泛型只是编译器提供的语法糖,所以这里的数组实际上是一个Object数组,以便能够容纳任何类型的对象;

      每个ArrayList都有一个capacity,表示底层数组的容量;还有个size表示数组中实际存放的元素的数量;size小于等于capacity;

       当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小;其中size(), isEmpty(), get(), set()方法均能在常数时间内完成,add()方法的时间开销跟插入位置有关,addAll()方法的时间开销跟添加元素的个数成正比;其余方法大都是线性时间;

ArrayList的属性

    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * 默认初始容量为10
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 用户指定ArrayList容量为0时,返回该空数组
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 该数组是默认返回的,默认大小为DEFAULT_CAPACITY = 10
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * 实际保存数据的地方
     */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList的实际大小
     */
    private int size;

transient和elementData

       当一个对象被序列化的时候,transient修饰的变量的值是不包括在序列化的结果中的;但是ArrayList又是可序行化的类,elementData是ArrayList具体存放元素的成员,用transient来修饰elementData,岂不是反序列化后的ArrayList丢失了原先的元素?

     ArrayList在序列化的时候会调用writeObject,直接将size和element写入ObjectOutputStream;反序列化时调用readObject,从ObjectInputStream获取size和element,再恢复到elementData;

       为什么不直接用elementData来序列化,而采用上诉的方式来实现序列化呢?原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上述的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间;

    /**
     * Save the state of the ArrayList instance to a stream (that
     * is, serialize it).
     *
     * @serialData The length of the array backing the ArrayList
     *             instance is emitted (int), followed by all of its elements
     *             (each an Object) in the proper order.
     */
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // Write out all elements in the proper order.
        //注意这个地方size的大小和elementData的长度关系,它们之间是size<=elementData.length,什么时候相等呢?
        //那就是刚好size+1=容量大小从而不需要扩容,然后执行elementData[size++] = e;
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

    /**
     * Reconstitute the ArrayList instance from a stream (that is,
     * deserialize it).
     */
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        elementData = EMPTY_ELEMENTDATA;

        // Read in size, and any hidden stuff
        s.defaultReadObject();

        // Read in capacity
        s.readInt(); // ignored

        if (size > 0) {
            // be like clone(), allocate array based upon size not capacity
            //刚好分配序列化之前容量中变化值长度大小,通俗讲就是实际容量中数据的个数
            int capacity = calculateCapacity(elementData, size);
            SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
            ensureCapacityInternal(size);

            Object[] a = elementData;
            // Read in all elements in the proper order.
            for (int i=0; i<size; i++) {
                a[i] = s.readObject();
            }
        }
    }

ArrayList的构造方法

    /**
     * 如果初始化的时候,指定了容量,那就创建指定大小的数组
     */
    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);
        }
    }

    /**
     * 如果没有指定初始化的容量,那就创建默认大小为10的数组
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    /**
     * 将一个集合作为ArrayList构造函数的初始化参数
     */
    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;
        }
    }

add方法

add(E e)

    /**
     * 直接将元素添加到List的末尾
     */
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 检测是否需要扩容:确认list容量,尝试容量加1,看看有无必要
        elementData[size++] = e;     // 插入新的元素
        return true;
    }

       1.ensureCapacityInternal()方法是如何工作的?

    /**
     * 检查是否需要扩容
     */
    private void ensureCapacityInternal(int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            // 计算扩容后,最小需要的容量大小
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }

        ensureExplicitCapacity(minCapacity);  //确定最终的容量大小
    }

        2.ensureExplicitCapacity()方法是如何确定最终大小的?

    /**
     * 确定最终的容量大小
     */
    private void ensureExplicitCapacity(int minCapacity) {
        //在使用迭代器遍历的时候,用来检查列表中的元素是否发生结构性变化(列表元素数量发生改变)了;
        //主要在多线程环境下需要使用,防止一个线程正在迭代遍历,另一个线程修改了这个列表的结构;
        //好好认识下这个异常:ConcurrentModificationException
        modCount++;

        // 如果要的最小容量比数组的长度要大,那么就调用grow()方法来扩容
        // 举例,当添加第11个元素时,minCapacity为11,数组长度为10,那么就需要扩容了
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

        3.grow()方法是如何实现扩容的?

    /**
     * 完成最终的扩容
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);   //扩容1.5倍

        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;       //扩容1.5倍后,任然不够

        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);

        // 使用Arrays.copy()方法完成最终的扩容
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0)                     // 溢出
            throw new OutOfMemoryError();
        // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

总结:

       1.检查数组的容量是否足够

       2.如果足够,直接添加新的元素;

       3.如果不足够,就扩容到原来的1.5倍;如果扩容1.5倍后,还是小于minCapacity,就将容量扩容至minCapacity;


add(int index, E element)

    /**
     * 将元素插入到特定的位置
     */
    public void add(int index, E element) {
        rangeCheckForAdd(index);    //检查index脚标是否越界

        ensureCapacityInternal(size + 1);       //扩容,和之前的流程一样

        //在index的位置,插入新的元素
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;

        size++;   //容量加1
    }
    //size是一个成员变量初始值为0,也可以这样理解size的大小就是ArrayList中元素的实际个数,
    //在指定位置添加数据是不能超过size大小的,可以结合上图示进行理解。
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
    //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);


关于arraycopy()方法,可以参考这个


modCount

       该字段表示list结构上被修改的次数;结构上的修改指的是,改变了list的长度或者使得遍历过程中产生不正确的结果的其它方式;

    该字段被Iterator以及list Iterator的实现类所使用,如果该值被意外更改,Iterator或者list Iterator 将抛出ConcurrentModificationException异常;这是在面对迭代遍历的时候为了避免不确定性而采取的快速失败原则;

      子类对此字段的使用是可选的,如果子类希望支持快速失败,只需要覆盖该字段相关的所有方法即可。单线程调用不能添加或删除Iterator正在遍历的对象,否则将可能抛出ConcurrentModificationException异常,如果子类不希望支持快速失败,该字段可以直接忽略;

    public static void main(String[] args) {
        List<String> list = new ArrayList();
        list.add("d");
        list.add("q");
        list.add("q");
        list.add("z");
        list.add("j");
        Iterator it = list.iterator();
        int i = 0;
        while(it.hasNext()){
            if(i==4){
                //list.remove(it.next());
                it.remove();// 如果用list.remove(it.next());会报异常
            }
            System.out.println("第"+i+"个元素"+it.next());
            i++ ;
        }
        System.out.println("----------------");
        Iterator it2 = list.iterator();
        while(it2.hasNext()){
            System.out.println(it2.next());
        }

    }
       如果用list.remove(it.next());会报ConcurrentModificationException异常,原因参上。

      另:注意it.remove()删除的是最近的一次it.next()获取的元素,而不是当前iterator中游标指向的元素;因此,本例中i==4时,删除的其实是z,而不是j,这很容易被忽视或者误解。如果想删掉j正确操作是先调用it.next()获取到具体元素,再判断;而且由于调用了it.next(),此时游标已经指向我们期望删除的值了。想直接数数字进行删除,在这里会容易出错误。后续章节会深入讲解iterator迭代器源码;

这部分来自dqqzj

get方法

    /**
     * Returns the element at the specified position in this list.
     *
     * @param  index index of the element to return
     * @return the element at the specified position in this list
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        rangeCheck(index);  //检查index是否越界

        return elementData(index);  //返回具体元素
    }

set方法

    public E set(int index, E element) {
        rangeCheck(index);    //检查index是否越界

        E oldValue = elementData(index);  //保存旧值
        elementData[index] = element;     //替代旧值
        return oldValue;                  //返回旧值
    }   

remove方法

    public E remove(int index) {
        rangeCheck(index);     //检查index是否越界

        modCount++;            //容量改变值增加,计数加1
        E oldValue = elementData(index);   //保存旧值

        int numMoved = size - index - 1;   //需要移动的元素个数
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);    //移动元素,达到删除index的目的
        elementData[--size] = null;        //设置为null,让GC回收

        return oldValue;
    }

trimToSize()

      trimToSize() 的作用只是去掉预留元素位置,比如初始化值10,然后通过增加方法,多增加了1个元素,导致扩容为就是size=size+elementData>>1最后是15,但是实际占有11个元素,trimToSize()就是删除多余的没有使用的4个元素空间,改为只申请11,内存紧张的时候会用到;
    /**
     * Trims the capacity of this ArrayList instance to be the
     * list's current size.  An application can use this operation to minimize
     * the storage of an ArrayList instance.
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

contains(Object o),indexOf(Object o)和lastIndexOf(Object o)

    /**
     * Returns true if this list contains the specified element.
     * More formally, returns true if and only if this list contains
     * at least one element e such that
     * (o==null ? e==null : o.equals(e)).
     *
     * @param o element whose presence in this list is to be tested
     * @return true if this list contains the specified element
     */
    public boolean contains(Object o) {
        return indexOf(o) >= 0;
    }

    /**
     * Returns the index of the first occurrence of the specified element
     * in this list, or -1 if this list does not contain the element.
     * More formally, returns the lowest index i such that
     * (o==null ? get(i)==null : o.equals(get(i))),
     * or -1 if there is no such index.
     */
    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;
    }

    /**
     * Returns the index of the last occurrence of the specified element
     * in this list, or -1 if this list does not contain the element.
     * More formally, returns the highest index i such that
     * (o==null ? get(i)==null : o.equals(get(i))),
     * or -1 if there is no such index.
     */
    public int lastIndexOf(Object o) {
        if (o == null) {
            for (int i = size-1; i >= 0; i--)
                if (elementData[i]==null)
                    return i;
        } else {
            for (int i = size-1; i >= 0; i--)
                if (o.equals(elementData[i]))
                    return i;
        }
        return -1;
    }

clone()

    /**
     * Returns a shallow copy of this ArrayList instance.  (The
     * elements themselves are not copied.)
     *
     * @return a clone of this ArrayList instance
     */
    public Object clone() {
        try {
            ArrayList v = (ArrayList) super.clone();
            v.elementData = Arrays.copyOf(elementData, size);
            v.modCount = 0;
            return v;
        } catch (CloneNotSupportedException e) {
            // this shouldn't happen, since we are Cloneable
            throw new InternalError(e);
        }
    }
       返回一个Object对象,所以在使用此方法的时候要强制转换;ArrayList的本质是维护了一个Object的数组,所以克隆也是通过数组的复制实现的,属于浅复制;
       编程人员经常误用各个集合类提供的拷贝构造函数作为克隆List,Set,ArrayList,HashSet或者其他集合实现的方法。需要记住的是,Java集合的拷贝构造函数只提供浅拷贝而不是深拷贝。这意味着存储在原始List和克隆List中的对象是相同的,指向Java堆内存中相同的位置。增加了这个误解的原因之一是对于不可变对象集合的浅克隆。由于不可变性,即使两个集合指向相同的对象是可以的。字符串池包含的字符串就是这种情况,更改一个不会影响到另一个。

不可变类:所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值。如JDK内部自带的很多不可变类:Interger、Long和String等。
可变类:相对于不可变类,可变类创建实例后可以改变其成员变量值,开发中创建的大部分类都属于可变类。

一、浅度克隆
浅度克隆对于要克隆的对象,对于其基本数据类型的属性,复制一份给新产生的对象,对于非基本数据类型的属性,仅仅复制一份引用给新产生的对象,即新产生的对象和原始对象中的非基本数据类型的属性都指向的是同一个对象。

二、深度克隆
在浅度克隆的基础上,对于要克隆的对象中的非基本数据类型的属性对应的类,也实现克隆,这样对于非基本数据类型的属性,复制的不是一份引用,即新产生的对象和原始对象中的非基本数据类型的属性指向的不是同一个对象

ArrayList总结

      1.基于动态数组实现的,在增删时候,需要数组的拷贝复制;

      2.默认初始化容量是10,每次扩容时候增加原先容量的一半,也就是变为原来的1.5倍;

      3.删除元素时不会减少容量。若希望减少容量,可以调用trimToSize();

      4.不是线程安全的,能存放null值;

    /**
     * Trims the capacity of this <tt>ArrayList</tt> instance to be the
     * list's current size.  An application can use this operation to minimize
     * the storage of an <tt>ArrayList</tt> instance.
     */
    public void trimToSize() {
        modCount++;
        if (size < elementData.length) {
            elementData = (size == 0)
              ? EMPTY_ELEMENTDATA
              : Arrays.copyOf(elementData, size);
        }
    }

Vector与ArrayList的区别

       1.底层实现也是数组,但Vector是线程安全的;

       2.Vector每个方法都是通过加synchronized关键字来实现线程安全的,所以性能一般比较差;

       3.ArrayList在底层数组不够用时在原来的基础上扩展0.5倍,Vector是扩展1倍;

LinkedList详解

LinkedList的底层实现双向链表;

LinkedList实现了Deque接口,因此,我们可以操作LinkedList像操作队列和栈一样;

LinkedList的属性

    transient int size = 0;    //LinkedList的大小

    /**
     * 指向头结点
     */
    transient Node<E> first;

    /**
     * 指向最后的那个结点
     */
    transient Node<E> last;

构造方法

    /**
     * 创建一个空的LinkedList
     */
    public LinkedList() {
    }

    /**
     * 用集合c初始化LinkedList
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

add方法

    /**
     * 直接将e添加到链表尾部
     */
    public boolean add(E e) {
        linkLast(e);
        return true;
    }
    /**
     * 在特定位置插入新的结点
     */
    public void add(int index, E element) {
        checkPositionIndex(index);   //检测index是否越界

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

remove方法

    /**
     * 删除第一次出现的元素(与o相等)
     */
    public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);   //删除元素
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {   //看o是否在List里面
                    unlink(x);            //删除元素
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * 在链表中删除特定的结点
     */
    E unlink(Node<E> x) {
        // assert x != null;
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }

        x.item = null;
        size--;
        modCount++;
        return element;
    }

get方法

    /**
     * 获得链表中处于特定位置的元素
     */
    public E get(int index) {
        checkElementIndex(index);  //检查index是否越界
        return node(index).item;   //返回元素
    }
    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {   
            //index处于链表的前半部分,那就从头结点开始寻找
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            //index处于链表的后半部分,那就从尾结点开始寻找
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

set方法

    /**
     * 替换特定位置的结点
     */
    public E set(int index, E element) {
        checkElementIndex(index);    //检查index是否越界
        Node<E> x = node(index);     //获得特定位置的结点
        E oldVal = x.item;           //保存旧值
        x.item = element;            //替换成新值
        return oldVal;               //返回旧值
    }

        set方法和get方法其实差不多,根据下标来判断是从头遍历还是从尾遍历;

LinkedList的其他方法,可以参考下面的几篇博客

本文总结

ArrayList:

     1.底层实现是数组;

     2.默认初始容量为10,每次扩容的时候增加原来容量的一半,即变为原来的1.5倍;

     3.添加删除元素的时候,需要数组元素的整体搬动,这是借助native方法实现的;

     4.查询多于增删的时候,应该用ArrayList;

LinkedList:

     1.底层实现是双向链表;

     2.增删多余查找的时候,用LinkedList;

Vector:

     1.底层实现是数组;

     2.所有的方法是借助synchronized关键字实现,性能较差;

     3.初始容量大小为10,扩容时,变为原来的2倍;

ArrayList增删慢不是绝对的(在数量大的情况下,已测试):
     1.如果增加元素一直是使用add()(增加到末尾)的话,那是ArrayList要快
     2.一直删除末尾的元素也是ArrayList要快【不用复制移动位置】
     3.至于如果删除的是中间的位置的话,还是ArrayList要快!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值