在上一节中,介绍了Java集合的总体情况。从这节開始,将介绍详细的类。这里不单单介绍类的使用方法。还会试图从源代码的角度分析类的实现。这一节将介绍List接口及实现类。即列表中的链表LinkedList和数组列表ArrayList。

1 List接口及抽象类

List接口扩展自Collection接口,这个接口设计了一些适合列表操作的方法。List是一个有序集合。元素能够加入到容器中某个特定的位置。

使用javac编译List.java源代码后,能够使用javap反编译源代码获得接口的详细信息。例如以下是调用后的结果:

  1. Compiled from "List.java"
  2. public interface java.util.List<E> extends java.util.Collection<E> {
  3. public abstract int size();
  4. public abstract boolean isEmpty();
  5. public abstract boolean contains(java.lang.Object);
  6. public abstract java.util.Iterator<E> iterator();
  7. public abstract java.lang.Object[] toArray();
  8. public abstract <T> T[] toArray(T[]);
  9. public abstract boolean add(E);
  10. public abstract boolean remove(java.lang.Object);
  11. public abstract boolean containsAll(java.util.Collection<?>);
  12. public abstract boolean addAll(java.util.Collection<? extends E>);
  13. public abstract boolean addAll(int, java.util.Collection<? extends E>);
  14. public abstract boolean removeAll(java.util.Collection<?>);
  15. public abstract boolean retainAll(java.util.Collection<?>);
  16. public void replaceAll(java.util.function.UnaryOperator<E>);
  17. public void sort(java.util.Comparator<? super E>);
  18. public abstract void clear();
  19. public abstract boolean equals(java.lang.Object);
  20. public abstract int hashCode();
  21. public abstract E get(int);
  22. public abstract E set(int, E);
  23. public abstract void add(int, E);
  24. public abstract E remove(int);
  25. public abstract int indexOf(java.lang.Object);
  26. public abstract int lastIndexOf(java.lang.Object);
  27. public abstract java.util.ListIterator<E> listIterator();
  28. public abstract java.util.ListIterator<E> listIterator(int);
  29. public abstract java.util.List<E> subList(int, int);
  30. public java.util.Spliterator<E> spliterator();
  31. }

List接口提供了这些方法,大部分是Abstract的,但也有一部分不是,这部分方法是JDK 1.8 新增的default方法。比方sort方法。

List接口提供了随机訪问方法,比方get(int)方法,可是List并无论这些方法都某个特定的实现是否高效。

为了避免运行成本较高的随机訪问操作,Java SE 1.4 引入了一个标记接口RandomAccess。

这个接口没有不论什么方法,但能够用来检測一个特定的集合是否支持高效的随机訪问:

  1. if(c instanceof RandomAccess)
  2. {
  3. use random access algorighm
  4. }
  5. else
  6. {
  7. use sequential access algorithm
  8. }

ArrayList就实现了这个接口。

List接口中的例行方法在抽象类AbstractList中实现了,这样就不须要在详细的类中实现,比方isEmpty方法和contains方法等。

这些例行方法比較简单。含义也明显。对于随机訪问元素的类(比方ArrayList),优先继承这个抽象类。

在AbstractList抽象类中。有一个重要的域。叫modCount:

  1. protected transient int modCount = 0;

这个域能够用来跟踪列表结构性改动的次数,什么是结构性改动呢?就是改变列表长度的改动,比方添加、删除等。

对于仅仅改动某个节点的值不算结构性改动。

这个域在后面的迭代器中很实用。

迭代器能够使用这个域来检測并发改动问题,这个问题会在LinkedList类中介绍。

抽象类AbstractSequentialList实现了List接口中的一些方法,对于顺序訪问元素的类(比方LinkedList),优先继承这个抽象类。

2 链表:LinkedList

链表是一个大家很熟悉的数据结构。

链表攻克了数组列表插入和删除元素效率太低的问题,链表的插入和删除就很高效。

链表将每一个对象存放在独立的节点中。

Java中的LinkedList链表,每一个节点除了有后序节点的引用外,另一个前序节点的引用,也就是说。LinkedList是一个双向链表。

