【Java】高级8:List集合接口专题

List-有序可重复的集合接口

List接口继承自Collection接口,集合内元素有序可重复
ArrayList,LinkedList,Vrctor都是其实现类,区别在于内部的具体实现上。不同的实现开销不一样,适用的场景也不一样。

抽象实现类-AbstractList


public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
	// ... ...
}

AbstractListList的抽象类,实现了部分方法,定义了部分方法。

定义的抽象方法:
Iterator iterator()//迭代器
int size()//返回集合长度
boolean add(E e)
… …

!add方法的方法体里面只抛出了异常,具体的实现需要在继承的子类中重写。

已经实现的方法:
boolean isEmpty() //是否为空
boolean contains(Object o);//包含
Object[] toArray();//转数组
String toString()
… …


数组结构实现类-ArrayList


源码👇

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
	private static final int DEFAULT_CAPACITY = 10;//默认容量
	
	/*
		空实例,在第一次添加元素后将值赋值给elementData
	*/
	private static final Object[] EMPTY_ELEMENTDATA = {};
	/*
		长度为0 的空实例,无参构造的时候会将这个空数组赋值给 elementData数组。
	*/
	private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
	
	//数组缓存区
	transient Object[] elementData;
	//size表示集合长度 = 数组已插入元素的个数
	private int size;
	
	// ... ... 具体方法省略 ... ...
}

ArrayList常用方法代码分析

add

