List<E>之实现类ArrayList&Vector&LinedList&Stack浅析

☛我们可以看到List的已知实现类很多,在我们的学习、工作中,常见的、常用的有ArrayList、Vector、LinedList、Stack这四个类。似乎在开发过程,我们大多时候就是用到List,就会想到ArrayList,大家有没有这种感想,源码不熟练,就会只知其然不知所以然,接下来我会从上面说的四个实现List接口的常用类浅谈下它们的区别,希望这次分享能够给大家带来一定的收获!

java.util 接口 List

  • 所有超级接口:

Collection, Iterable

  • 所有已知实现类:

AbstractList, AbstractSequentialList, ArrayList, AttributeList, CopyOnWriteArrayList, LinkedList, RoleList, RoleUnresolvedList, Stack, Vector

1.常见的四个实现类

①ArrayList:是List 接口的大小可变数组的实现。(动态数组)

②Vector:可以实现可增长的对象数组。与数组一样,它包含了可使用整数索引进行访问的组件。(动态数组)

③LinedList:是List 接口的链接列表实现。(双链表)

④Stack:它表示后进先出(LIFO)的对象堆栈。(栈)

2.ArrayList与Vector区别在哪?

★相同点:它们两个底层都是动态数组,即物理结构都是数组。

☆不同点:

①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版本在不断升级,实现类在演变的过程中线程却不安全?

其实也不难回答,效率提高了,一些场景,单线程的处理也比较多,所以我们还是需要这样的类!

3.动态数组(ArrayList、Vector)与LinedList区别在哪?

①动态数组的物理结构:数组。

②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;
    }
}	
4.如何选择动态数组(ArrayList、Vector)与链表(LinedList)?

①如果查询操作多,那么使用ArrayList更快。
② 如果添加,删除等操作多,而且元素的个数非常不确定,但是比较多,那么建议使用LinkedList更好

5.链表(LinedList)提供的方法

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()

6.Stack类

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/  欢迎转载,一起技术交流吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值