List这种数据结构可以说在我们的实际工作中经常会使用到,最常见的就是ArrayList和LinkedList两种实现类了,两种结构的操作看似相似,但是原理却差别巨大。关于这两者的差别,小编特意总结了一些干货和各位读者们进行分享。
ArrayList和LinkedList的区别?
通过阅读util包里面的源码可以很容易的看出两者的大致区别:
ArrayList是一种具有动态扩容特性且基于数组基础的数据结构,而LinkedList则是一种基于链表的数据结构。
在进行元素查找的时候适合用ArrayList进行操作,在进行元素的添加和删除的时候适合用LinkedList。由于ArrayList是采用数组作为数据结构的,因此在进行查找的时候只需要根据下标的索引进行判断即可。
LinkedList数据结构则是采用链表的结构进行设计的,因此在查找的时候需要进行逐一比较,所以效率会比较慢(并非是全链查询,而是采用了折半搜索的方式来进行优化)。在添加或者删除元素的时候,由于ArrayList需要进行元素的位置移动,而链表的移动和删除只需要将链表节点的头尾指针进行修改即可。
ArrayList的动态扩容有什么特点?
当我们在进行ArrayList的插入元素时候,相应的元素会被插入到动态数组里面,但是由于数组本身所能存储的数据量是有限制的,因此在插入数据的时候,需要进行相应的动态扩容,在看源码的时候,可以看到相应的代码部分:
add操作源码:
1 /** 2 * Appends the specified element to the end of this list. 3 * 4 * @param e element to be appended to this list 5 * @return <tt>true</tt> (as specified by {@link Collection#add}) 6 */ 7 public boolean add(E e) { 8 ensureCapacityInternal(size + 1); // Increments modCount!! 9 elementData[size++] = e; 10 return true; 11 } 复制代码
核心扩容部分的实现:
1 private void ensureCapacityInternal(int minCapacity) { 2 if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { 3 minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); 4 } 5 6 ensureExplicitCapacity(minCapacity); 7 } 8 9 private void ensureExplicitCapacity(int minCapacity) { 10 modCount++; 11 12 // overflow-conscious code 13 if (minCapacity - elementData.length > 0) 14 grow(minCapacity); 15 } 复制代码
ArrayList默认数组的大小为10,扩容的时候采用的是采用移位运算
11 int newCapacity = oldCapacity + (oldCapacity >> 1); 复制代码
这里也可以看出ArrayList的扩容因子为1.5。(4>>1 就相当于4除以2 即缩小了一半)。
什么时候会选择使用ArrayList
这又是一个大多数面试者都会困惑的问题。多数情况下,当遇到访问元素比插入或者是删除元素更加频繁的时候,应该使用ArrayList。另外一方面,当在某个特别的索引中,插入或者是删除元素更加频繁,或者压根就不需要访问元素的时候,不妨考虑选择LinkedList。
这里的主要原因是,在ArrayList中访问元素的最糟糕的时间复杂度是O(1),而在LinkedList中可能就是O(n)了。在ArrayList中增加或者删除某个元素时候,如果触发到了扩容机制,那么底层就会调用到System.arraycopy方法,如果有兴趣深入挖掘jdk源码的话,会发现这是一个本地调用方法,被native修饰,该方法会直接通过内存复制,省去了大量的数组寻址访问等时间,但是相比于LinkedList而言,在频繁的修改元素的情况下,选用LinkedList的性能会更加好一点。
1 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { 2 @SuppressWarnings("unchecked") 3 T[] copy = ((Object)newType == (Object)Object[].class) 4 ? (T[]) new Object[newLength] 5 : (T[]) Array.newInstance(newType.getComponentType(), newLength); 6 //复制集合 7 System.arraycopy(original, 0, copy, 0, 8 Math.min(original.length, newLength)); 9 return copy; 10 } 复制代码
如果读者有去学习过jvm的话,应该会对“内存碎片“这个名词比较熟悉。基于数组结构的数据在存储信息的时候都需要有连续的内存空间,所以如果当内存碎片化情况较为严重的时候,可能在使用ArrayList的时候会有OOM的异常抛出。
如何复制某个ArrayList到另一个ArrayList中去?
1.使用clone()方法,比如ArrayList newArray = oldArray.clone();
2.使用ArrayList构造方法,比如:ArrayList myObject = new ArrayList(myTempObject);
3.使用Collection的copy方法。
关于list集合的拷贝问题在面试中可能还会引申出深拷贝和浅拷贝的对比,小编后期会去专门写一篇笔记做讲解。
请说说ArrayList、Vector和LinkedList的区别
相同点
这三者都是单列集合Collection下List集合的实现类,所以他们的共同点,元素有序,允许重复元素 。
不同点
1.ArrayList和Vector底层都是数组实现,这样的实现注定查找快、增删慢 。
2.Vector支持线程同步,是线程访问安全的,ArrayList线程不安全 。
3.LinkedList底层是链表结构,查找元素慢、增删元素速度快,线程不安全。
Collection和Collections的区别
Collection是单列集合的父接口,翻译过来则是指容器的意思,常见的Set接口,List接口都是属于Collection接口下边的内容。
Collections是工具类,提供了关于集合的常用操作,例如,排序、二分法查找、反转元素等。
常用的一些Collections的api:
1 Collections.sort(list); 2 Collections.emptyList(); 3 Collections.binarySearch(list,"your key"); 4 Collections.copy(list,list1); 5 System.arraycopy(list,1,list1,2,1); 复制代码
其实java里面已经默认提供好了很多有用的工具类给开发者们,我们除了要熟悉里面的api使用之外,还需要了解各种api的使用策略以及不同场景下的使用优势。
modCount参数的意义
在ArrayList设计的时候,其实还包含有了一个modCount参数,这个参数需要和expectedModCount 参数一起使用,expectedModCount参数在进行修改的时候会被modCount进行赋值操作,当多个线程同时对该集合中的某个元素进行修改之前都会进行expectedModCount 和modCount的比较操作,只有当二者相同的时候才会进行修改,两者不同的时候则会抛出异常。这个机制也被称之为fail-fast机制。
COW容器
jdk1.5之前,由于常用的ArrayList并不具有线程安全的特性,因此在1.5之后的并发包里面出现了CopyOnWrite容器,简称为COW。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
并发包juc里面的CopyOnWriteArrayList中,核心原理主要是通过加入了ReentrantLock来保证线程安全性,从而解决了ArrayList的线程安全隐患问题。
相应的add操作源码如下
1 /** 2 * Appends the specified element to the end of this list. 3 * 4 * @param e element to be appended to this list 5 * @return {@code true} (as specified by {@link Collection#add}) 6 */ 7 public boolean add(E e) { 8 final ReentrantLock lock = this.lock; 9 lock.lock(); 10 try { 11 Object[] elements = getArray(); 12 int len = elements.length; 13 Object[] newElements = Arrays.copyOf(elements, len + 1); 14 newElements[len] = e; 15 setArray(newElements); 16 return true; 17 } finally { 18 lock.unlock(); 19 } 20 } 复制代码
如果读者对于ArrayList的具体实现充满好奇心的话,不妨可以试着自己边看源码边动手写一套相应的数据结构,相信坚持下来一定会大有收获。