Java常用容器类之List

先从Collection说起

Java中的容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
Collection是集合的顶层接口,定义了集合中常用的方法。常用子接口有:

  1. List
      实现类:ArrayList、Vector、LinkedList
  2. Set
      实现类:HashSet、TreeSet
  3. Queue
    实现类:LinkedList、PriorityQueue
public interface Collection<E> extends Iterable<E> {

    int size();
    
    boolean isEmpty();
    
    boolean contains(Object o);
    
    //集合专用的遍历方式:迭代器
    //iterator对象可以接受任何容器调用iterator函数对它的初始化。可以统一不同类型容器的代码重用问题。
    Iterator<E> iterator();
    
    Object[] toArray();
    
    <T> T[] toArray(T[] a);

    boolean add(E e);
    
    boolean remove(Object o);

    boolean containsAll(Collection<?> c);

    boolean addAll(Collection<? extends E> c);

    boolean removeAll(Collection<?> c);

    //接口可以有默认实现(从1.8开始)
    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

    boolean retainAll(Collection<?> c);

    void clear();
	
	//子类重写equals和hashcode  若重写equals,必须重写hashcode
    boolean equals(Object o);

    int hashCode();

	//多线程并行迭代的迭代器
    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, 0);
    }

    default Stream<E> stream() {
        return StreamSupport.stream(spliterator(), false);
    }

    default Stream<E> parallelStream() {
        return StreamSupport.stream(spliterator(), true);
    }
}

List

List接口继承了Collection类,在Collection类的通用方法外,定义了List特有的抽象方法,交给子类去实现。可以看到,List类主要增加了根据下标来添加删除元素和寻找指定元素下标的方法。

public interface List<E> extends Collection<E> {

    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);
    boolean add(E e);
    boolean remove(Object o);
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    
	//在指定下标index处添加元素
    boolean addAll(int index, Collection<? extends E> c);
    boolean removeAll(Collection<?> c);
    boolean retainAll(Collection<?> c);
    default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }

	//默认排序方法
	//在定义接口的时候使用default来修饰具体的方法
    @SuppressWarnings({"unchecked", "rawtypes"})
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

    void clear();
    boolean equals(Object o);
    int hashCode();
	
	//list类中的方法
    E get(int index);
    E set(int index, E element);
    void add(int index, E element);
    E remove(int index);
    int indexOf(Object o);
    int lastIndexOf(Object o);
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);
    List<E> subList(int fromIndex, int toIndex);

    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.ORDERED);
    }
}

List中的具体实现类都没有直接实现List接口,而是先继承AbstractList类再实现List中的方法。这体现了Java中一种重要的设计模式:模板方法模式。

接口中全都是抽象的方法(jdk1.8之后才可以有默认实现),而抽象类中可以有抽象方法,还可以有具体的实现方法,正是利用了这一点,让AbstractList实现接口中一些通用的方法,而具体的类,如ArrayList就继承这个AbstractList类,拿到一些通用的方法,然后自己再实现一些自己特有的方法,这样一来,让代码更简洁,将继承结构最底层的类中通用的方法都抽取出来,先一起实现了,减少重复代码。

这种写法在Java源码中很常见,如线程池中的AbstractExcutorService等。

ArrayList

1. 概览

因为 ArrayList 是基于数组实现的,所以支持快速随机访问。RandomAccess 接口标识着该类支持快速随机访问。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

数组的默认大小为 10。

private static final int DEFAULT_CAPACITY = 10;

2. 添加元素

//在尾部添加元素,返回添加成功/失败
public boolean add(E e) {
    //确定内部容量是否足够。先判断size+1的这个个数数组能否放得下。
    ensureCapacityInternal(size + 1);  
    elementData[size++] = e;
    return true;
}

//在指定下标添加元素
public void add(int index, E element) {
    //检查插入的位置是否合理
    rangeCheckForAdd(index);
    ensureCapacityInternal(size + 1);  
    //将elementData在插入位置后的所有元素往后面移一位
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index);
    elementData[index] = element;
    size++;
}

添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。

扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