LinkedList类有三个域,各自是大小、头结点和尾节点:

  1. transient int size;
  2. transient Node<E> first;
  3. transient Node<E> last;

还有两个构造器,一个无參构造器和一个含參构造器:

  1. public java.util.LinkedList();
  2. public java.util.LinkedList(java.util.Collection<? extends E>);

当中无參构造器构造一个空的链表,含參构造器依据传进来的一个集合构造一个链表。

2.1 Node<E>内部类

LinkedList类中,定义了一个Node<E>内部类来表示一个节点。

这个类的定义例如以下:

  1. private static class Node<E> {
  2. E item;
  3. Node<E> next;
  4. Node<E> prev;
  5.  
  6. Node(Node<E> prev, E element, Node<E> next) {
  7. this.item = element;
  8. this.next = next;
  9. this.prev = prev;
  10. }
  11. }

这是一个静态内部类,也没有对外部的引用。这个类有三个域:值。前序节点的引用,后序节点的引用,也有一个构造方法。定义非常easy。

假设要创建一个Node节点,能够这样:

  1. Node<E> node=new Node<>(pre,item,next);

当中,pre和next各自是前序节点和后序节点的引用。

2.2 链表操作的基本方法

既然是链表。就少不了链表节点的加入与删除。

在LinkedList类中,提供了六个主要的链表操作的方法。这些方法都对链表的结构进行改动,因此会改变AbstractList类中的modCount域,这六个方法例如以下:

  1. private void linkFirst(E);//在链表头部加入给定值的节点作为头结点
  2. void linkLast(E);//在链表尾部加入一个给定值的节点作为尾节点
  3. void linkBefore(E, java.util.LinkedList$Node<E>);//在给定的节点前插入一个节点
  4. private E unlinkFirst(java.util.LinkedList$Node<E>);//删除头结点。并返回头结点的值
  5. private E unlinkLast(java.util.LinkedList$Node<E>);//删除尾节点,并返回尾节点的值
  6. E unlink(java.util.LinkedList$Node<E>);//删除给定的节点

这些方法都是私有的(或包内私有的)。因此能够称为工具方法,LinkedList类中的全部结构性改动操作都是基于这六个方法实现的。

这六个方法都是链表的基本操作。代码比較简单。只是给出实现能够看看源代码实现者的写法,对于自己编程还是有帮助的:

  1. /**
  2. * Links e as first element.
  3. */
  4. private void linkFirst(E e) {
  5. final Node<E> f = first;
  6. final Node<E> newNode = new Node<>(null, e, f);
  7. first = newNode;
  8. if (f == null)
  9. last = newNode;
  10. else
  11. f.prev = newNode;
  12. size++;
  13. modCount++;
  14. }
  15.  
  16. /**
  17. * Links e as last element.
  18. */
  19. void linkLast(E e) {
  20. final Node<E> l = last;
  21. final Node<E> newNode = new Node<>(l, e, null);
  22. last = newNode;
  23. if (l == null)
  24. first = newNode;
  25. else
  26. l.next = newNode;
  27. size++;
  28. modCount++;
  29. }
  30.  
  31. /**
  32. * Inserts element e before non-null Node succ.
  33. */
  34. void linkBefore(E e, Node<E> succ) {
  35. // assert succ != null;
  36. final Node<E> pred = succ.prev;
  37. final Node<E> newNode = new Node<>(pred, e, succ);
  38. succ.prev = newNode;
  39. if (pred == null)
  40. first = newNode;
  41. else
  42. pred.next = newNode;
  43. size++;
  44. modCount++;
  45. }
  46.  
  47. /**
  48. * Unlinks non-null first node f.
  49. */
  50. private E unlinkFirst(Node<E> f) {
  51. // assert f == first && f != null;
  52. final E element = f.item;
  53. final Node<E> next = f.next;
  54. f.item = null;
  55. f.next = null; // help GC
  56. first = next;
  57. if (next == null)
  58. last = null;
  59. else
  60. next.prev = null;
  61. size--;
  62. modCount++;
  63. return element;
  64. }
  65.  
  66. /**
  67. * Unlinks non-null last node l.
  68. */
  69. private E unlinkLast(Node<E> l) {
  70. // assert l == last && l != null;
  71. final E element = l.item;
  72. final Node<E> prev = l.prev;
  73. l.item = null;
  74. l.prev = null; // help GC
  75. last = prev;
  76. if (prev == null)
  77. first = null;
  78. else
  79. prev.next = null;
  80. size--;
  81. modCount++;
  82. return element;
  83. }
  84.  
  85. /**
  86. * Unlinks non-null node x.
  87. */
  88. E unlink(Node<E> x) {
  89. // assert x != null;
  90. final E element = x.item;
  91. final Node<E> next = x.next;
  92. final Node<E> prev = x.prev;
  93.  
  94. if (prev == null) {
  95. first = next;
  96. } else {
  97. prev.next = next;
  98. x.prev = null;
  99. }
  100.  
  101. if (next == null) {
  102. last = prev;
  103. } else {
  104. next.prev = prev;
  105. x.next = null;
  106. }
  107.  
  108. x.item = null;
  109. size--;
  110. modCount++;
  111. return element;
  112. }

