面试3-Java常见集合源码List

一、ArrayList

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
       
        // 默认容量10.
        private static final int DEFAULT_CAPACITY = 10;
        // 容量为0的数组(有参构造且initialCapacity = 0 时使用这个)
        private static final Object[] EMPTY_ELEMENTDATA = {};
        // 默认元素为0的数组(无参构造使用这个)
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
        //元素数组,用于存储元素。
        transient Object[] elementData;

...
}
1、ArrayList的默认容量大小&扩容机制
    // 容量相关:
    
    //使用无参构造时,elementData初始化为空数组,默认容量为0。
    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    // 创建集合并初始化容量
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    } 
    
    // 以初次使用为栗子
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 处理容量,可见初次使用集合add时这里值为1
        elementData[size++] = e; // 元素添加到集合末尾。
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
       // 如果是无参构造,则走这个if语句
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //集合的最小容量。初次使用时Math.max的值肯定就是10
            minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
        }
      
        ensureExplicitCapacity(minCapacity);
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        //当最小容量大于集合大小时要进行扩容。
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }  

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length; // 老容量
        int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量(老容量1.5倍)
        // 前10次添加元素都走这里if。 例如第一次10>1
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        //大容量处理
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // 重新啊生成一个新的数组,进行元素copy。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }          

在JDK1.8中,ArrayList默认初始化为一个容量为0的Object数组,也即底层Object[] elementData 初始化为{},只有当第一次调用add()方法时,底层才创建了长度为 10 的数组,并将数据添加到数组中。

扩容就是构建一个新的数组,将老数组数据原封不动拷贝到新数组中。扩容的方法在grow中,后续扩容是会增长到原来的1.5倍(新容量=老容量+老容量>>1,右移一位代表除以2),

当ArrayList很大的时候,这样扩容还是挺浪费空间的,甚至会导致内存不足抛出OutOfMemoryError。扩容的时候还需要对数组进行拷贝,这个也挺费时的。所以我们使用的时候要竭力避免扩容,提供一个初始估计容量参数,以免扩容对性能带来较大影响。

2、什么情况下你会使用ArrayList?什么时候你会选择LinkedList?

多数情况下,当访问元素比插入或者是删除元素更加频繁的时候,应该使用ArrayList。另外一方面,当在某个特别的索引中,插入或者是删除元素更加频繁,或者你压根就不需要访问元素的时候,你会选择LinkedList。

这里的主要原因是,在ArrayList中访问元素的最糟糕的时间复杂度是”1″,而在LinkedList中可能就是”n”了。在ArrayList中增加或者删除某个元素,通常会调用System.arraycopy方法,这是一种极为消耗资源的操作,因此,在频繁的插入或者是删除元素的情况下,LinkedList的性能会更加好一点。

3、ArrayList插入删除一定慢么?

取决于你删除的元素离数组末端有多远,ArrayList拿来作为栈来用还是挺合适的,push和pop操作完全不涉及数据移动操作。

4、ArrayList是线程安全的么?
public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    
   
    protected Object[] elementData;
    // 默认容量10
    public Vector() {
        this(10);
    }
    // add 操作,与ArrayList基本类似。
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }        
...
}

当然不是,线程安全版本的数组容器是Vector。Vector的实现很简单,就是把所有的方法统统加上synchronized就完事了。你也可以不使用Vector,用Collections.synchronizedList把一个普通ArrayList包装成一个线程安全版本的数组容器也可以,原理同Vector是一样的,就是给所有的方法套上一层synchronized。

5、ArrayList用来做队列合适么?

队列一般是FIFO的,如果用ArrayList做队列,就需要在数组尾部追加数据,数组头部删除数组,反过来也可以。但是无论如何总会有一个操作会涉及到数组的数据搬迁,这个是比较耗费性能的。

ArrayList固然不适合做队列,但是数组是非常合适的。比如ArrayBlockingQueue内部实现就是一个环形队列,它是一个定长队列,内部是用一个定长数组来实现的。

6、ArrayList频繁扩容导致添加性能急剧下降,如何处理?

使用ArrayList时,可以 new ArrayList(初始容量)构造方法来指定集合的大小,以减少扩容的次数,提高写入效率。

7、ArrayList插入或删除元素一定比LinkedList慢么?

取决于删除的元素离数组末端有多远,ArrayList拿来作为堆栈来用还是挺合适的,push和pop操作完全不涉及数据移动操作。时间复杂度O(1)

8、如何复制某个ArrayList到另一个ArrayList中去?

(1)使用clone()方法
(2)使用ArrayList构造方法
(3)使用addAll方法

9、序列化:
    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        // Write out element count, and any hidden stuff
        int expectedModCount = modCount;
        s.defaultWriteObject();

        // Write out size as capacity for behavioural compatibility with clone()
        s.writeInt(size);

        // 只序列化有数据的部分
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }

        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException();
        }
    }

仔细看的话肯定能看到前面的elementData是用transient修饰的,也就是拒绝数据被自动序列化。因为ArrayList并不是所有位置都有数据,所以没必要全部序列话,应该只序列化有数据的部分。

