Java集合:List总结

1. 概述

在Java的集合类中,定义了各种数据结构,按接口的实现,可分为两类:一类是实现Collection接口;另一类是实现Map接口,而List就是Collection的一个重要的子接口,代表有序的列表。
在这里插入图片描述

  • List特点:是有序、可重复的集合,该集合是有索引的。
  • List实现类:ArrayList、LinkedList、Vector、Stack等
    • ArrayList是基于数组实现的,是一个数组队列。
    • LinkedList是基于链表实现的,是一个双向循环列表。
    • Vector是基于数组实现的,是一个矢量队列,是线程安全的。
    • Stack是基于数组实现的,是栈,它继承与Vector,特性是FILO(先进后出)

2.ArrayList

ArrayList是一个数组结构,允许pull空值,实现了自动扩容,是非线程安全的。在源码中通过elementData[]对象数组存储数据,通过一定的扩容规则实现动态存储;

    /**
     * Default initial capacity.
     * 默认初始化大小
     */
    private static final int DEFAULT_CAPACITY = 10;
    
    /*
    * Shared empty array instance used for default sized empty instances
    */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    /**
     * The array buffer into which the elements of the ArrayList are stored.
     * The capacity of the ArrayList is the length of this array buffer. Any
     * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
     * will be expanded to DEFAULT_CAPACITY when the first element is added.
     * 存入Arraylist的元素数组缓存,Arraylist的存储大小与elementData的数组长度
     * 相同,在加入第一个元素时,任何空数组的缓存 elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA时,将被扩容至DEFAULT_CAPACITY大小
     */	
     transient Object[] elementData
     // 当前数组大小,类型 int,没有使用 volatile 修饰,非线程安全的;
      private int size;

2. 1. 新增和扩容

新增add(): 就是往数组中添加元素,分成两步:
 1.判断是否需要扩容,如果需要,则执行扩容操作;
 2.直接赋值。

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

扩容的判断:
1.判断Arraylist是否为为加入第一个值;
2.判断期望容量是否小于现有缓存容量;
3.通过grow(minCapacity),实现扩容;

      private static int calculateCapacity(Object[] elementData, int minCapacity) {
			// 1.判断elementData是否为空,返回elementData和
			// DEFAULTCAPACITY_EMPTY_ELEMENTDATA的最大值
	        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
	            return Math.max(DEFAULT_CAPACITY, minCapacity);
	        }
	        // 非空
	        return minCapacity;
		}

   // 判断是否执行扩容,若期望的最小大小minCapacity > elementData.length 执行grow扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

扩容操作:

  1. 设定新扩容大小为原容量大小的1.5倍。newCapacity = oldCapacity + (oldCapacity >> 1);
  2. 若新扩容大小 < 期望容量大小,则新扩容大小 = 期望容量大小;
  3. 判断 期望容量大小 的上下上下限,下限为0,上限为 Integer.MAX_VALUE;
  4. 通过Arrays.copyOf(),复制原数组扩充大小;
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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);
    }

总结:

  • 新增对值没有校验,可以为空;
  • 扩容大小是原容量大小的1.5倍;
  • ArrayList的最大上限是Integer.MAX_VALUE,超过这个值,JVM不再分配空间;
  • 扩容的本质是通过对象数组的拷贝新建一个期望容量的数组;

2.2. 删除和清空

  1. 删除判断:
  • 允许删除null值;
  • 非null值,通过equals方法对比判断;
  1. 删除操作:与扩容类似,同样通过arraycopy()复制数组进行操作,将删除后的元素前移;
    public boolean remove(Object o) {
        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 remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        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
    }

清空操作:遍历置null

  /**
     * Removes all of the elements from this list.  The list will
     * be empty after this call returns.
     */
    public void clear() {
        modCount++;
        // clear to let GC do its work
        for (int i = 0; i < size; i++)
            elementData[i] = null;
        size = 0;
    }

2. 3. 线程安全

只有当 ArrayList 作为共享变量时,才会有线程安全问题,当 ArrayList 是方法内的局部变量时,是没有线程安全的问题的。

ArrayList 有线程安全问题的本质,是因为 ArrayList 自身的 elementData、size、modConut 在进行各种操作时,都没有加锁,而且这些变量的类型并非是可见(volatile)的,所以如果多个线程对这些变量进行操作时,可能会有值被覆盖的情况。

类注释中推荐我们使用 Collections#synchronizedList 来保证线程安全,SynchronizedList是通过在每个方法上面加上锁来实现,虽然实现了线程安全,但是性能大大降低。

3. LinkedList

LinkedList是一个链表结构,是非线程安全的,有链表结构的特性,在队列中频繁使用。通过链表的节点Node实现动态存储,及前后链接;
Node节点:

    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;

        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }

3. 1. 增加和删除