源码分析👇

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 确保内部数组容量可以增加一个元素
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
	/*
		计算容量
		1、空集合,elementData的长度就是0,给一个默认长度10;
		2、不是空集合,返回最小容量。
		这个最小容量是指集合需要容纳元素的数量,注意是需要!!比如集合单个添加元素,
		最小容量就是当前的size+1;批量添加4元素,最小容量就是当前size+4.
	*/
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
	/*
		minCapacity = 集合需要容纳的最小数量
	*/
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // 最小容量小于数组容量,进行扩容。
        //--------------------------------------------------------------------------
        //解释:比如你批量插入15个元素,已经插入了10个元素,那么最小容量应该是10+15=25;
        //		如果elementData.length = 10;显然需要新添加进去的15个元素根本没地方插入,
        //		所以这里就需要扩容数组才行!!
        //--------------------------------------------------------------------------
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {//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、代码中申明的size是集合长度,就是已经插入到elementData数组中的元素的个数。
2、理解长度和下标,长度是从1开始,下标是从0开始。
3、数组添加元素是添加在最后,所以它是有次序性的,size可以看作是对elementData数组添加元素操作的次序。

👆

例:


比如,size=8表示数组中已经存在了8个元素,这仅表示数组中已经有元素的个数。数组插入数据是有次序性的,那么第5个插入的元素其实就是数组下标为5-1的元素。

remove


源码分析👇

    public E remove(int index) {
        rangeCheck(index);//检查下标是否在数组长度内,超出则抛出下标越界异常

        modCount++;//记录结构性变化的次数
        E oldValue = elementData(index);
		/*		
			index = 下标,是从0开始的,操纵数组要用下标;
			size = 缓存数组的容量也是集合长度,是从1 开始的;
			numMoved(删除该元素后的剩余元素的个数) = 集合长度 -  要删除元素的下标 - 1;
			
			⭐⭐这个地方有点绕脑子,比如String[] strArr = new String[]{"a","b","c","d","e","f"};
			size=6,要删除元素c,元素c的index=2;所以删后边剩余的元素个数就是6-2-1;为什么要-1,
			因为6是集合长度,index是下标,长度本来就比下标多了1,所以这里需要-1;
		*/
        int numMoved = size - index - 1;//剩余元素的个数
        if (numMoved > 0)
        	//整个删除的逻辑就是将删除元素后边是剩余元素复制到原数组
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        //最后一位重复,所以设为null
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

set

源码分析👇

    public E set(int index, E element) {
        rangeCheck(index);

        E oldValue = elementData(index);
        elementData[index] = element;//直接操作数组替换原来的内容就行,逻辑很简单
        return oldValue;
    }

get

    public E get(int index) {
        rangeCheck(index);
		//没什么复杂的逻辑,不再说明
        return elementData(index);
    }

ArrayList总结

源码解析只分析了查、功能的源码逻辑,其他的没操作无非是遍历集合然后进行操作没什么复杂的逻辑。从源码分析中可以看出,addremove操纵涉及到数组的赋值,要是你向一个集合中插入一万条数据,就意味着需要频繁操作数组一万次,在庞大的插入量之下这个开销还是很大的。但是由于他可以直接操作下标,查询性能反而比较好一些。

纵观来看,整个类的代码逻辑中比较复杂的地方是数组的扩容,其他操作倒是没有特别复杂的地方。

比较重要的几点👇

  • ArrayList内部是通过数组实现的,数组在内存中是一块连续的区域,数组有下标有数据,一般是使用下标来操作数据。所以说他的增删开销大也是跟他内部的数据存储结构有关系。
  • ⭐由于其内部是基于数组实现的所以它的增删性能开销大,因为它每次添加元素或者删除元素都要重新计算下标、扩容。好处是它的查找元素的效率很高
  • ArrayList不是线程安全的容器,在两个线程同时修改的话就会出现线程安全问题。集合容器的工具类提供了线程安全 ArrayList - Collections.synchronizedList。这个方法的具体实现是写了一个名字叫synchronizedList的内部类,然后实现了List接口给方法都加上了synchronizedList关键字。
  • 遍历集合优先使用迭代器或者增强for循环(内部也是迭代器实现),效率会比普通的的for循环高。
  • ⭐ 初始大小是10

链表结构实现类-LinkedList


AbstractSequentialList

  • AbstractSequentialListLinkedList的抽象类,实现了部分功能,也定义了部分功能。
  • LinkedList继承自该类。
public abstract class AbstractSequentialList<E> extends AbstractList<E> {
  
		// ... 具体内容略 ...
		// 看源码的话可以发现,其实内部功能都是通过迭代器来操作的 
}

LinkedList源码👇

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
	//size表示集合元素的数量,也表示元素插入的次序
    transient int size = 0;
	/*
		第一个节点的地址
		1、集合中的链表结构是双向链表,有前驱和后继。
		2、链表的第一个元素是没有前驱的,有后续。(就像站队一样,排头前边没有人)
	*/
    transient Node<E> first;
	/*
		最后一个节点的地址
		1、最后一个节点只有前驱没有后继。(就像站队一样,排尾后边没有人)
	*/
    transient Node<E> last;
    
    // ... ...
}

LinkedList常用方法解析

add

源码👇

  public boolean add(E e) {
        linkLast(e);
        return true;
    }
    void linkLast(E e) {
    	/*
    	1、链表添加元素是往最后一个元素后边添加。
    	2、添加元素之前new一个新的节点,这个节点的上一个节点(prev)地址就是最后一个节点的内存地址,
    	   下一个节点的地址(next)默认为null。因为最后一个元素的后续没有元素,所以默认为null没有问题
   		 */
        final Node<E> l = last;//注意这里,用了关键字final,初始化之后就不变了
        final Node<E> newNode = new Node<>(l, e, null);
        last = newNode;//注意,将添加的值赋值给列最后后一个节点,每次new完新节点后last节点的值会变
        if (l == null)
            first = newNode;//注意,如果是第一次添加元素的话,第一个节点个最后一个节点的值是一样的
        else
            l.next = newNode;//注意这里的l,它是插入元素前的最后一个节点
        size++;
        modCount++;
    }
    private static class Node<E> {
        E item;
        Node<E> next;
        Node<E> prev;
		//如果是链表的第一个元素,那么他的上个元素的地址肯定是null
		//如果是链表的最后一个元素,那么他的下一个元素的地址肯定是null
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;//插入的元素
            this.next = next;//下个元素地址
            this.prev = prev;//上个元素地址
        }
    }

remove

源码👇

!LinkedList无参的删除方法默认删除的是第一个节点的元素

    public E remove() {
        return removeFirst();
    }
    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;//第一个元素后边的元素将作为第一个元素
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;//
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

set

源码👇

    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }
    Node<E> node(int 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;
        }
    }

get

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

LinkedList总结

源码只详看了添加、删除、修改、查找,其他的操作都是基于这些基本操纵实现的没有再详细的解析。

从源码来看,每次插入都是往最后边插入,不用关心具体的容量,没有太多的逻辑操纵,效率还是比较高的。但是修改查询的时候都是需要遍历,从头或者从尾部挨个取出元素,从前往后找的话是找它的next,就是后边的那个元素的地址,从后往前找的话是找它前边元素,prev的地址。

再细说一下链表根据下标取元素, 链表中定义了first、last、size,虽然它没有实际意义上的下标,但是它有插入动作的次序,里面定义的 size这个字段实际上就是它的插入次序。一共插入了5次,那么它就有5个元素。同样的道理,第五个元素一定是在第五次操作的时候添加的,所以,先找出第一个节点元素,然后往后next5次就可以找到第五次添加的这个节点元素对象。

比较重要的几点:

  • LinkedList内部是链表结构
  • 不是线程安全的,多线程同时操作会出现问题

