List接口的实现类
List表示有序的集合(元素可以重复),根据索引来寻找元素,放入其中的元素的存储顺序和放入顺序是一致的。
ArrayList
0.继承自AbstractList,拥有通用的方法如Iterator迭代器。实现了List接口。
1.底层是transient Object[] elementData
数组。可以看到默认大小是10。
2.不同的初始化方式,有一点区别。
// 未指定,默认是10。构造的数组大小为0,要等到放入第一个元素时才会扩容成10个。
List<String> list1 = new ArrayList<>();
// 构造的数组大小为0。
List<String> list2 = new ArrayList<>(0);
// 构造的数组大小为14
List<String> list3 = new ArrayList<>(14);
3.数组扩容
默认初始化的内部数组大小是10,当放入第11个元素时会进行第一次扩容:newCapacity = oldCapacity + oldCapacity >> 1
,也就是变成原来的1.5倍。(所以默认情况下,放入第11个元素时,扩容成15个;放入第16个元素时,扩容成22个;放入第23个元素时,扩容成33个。)
数组扩容的操作是进行数组的复制,所以扩容消耗资源,应该尽量先指明所需的容量,来减少扩容操作。对于已经存在的ArrayList,在放入大量元素前,可以手动进行扩容:ensureCapacity(capacity)
方法来重新设置内部数组的大小。
4.有一个骚操作,把ArrayList转化成对象数组:
// 调用toArray方法,传入对象数组接收,返回Object[]数组
Object[] objects2 = appleList.toArray(new Apple[0]);
// 类型转换
Apple[] apples3 = (Apple[]) objects2;
5.ArrayList的add(int index, Object obj)
和remove(int index) / remove(Object obj)
,即随机的增加、删除操作都会进行内部数组的复制,所以ArrayList只适合顺序插入、删除,随机访问get(int index)
,不适合随机增加、删除操作多的场景。
ArrayList总结
1.初始化时应该指明容量,避免多次的扩容操作。如果已经分配了容量但是不够用,建议先手动扩容大小后再添加元素。
2.使用场景应该是随机查找比较多而随机增加、删除操作比较少的场景。
LinkedList
1.继承自AbstractSequentialList,拥有通用的方法如iterator。实现List接口。实现Queue接口,拥有队列的特性;实现Deque接口,拥有双端队列的特性。
2.LinkedList内部的节点。拥有前后指针,实现的是双端队列的性质。
内部的私有属性,存储了链表的节点个数以及保存了链表的头尾指针。
3.因为LinkedList实现了Queue接口、Deque接口,所以它既能作为队列也能作为堆栈来使用。又因为实现了List接口,所以又是有序的Collection。这意味着它有3种数据结构的作用,我们应该在正确的场景下正确语义化使用,这表示我们要使用合适的接口来声明LinedList。(注意双端队列Deque接口提供了传统Stack的操作方法声明,所以它可以作为堆栈Stack使用)
4.对于LinkedList的随机访问操作,它内部有一个优化。如果index<(size/2),就从头部开始查找;否则从尾部开始查找。
LinkedList总结
1.使用场景应该是随机增加、删除操作多的情况,而随机访问操作少的情况。
2.LinkedList有3种数据结构的身份,我们应该在正确的场景下进行正确的接口声明,并且使用与之对应的语义化方法来操作LinkedList。我们在使用栈stack结构的时候可以使用LinkedList,在使用队列Queue的时候可以使用LinkedList,在使用有序集合(链表)的时候可以使用LinkedList。
Vector
vector作为古老的集合类,是jdk1.2之后才改为实现List接口的。它与ArrayList的性质很像(2者的继承图是一样的),但是其内部方法都使用了synchronized修饰来保证线程安全,这使得对Vector的操作在多线程下变成了串行操作。要注意的是,它的实现是使用synchronized修饰整个方法,而不是方法内部的某段代码,这使得其锁的粒度特别大,效率十分低!
Vector已经不推荐使用了,单线程下我们建议选用ArrayList,多线程下我们建议选用java.util.concurrent包下的CopyOnWriteArray或使用Collections.synchronizedList(ArrayList list)修饰过的ArrayList。
列举synchronized修饰的add、get操作:
1.默认初始化大小为10,扩容操作时,如果构造时指明了增大的容量,则增加;否则默认变成原来的2倍。
Stack
Stack作为Vector的子类,可用于实现堆栈。但是不建议使用,而是使用Deque接口的实现类如LinkedList、ArrayList来替代堆栈结构。
为什么不使用Stack,而推荐使用LinkedList呢?原因还是因为它继承自Vector,所以它的栈相关的操作也是synchronized修饰的!所以单线程下,我们还是推荐使用LinkedList所能实现的栈结构;多线程下则可以考虑java.util.concurrent包下的ConcurrentLinkedDeque所能实现的并发栈结构或Collections.synchronizedList(LinkedList list)修饰的LinkedList。
当然,我们也可以使用ArrayDeque类!
讲讲List集合对应的并发类
- ArrayList对应的,java.util.concurrent.CopyOnWriteArrayList类和Collections.synchronizedList(ArrayList list)所修饰的类。
- LinkedList对应的,就是Collections.synchronizedList(LinkedList list)所修饰的类。
- 要注意的是,到了并发集合(java.util.concurrent包)这一块,都是按照接口性质设置的并发类。所以应该讲成List接口对应的并发类是java.util.concurrent.CopyOnWriteArrayList类。
java.util.concurrent包下的集合并发类与Collections.synchronizedList()等方法装饰的类有什么不同?
- 先讲一下Vector这个线程安全的List类。其线程安全的实现方式是对所有操作都加上了synchronized关键字,其锁的粒度是整个方法,这种方式严重影响效率,使得程序串行进行。
- 而Collections.synchronizedList等方法,是采用了装饰器的模式来返回一个包装类。以Collections.synchronizedList(ArrayList list)为例,返回的包装类的部分操作如add、get、remove方法是在方法内部的代码块加上了synchronized关键字,这使得锁的粒度较小!
-
而java.util.concurrent包下的CopyOnWriteArrayList,其写操作是写时复制(就和名字一样),通过可重入锁显式加锁来达到同步互斥的目的。而且每次新加入元素,都复制原数组一份,然后对新数组进行增加操作,然后在替换原引用,这就达到了写入分离。(删除操作同样的道理)这里很重要,下面要讲一下get方法!
get方法没有任何加锁同步操作!这里就很有趣了!根据对写入操作的分析,如果并发时一个线程在写入,另外一个线程在读取,那么写入未完成之前,读取操作所读到的数组是原先的数组!
所以,这正是CopyOnWrite容器的缺点:CopyOnWrite只能保证数据最终的一致性,不能保证数据的实时一致性。并发情况下,某个线程读取到的数据可能是旧数据!
其次,对内存有消耗,如果多个线程并发执行,那么数组复制时需要新数组来保存,就占用了2份内存!如果数组占用的内存较大,就会引发频繁的垃圾回收行为,降低性能。
所以对于CopyOnWrite容器来说,它适合读操作频繁,而写操作少的并发场景,比如说数据的缓存!
List集合图
下面对Collection集合下的List有序集合进行一个类图的整合:
可以看到的是ArrayList和Vector的继承图是一致的!它们的区别就是内部实现不同,所以我们可以大胆地放弃Vector类了。而LinkedList较之于ArrayList,多实现了Deque接口,这使得它不仅具备了List的特性,还拥有双端队列的特性,可以拿来做栈、队列等结构!
Collection接口继承自Iterable,这意味着所有的集合类都可以返回迭代器进行遍历。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。