2.3 列表迭代器:ListIterator接口

链表是一个有序集合。每一个对象的位置十分重要。LinkedList.add方法仅仅是将节点加到尾部,然而对于链表的操作还有非常大一部分须要将节点加入到链表中间。

因为迭代器是秒数集合中的位置的。所以这样的依赖位置的加入方法将由迭代器负责。

仅仅有对自然有序的集合使用迭代器加入元素才有意义。

比方,对于无序的集合set,在Iterator接口中就没有add方法。相反的,在集合类库中提供了ListIterator接口,当中就有add方法:

  1. interface ListIterator<E> extends Iterator<E>
  2. {
  3. void add(E element);
  4. ...
  5. }

与Collection接口中的add方法不同,这种方法不返回boolean类型的值。由于它假定加入操作总是改变链表。

另外。除了hasNext和next方法,ListIterator接口还提供了以下的两个方法:

  1. E previous();
  2. boolean hasPrevious();

这两个方法用来反向遍历链表。previous也像next一样,返回越过的对象。

LinkedList类的listIterator方法返回一个迭代器对象:

  1. ListIterator<String> iter=list.listIterator();

在介绍接口时我们知道。不能实例化一个接口对象。但能够声明一个接口对象然后引用一个实现了该接口的类的实例。那么listIterator方法返回的就必定是一个类的实例,而这个类也必定实现了这个接口,问题是。这个类是什么?

这个类事实上是LinkedList的一个内部类,即ListItr:

  1. Compiled from "LinkedList.java"
  2. class java.util.LinkedList$ListItr implements java.util.ListIterator<E> {
  3. private java.util.LinkedList$Node<E> lastReturned;
  4. private java.util.LinkedList$Node<E> next;
  5. private int nextIndex;
  6. private int expectedModCount;
  7. final java.util.LinkedList this$0;
  8. java.util.LinkedList$ListItr(java.util.LinkedList, int);
  9. public boolean hasNext();
  10. public E next();
  11. public boolean hasPrevious();
  12. public E previous();
  13. public int nextIndex();
  14. public int previousIndex();
  15. public void remove();
  16. public void set(E);
  17. public void add(E);
  18. public void forEachRemaining(java.util.function.Consumer<? super E>);
  19. final void checkForComodification();
  20. }

上面也是使用javap反编译的结果。能够看到。这个内部类实现了ListIterator接口,并实现了这个接口的方法。

这正是理解迭代器的关键。我们知道,迭代器能够看做是一个位置。这个位置在两个节点的中间,也就是说,对于一个大小为n的链表,迭代器的位置有n+1个:

| a | b | ...| z |

在这个样例中。链表表示26个字母,迭代器的位置就有27个。