//确保插入时数组有足够容量,如果没有则触发扩容。
private void ensureCapacityInternal(int minCapacity) {
    //如果原数组为空,添加第一个数组,此时,minCapacity=1,而DEFAULT_CAPACITY=10
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }
    ensureExplicitCapacity(minCapacity);
}

private void ensureExplicitCapacity(int minCapacity) {
	//修改次数加1
	//这个变量主要是为了检测遍历时数组是否被修改。若修改则迭代快速失败。
    modCount++;
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    //1.5倍扩容
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    //最大容量是Integer.MAX_VALUE - 8
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // 将原数组整个复制到新数组中
    elementData = Arrays.copyOf(elementData, newCapacity);
}

3. 删除元素

需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看到 ArrayList 删除元素的代价是非常高的。

//根据下标删除
//返回被删除的元素
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);
    elementData[--size] = null; // clear to let GC do its work
    return oldValue;
}

//根据数组元素删除
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 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
}

4、clone()方法
ArrayList中的克隆,是通过数组的复制实现的,属于浅拷贝(指向同一对象)。

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

5、ArrayList中的迭代器

首先回顾一下ArrayList使用迭代器遍历集合的方法:

 ArrayList<String> arrayList = new ArrayList<>();
    arrayList.add("s1");
    arrayList.add("s2");
    arrayList.add("s3");
    ListIterator<String> listIterator = arrayList.listIterator();
    while (listIterator.hasNext()){
        String str = listIterator.next();
        //迭代修改
        if(str.equals("s2")){
            listIterator.set("s3");
        }
    }

首先来看Itr类,这是ArrayList中的一个内部类。

private class Itr implements Iterator<E> {
    int cursor;       // 下一个迭代元素的下标
    int lastRet = -1; // 上一次迭代返回的下标
    //迭代过程中的快速失败机制
    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();
    }
}

listItr类继承了Itr和ListIterator,ListIterator有插入和修改删除方法,同时还具有向前遍历的方法,所以ListIterator就具备了增删改查的功能,比Itr的功能更加齐全。

private class ListItr extends Itr implements ListIterator<E> {
    ListItr(int index) {
        super();
        cursor = index;
    }

    public boolean hasPrevious() {
        return cursor != 0;
    }

    public int nextIndex() {
        return cursor;
    }

    public int previousIndex() {
        return cursor - 1;
    }

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

    public void set(E e) {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();
        try {
            ArrayList.this.set(lastRet, e);
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    public void add(E e) {
        checkForComodification();
        try {
            int i = cursor;
            ArrayList.this.add(i, e);
            cursor = i + 1;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }
}

ArrayList循环删除问题

我们来看以下代码:

public class ArrayListTest {
    public static void main(String[] args) {        
        ArrayList<String> list = new ArrayList<String>();
        list.add("aa");
        list.add("bb");
        list.add("bb");
        list.add("aa");
        list.add("cc");
        
        // 删除元素 bb
        remove(list, "bb");
        
        for (String str : list) {
            System.out.println(str);
        }
    }

    public static void remove(ArrayList<String> list, String elem) {
        
      // 方法一:普通for循环正序删除,删除过程中元素向左移动。在删除重复但不连续的元素时,正常删除;在删除重复且连续的元素时,会出现删除不完全的问题
      //以删除 “bb” 为例,当指到下标为 1 的元素时,发现是 "bb",此处元素应该被删除,调用remove(Object o)方法,删除位置后面的元素要向前移动,移动之后 “bb” 后面的 “bb” 元素下标为1,后面的元素下标也依次减1,这是在 i = 1 时循环的操作。在下一次循环中 i = 2,第二个 “bb” 元素就被遗漏了,所以这种删除方法在删除连续重复元素时会有问题。
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).equals(elem)) {
                list.remove(list.get(i));
            }
        }
        
      // 方法二:普通for循环倒序删除,删除过程中元素向左移动,可以删除重复的元素
        for (int i = list.size() - 1; i >= 0; i--) {
            if (list.get(i).equals(elem)) {
                list.remove(list.get(i));
            }
        }

      // 方法三:增强for循环删除,使用ArrayList的remove()方法删除,产生并发修改异常 ConcurrentModificationException
      //foreach写法是对实际的Iterable、hasNext、next方法的简写,问题同样出在fastRemove方法中。
      //fastRemove将modCount修改次数加1,但在ArrayList返回的迭代器会做迭代器内部修改次数检查,因为上面的remove(Object)方法修改了modCount的值,所以才会报出并发修改异常。要避免这种情况的出现则在使用迭代器迭代时(显示或for-each的隐式)不要使用ArrayList的remove,改为用Iterator的remove即可。
        for (String str : list) {
            if (str.equals(elem)) {
                list.remove(str);
            }
        }

        //方法四:迭代器,使用ArrayList的remove()方法删除,同方法四,产生并发修改异常 ConcurrentModificationException
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            if(iterator.next().equals(elem)) {
                list.remove(iterator.next());
            }
        }
        
