List集合框架面試題


  常見的集合框架中,實現的List<E>的主要有VectorArrayList以及LinkedList,其中最常見和最常使用的就是ArrayList了

本文的源碼基於jdk1.8

第一問:Vector和ArrayList以及LinkedList區別和聯系,以及分別的應用場景?

1:Vector

Vector的底層的實現其實是一個數組

 protected Object[] elementData;

他是線程安全的,為什么呢?

由於經常使用的add()方法的源碼添加synchronized,所以說他是一個同步方法 ,就連不會對數據結構進行修改的get()方法上也加了synchronized

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

如果不手動指定它的容量的話,它默認的容量是10

  /**
     * Constructs an empty vector so that its internal data array
     * has size {@code 10} and its standard capacity increment is
     * zero.
     */
    public Vector() {
        this(10);
    }

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

它是線程不安全的,所有的方法都沒有加鎖或者進行同步

   public boolean add(E e) {
        linkLast(e);
        return true;
    }


    /**
     * Links e as last element.
     */
    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.ArrayList

這里先簡單介紹一下,下面會對ArrayList的擴容機制進行分析

ArrayList是線程不安全的,如果不指定它的初始容量,那么它的初始容量是0,當第一次進行添加操作的時候它的容量將擴容為10

三種集合的使用場景

  1. Vector很少用,有其他線程安全的List集合
  2. 如果需要大量的添加和刪除則可以選擇LinkedList   原因是:它查詢的時候需要遍歷整個鏈表,插入和刪除的時候無需移動節點
  3. 如果需要大量的查詢和修改則可以選擇ArrayList           原因:底層為數組,刪除和插入需要移動其他元素,查詢的時候根據下標來查

第二問:我們想要使用線程安全的List集合,你有什么辦法?

1:可以使用Vector

2.自己重寫類似於ArrayList的但是線程安全的集合

3.可以使用Collections(工具類)中的方法,將ArrayList變成一個線程安全的集合

4.可以使用java.util.concurrent包下的CopyOnWriteArrayList,它是線程安全的

第三問:那你說說CopyOnWriteArrayList是怎么實現線程安全的?

它是juc包下的,專門用於並發編程的,他的設計思想是:讀寫分離,最終一致,寫時復制

它不能指定容量,初始容量是0.它底層也是一個數組,集合有多大,底層數組就有多大,不會有多余的空間

最常使用的add()方法的源碼