这里也是把迭代器形象化为光标。next方法就是光标移到下一个位置,饭后返回刚刚越过的元素,同理previous也是一样。仅仅只是是左移一个位置。然后返回刚刚越过的元素。以下是这两个方法的代码:

  1. public E next() {
  2. checkForComodification();
  3. if (!hasNext())
  4. throw new NoSuchElementException();
  5.  
  6. lastReturned = next;
  7. next = next.next;
  8. nextIndex++;
  9. return lastReturned.item;
  10. }
  11.  
  12. public E previous() {
  13. checkForComodification();
  14. if (!hasPrevious())
  15. throw new NoSuchElementException();
  16.  
  17. lastReturned = next = (next == null) ? last : next.prev;
  18. nextIndex--;
  19. return lastReturned.item;
  20. }

这两个方法首先调用checkForComodifcation方法检查并发改动问题。前面说过,AbstractList的modCount记录了链表的改动次数,而每个迭代器都通过以下的字段维护一个独立的计数器:

  1. private int expectedModCount = modCount;

这个域初始化为类的modCount改动次数。而checkForComodification检查迭代器自己维护的计数器是否和类的modCount相等,假设不等。就会抛出一个ConcurrentModificationException。

并发改动检查通过后。会调用hasNext或hasPrevious方法检查是否有待訪问的元素。ListItr类有一个nextIndex域:

  1. private int nextIndex;

这个域维护迭代器的当前位置。当然,对于LinkedList来说,因为迭代器指向两个元素中间,所以能够同一时候产生两个索引:nextIndex方法返回下一次调用next方法时返回元素的整数索引。previousIndex返回下一次调用previous方法时返回元素的索引,这个索引比nextIndex小1。

hasNext和hasPrevious方法就是检查nextIndex和previousIndex是否在正确范围来确实是否有待訪问元素的。

ListItr类还有两个域:

  1. private Node<E> lastReturned;
  2. private Node<E> next;

lastReturned用来保存上次返回的节点,next就是迭代器位置的下一个元素。也能够看做光标的下一个元素(下一个元素总是光标的右面那个元素)。调用next方法后,光标右移一位。越过next域保存的节点,然后更新这两个域的值,即刚才的next变为lastReturned,next就是再下一个元素,然后nextIndex增1。

previous相对于next操作来说相当于光标左移一位,在更新lastReturned和next时,须要考虑next是否为null。假设next为null,说明在没运行previous时。迭代器在最后一个位置,所以运行previous后。next应该是链表的尾节点last。假设next不是null,那么next更新为next的前序节点。而lastReturned为光标刚越过的元素。即如今的next节点,这时,lastReturned和next节点指向同一个元素。

ListItr类有三个能够改动链表的方法:add、remove和set。当中add和remove会改变迭代器的位置。由于这两个方法改动了链表的结构;而set方法不会改动迭代器的位置。由于它不改动链表的结构。

这三个方法的代码例如以下:

  1. public void remove() {
  2. checkForComodification();
  3. if (lastReturned == null)
  4. throw new IllegalStateException();
  5.  
  6. Node<E> lastNext = lastReturned.next;
  7. unlink(lastReturned);
  8. if (next == lastReturned)
  9. next = lastNext;
  10. else
  11. nextIndex--;
  12. lastReturned = null;
  13. expectedModCount++;
  14. }
  15.  
  16. public void set(E e) {
  17. if (lastReturned == null)
  18. throw new IllegalStateException();
  19. checkForComodification();
  20. lastReturned.item = e;
  21. }
  22.  
  23. public void add(E e) {
  24. checkForComodification();
  25. lastReturned = null;
  26. if (next == null)
  27. linkLast(e);
  28. else
  29. linkBefore(e, next);
  30. nextIndex++;
  31. expectedModCount++;
  32. }

值得注意的是remove方法。

在每次调用remove方法后,都会将lastReturned置为null。也就是说。假设连续调用remove方法,第二次调用就会抛出一个IllegalStateException异常。

因此。remove操作必须跟在next或previous操作之后。

如今已经介绍了ListIterator接口的基本方法,能够从前后两个方向遍历链表中的元素,并能够加入、删除元素。

记住一点:链表的任何位置加入与删除节点的操作是ListIterator迭代器提供的,类本身的add方法仅仅能在结尾加入。

