Collection是咱们日常开发中运用频率十分高的调集,它的主要完成有List
和Set
,区别是List
是有序的,元素可以重复;Set
是无序的,元素不可以重复,咱们简略看下承继联系:
-
List的完成类主要线程不安全的
ArrayList
和LinkedList
以及线程安全的CopyOnWriteArrayList
; -
Set的完成类主要有线程不安全的
HashSet
和TreeSet
以及线程安全的CopyOnWriteArraySet
。
个人觉得,以上调集组件类其实并不难,所以我不打算分析源码,其间HashSet
和TreeSet
底层运用分别是HashMap和TreeMap, 所以只需看下之前的文章就比较简单理解了。
1.ArrayList VS LinkedList
ArrayList:
- 底层是经过
数组
(内存地址接连)完成的,直接可以经过下标找到目标元素,时刻复杂度是O(1),而LinkedList
需求移动指针遍历每个元素,时刻复杂度是O(N),所以 Arraylist查找元素的速度比Linkedlist
快. -
Arraylist在新增和删去元素时,或许扩容和仿制数组。而
Linkedlist
实例化对象只需求修正指针即可. - Arraylist 或许会形成一定空间的浪费,由于它要预先拓荒空间
- 默许的初始容量是10个,每次扩容为本来的1.5倍(当数组满了才会扩容,不像HashMap有扩容因子)
LinkedList:
- 底层是经过
线性表(链表)
(内存空间不接连)来完成的,是一个双向链表 - LinkedList不支持高效的随机拜访,它需求移动指针遍历每个元素
- 没有初始容量,没有扩容机制
时刻复杂度比照:
操作 | 数组 | 链表 | 阐明 |
---|---|---|---|
随机拜访 | O(1) | O(N) | 随机拜访数组比链表快 |
头部刺进 | O(N) | O(1) | 刺进:链表快于数组,由于数组要移动其他元素的方位 |
头部删去 | O(N) | O(1) | 删去:链表快于数组,由于数组要移动其他元素的方位 |
尾部刺进 | O(1) | O(1) | 刺进速度相同快,可是数组有或许是触发扩容动作 |
尾部删去 | O(1) | O(1) | 删去速度相同快 |
指定方位刺进 | O(N) | O(N) | 数组:在第几个元素后面刺进,后面的元素需求向后移动,链表:需求先找到第几个元素,然后修正指针指向操作 |
总体来说:ArrayList查询速度快,LinkedList刺进删去速度快。
运用场景:
- 假如应用程序对数据有较多的随机拜访,
ArrayList
性能要优于LinkedList
; - 假如应用程序有更多的刺进或许删去操作,较少的随机拜访,
LinkedList
性能要优于ArrayList
; - 不过
ArrayList
的刺进,删去操作也不一定比LinkedList
慢,假如在ArrayList
接近结尾的地方刺进,那么ArrayList
只需求移动较少的数据(或许无需移动数据),此时二者耗时几乎差不多(LinkedList
是双向链表,可以快速定位头尾部的节点)
2.HashSet VS TreeSet
HashSet 和 TreeSet 都是元素不能重复的调集,其间TreeSet具有排序的功能。
HashSet源码分析:
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
/**
*
* HashSet底层用的便是HashMap,只不过只运用了HashMap的key
*/
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* 无参构造器:内部创建一个HashMap
*/
public HashSet() {
map = new HashMap<>();
}
/**
* 参数是调集
*/
public HashSet(Collection<? extends E> c) {
map = new HashMap<>(Math.max((int) (c.size()/0.75f) + 1, 16));
addAll(c);
}
/**
* 指定容量的构造器,这个容量将传递给HashMap
*/
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
/**
* add元素,发现它确实是增加到了HashMap中
*/
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
//.....省掉其他代码
}
不难发现,
HashSet
底层运用的便是HashMap,只不过是只运用了HashMap
的key
,依据HashMap
的特点,key假如相同,则旧值掩盖新值,所以达到去重的效果。
TreeSet源码分析:
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
/**
* 内存真正存储元素的组件:TreeMap, 只不过是只运用了key
*/
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* 空参数构造器:其内部创建了TreeMap, 可是只运用TreeMap的key
*/
public TreeSet() {
this(new TreeMap<E,Object>());
}
/**
* 可以传入一个比较器,这个比较器将交给TreeMap
*/
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
/**
* 增加元素:将增加到TreeMap中
*/
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
不难发现,
TreeSet
底层运用的是TreeMap,只不过是只运用了TreeMap
的key
,依据TreeMap的特点,默许是依照天然排序
(key
必须要完成Comparable
接口),或许指定比较器Comparator
,自定义比较规矩。
3.线程安全的CopyOnWriteArrayList
CopyOnWriteArraySet 底层运用的也是
CopyOnWriteArrayList
先简略总结下它的底层原理:
-
CopyOnWriteArrayList
内部也是经过数组来完成的,在向CopyOnWriteArrayList
增加元素时,会仿制一个新的数组,写操作在新数组上进行,读操作在原数组上进行(写时仿制的思维) -
写操作会加锁
,避免并发写入形成数据丢失的问题 - 写操作完毕后会把原数组指向新数组
-
CopyOnWriteArrayList
答应在进行写操作的同时来读取数据,大大提高了读的性能,因此适合读多写少的场景(写多读少的话,会大量仿制数组,十分消耗内存),可是CopyOnWriteArrayList
或许读到的数据不是最新的数据,所以不适合实时性要求高的场景(数据不共同的问题)。
只能确保终究共同,不能确保实时共同
咱们看下源码:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/**
* 经过 ReentrantLock 来确保写时的线程安全
*/
final transient ReentrantLock lock = new ReentrantLock();
/**
* 底层依然是运用数组来存储数据
*/
private transient volatile Object[] array;
/**
* 读取数据,没有加锁,直接读就可以
*/
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
/**
* 增加元素(写)
*
* 1.首要加锁
* 2.获取原数组
* 3.依据原数据仿制一个新的数组
* 4.然后对新数组进行写操作(这中心读的话,依然读的是原数组)
* 5.将新数组赋给原数组
*/
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
/**
* 删去元素(写):和 add相同
*
* 1.首要加锁
* 2.获取原数组
* 3.依据原数据仿制一个新的数组
* 4.然后对新数组进行写操作(这中心读的话,依然读的是原数组)
* 5.将新数组赋给原数组
*/
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();
}
}
}
源码并不复杂,便是写时仿制的思维,咱们简略用一张图来展现下:
好了,关于Collection调集下常用的组件就分析到这里吧,比照Map,这些就显得比较简略了,源码也很简单理解。
限于作者水平,文中难免有错误之处,欢迎指正,勿喷,感谢感谢