java集合:(二)ArrayList、LinkedList、Vector源码原理分析以及FailFast机制(JDK1.8)

上一篇:java集合:(一)认识集合的本质、java两大集合派系的关系梳理

一、ArrayList

在这里插入图片描述
在这里插入图片描述

ArrayList继承关系图
在这里插入图片描述

1. 源码解析

在看下面分析时,建议自己翻出源码对照着看。

1.1 类中属性

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
 	//数组默认初始化大小
    private static final int DEFAULT_CAPACITY = 10;

    //空数组
    private static final Object[] EMPTY_ELEMENTDATA = {};

    //空数组(和EMPTY_ELEMENTDATA的区别没仔细研究)
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    //最终存储元素的数组结构(重要)
    transient Object[] elementData; // non-private to simplify nested class access

    //elementData数组中存放元素的个数
    private int size;
    
    //elementData最大储存容量
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

1.2 构造函数

    /**
     * 无参构造函数
     */
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
     /**
     * 有参构造函数1
     */
    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);
        }
    }

 	/**
     * 有参构造函数2
     */
    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;
        }
    }

总结:ArrayList的构造方法就做一件事情,就是初始化一下储存数据的容器,其实底层本质就是elementData数组。

1.3 核心方法

1.3.1 boolean add(E e)

默认直接在末尾添加元素

    public boolean add(E e) {
        //size是数组中数据的个数,因为要添加一个元素,所以size+1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
         
        /**
         *elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,
         *即判断elementData是不是空的数组。然后找出默认容量(10)
         *和参数容量中大的。
         *可以得出ArrayList在实例化时是不初始化elementData的,
         *在使用add方法添加元素时才开始初始化elementData数组。
         */
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

    //判断容量是否够用的方法,如果不够用则扩容
    private void ensureExplicitCapacity(int minCapacity) {
  
        /**
         *modCount代表集合类内部结构的修改次数,即集合中的删除、添加
         *等等操作都会增加该变量的数量。
         *这个属性的作用很多,个人认为最主要的就是用来检测快速失败(后面会讲到)
         */
        modCount++;

        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    private void grow(int minCapacity) {
        //将扩充前的elementData大小给oldCapacity
        int oldCapacity = elementData.length; 
        /**
         *oldCapacity >> 1 = oldCapacity ÷ 2
         *即
         *oldCapacity + (oldCapacity >> 1) = oldCapacity * 1.5
         *所以这一步就相当于扩容1.5倍
         */
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            //如果newCapacity超过了最大的容量限制,就调用hugeCapacity,也就是将能给的最大值给newCapacity
            newCapacity = hugeCapacity(minCapacity);
        //新的容量大小已经确定好了,就copy数组,改变容量大小
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
1.3.2 void add(int index, E element)

在特定位置添加元素,也就是插入元素

    public void add(int index, E element) {
        //检查index也就是插入的位置是否合理,即数组下标是否越界
        rangeCheckForAdd(index);
		//和上面add方法中的ensureCapacityInternal作用一致
        ensureCapacityInternal(size + 1); 
        //这个方法就是用来在插入元素之后,要将index之后的元素都往后移一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
    
    private void rangeCheckForAdd(int index) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
    }
1.3.3 E 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);
        //将最后一个元素置空,然后GC机制会回收该内存,并且ArrayList的长度-1                
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }
1.3.4 boolean remove(Object o)

删除指定数据的元素

    public boolean remove(Object o) {
        //可以看出arrayList是可以存放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)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }
1.3.5 int indexOf(Object o)

查找数组里面是否存在指定元素。遍历数组,找到第一个和指定元素相等的元素,返回对应的下标,如果找不到相同的则返回-1。

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

本文就不详细分析每个方法了,其余的核心方法自己去翻下源码看看:
void clear():将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉

boolean removeAll(Collection<?> c):批量删除

boolean addAll(Collection<? extends E> c):批量添加

E get(int index):根据下标获得元素

2. 总结

1)arrayList可以存放null。
2)arrayList本质上就是一个elementData数组。
3)arrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是gorw()方法。
4)arrayList中removeAll(collection c)和clear()的区别就是removeAll可以删除批量指定的元素,而clear是删除集合中的全部元素。
5)arrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入删除这些方面,性能下降很多,因为需要移动很多数据才能达到应有的效果
6)arrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环(RandomAccess相关介绍)。

二、LinkedList