2.4 随机訪问

在Java类库中,还提供了很多理论上存在一定争议的方法。链表不支持高速随机訪问。假设要查看链表中的第n个元素,就必须从头開始。越过n-1个元素,没有捷径可走。鉴于这个原因。在程序须要採用整数索引訪问元素时。一般不选用链表。

虽然如此,LinkedList类还提供了一个用来訪问某个特定元素的get方法:

  1. LinkedList<String> list=...;
  2. String s=list.get(n);

当然,这种方法的效率不太高。

绝不应该使用这样的让人误解的随机訪问方法来遍历链表。以下的代码效率极低:

  1. for(int i=0;i<list.size();i++)
  2. {
  3. dosomething with list.get(i);
  4. }

每次查找一个元素都要从头開始又一次搜索。LinkedList对象根本不做不论什么缓存位置信息的处理。

事实上。在LinkedList类中,get方法会推断当前的位置距离头和尾哪一端更近,然后推断从左向右遍历还是从右向左遍历。

2.5 样例

以下的代码演示了LinkedList类的基本操作。

它简单的创建两个链表,将它们合并在一起,然后从第二个链表中每间隔一个元素删除一个元素。最后測试removeAll方法:

  1. import java.util.*;
  2. public class LinkedListTest {
  3. public static void main(String[] args) {
  4. List<String> a=new LinkedList<>();
  5. a.add("A");
  6. a.add("C");
  7. a.add("E");
  8.  
  9. List<String> b=new LinkedList<>();
  10. b.add("B");
  11. b.add("D");
  12. b.add("F");
  13. b.add("G");
  14.  
  15. ListIterator<String> aIter=a.listIterator();
  16. Iterator<String> bIter=b.iterator();
  17.  
  18. while(bIter.hasNext()){
  19. if(aIter.hasNext())aIter.next();
  20. aIter.add(bIter.next());
  21. }
  22. System.out.println(a);
  23.  
  24. bIter=b.iterator();
  25. while(bIter.hasNext()){
  26. bIter.next();
  27. if(bIter.hasNext()){
  28. bIter.next();
  29. bIter.remove();
  30. }
  31. }
  32. System.out.println(b);
  33.  
  34. a.removeAll(b);
  35. System.out.println(a);
  36. }
  37. }

结果例如以下:

3 数组列表:ArrayList

前面介绍了List接口和实现了这个接口的LinkedList类。List接口用于描写叙述一个有序集合,而且集合中每一个元素的位置十分重要。

有两种訪问元素的协议:一种是用迭代器。还有一种使用get和set方法随机訪问每一个元素。

后者不适用于链表,但对数组非常实用。集合类库提供了一个大家非常熟悉的ArrayList类,这个类也实现了List接口。ArrayList类封装了一个动态再分配的对象数组。

Java集合类库中另一个动态数组:Vector类。只是这个类的全部方法是同步的,能够由两个线程安全的訪问一个Vector对象。

可是,假设一个线程訪问Vector。代码要在同步上消耗大量的时间。

而ArrayList方法不是同步的,因此。假设不须要同步时使用ArrayList。

ArrayList具体解释中具体介绍了类的实现及方法的使用。

