集合(2)List——LinkedList和ArrayList(Vector、Stack)

目录

 

一、List简介

二、链表(LinkedList,listIterator/add/previous/remove/set/get)

三、数组列表(ArrayList)

四、四种List总结

ArrayList:

LinkedList:

Vector和Stack:


一、List简介

List中元素是有序的可重复,可以对列表中每个元素的插入位置进行精确地控制。

除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素,还能向前或向后遍历。  

实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。

二、链表(LinkedList,listIterator/add/previous/remove/set/get)

有很多示例已经使用了数组以及动态的ArrayList类。然而,数组和数组列表都有一个重大的缺陷。这就是从数组的中间位置删除一个元素要付出很大的代价,其原因是数组中处于被删除元素之后的所有元素都要向数组的前端移动,在数组中间的位置上插入一个元素也是如此。

链表(linked list)解决了这个问题。数组在连续的存储位置上存放对象引用,但链表却将每个对象存放在独立的节点中。每个节点还存放着序列中下一个节点的引用。在Java程序设计语言中,所有链表实际上都是双向链接的——即每个节点还存放着指向前驱节点的引用。(表头head,不包含数据,不算节点个数

从链表中间删除一个元素是一个很轻松的操作,即需要对被删除元素附近的节点更新一下即可。 在下面的代码示例中,先添加3个元素,然后再将第2个元素删除:

List<String> staff = new LinkedList<String>(); // LinkedList implements List

staff.add("Amy");

staff.add("Bob");

staff.add("Carl");

Iterator iter = staff.iterator();

String first = iter.next(); // visit first element

String second = iter.next(); // visit second element

iter.remove(); // remove last visited element

但是,链表与泛型集合之间有一个重要的区别链表是一个有序集合。每个对象的位置十分重要。LinkedList.add方法将对象添加到链表的尾部。但是,常常需要将元素添加到链表的中间。由于迭代器是描述集合中位置的,所以这种依赖于位置的add方法将由迭代器负责只有对自然有序的集合使用迭代器添加元素才有实际意义。例如,下一节将要讨论的集(set)类型,其中的元素完全无序。因此,在Iterator接口中就没有add方法。相反地,集合类库提供了子接口ListIterator,其中包含add方法:

interface ListIterator<E> extends Iterator<E> {

    void add(E element);

}

与Collection.add不同,这个方法不返回boolean类型的值,它假定添加操作总会改变链表。add方法在迭代器位置之前添加一个新对象。例如,下面的代码将越过链表中的第一个元素,并在第二个元素之前添加“Juliet”:

List<String> staff = new LinkedList<String>();

staff.add("Amy");

staff.add("Bob");

staff.add("Carl");

ListIterator iter = staff.listIterator();

iter.next(); // skip pass first element

iter.add("Juliet");

如果多次调用add方法,将按照提供的次序把元素添加到链表中。它们被依次添加到迭代器当前位置之前。另外,ListIterator接口有两个方法,可以用来反向遍历链表

E previous();

boolean hasPrevious();

 与next方法一样,previous方法返回越过的对象。

当用一个刚刚由iterator方法返回,并且指向链表表头的迭代器调用add操作时,新添加的元素将变成列表的新表头。当迭代器越过链表的最后一个元素时(即hasNext返回false),添加的元素将变成列表的新表尾。如果链表有n个元素,有n+1个位置可以添加新元素。这些位置与迭代器的n+1个可能的位置相对应。例如,如果链表包含3个元素,A、B、C,就有4个位置(标有|)可以插入新元素: |ABC   A|BC   AB|C  ASC|

ps:在用“光标”类比时要格外小心。remove操作与BACKSPACE键的工作方式不太一样。在调用next之后,remove方法确实与BACKSPACE键一样删除了迭代器左侧的元素。但是,如果调用previous就会将右侧的元素删除掉,并且不能在同一行中调用两次remove。add方法只依赖于迭代器的位置,而remove方法依赖于迭代器的状态。

ppsremove操作删除的是迭代器刚刚越过的那个元素。previous()方法和next()方法一样,都是返回迭代器刚刚越过的那个元素,迭代器都会移动,只不过previous()方法迭代器往前移动,next()方法迭代器往后移动。

set方法用一个新元素取代调用next或previous方法返回的上一个元素。例如,下面的代码将用一个新值取代链表的第一个元素:

Listlterator<String> iter = list.listIterator();

String oldValue = iter.next(); // returns first element

iter.set(newValue); // sets first element to newValue

可以想像,如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。例如,一个迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不应该再使用。链表迭代器能够检测到这种修改。如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException异常。例如,看一看下面这段代码:

List<String> list = ...;

ListIterator<String> iterl = list.listIterator();

ListIterator<String> iter2 = list.listIterator();

iter1.next();

iter1.remove();

iter2.next(); // Throws ConcurrentModificationException

为了避免发生并发修改的异常,请遵循下述简单规则:可以根据需要给容器附加许多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的迭代器。

 有一种简单的方法可以检测到并发修改的问题。集合可以跟踪改写操作(诸如添加或删除元素)的次数。每个迭代器都维护一个独立的计数值。在每个迭代器方法的开始处检査自己改写操作的计数值是否与集合的改写操作计数值一致。如果不—致,抛出一个ConcurrentModificationException。

ps:对于并发修改列表的检测有一个奇怪的例外。链表只负责跟踪对列表的结构性修改,例如,添加元素、刪除元素。set操作不被视为结构性修改。可以将多个迭代器附加给一个链表,所有的迭代器都调用set方法对现有节点的内容进行修改。在本章后面所介绍的Collection类的许多算法都需要使用这个功能

链表不支持快速地随机访问。如果要査看链表中第n个元素,就必须从头开始,越过n-1个元素。没有捷径可走。鉴于这个原因,在程序需要采用整数索引访问元素时,程序员通常不选用链表

尽管如此,LinkedList类还是提供了一个用来访问某个特定元素的get方法:

LinkedList<String> list = ...;

String obj = list.get(n);

这个方法的效率并不太高。如果发现自己正在使用这个方法,说明有可能对于所要解决的问题使用了错误的数据结构。

绝对不应该使用这种让人误解的随机访问方法来遍历链表。下面这段代码的效率极低:

for (int i = 0; i < list.size(); i++)

     doSomethingWith(list.get(i));

每次查找一个元素都要从列表的头部重新开始搜索。LinkedList对象根本不做任何缓存位置信息的操作。

ps:get方法做了微小的优化:如果索引大于size()/2就从列表尾端开始搜索元素

列表迭代器接口还有一个方法,可以告之当前位置的索引。实际上,从概念上讲,由于Java迭代器指向两个元素之间的位置,所以可以同时产生两个索引:nextIndex方法返回下一次调用next方法时返回元素的整数索引,previousIndex方法返回下一次调用previous方法时返回元素的整数索引。当然,这个索引只比nextIndex返回的素引值小1。这两个方法的效率非常高,这是因为迭代器保持着当前位置的计数值。最后需要说一下,如果有一个整数索引n,list.listIterator(n)将返回一个迭代器,这个迭代器指向索引为n的元素前面的位置。也就是说,调用next与调用list.get(n)会产生同一个元素,只是获得这个迭代器的效率比较低。

 如果链表中只有很少几个元素,就完全没有必要为get方法和set方法的开销而烦恼。但是,为什么要优先使用链表呢?使用链表的唯一理由是尽可能地减少在列表中间插入或删除元素所付出的代价如果列表只有少数几个元素,就完全可以使用ArrayList。

更多:LinkedList详细介绍(源码解析,遍历方式等)和使用示例

三、数组列表(ArrayList)

上一节中,介绍了List接口和实现了这个接口的LinkedList类。List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。有两种访问元素的协议:一种是用迭代器,另一种是用get和set方法随机地访问每个元素。后者不适用于链表,但对数组却很有用。集合类库提供了一种大家熟悉的ArrayList类,这个类也实现了List接口,ArrayList封装了一个动态再分配的对象数组。

ps:对于一个经验丰富的Java程序员来说,在需要动态数组时,可能会使用Vector类。为什么要用ArrayList取代Vector呢?原因很简单:Vector类的所有方法都是同步的,可以由两个线程安全地访问一个Vector对象。但是,如果由一个线程访问Vector,代码要在同步操作上耗费大量的时间。这种情况还是很常见的,而ArrayList方法不是同步的,因此,建仪在不需要同步时使用ArrayList,而不要使用Vector。

更多:ArrayList详细介绍(源码解析、迭代方式等)和使用示例

更多:fail-fast总结(通过ArrayList说明)

四、四种List总结

ArrayList:

  • ArrayList底层使用了Object的数组作为容器去存储数据,允许null元素
  • ArrayList 提供了使用索引的随意访问数据
  • ArrayList 是线程非安全的,效率较高,查询速度高

LinkedList:

  • LinkedList底层使用了链表的数据结构,允许null元素
  • LinkedList随机位置插入、删除数据时比线性表快,遍历比线性表慢。
  • 相对于ArrayList,LinkedList 对于经常需要从 List 中添加或删除元素的场合更为合适。
  • 和ArrayList 一样,LinkedList也是非同步的(unsynchronized)

Vector和Stack:

  • Vector非常类似ArrayList,但是Vector是同步的,效率相对比较低
  • Vector的底层结构也是数组,但是它们对数组的扩容方式不同
  • 当Vector或ArrayList中的元素超过它的初始大小时,Vector默认会将它的容量翻倍,而ArrayList只增加50%的大小,这样ArrayList就有利于节约内存空间。(ArrayList不可以设置扩展的容量,但Vector可以设置)
    即默认情况下,Vector增长原来的一倍,ArrayList增加原来的0.5倍。
  • ArrayList支持序列化,而Vector不支持;即ArrayList有实现java.io.Serializable接口,而Vector没有实现该接口
  • Vector支持通过Enumeration去遍历,而ArrayList不支持

Stack栈继承于Vector,栈的存储特点是后进先出,
它基于动态数组实现的一个线程安全的栈,所以栈是线程安全的。

Vector详解

Stack详解

参考:《Java核心技术 卷I》

https://www.cnblogs.com/skywang12345/p/3308556.html

https://www.cnblogs.com/skywang12345/p/3308807.html

https://www.cnblogs.com/dooor/p/5285487.html

https://www.jianshu.com/p/20a56b81157e

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值