在这里插入图片描述
在这里插入图片描述

LinkedList继承关系图
在这里插入图片描述

1. 源码解析

1.1 类中属性

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
	//链表目前的容量
    int size = 0;
    
    //链表中的头节点
    transient Node<E> first;

    //链表中的尾节点
    transient Node<E> last;
    
    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;
        }
    }
}

1.2 构造方法

    //无参构造方法
    public LinkedList() {
    }

    //有参构造方法
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }

1.3 核心方法

1.3.1 boolean add(E e)

添加元素时,是直接添加在链表的结尾

    public boolean add(E e) {
        //将e作为最后一个元素插入
        linkLast(e);
        return true;
    }
    
    void linkLast(E e) {
    	//取出当前最后一个节点
        final Node<E> l = last;
        //创建一个新节点,注意其前驱节点为l,后续节点为null
        final Node<E> newNode = new Node<>(l, e, null);
        //将新的节点记为最后一个节点
        last = newNode;
        if (l == null)
             //如果最后一个节点为空,则表示链表为空,则将first节点也赋值为newNode
            first = newNode;
        else
        	 //关联l的next节点,构成双向节点
            l.next = newNode;
        //元素总数加1
        size++;
        //修改次数自增
        modCount++;
    }
1.3.2 E get(int index)

根据下标获取元素。
上篇文章中我们说过,链表不能像数组一样通过下标索引访问对应元素,linkedList提供的E get(int index)方法其实是根据类似二分的方法,判断是从first节点遍历查找还算是从last节点遍历查找 ,根据下面的源码就可得出。

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    
    Node<E> node(int index) {
        // assert isElementIndex(index);
	    // index如果小于链表长度的1/2
        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;
        }
    }
1.3.3 void add(int index, E element)

在特定位置添加元素

    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            //上面已解释
            linkLast(element);
        else
            //插入到指定元素前
            linkBefore(element, node(index));
    }

    void linkBefore(E e, Node<E> succ) {
        // assert succ != null;
        final Node<E> pred = succ.prev;
        //创建新的节点 前驱节点为succ的前驱节点,后续节点为succ,则e元素就是插入在succ之前的
        final Node<E> newNode = new Node<>(pred, e, succ);
        //构建双向链表,succ的前驱节点为新节点
        succ.prev = newNode;
        //如果前驱节点为空,则first为newNode
        if (pred == null)
            first = newNode;
        else
            //构建双向链表
            pred.next = newNode;
        size++;
        modCount++;
    }
1.3.4 boolean addAll(Collection<? extends E> c)和boolean addAll(int index, Collection<? extends E> c)

批量添加(在链表末尾添加)
批量添加(随机某个位置添加)
这块逻辑不复杂,但是纯看代码不一定能理解,建议自己写个demo调试一下。

    //这个addAll方法也是上面有参构造方法中的addAll方法
    public boolean addAll(Collection<? extends E> c) {
        return addAll(size, c);
    }
    
    public boolean addAll(int index, Collection<? extends E> c) {
        // 检查index是否越界
        checkPositionIndex(index);

        Object[] a = c.toArray();
        int numNew = a.length;
        // 如果插入集合无数据,则直接返回
        if (numNew == 0)
            return false;

        Node<E> pred, succ;
        // 如果index与size相同
        if (index == size) {
            // succ的前驱节点直接赋值为最后节点
            // succ赋值为null,因为index在链表最后
            succ = null;
            pred = last;
        } else {
            //取出index上的节点
            succ = node(index);
            pred = succ.prev;
        }

        // 遍历插入集合
        for (Object o : a) {
            @SuppressWarnings("unchecked") E e = (E) o;
            // 创建新节点 前驱节点为succ的前驱节点,后续节点为null
            Node<E> newNode = new Node<>(pred, e, null);
            if (pred == null)
                // succ的前驱节点为空,则表示succ为头,则重新赋值第一个结点
                first = newNode;
            else
                // 构建双向链表
                pred.next = newNode;
            // 将前驱节点移动到新节点上,继续循环
            pred = newNode;
        }

        // index位置上为空 赋值last节点为pred,因为通过上述的循环pred已经走到最后了
        if (succ == null) {
            last = pred;
        } else {
            // 构建双向链表
            // 从这里可以看出插入集合是在succ[index位置上的节点]之前
            pred.next = succ;
            succ.prev = pred;
        }

        size += numNew;
        modCount++;
        return true;
    }