       // 方法五:迭代器,使用迭代器的remove()方法删除,可以删除重复的元素,但不推荐
       //多线程中,方法五不能保证两个变量修改的一致性,结果具有不确定性,所以不推荐这种方法。
        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            if(iterator.next().equals(elem)) {
                iterator.remove();
            }
        }

    }

}

我们看第五个删除方法,使用迭代器中的删除方法:

public void remove() {
    if (lastRet < 0)
        throw new IllegalStateException();
    //检查修改次数
    //如果 modCount 和 expectedModCount 不相等,那么就抛出 ConcurrentModificationException,
    checkForComodification();

    try {
        ArrayList.this.remove(lastRet);
        cursor = lastRet;
        lastRet = -1;
        expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
        throw new ConcurrentModificationException();
    }
}

快速失败

在 java.util 包的集合类就都是快速失败的;而 java.util.concurrent 包下的类都是安全失败

快速失败
在使用迭代器对集合对象进行遍历的时候,如果 A 线程正在对集合进行遍历,此时 B 线程对集合进行修改(增加、删除、修改),或者 A 线程在遍历过程中对集合进行修改,都会导致 A 线程抛出 ConcurrentModificationException 异常。

原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。

注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。

安全失败:

采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。

原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException

缺点:基于拷贝内容的优点是避免了ConcurrentModificationException,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

场景:java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

6、总结

  • ArrayList可以存放null;
  • ArrayList本质上就是一个elementData数组;
  • ArrayList区别于数组的地方在于能够自动扩展大小,其中关键的方法就是grow()方法;
  • ArrayList由于本质是数组,所以它在数据的查询方面会很快,而在插入、删除、扩容这些方面,性能下降很多,要移动很多数据才能达到应有的效果;
  • ArrayList实现了RandomAccess,所以在遍历它的时候推荐使用for循环。

(RandomAccess:标志接口,表明实现这个这个接口的 List 集合支持快速随机访问。LinkedList没有实现这个接口。如果是实现了这个接口的 List,那么使用for循环的方式获取数据会优于用迭代器获取数据。因为for循环中使用的get()方法,采用的即是随机访问的方法)

Vector

1、概览

Vector 类实现了一个动态数组。和 ArrayList 很相似,但是两者是不同的:

  • Vector 是同步访问的。
  • Vector 包含了许多传统的方法,这些方法不属于集合框架。
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable

2、 添加元素

它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。

//1、在尾部添加元素
public synchronized boolean add(E e) {
    modCount++;
    //判断容量大小,是否需要扩容
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
       grow(minCapacity);
}

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //如果没有给定扩容增长系数capacityIncrement,默认按两倍扩容。
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}


//2、在指定位置添加元素
public void add(int index, E element) {
    insertElementAt(element, index);
}

public synchronized void insertElementAt(E obj, int index) {
    //fail-fast机制
    modCount++;
    //判断下标合法性
    if (index > elementCount) {
        throw new ArrayIndexOutOfBoundsException(index
                                                     + " > " + elementCount);
    }
    //判断容量大小
    ensureCapacityHelper(elementCount + 1);
    //数组拷贝
    System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
    elementData[index] = obj;
    elementCount++;
}

3、删除元素

它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。

