List集合框架面试题

Posted rzbwyj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了List集合框架面试题相关的知识,希望对你有一定的参考价值。

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

以上是关于List集合框架面试题的主要内容,如果未能解决你的问题,请参考以下文章

Java面试题-集合框架篇二

Java面试题-集合框架篇三

java面试题——集合框架

面试Java集合面试题

java面试题之简单介绍一下集合框架

JAVA集合以及面试题