其余的一些方法也没什么特别的,自己看源码即可。

2. 总结

1)LinkedList也是继承了List的接口,所以在LinkedList中存储的也是有序的,不唯一的数据。它采用的是链表式储存,所以比较适合用来执行插入,删除等功能
2) LinkedList总结

三、Vector

Vector底层源码基本和ArrayList是一致的,只不过Vector是线程同步的(Synchronized),ArrayList是线程不同步的。但是Vector基本上已经被大家慢慢放弃了,是一个过时的类。
在这里插入图片描述

Vector继承关系图
在这里插入图片描述

1. ArrayList、LinkedList、Vector对比

ArrayList
优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程不安全,效率高
Vector
优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程安全(不绝对),效率低
LinkedList
优点: 底层数据结构是链表,查询慢,增删快。
缺点: 线程不安全,效率高

Vector为什么慢慢被遗弃
Vector中对每一个独立操作都实现了同步,这通常不是我们想要的做法。对单一操作实现同步通常不是线程安全的(举个例子,比如你想遍历一个Vector实例。你仍然需要申明一个锁来防止其他线程在同一时刻修改这个Vector实例。如果不添加锁的话通常会在遍历实例的这个线程中导致一个ConcurrentModificationException(快速失败机制,后面会讲))同时这个操作也是十分慢的(在创建了一个锁就已经足够的前提下,为什么还需要重复的创建锁)当然,即使你不需要同步,Vector也是有锁的资源开销的。总的来说,在大多数情况下,这种同步方法是存在很大缺陷的。

2. Collections.synchronizedList

要实现List的线程安全,建议使用Collections.synchronizedList方法。

Collections.synchronizedList使用同步代码块的方式,而Vector是同步方法。

同步代码块和同步方法的区别:

  1. 同步代码块在锁定的范围上可能比同步方法要小,一般来说锁的范围大小和性能是成反比的。
  2. 同步块可以更加精确的控制锁的作用域(锁的作用域就是从锁被获取到其被释放的时间),同步方法的锁的作用域就是整个方法。
  3. 静态代码块可以选择对哪个对象加锁,但是静态方法只能给this对象加锁。
    //SynchronizedRandomAccessList也继承了SynchronizedList,所以我们看看SynchronizedList里面是怎么写的
    public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }
    
    /**
     * 由源码可以看出add、get、remove等等很多方法都加上了同步代码块的锁
     */
    static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
        private static final long serialVersionUID = -7754090372962971524L;

        final List<E> list;

        SynchronizedList(List<E> list) {
            super(list);
            this.list = list;
        }
        SynchronizedList(List<E> list, Object mutex) {
            super(list, mutex);
            this.list = list;
        }

        public boolean equals(Object o) {
            if (this == o)
                return true;
            synchronized (mutex) {return list.equals(o);}
        }
        public int hashCode() {
            synchronized (mutex) {return list.hashCode();}
        }

        public E get(int index) {
            synchronized (mutex) {return list.get(index);}
        }
        public E set(int index, E element) {
            synchronized (mutex) {return list.set(index, element);}
        }
        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }
        public E remove(int index) {
            synchronized (mutex) {return list.remove(index);}
        }

        public int indexOf(Object o) {
            synchronized (mutex) {return list.indexOf(o);}
        }
        public int lastIndexOf(Object o) {
            synchronized (mutex) {return list.lastIndexOf(o);}
        }

        public boolean addAll(int index, Collection<? extends E> c) {
            synchronized (mutex) {return list.addAll(index, c);}
        }

        public ListIterator<E> listIterator() {
            return list.listIterator(); // Must be manually synched by user
        }

        public ListIterator<E> listIterator(int index) {
            return list.listIterator(index); // Must be manually synched by user
        }

        public List<E> subList(int fromIndex, int toIndex) {
            synchronized (mutex) {
                return new SynchronizedList<>(list.subList(fromIndex, toIndex),
                                            mutex);
            }
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            synchronized (mutex) {list.replaceAll(operator);}
        }
        @Override
        public void sort(Comparator<? super E> c) {
            synchronized (mutex) {list.sort(c);}
        }
        
        private Object readResolve() {
            return (list instanceof RandomAccess
                    ? new SynchronizedRandomAccessList<>(list)
                    : this);
        }
    }

使用SynchronizedList,在遍历时要手动进行同步处理