Java集合(二):List列表的更多相关文章

  1. Java集合(二)--Iterator和Iterable

    Iterable: public interface Iterable<T> { Iterator<T> iterator(); } 上面是Iterable源码,只有一个ite ...

  2. JAVA集合二:HashMap和Hashtable

    参考链接: HOW2J.CN HashMap HashMap实现了JAVA的Map接口,类似于C++的STL框架的Map,是存储键值对的数据结构.键(key)是唯一的,但值(value)可以重复,如果 ...

  3. 【由浅入深理解java集合】(四)——集合 Queue

    今天我们来介绍下集合Queue中的几个重要的实现类.关于集合Queue中的内容就比较少了.主要是针对队列这种数据结构的使用来介绍Queue中的实现类. Queue用于模拟队列这种数据结构,队列通常是指 ...

  4. 【由浅入深理解java集合】(五)——集合 Map

    前面已经介绍完了Collection接口下的集合实现类,今天我们来介绍Map接口下的两个重要的集合实现类HashMap,TreeMap.关于Map的一些通用介绍,可以参考第一篇文章.由于Map与Lis ...

  5. 【由浅入深理解java集合】(三)——集合 List

    第一篇文章中介绍了List集合的一些通用知识.本篇文章将集中介绍List集合相比Collection接口增加的一些重要功能以及List集合的两个重要子类ArrayList及LinkedList. 一. ...

  6. 【由浅入深理解java集合】(一)——集合框架 Collction、Map

    本篇文章主要对java集合的框架进行介绍,使大家对java集合的整体框架有个了解.具体介绍了Collection接口,Map接口以及Collection接口的三个子接口Set,List,Queue. ...

  7. Java集合—Set(转载)

    Set集合中包含了三个比较重要的实现类:HashSet.TreeSet和EnumSet.本篇文章将重点介绍这三个类. 一.HashSet类 HashSet简介 HashSet是Set接口的典型实现,实 ...

  8. Java集合(一):Java集合概述

    注:本文基于JDK 1.7 1 概述 Java提供了一个丰富的集合框架,这个集合框架包括了很多接口.虚拟类和实现类. 这些接口和类提供了丰富的功能.可以满足主要的聚合需求. 下图就是这个框架的总体结构 ...

  9. Java 集合 散列表hash table

    Java 集合 散列表hash table @author ixenos 摘要:hash table用链表数组实现.解决散列表的冲突:开放地址法 和 链地址法(冲突链表方式) hash table 是 ...

随机推荐

  1. tinyxml使用

    1.下载地址 http://sourceforge.net/projects/tinyxml/ 2.tinyxml不仅支持Linux编译,同时也支持windows下编译,由于tinyxml仅有6个文件 ...

  2. 免费获取Bootstrap模板的方法

    Bootstrap是Twitter推出的一个开源的用于前端开发的工具包,其中中包含了丰富的Web组件,根据这些组件,可以快速的搭建一个漂亮.功能完备的网站. 最近通过了Bootstrap中文网学习了其 ...

  3. .net 获取当前网页的的url

    正确的方法是:HttpContext.Current.Request.Url.PathAndQuery1.通过ASP.NET获取 如果测试的url地址是http://www.test.com/test ...

  4. JavaWeb中使用到的类与接口整理(一)servlet包

    javaweb学了半本,整理了一下Servlet技术模型.servlet容器模型.jsp技术模型中的类与接口,有助于理解web应用中的页面跳转和参数传递,目录: HttpServlet 可作Scope ...

  5. ADODB.RecordSet常用方法查询

    rs = Server.CreateObject("ADODB.RecordSet") rs.Open(sqlStr,conn,1,A) 注:A=1表示读取数据:A=3表示新增.修 ...

  6. Spring aop(实验写法)

    1. 创建通知:定义一个接口 Public interface Sleepable { voidsleep(); }然后写一个Human类,他实现了这个接口 publicHuman implement ...

  7. Rx (Reactive Extensions)介绍

    Reactive Extensions (Rx) 原来是由微软提出的一个综合了异步和基于事件驱动编程的库包,使用可观察序列和LINQ-style查询操作. 使用Rx, 开发者可以用Observable ...

  8. 取得Linux系统的各种统计信息

    本文基于Linux 2.6.x内核 一.取得CPU信息(相关文件/proc/stat) 在一个系统中的/proct/stat文件内容如下 $ cat /proc/stat cpu 1039426 17 ...

  9. Java包名称中通配符的含义

    "com.abc 表示的意义为:系统从com.abc这个包及其子孙包扫描组件 "com.abc.* 表示的意义为:系统从com.abc这个包的子孙包扫描组件

  10. nlogn求逆序对&&陌上花开

    前置: nlogn逆序对: 前一个小时我还真的不会这个Orz 这里运用归并排序的思想. 对于一个序列,我们把它先分开,再合并成一个有序序列. 引自https://blog.csdn.net/qq_30 ...