二、LinkedList

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable{
    
    transient Node<E> first;// 头结点
    transient Node<E> last; // 尾结点   
    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;
        }
    }

}
1、特点
  • 既然实现了 Deque 接口,就说明 LinkedList 其实是一个双向队列。

  • 底层双向链表实现

  • 增删的优缺点:底层链表,插入删除比较快,只需修改要操作元素的指针即可(不考虑遍历时时,链表只需修改插入位置指针,数组还要移动。)。查询慢,需要从头遍历。

  • 尾结点的设计:这里为什么要存在一个成员变量尾节点?LinkedList 并没有采用从头循环到尾的做法,而是采取了简单二分法,首先看看 index 是在链表的前半部分,还是后半部分。如果是前半部分,就从头开始寻找,反之亦然。通过这种方式,使循环的次数至少降低了一半,提高了查找的性能,这种思想值得我们借鉴。如add(int index E element)添加任意位置,这里的思想非常巧妙,如果index在链表的前半部分,那么从first开始往后查找否则,从last往前面查找

  • LinkedList实现了栈和队列的操作方法,因此也可以作为栈、队列和双端队列来使用。

  • LinkedList元素允许为null,允许重复元素

2、ArrayList和LinkedList的区别?

ArrayList:

  • 基于动态数组的数据结构
  • 对于随机访问的get和set,ArrayList要优于LinkedList
  • 对于随机操作的add和remove,ArrayList不一定比LinkedList慢 (ArrayList底层由于是动态数组,因此并不是每次add和remove的时候都需要创建新数组)

LinkedList:

  • 基于双向链表的数据结构(双向链表遍历效率可能优于单向链表,因为双向链表可以在查找元素时,判断靠近头还是靠近尾,如果靠近头从头开始找,如果靠近尾从尾开始找)
  • 对于顺序操作,LinkedList不一定比ArrayList慢
  • 对于随机操作,LinkedList效率明显较低
3、modCount属性的作用?

modCount属性代表为结构性修改的次数。该属性被Iterator以及ListIterator的实现类所使用。初始化迭代器时会给这个modCount赋值,如果在遍历的过程中,一旦发现这个对象的modCount和迭代器存储的modCount不一样,Iterator或者ListIterator 将抛出ConcurrentModificationException异常,jdk在面对迭代遍历的时候为了避免不确定性而采取的 fail-fast(快速失败)原则。

单线程中最常见的就是Iterator遍历集合时对集合进行了修改操作。

  ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==2)
                list.remove(integer); 
                // iterator.remove();   
                //注意这个地方, 这样修改即可避免
        }

多线程环境的ConcurrentModificationException解决

注意多线程下Vector 解决不了这个问题

虽然Vector的方法采用了synchronized进行了同步,但是实际上通过Iterator访问的情况下,每个线程里面返回的是不同的iterator,也即是说expectedModCount是每个线程私有。假若此时有2个线程,线程1在进行遍历,线程2在进行修改,那么很有可能导致线程2修改后导致Vector中的modCount自增了,线程2的expectedModCount也自增了,但是线程1的expectedModCount没有自增,此时线程1遍历时就会出现expectedModCount不等于modCount的情况了。

多线程下的解决方案:

1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;

2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector。

三、Vector

和ArrayList很相似,同样实现了List、RandomAccess接口,可以插入空数据以及支持随机访问。内部通过动态数组来实现,不过add、get方法都加了synchronized同步锁。
所以,可以把Vector理解为一个加锁的ArrayList。

四、CopyOnWriteArrayList

在 ArrayList 的类注释上,JDK 就提醒了我们,如果要把 ArrayList 作为共享变量的话,是线程不安全的,推荐我们自己加锁或者使用 Collections.synchronizedList 方法,其实 JDK 还提供了另外一种线程安全的 List,叫做 CopyOnWriteArrayList,这个 List 具有以下特征:

  • 线程安全的,多线程环境下可以直接使用,无需加锁
  • 通过锁 + 数组拷贝 + volatile 关键字保证了线程安全
  • 每次数组操作,都会把数组拷贝一份出来,在新数组上进行操作,操作成功之后再赋值回去
1、迭代

在 CopyOnWriteArrayList 类注释中,明确说明了,在其迭代的过程中,即使数组的原值被改变了,也不会抛出 ConcurrentModificationException 异常,其根源在于数组的每次变动,都会生成新的数组,不会影响老数组,迭代过程中,根本就不会发生迭代数组的变动。

2、新增
  public void add(int index, E element) {
        synchronized (lock) {
            Object[] es = getArray();
            int len = es.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException(outOfBounds(index, len));
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(es, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(es, 0, newElements, 0, index);
                System.arraycopy(es, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        }
    }

add 到指定位置时,是需要拷贝两次的,这里有一个问题,都已经加锁了,为什么还需要拷贝数组呢,而不是在原来数组上面进行操作呢,原因主要为:

  • volatile关键字修饰的是数组,如果我们简单的在原来数组上修改其中某几个元素的值,是无法触发可见性的,我们必须通过修改数组的内存地址才行,也就说要对数组进行重新赋值才行
  • 在新的数组上进行拷贝,对老数组没有任何影响,只有新数组完全拷贝完成之后,外部才能访问到,降低了在赋值过程中,老数组数据变动的影响
3、批量移除:

在进行批量移除的时候,并不会直接对数组中的元素进行挨个删除,而是先对数组中的值进行循环标记判断,把我们不需要删除的数据放到临时数组中,最后临时数组中的数据就是我们不需要删除的数据。

这个和 ArrayList 的批量删除的思想类似,所以我们在需要删除多个元素的时候,最好都使用这种批量删除的思想,而不是采用在 for 循环中使用单个删除的方法,单个删除的话,在每次删除的时候都会进行一次数组拷贝,很消耗性能。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值