/**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
    //獲取一把鎖
        final ReentrantLock lock = this.lock;
    //加鎖
        lock.lock();
        try {
       //獲取當前集合(數組)
            Object[] elements = getArray();
        //獲取當前集合的長度
            int len = elements.length;
       //復制一個新的數組,由於是添加操作,新數組的長度比原數組長度大1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
        //原數組的長度就是新數組最大下標,將要添加的元素添加到最后
            newElements[len] = e;
        //更改引用,新數組替代原數組
            setArray(newElements);
            return true;
        } finally {
    //釋放鎖
            lock.unlock();
        }
    }

remove()方法的實現邏輯也是大同小異,只不過需要移動元素,新數組是減1

    /**
     * Removes the element at the specified position in this list.
     * Shifts any subsequent elements to the left (subtracts one from their
     * indices).  Returns the element that was removed from the list.
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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();
        }
    }

CopyOnWriteArrayList的缺點

底層是數組,刪除插入的效率不高,寫的時候需要復制,占用內存,浪費空間,如果集合足夠大的時候容易觸發GC

數據一致性問題。CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。【當執行add或remove操作沒完成時,get獲取的仍然是舊數組的元素】。CopyOnWriteArrayList讀取時不加鎖只是寫入和刪除時加鎖

應用場景:讀操作遠大於寫操作的時候

CopyOnWriteArrayList和Collections.synchronizedList區別

CopyOnWriteArrayList和Collections.synchronizedList是實現線程安全的列表的兩種方式。兩種實現方式分別針對不同情況有不同的性能表現,其中CopyOnWriteArrayList的寫操作性能較差,而多線程的讀操作性能較好。而Collections.synchronizedList的寫操作性能比CopyOnWriteArrayList在多線程操作的情況下要好很多,而讀操作因為是采用了synchronized關鍵字的方式,其讀操作性能並不如CopyOnWriteArrayList。因此在不同的應用場景下,應該選擇不同的多線程安全實現類。

第四問:說一下ArrayList的擴容機制

廢話不多說,直接擼源碼,紅色的方法名代表會有解析

無參構造方法

  /**
     * Constructs an empty list with an initial capacity of ten.
     */
    public ArrayList() {
    //其實就是空數組
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

/**
 * Shared empty array instance used for default sized empty instances. We
 * distinguish this from EMPTY_ELEMENTDATA to know how much to inflate when
 * first element is added.
 */
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.
     */
    transient Object[] elementData; // non-private to simplify nested class access
transient 這個關鍵字的用處是:ArrayList實現了Serializable接口,用transient修飾的字段或者對象不會進行實例化

擴容是再添加元素時才會出現的情況,有的情況是不指定初始容量第一次添加元素時,直接看add()方法

    /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return <tt>true</tt> (as specified by {@link Collection#add})
     */
    public boolean add(E e) {
        //先將集合的大小加一,代表有一個元素要加進來,開口有沒有它的容身之處
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        //將新元素添加到集合中
        elementData[size++] = e;
        return true;
    }
    

跳轉到ensureCapacityInternal方法中進行驗證

 private void ensureCapacityInternal(int minCapacity) {
        //DEFAULTCAPACITY_EMPTY_ELEMENTDATA初始化的值,也就是空
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
         //如果是為空的話,默認的DEFAULT_CAPACITY=10傳入的minCapacity哪個大取哪個
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        ensureExplicitCapacity(minCapacity);
    }    

繼續調用ensureExplicitCapacity方法,傳入判斷之后的值,第一次add的話這個就是默認的10

   private void ensureExplicitCapacity(int minCapacity) {
    //對集合操作的次數
        modCount++;

        // overflow-conscious code
        //傳入的參數減去數組的長度是否大於0,大於0的話就代表要進行擴容了
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

判斷傳入的參數(第一次為10)減去數組的長度是否大於0,大於0的話調用grow擴容方法,數組的長度是elementData.length也可以說是容量,集合的大小是size,兩個值是不同的

 /**
     * Increases the capacity to ensure that it can hold at least the
     * number of elements specified by the minimum capacity argument.
     *
     * @param minCapacity the desired minimum capacity
     */
    private void grow(int minCapacity) {
        // overflow-conscious code
     //舊的容量為當前數組的長度
        int oldCapacity = elementData.length;
     //新的容量為舊容量1.5倍,>>1代表右移一位,也就是÷2
        int newCapacity = oldCapacity + (oldCapacity >> 1);
     //新容量-舊容量是否小於0,一般是不指定容量,第一次add時才會進
        if (newCapacity - minCapacity < 0)
      //新容量等於傳入的參數
            newCapacity = minCapacity;
      //如果新的容量超過了集合的閾值
        if (newCapacity - MAX_ARRAY_SIZE > 0)
      //調用hugeCapacity方法進行在一步的計算
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
      
    //底層的數組進行copy后長度變為新的容量
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

當新容量大於集合的閾值時,調用hugeCapacity方法

   private static int hugeCapacity(int minCapacity) {
    //為負數的話拋出異常,一般沒這個可能
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
//三元表達式:新容量大於集合容量閾值時,新的容量為Integer的最大閾值,否則為集合的閾值
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
MAX_ARRAY_SIZE的值其實為Integer.MAX_VALUE-8,為什么要減8呢?因為數組也是一個對象,對象需要一定的內存存儲對象頭信息,對象頭信息最大占用內存不可超過8字節。

整個ArrayList擴容的機制就如上所示,自己理解的,有不對之處還望指教

第五問:你能自己重寫一個ArrayList嗎?

這個就考驗你對源碼的理解程度,我自己對照着ArrayList寫了一個,就是主要的增刪改查還有擴容,寫的比較拙劣,做個參考嘛

package com.yjc.list;

import java.util.AbstractList;
import java.util.Arrays;
import java.util.List;

public class MyList<E> extends AbstractList<E> implements List<E> {
    //定義無參構造方法
    public MyList(){
        this.elementData=EMPTY_ELEMENT_DATA;
    }

    //定義帶參構造方法
    public  MyList(int capacity){
        //驗證容量是否合法
        if (capacity>0) {
            this.elementData = new Object[capacity];
        }else if(capacity==0){
            this.elementData=EMPTY_ELEMENT_DATA;
        }else{
            //為負數則拋出異常
            throw  new IllegalArgumentException("參數"+capacity+"不合法,參數不能為負數");
        }
    }

    //定義底層數據結構
    transient public   Object [] elementData;

    //定義初始化容量
   private static  final  Integer DEFAULT_CAPACITY=10;
   //集合的最大容量
   private  static final Integer MAX_CAPACITY=Integer.MAX_VALUE-8;
   //創建一個空的數組,
   private static final Object[] EMPTY_ELEMENT_DATA = {};
    //用於記錄當前數組的大小
   private int size;


    public boolean add(E e) {
        ensureCapacityInternal(size+1);
        //將size+1空間判斷是否夠用
        elementData[size++]=e;
        return true;
    }

    //用於判斷數組是否夠用
   private void ensureCapacityInternal(Integer capacity){
        //代表是第一次添加數據
        if (elementData==EMPTY_ELEMENT_DATA){
            capacity=DEFAULT_CAPACITY;
        }
        if(capacity-elementData.length>0){
            //擴容
            grow(capacity);
        }

   }

    private void grow(Integer capacity) {
        //獲取原數組長度
        int oldCapacity=elementData.length;
        //右移兩位,相當於除以2
        int newCapacity=oldCapacity+(oldCapacity>>1);
        //不指定初始大小的時候,第一次執行add方法會走到這
        if (newCapacity-capacity<0){
            newCapacity=capacity;
        }
        //代表超過集合的最大容量
        if (newCapacity-MAX_CAPACITY>0){
            newCapacity=(capacity>MAX_CAPACITY)?Integer.MAX_VALUE:MAX_CAPACITY;
        }
        elementData= Arrays.copyOf(elementData,newCapacity);
    }

    @Override
    public E set(int index, E element) {
        checkIndex(index);
        Object oldValue=elementData[index];
        elementData[index]=element;
        //返回舊值
         return (E) oldValue;
    }

    @Override
    public E remove(int index) {
        checkIndex(index);
        Object oldValue=elementData[index];
        size--;
        for (int i = index; i <size-1; i++) {
            elementData[i]=elementData[i+1];
        }
        return (E) oldValue;
    }

    @Override
    public void add(int index, E element) {
        checkIndex(index);
        ensureCapacityInternal(size+1);

        for (int i = size+1; i> index; i--) {
          elementData[i]=elementData[i-1];
        }
        elementData[index]=element;
        size++;

    }

    @Override
    public int size() {
        return this.size;
    }

    @Override
    public E get(int index) {
        checkIndex(index);
        return (E) elementData[index];
    }
     private   void checkIndex(int index){
     //驗證下標是否正確
     if ((index>=size)||(index<0)){
         throw  new IndexOutOfBoundsException("輸入的下標不正確,當前集合大小為:"+size);
     }
 }
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM