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
三种集合的使用场景
- Vector很少用,有其他线程安全的List集合
- 如果需要大量的添加和删除则可以选择LinkedList 原因是:它查询的时候需要遍历整个链表,插入和删除的时候无需移动节点
- 如果需要大量的查询和修改则可以选择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集合框架面试题的主要内容,如果未能解决你的问题,请参考以下文章