//1、按元素值删除元素
public boolean remove(Object o) {
    return removeElement(o);
}

public synchronized boolean removeElement(Object obj) {
    modCount++;
    int i = indexOf(obj);
    if (i >= 0) {
        removeElementAt(i);
        return true;
    }
    return false;
}

public synchronized void removeElementAt(int index) {
    modCount++;
    if (index >= elementCount) {
       throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
    }
    else if (index < 0) {
       throw new ArrayIndexOutOfBoundsException(index);
    }
    int j = elementCount - index - 1;
    if (j > 0) {
        System.arraycopy(elementData, index + 1, elementData, index, j);
    }
    elementCount--;
    elementData[elementCount] = null; /* to let gc do its work */
}

4、 与 ArrayList 的比较

  • Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
  • Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍。

5、替代方案

可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。

List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);

也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。

List<String> list = new CopyOnWriteArrayList<>();

CopyOnWriteArrayList

1、概述

在很多应用场景中,读操作可能会远远大于写操作。由于读操作根本不会修改原有的数据,因此对于每次读取都进行加锁其实是一种资源浪费。我们应该允许多个线程同时访问 List 的内部数据,毕竟读取操作是安全的。

这和 ReentrantReadWriteLock 读写锁的思想非常类似,也就是读读共享、写写互斥、读写互斥、写读互斥。JDK 中提供了 CopyOnWriteArrayList 类比相比于读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作。只有写入和写入之间需要进行同步等待。这样一来,读操作的性能就会大幅度提升。(通过复制实现)

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

//写操作加锁
final transient ReentrantLock lock = new ReentrantLock();

//保证可见性
private transient volatile Object[] array;

2、读写分离

写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。

写操作需要加锁,防止并发写入时导致写入数据丢失。

写操作结束之后需要把原始数组指向新的复制数组。

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

    final transient ReentrantLock lock = new ReentrantLock();

    private transient volatile Object[] array;
    
    //读取操作没有任何同步控制和锁操作,理由就是内部数组 array 不会发生修改,只会被另外一个 array 替换,array又保证了可见性,因此可以保证数据安全。
    public E get(int index) {
        return get(getArray(), index);
    }
    
    private E get(Object[] a, int index) {
        return (E) a[index];
    }
    
    final Object[] getArray() {
        return array;
    }
    ...
}
//add() 方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会 copy 出多个副本出来。
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

final void setArray(Object[] a) {
    array = a;
}

2. 适用场景

CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。

但是 CopyOnWriteArrayList 有其缺陷:

  • 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
  • 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。

所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。

LinkedList

1、概览

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

基于双向链表实现,使用 Node 存储链表节点信息。

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

每个链表存储了 first 和 last 指针:

transient Node<E> first;
transient Node<E> last;

2、添加元素

LinkedList即实现了List接口,又实现了Deque接口,所以LinkedList既可以添加将元素添加到尾部,也可以将元素添加到指定索引位置,还可以添加整个集合;另外既可以在头部添加,又可以在尾部添加。

//1、将元素添加到链表尾部
public boolean add(E e) {
    linkLast(e);
    return true;
}

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

//2、在指定位置插入节点
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节点
    final Node<E> newNode = new Node<>(pred, e, succ);
    succ.prev = newNode;
    if (pred == null)
        first = newNode;
    else
        pred.next = newNode;
    size++;
    modCount++;
}

3、与 ArrayList 的比较

ArrayList 基于动态数组实现,LinkedList 基于双向链表实现。ArrayList 和 LinkedList 的区别可以归结为数组和链表的区别:

顺序插入的速度ArrayList会快些,LinkedList的速度稍慢一些。因为ArrarList只是在指定的位置上赋值即可,而LinkedList则需要创建Node对象,并且需要建立前后关联。

  • 数组支持随机访问,但插入删除的代价很高,需要移动大量元素;
  • 链表不支持随机访问,但插入删除只需要改变指针。
发布了2 篇原创文章 · 获赞 0 · 访问量 43

猜你喜欢

转载自blog.csdn.net/weixin_43867524/article/details/105582059
今日推荐