LinkedList维护了两个头尾指针first和last:实现了链表头部的插入删除、尾部的插入删除;其方式就是链表节点的操作。

    /**
     * Pointer to first node.
     * Invariant: (first == null && last == null) ||
     *            (first.prev == null && first.item != null)
     */
    transient Node<E> first;
        /**
     * Pointer to last node.
     * Invariant: (first == null && last == null) ||
     *            (last.next == null && last.item != null)
     */
    transient Node<E> last;

3. 2. 查询

链表查询节点较慢,需要挨个查询节点,在linkedList中并没有使用从头到尾的遍历,而使用了二分查找的思想,先判断index是位于链表的前半段还是后半段,在决定是从前向后还是从后向前;

    Node<E> node(int index) {
        // assert isElementIndex(index);

        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

3.3. LinkedList类方法总结

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

LinkedList作为List使用时,一般采用add / get方法来压入/获取对象,
作为Queue使用时,才会采用 offer/poll/take等方法。

4. Vector

Vector 类可以实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件。但是,Vector 的大小可以根据需要增大或缩小,以适应创建 Vector 后进行添加或移除项的操作。Vector 是同步的,可用于多线程。

public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • Vector 继承了AbstractList,实现了List;所以,它是一个队列,支持相关的添加、删除、修改、遍历等功能。

  • Vector实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在Vector中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。

由于在Vector中的方法和关键字都被synchronized 修饰的,可以实现线程的同步是线程安全的;
其基本操作和存储结构与ArrayList类似,底层都是通过数组存储元素,通过扩容机制和数组复制的方式实现元素的增加和删除;
Vector.add()源码:

public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }

Vector.remove()源码:

    public synchronized E remove(int index) {
        modCount++;
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
        E oldValue = elementData(index);

        int numMoved = elementCount - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--elementCount] = null; // Let gc do its work

        return oldValue;
    }

5. List总结

类名实现机制线程安全应用场景
ArrayList数组增删慢,查询快
LinkedList双向链表增删快,查询慢 。队列,链表的场景
Vector数组增删慢,查询快,多线程场景
Stack数组继承于Vector,是符合栈特性的场景:FILO(先进后出)

1). Array 和 ArrayList 有什么区别?什么时候该应 Array 而不是 ArrayList 呢?

答:它们的区别是:

Array 可以包含基本类型和对象类型,ArrayList 只能包含对象类型。
Array 大小是固定的,ArrayList 的大小是动态变化的。
ArrayList 提供了更多的方法和特性,比如:addAll(),removeAll(),iterator() 等等。

2). ArrayList、LinkedList能否通过for遍历删除元素?
不能,①.使用普通for循环时,由于每次删除会导致数组元素拷贝前移,会导致删除不干净;②.使用增强for-each循环时,会报 ConcurrentModificationException 的错误。是因为增强 for 循环过程其实调用的就是迭代器的 next () 方法,当你调用 list.remove () 方法进行删除时,modCount 的值会 +1,而这时候迭代器中的 expectedModCount 的值却没有变,导致在迭代器下次执行 next () 方法时,expectedModCount != modCount,导致错误;
应该使用迭代器进行删除,在Iterator.remove () 方法在执行的过程中,会把最新的 modCount 赋值给 expectedModCount,这样在下次循环过程中,modCount 和 expectedModCount 两者就会相等。能够正常删除元素。

3). ArrayList 和 Vector 的区别?
答:

这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合,即存储在这两个集合中的元素位置都是有顺序的,相当于一种动态的数组,我们以后可以按位置索引来取出某个元素,并且其中的数据是允许重复的,这是与 HashSet 之类的集合的最大不同处,HashSet 之类的集合不可以按索引号去检索其中的元素,也不允许有重复的元素。

ArrayList 与 Vector 的区别主要包括两个方面:

同步性:
Vector 是线程安全的,也就是说它的方法之间是线程同步(加了synchronized 关键字)的,而 ArrayList 是线程不安全的,它的方法之间是线程不同步的。如果只有一个线程会访问到集合,那最好是使用 ArrayList,因为它不考虑线程安全的问题,所以效率会高一些;如果有多个线程会访问到集合,那最好是使用 Vector,因为不需要我们自己再去考虑和编写线程安全的代码。

数据增长:
ArrayList 与 Vector 都有一个初始的容量大小,当存储进它们里面的元素的个人超过了容量时,就需要增加 ArrayList 和 Vector 的存储空间,每次要增加存储空间时,不是只增加一个存储单元,而是增加多个存储单元,每次增加的存储单元的个数在内存空间利用与程序效率之间要去的一定的平衡。Vector 在数据满时(加载因子1)增长为原来的两倍(扩容增量:原容量的 2 倍),而 ArrayList 在数据量达到容量的一半时(加载因子 0.5)增长为原容量的 (0.5 倍 + 1) 个空间。

引用
https://www.imooc.com/read/47#catalog
https://www.jianshu.com/p/939b8a672070
https://blog.csdn.net/zy1994hyq/article/details/82984523
https://dzone.com/articles/an-introduction-to-the-java-collections-framework

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值