List接口总结

List接口总结


List接口概述
List接口,成为有序的Collection也就是序列。该接口可以对列表中的每一个元素的插入位置精确的控制,同时用户可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。所以在查找操作过多时,应优先使用ArrayList。

1.ArrayList

底层基于数组实现容量大小动态变化。允许 null 的存在。同时还实现了 RandomAccess、Cloneable、Serializable 接口,所以ArrayList 是支持快速访问、复制、序列化的。
ArrayList 底层是基于数组来实现容量大小动态变化的。

//The size of the ArrayList (the number of elements it contains).
private int size;
transient Object[] elementData;

初始默认容量为10

private static final int DEFAULT_CAPACITY = 10;

构造函数
初始给数组elements赋值了一个空数组,在第一次添加元素时将数组容量扩大至10。

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

add操作
将指定的元素追加到此列表的末尾。

public boolean add(E e) {
        modCount++;
        add(e, elementData, size);
        return true;
    }
public void ensureCapacity(int minCapacity) {
        if (minCapacity > elementData.length
            && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
                 && minCapacity <= DEFAULT_CAPACITY)) {
            modCount++;
            grow(minCapacity);
        }
    }

每次添加元素前,都会确认集合容量大小,在ensureCapacity函数中判断如果DEFAULTCAPACITY_EMPTY_ELEMENTDATA=elementDate就取minCapacity和DEFAULT_CAPACITY的最大值也就是10。
grow操作
默认每次扩容至原函数的1.5倍。

 private Object[] grow(int minCapacity) {
        return elementData = Arrays.copyOf(elementData,
                                           newCapacity(minCapacity));
    }
 private Object[] grow(int minCapacity) {
        return elementData = Arrays.copyOf(elementData,
                                           newCapacity(minCapacity));
    }
private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity <= 0) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

remove操作
当我们调用remove函数时,会先检查index的合法性,然后判断索要删除元素是否属于最后一个元素,如果不是最后一个元素,则会调用fastRemove函数使用System.arraycopy方法拷贝数组,将index+1位置及后面所有元素位置向前移一位。

public E remove(int index) {
        Objects.checkIndex(index, size);
        final Object[] es = elementData;
        @SuppressWarnings("unchecked") E oldValue = (E) es[index];
        fastRemove(es, index);
        return oldValue;
        }

get操作

public E get(int index) {
        Objects.checkIndex(index, size);
        return elementData(index);
    }

因为ArrayList是基于数组实现的,所以可以直接使用下表来获取所在下标的元素。

2.LinkedList

LinkedList同时实现了List接口和Deque对口,也就是收它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(stack)。
LinkedList 继承自 AbstractSequentialList,从实现上,AbstractSequentialList 提供了一套基于顺序访问的接口。通过继承此类,子类仅需实现部分代码即可拥有完整的一套访问某种序列表(比如链表)的接口。

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

LinkedList底层通过双向链表实现,双向链表的每个节点用内部类Node表示。LinkedList通过first和last引用分别指向链表的第一个和最后一个元素。

