☛我们可以看到List的已知实现类很多,在我们的学习、工作中,常见的、常用的有ArrayList、Vector、LinedList、Stack这四个类。似乎在开发过程,我们大多时候就是用到List,就会想到ArrayList,大家有没有这种感想,源码不熟练,就会只知其然不知所以然,接下来我会从上面说的四个实现List接口的常用类浅谈下它们的区别,希望这次分享能够给大家带来一定的收获!
java.util 接口 List
- 所有超级接口:
Collection, Iterable
- 所有已知实现类:
AbstractList, AbstractSequentialList, ArrayList, AttributeList, CopyOnWriteArrayList, LinkedList, RoleList, RoleUnresolvedList, Stack, Vector
①ArrayList:是List 接口的大小可变数组的实现。(动态数组)
②Vector:可以实现可增长的对象数组。与数组一样,它包含了可使用整数索引进行访问的组件。(动态数组)
③LinedList:是List 接口的链接列表实现。(双链表)
④Stack:它表示后进先出(LIFO)的对象堆栈。(栈)
★相同点:它们两个底层都是动态数组,即物理结构都是数组。
☆不同点:
①Vector是JDK1.0就已经具备了,线程是安全的,Vector支持旧版的迭代器Enumeration迭代器,当然也支持新版的Iterator迭代器;Vector扩容机制是2倍,初始容量是10。
由 Vector 的 iterator 和 listIterator 方法所返回的迭代器是快速失败的:如果在迭代器创建后的任意时间从结构上修改了向量(通过迭代器自身的 remove 或 add 方法之外的任何其他方式),则迭代器将抛出 ConcurrentModificationException。因此,面对并发的修改,迭代器很快就完全失败,而不是冒着在将来不确定的时间任意发生不确定行为的风险。Vector 的 elements 方法返回的 Enumeration 不是 快速失败的。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在不同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出
ConcurrentModificationException
。因此,编写依赖于此异常的程序的方式是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测 bug。从 Java 2 平台 v1.2 开始,此类改进为可以实现
List
接口,使它成为 Java Collections Framework 的成员。与新 collection 实现不同,Vector
是同步的。
②ArrayList是JDK1.2有的,线程不安全,ArrayList只支持Iterator迭代器。ArrayList扩容机制是1.5倍,初始容量是10,扩容频率比较高,但节省内存。
每个
ArrayList
实例都有一个容量。该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。随着向 ArrayList 中不断添加元素,其容量也自动增长。并未指定增长策略的细节,因为这不只是添加元素会带来分摊固定时间开销那样简单。在添加大量元素前,应用程序可以使用
ensureCapacity
操作来增加ArrayList
实例的容量。这可以减少递增式再分配的数量。**注意,此实现不是同步的。**如果多个线程同时访问一个
ArrayList
实例,而其中至少一个线程从结构上修改了列表,那么它必须 保持外部同步。(结构上的修改是指任何添加或删除一个或多个元素的操作,或者显式调整底层数组的大小;仅仅设置元素的值不是结构上的修改。)这一般通过对自然封装该列表的对象进行同步操作来完成。如果不存在这样的对象,则应该使用Collections.synchronizedList
方法将该列表“包装”起来。这最好在创建时完成,以防止意外对列表进行不同步的访问:List list = Collections.synchronizedList(new ArrayList(...));
▲于此之前,我们应该注意到StringBuffer(线程安全的,JDK1.0)和StringBuilder(线程不安全的,JDK1.5)这个类,在单线程情况下,我们一般认为StringBuilder效率更高。
◆小结:为什么JDK版本在不断升级,实现类在演变的过程中线程却不安全?
其实也不难回答,效率提高了,一些场景,单线程的处理也比较多,所以我们还是需要这样的类!
①动态数组的物理结构:数组。
②LinedList的物理结构:双向链表。
代码演示01:
//单向链表的结点
public class OneLinkedList {
private Node<E> head; //头结点
private int total;//结点数
//静态内部类
private staic class Node<E>{
E element; //元素
Node<E> next;//下一个元素的地址
}
}
代码演示02:
//双向链表的结点
class DoubleLinkedList{
private Node<E> first; //头结点
private Node<E> last;//尾结点
private int size;//双链表的结点大小
private static class Node<E>{
Node<E> prev;//上一个结点的地址
E element;//元素(数据)
Node<E> next;//下一个结点的地址
}
}
③数组:
(1)可以通过[index]快速的定位到某个元素【优点】;
(2)当插入、删除元素的时候,需要移动元素System.arraycopy(…);
(3)当添加时,还需要自动扩容,当不断扩容时,空闲的元素的越来越多,空间利用率低;
(4)数组需要连续的存储空间,当数组比较大的时候,寻找连续的存储空间比较费劲。
④双链表:
(1)当插入、删除元素时,不需要移动元素,只要修改前后元素的引用(prev、next )关系;
例如:在双链表,插入newNode,在node1和node2之间插入
node1.next = newNode //找到插入结点(newNode)上一个结点(node1)尾指向 存储 结点(newNode)
newNode.prev = node1;//结点(newNode)头指向 存储 插入结点(newNode)上一个结点(node1)
node2.prev = newNode;//找到插入结点(newNode)下一个结点(node2)尾指向 存储 结点(newNode)
newNode.next = node2;//结点(newNode)头指向 存储 插入结点(newNode)下一个结点(node2)
例如:删除node 找到node的前一个 node1,以及node的后一个结点node2
node1.next = node2;//删除结点(node)的前一个结点(node1)尾指向存储 删除结点的后一个结点(node2)地址
node2.prev = node1;//删除结点(node)的后一个结点(node2)头指向存储 删除结点的后一个结点(node1)地址
以下操作,使得node结点彻底称为垃圾,方便被GC回收
node.prev = null;//结点的头指向地址修改为空
node.next = null;//结点的尾指向地址修改为空
node.element = null;//元素修改为空
(2)当不断添加元素时,只是增加结点的个数,不会有多余的空间浪费;
(3)链表的元素的地址不需要连续,因为前后元素的关系不是靠下标,而是通过next,prev来查找即可。这个更灵活;
(4)在查找时,速度比较慢,如果是单链表只能从前往后开始查找如果是双链表,可以从前往后,或从后往前。
(5)因为链表没有下标,如果你调用和[index]相关的方法时,都要从头或从尾现数。LindedList也是List的实现类,List接口中有和[index]相关的方法。
★思考题,假设:LinkedList要实现get(int index)的方法,怎么实现?
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
①如果查询操作多,那么使用ArrayList更快。
② 如果添加,删除等操作多,而且元素的个数非常不确定,但是比较多,那么建议使用LinkedList更好。
LinkedList它因为是双向链表,而且同时还提供了很多方法,能够使得LinkedList又能作为:双端队列,栈,普通的队列等数据结构使用。
①栈结构:先进后出(FILO),或者说 后进先出(LIFO) FILO(First In Last Out)
(1)peek:查看栈顶元素,不弹出
(2)pop:弹出栈
(3)push:压入栈 即添加到链表的头
② 队列:先进先出(FIFO) LinkedList同时实现了List、Queue接口
(1)offer(e):放进去
(2)poll():拉出来
(3)peek():只看不移走
③ 双端队列:可以从队头移走元素,也可以从对尾移走元素 , LinkedList同时实现了List、Queue,Deque接口
说明:名称 deque 是“double ended queue(双端队列)”的缩写,通常读为“deck
下表总结了上述 12 种方法:
第一个元素(头部) 最后一个元素(尾部)
抛出异常 特殊值 抛出异常 特殊值
插入 addFirst(e) offerFirst(e) addLast(e) offerLast(e)
移除 removeFirst() pollFirst() removeLast() pollLast()
检查 getFirst() peekFirst() getLast() peekLast()
Stack是Vector的子类
比List多了几个方法
(1)peek:查看栈顶元素,不弹出
(2)pop:弹出栈
(3)push:压入栈 即添加到链表的头
建议,如果想要使用堆栈的数据结构来解决问题,建议使用ArrayQueue或LinkedList,而不是Stack。
推荐阅读往期博文:
•建议收藏|JavaSE集合篇#Collection&Map等系列#结构关系图解
•JavaSE集合篇#Set之实现类HashSet&TreeSet&LinkedHashSet浅析
•JavaSE集合篇#Map集合之实现类HashMap&Hashtable&TreeMap&LinkedHashMap&Properties浅析
☝上述分享来源个人总结,如果分享对您有帮忙,希望您积极转载;如果您有不同的见解,希望您积极留言,让我们一起探讨,您的鼓励将是我前进道路上一份助力,非常感谢!我会不定时更新相关技术动态,同时我也会不断完善自己,提升技术,希望与君同成长同进步!
☞本人博客:https://coding0110lin.blog.csdn.net/ 欢迎转载,一起技术交流吧!