线程安全实现类-Vector


ArrayList差别不大,只不过里面的方法家里同步锁,实际用到的场景不是很多!

比较重要的点

  • ⭐ 初始大小是10
  • ⭐同步,是线程安全的

高效并发实场景现类-CopyOnWriteArrayList


public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}

概述

是一个线程安全的变体ArrayList ,支持高效率并发。读操作无锁,其中所有可变操作( add , set ,等等)通过对底层数组的最新副本实现。可变操作完成后将实体地址指向最新副本地址,实体设为null等待GC回收处理。

⭐该类适用于超大数据量下读操作大于写操纵的场景,原因是:一方面其add、set操纵需要将整个数组赋值,作为副本操作,在数据量特别大的情况下,复制副本这个开销会很大;另一方面复制副本后,实体还是存在的,副本add、set操作完成后才会将实体设为null等待回收。GC并不是立马回收,所以在这个过程中内存开销也很大。

⭐该类不能用于实时读的场景,add、set操作都是需要先复制出来一个副本,然后操作副本进行add、set操纵,在这个操纵没有完成之前,读操作依然是实体,这里的时差性导致实时读写会出问题。

⭐ 因为可以并发读取,所以读取效率高。

CopyOnWriteArrayList常用方法源码解析

public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    private static final long serialVersionUID = 8673264195747942595L;

    /*
		同步锁,跟synchronized一样都是独占锁,但是又和synchronized有所区别。
		ReentrantLock需要手动加锁解锁,比较灵活,等待线程的分配更公平。
	 */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
// ... ...
 }
void add(E e)
    public boolean add(E e) {
    	//块锁,块锁内的同步操作,多线程情况下只允许一哥线程进行操作
        final ReentrantLock lock = this.lock;
        lock.lock();//加锁
        try {
            Object[] elements = getArray();
            int len = elements.length;
            //注意区分数组的长度和下标,长度从1开始,下标从0开始
            //newElements的长度就是len+1(原数组+1,因为要插入元素,长度+1)
            //newElements的最后一个元素的下标就是它的长度-1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;//将值插入到最后一个元素
            setArray(newElements);//替换
            return true;
        } finally {
            lock.unlock();//解锁
        }
    }

👆

!CopyOnWriteArrayList.add()方法跟ArrayList.add()相比,加了块锁,处理逻辑上也有区别。将整个数组复制然后将元素插入到最后。没有扩容数组的操作!

add
    public void add(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException("Index: "+index+
                                                    ", Size: "+len);
            Object[] newElements;
            int numMoved = len - index;//插入元素后边剩余元素的个数
            if (numMoved == 0)
                newElements = Arrays.copyOf(elements, len + 1);
            else {
                newElements = new Object[len + 1];
                //#1将插入元素之前的所有元素复制到副本数组
                System.arraycopy(elements, 0, newElements, 0, index);
                //#2将插入元素之后的所有元素复制到副本元素
                System.arraycopy(elements, index, newElements, index + 1,
                                 numMoved);
            }
            //替换元素
            //复制后,新数组的 newElements[index]和 newElements[index+1]是重复的。
            //所以set相当于是将插入index后边的元素整体后移了
            newElements[index] = element;
            setArray(newElements);
        } finally {
            lock.unlock();
        }
    }
set
    public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                //没什么逻辑,就是先复制再修改
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
remove
    public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;//移动元素的个数相应的也缩小一个
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                //复制需要删除元素之前的元素到新数组
                System.arraycopy(elements, 0, newElements, 0, index);
                //复制需要删除元素之后的元素,(不包括删除元素)到新数组
                System.arraycopy(elements, index + 1, newElements, index,numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
get
    private E get(Object[] a, int index) {
        return (E) a[index];
    }

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    public E get(int index) {
        return get(getArray(), index);
    }

get没有太多逻辑!

遍历

!
优先使用迭代器遍历,不能使用迭代器的情况下使用基本的for循环遍历。

        ArrayList<String> stringArrayList = new ArrayList<String>();
        stringArrayList.add("a");
        stringArrayList.add("b");
        stringArrayList.add("c");
        stringArrayList.add("d");

Demo1:for迭代器👇

        for (Iterator it = stringArrayList.iterator();it.hasNext();){
                System.out.println(it.next());
        }

Demo2:for each循环👇

!for each 内部是迭代器实现的,效率比普通for循环要高!

        for (String s: stringArrayList) {
            System.out.println(s);
        }

Demo3:for循环👇

        for (int i = 0; i < stringArrayList.size(); i++) {
            System.out.println(stringArrayList.get(i));
        }

附录

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值