transient int size = 0;
transient Node<E> first;
transient Node<E> last;
`

Node类中有两个指针next和prev,next指针指向每个结点的下一个结点,prev指针指向每个结点的前一个结点,方便获取前一个结点和后继节点,当删除和添加操作过多时,应优先使用LinkedList。

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;
        }
    }

linkFirst操作
头插操作,先新建一个结点,然后将该结点直接连接在原链表头结点前,再将此节点赋值为新的头结点。

private void linkFirst(E e) {
        final Node<E> f = first;
        final Node<E> newNode = new Node<>(null, e, f);
        first = newNode;
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        size++;
        modCount++;
    }

linklast操作
尾插操作,先新建一个结点,然后将该结点直接连接在原链表尾结点后,再将此节点赋值为新的尾结点。

void linkLast(E e) {
        final Node<E> l = last;
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        size++;
        modCount++;
    }

3.fast-fail

fail-fast 机制,即快速失败机制,是java集合(Collection)中的一种错误检测机制。当在迭代集合的过程中该集合在结构上发生改变的时候,就有可能会发生fail-fast,即抛出
ConcurrentModificationException异常。
从源码知道,每次调用next()方法,在实际访问元素前,都会调用checkForComodification方法,该方法源码如下:

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

可以看出,该方法才是判断是否抛出ConcurrentModificationException异常的关键。在该段代码中,当modCount != expectedModCount
时,就会抛出该异常。但是在一开始的时候,expectedModCount初始值默认等于modCount,为什么会出现modCount != expectedModCount,很明显expectedModCount在整个迭代过程除了一开始赋予初始值modCount外,并没有再发生改变,所以可能发生改变的就只有modCount,在前面关于ArrayList扩容机制的分析中,可以知道在ArrayList进行add,remove,clear等涉及到修改集合中的元素个数的操作时,modCount就会发生改变(modCount ++),所以当另一个线程(并发修改)或者同一个线程遍历过程中,调用相关方法使集合的个数发生改变,就会使modCount发生变化,这样在checkForComodification方法中就会抛出ConcurrentModificationException异常。
避免fail-fast
方法1
在单线程的遍历过程中,如果要进行remove操作,可以调用迭代器的remove方法而不是集合类的remove方法。看看ArrayList中迭代器的remove方法的源码:

        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();
 
            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

可以看到,该remove方法并不会修改modCount的值,并且不会对后面的遍历造成影响,因为该方法remove不能指定元素,只能remove当前遍历过的那个元素,所以调用该方法并不会发生fail-fast现象。该方法有局限性。
方法2
使用java并发包(java.util.concurrent)中的类来代替 ArrayList 和hashMap。
比如使用 CopyOnWriterArrayList代替 ArrayList, CopyOnWriterArrayList在是使用上跟 ArrayList几乎一样, CopyOnWriter是写时复制的容器(COW),在读写时是线程安全的。该容器在对add和remove等操作时,并不是在原数组上进行修改,而是将原数组拷贝一份,在新数组上进行修改,待完成后,才将指向旧数组的引用指向新数组,所以对于 CopyOnWriterArrayList在迭代过程并不会发生fail-fast现象。但 CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。
对于HashMap,可以使用ConcurrentHashMap, ConcurrentHashMap采用了锁机制,是线程安全的。在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据 ,iterator完成后再将头指针替换为新的数据 ,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变。即迭代不会发生fail-fast,但不保证获取的是最新的数据。

public static void main(String[] args) {      
     List<String> list = new ArrayList<>();      
          for (int i = 0 ; i < 10 ; i++ ) {               
          	 list.add(i + "");          
            }
     Iterator<String> iterator = list.iterator();  
     int i = 0 ;           
     while(iterator.hasNext()) {                
     if (i == 3) {                    
      	list.remove(3);               
       }               
      System.out.println(iterator.next());                i ++;           }    
    }

在这里插入图片描述
该段代码定义了一个Arraylist集合,并使用迭代器遍历,在遍历过程中,刻意在某一步迭代中remove一个元素,这个时候,就会发生fail-fast。

4.CopyOnWriteArrayList

一、CopyOnWriteArrayList它是ArrayList的线程安全的变体,大概原理就是:初始化的时候只有一个容器,很长一段时间,这个容器数据,数量等没有发生变化的时候,大家(大多数线程)都是读取(假设这段时间里只发生读取操作)同一个容器中的数据,这样大家读取到数据都是唯一,一致,安全的,但是后来有人往里面增加了一个数据,这个时候CopyOnWriteArrayList底层实现添加的原理是先copy出一个容器(简称副本),再往新的容器里添加这个新的数据,最后把新的容器的引用地址赋值给之前旧的容器地址,但是在添加这个数据期间,其他线程如果要读取数据,仍然是读取旧的容器里的数据。

①、CopyOnWriteArrayList,写数组的拷贝,支持高效率并发且是线程安全的,读操作无锁的ArrayList。所有可变操作都是通过对底层数组进行一次新的复制来实现。
②、CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。它不存在扩容的概念,每次写操作都要复制一个副本,在副本的基础上修改后改变Array引用。CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差。
③、CopyOnWriteArrayList 合适读多写少的场景,不过这类慎用 ,因为谁也没法保证CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次add/set都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

二、CopyOnWriteArrayList 有几个缺点:
1、由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致young gc或者full gc。
(1、young gc :年轻代(Young Generation):对象被创建时,内存的分配首先发生在年轻代(大对象可以直接被创建在年老代),大部分的对象在创建后很快就不再使用,因此很快变得不可达,于是被年轻代的GC机制清理掉(IBM的研究表明,98%的对象都是很快消亡的),这个GC机制被称为Minor GC或叫Young GC。
2、年老代(Old Generation):对象如果在年轻代存活了足够长的时间而没有被清理掉(即在几次Young GC后存活了下来),则会被复制到年老代,年老代的空间一般比年轻代大,能存放更多的对象,在年老代上发生的GC次数也比年轻代少。当年老代内存不足时,将执行Major GC,也叫 Full GC

2、不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求;

优点:解决开发工作中的多线程并发问题
缺点:

  1. 内存占用问题:很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以使用ConcurrentHashMap来代替。
  2. 数据一致性:CopyOnWriteArrayList容器只能保证数据的最终已执行,不能保证数据的实时一致性,所以如果希望写入的数据,马上能读取到,就不能使用CopyOnWriteArrayList。
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值