使用SynchronizedList的时候,进行遍历时要手动进行同步处理。可以根据上面的源码
看出执行add()等方法的时候是加了synchronized关键字的,但是listIterator(),
iterator()却没有加.所以在使用的时候需要加上synchronized,如下:
		List<String> list = Collections.synchronizedList(new ArrayList<String>());
		list.add("1");
		list.add("2");
		list.add("3");
		
		// Must be in synchronized block
		synchronized (list) {
		    Iterator i = list.iterator(); 
		    while (i.hasNext()) {
		        //foo(i.next());
		        System.out.println(i.next());
		    }
		}

SynchronizedList和Vector最主要的区别:

  1. SynchronizedList有很好的扩展和兼容功能。他可以将所有的List的子类转成线程安全的类。
  2. 使用SynchronizedList的时候,进行遍历时要手动进行同步处理。
  3. SynchronizedList可以指定锁定的对象

四、FailFast机制

在这里插入图片描述

1. 测试快速失败机制

创建三个类
ThreadAdd: 使用线程休眠间接性的向list中添加数据
ThreadIterate: 使用线程休眠迭代list中的值
FailFastTest: 执行上面两个类

public class ThreadAdd extends Thread {
    private List list;

    public ThreadAdd(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("add :" + i);
            try {
                Thread.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            list.add(i);
        }
    }
}
public class ThreadIterate extends Thread {
    private List list;

    public ThreadIterate(List list) {
        this.list = list;
    }

    @Override
    public void run() {
        while (true) {
            for (Iterator iteratorTmp = list.iterator(); iteratorTmp.hasNext(); ) {
                System.out.println("loop" + iteratorTmp.next());
                try {
                    Thread.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
public class FailFastTest {
    private static final List<String> list = new ArrayList<>();

    public static void main(String[] args) {
        new ThreadAdd(list).start();
        new ThreadIterate(list).start();
    }
}

运行结果:
运行该代码,抛出异常java.util.ConcurrentModificationException!即,产生fail-fast事件!
在这里插入图片描述
结果说明:
当某一个线程遍历list的过程中,list的内容被另外一个线程所改变了;就会抛出ConcurrentModificationException异常,产生fail-fast事件。

2. fail-fast解决办法

fail-fast机制,是一种错误检测机制。它只能被用来检测错误,因为JDK并不保证fail-fast机制一定会发生。若在多线程环境下使用fail-fast机制的集合,建议使用“java.util.concurrent包下的类”去取代“java.util包下的类”。
所以,本例中只需要将ArrayList替换成java.util.concurrent包下对应的类即可。
即,将代码

private static final List<String> list = new ArrayList<>();

替换为

private static final List<String> list = new CopyOnWriteArrayList<String>();

则可以解决该办法(虽然解决了,但是我是没有想到能应用到什么业务场景里)。

3. fail-fast原理

产生fail-fast事件,是通过抛出ConcurrentModificationException异常来触发的。
那么,ArrayList是如何抛出ConcurrentModificationException异常的呢?

ConcurrentModificationException是在操作Iterator时抛出的异常,所以我们看看iterator的相关源码。

    public Iterator<E> iterator() {
        return new Itr();
    }

    private class Itr implements Iterator<E> {
        int cursor;       // index of next element to return
        int lastRet = -1; // index of last element returned; -1 if no such
        //modCount记录了对象的修改次数,前面也讲过,集合在添加删除等等操作时都会使该值增加
        int expectedModCount = modCount;

        Itr() {}

        public boolean hasNext() {
            return cursor != size;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }

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

        @Override
        @SuppressWarnings("unchecked")
        public void forEachRemaining(Consumer<? super E> consumer) {
            Objects.requireNonNull(consumer);
            final int size = ArrayList.this.size;
            int i = cursor;
            if (i >= size) {
                return;
            }
            final Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length) {
                throw new ConcurrentModificationException();
            }
            while (i != size && modCount == expectedModCount) {
                consumer.accept((E) elementData[i++]);
            }
            // update once at end of iteration to reduce heap write traffic
            cursor = i;
            lastRet = i - 1;
            checkForComodification();
        }

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

可以发现在调用 next() 和 remove()时,都会执行checkForComodification()。若 “modCount 不等于 expectedModCount”,则抛出ConcurrentModificationException异常,产生fail-fast事件。

下一篇:java集合:(三)HashMap、ConcurrentHashMap源码分析以及与Hashtable、TreeMap的区别

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值