Java 教程是为 JDK 8 编写的。本页中描述的示例和实践未利用在后续版本中引入的改进。
List
是有序的 Collection
(有时称为 sequence (序列))。列表可能包含重复元素。除了从 Collection
继承的操作之外,List
接口还包括以下操作:
Positional access
根据列表中的数字位置操纵元素。这包括 get
,set
,add
,addAll
和 remove
等方法。Search
搜索列表中的指定对象并返回其数字位置。搜索方法包括 indexOf
和 lastIndexOf
。Iteration
继承 Iterator
语义以利用列表的顺序特性。listIterator
方法提供此行为。Range-view
sublist
方法在列表上执行任意 range operations (范围操作)。Java 平台包含两个泛型的 List
实现。ArrayList
,通常是性能更好的实现,LinkedList
在某些情况下提供更好的性能。
从 Collection
继承的操作都是关于你期望他们做的事情,假设你已经熟悉它们。如果你不熟悉 Collection
,现在是阅读 The Collection Interface 部分的好时机。remove
操作始终从列表中移除 第一个 匹配项。add
和 addAll
操作始终将新元素附加到列表的 结尾。因此,以下习惯用法将一个列表连接到另一个列表。
list1.addAll(list2);
这是这个习惯用法的非破坏性形式,它产生第三个 List
,由第一个附加的第二个列表组成。
List<Type> list3 = new ArrayList<Type>(list1); list3.addAll(list2);
请注意,习惯用法以其非破坏性形式利用了 ArrayList
的标准转换构造函数。
这是一个将一些名字聚合到 List
的示例(JDK 8 及更高版本):
List<String> list = people.stream() .map(Person::getName) .collect(Collectors.toList());
与 Set
接口类似,List
增强了对 equals
和 hashCode
方法的要求这样就可以比较两个 List
对象的逻辑相等性而不考虑它们的实现类。如果两个 List
对象包含相同顺序的相同元素,则它们是相等的。
基本的 位置访问
操作是 get
,set
,add
和 remove
。(set
和 remove
操作返回被覆盖或移除的旧值。)其他操作(indexOf
和 lastIndexOf
)返回列表中指定元素的第一个或最后一个索引。
addAll
操作从指定位置开始插入指定 Collection
的所有元素。元素按照指定的 Collection
的迭代器返回的顺序插入。此调用是 Collection
的 addAll
操作的位置访问模拟。
这是一个在 List
中交换两个索引值的方法。
public static <E> void swap(List<E> a, int i, int j) { E tmp = a.get(i); a.set(i, a.get(j)); a.set(j, tmp); }
当然,这有一个很大的不同。这是一个多态算法:它在任何 List
中交换两个元素,而不管它的实现类型如何。这是另一种使用前面的 swap
方法的多态算法。
public static void shuffle(List<?> list, Random rnd) { for (int i = list.size(); i > 1; i--) swap(list, i - 1, rnd.nextInt(i)); }
此算法包含在 Java 平台的 Collections
类中,使用指定的随机源随机置换指定的列表。它有点微妙:它从底部向上运行列表,反复地将随机选择的元素交换到当前位置。与大多数幼稚的洗牌尝试不同,它是 fair (公平的)(所有排列均以相同的可能性发生,假设无偏见的随机源)且 fast (快速的)(要求正好 list.size()-1
交换)。以下程序使用此算法以随机顺序打印其参数列表中的单词。
import java.util.*; public class Shuffle { public static void main(String[] args) { List<String> list = new ArrayList<String>(); for (String a : args) list.add(a); Collections.shuffle(list, new Random()); System.out.println(list); } }
事实上,这个程序可以更短,更快。Arrays
类有一个名为 asList
的静态工厂方法,它允许将数组视为 List
。此方法不会复制数组。List
中的更改将写入数组,反之亦然。生成的 List 不是通用的 List
实现,因为它没有实现(可选的)add
和 remove
操作:数组是不可调整大小。利用 Arrays.asList
并调用 shuffle
的库版本(使用默认的随机源),你将获得以下 tiny program
,其行为与之前的程序相同。
import java.util.*; public class Shuffle { public static void main(String[] args) { List<String> list = Arrays.asList(args); Collections.shuffle(list); System.out.println(list); } }
正如你所期望的那样,List
的 iterator
操作返回的 Iterator
以正确的顺序返回列表的元素。List
还提供了一个更丰富的迭代器,称为 ListIterator
,它允许你在任一方向遍历列表,在迭代期间修改列表,并获取迭代器的当前位置。
ListIterator
继承自 Iterator
的三个方法(hasNext
,next
,remove
)在两个接口中做同样的事情。hasPrevious
和 previous
操作是 hasNext
和 next
的精确类似物。前一个操作引用(隐式)光标之前的元素,而后者引用光标之后的元素。previous
操作向后移动光标,而 next
向前移动光标。
这是在列表中向后迭代的标准习惯用法。
for (ListIterator<Type> it = list.listIterator(list.size()); it.hasPrevious(); ) { Type t = it.previous(); ... }
请注意上述习惯用法中 listIterator
的参数。List
接口有两种形式的 listIterator
方法。没有参数的形式返回位于列表开头的 ListIterator
;带有 int
参数的形式返回位于指定索引处的 ListIterator
。索引引用初始调用 next
返回的元素。对 previous
的初始调用将返回索引为 index-1
的元素。在长度 n
的列表中,有 n+1
个 index
的有效值,从 0
到 n
,包括在内。
直观地说,光标总是在两个元素之间 通过调用 previous
返回的那个,以及通过调用 next
返回的那个。n+1
个有效 index
值对应于元素之间的 n+1
个间隙,从第一个元素之前的间隙到最后一个元素之后的间隙。下图 显示包含四个元素的列表中的五个可能的光标位置。
五个可能的光标位置。
调用 next
和 previous
可以混合,但你必须要小心。第一次调用 previous
会返回与上一次调用 next
相同的元素。类似地,在对 previous
的一系列调用之后,第一次调用 next
会返回与上次调用 previous
相同的元素。
毫无疑问,nextIndex
方法返回后续调用 next
返回的元素的索引,以及 previousIndex
返回后续调用 previous
返回的元素的索引。这些调用通常用于报告找到某些内容的位置或记录 ListIterator
的位置,以便可以创建具有相同位置的另一个 ListIterator
。
同样,nextIndex
返回的数字总是比 previousIndex
返回的数字大一点也就不足为奇了。这意味着两种边界情况的行为:(1)当光标在初始元素之前调用 previousIndex
返回 -1
,以及(2)调用 nextIndex
当光标位于最后一个元素之后返回 list.size()
。为了使所有这些具体化,以下是 List.indexOf
的可能实现。
public int indexOf(E e) { for (ListIterator<E> it = listIterator(); it.hasNext(); ) if (e == null ? it.next() == null : e.equals(it.next())) return it.previousIndex(); // Element not found return -1; }
请注意,indexOf
方法返回 it.previousIndex()
,即使它正在向前遍历列表。原因是 it.nextIndex()
将返回我们将要检查的元素的索引,并且我们想要返回我们刚刚检查的元素的索引。
Iterator
接口提供 remove
操作,以从 Collection
中移除 next
返回的最后一个元素。对于 ListIterator
,此操作将移除 next
或 previous
返回的最后一个元素。ListIterator
接口提供了两个额外的操作来修改列表 set
和 add
。set
方法用指定的元素覆盖 next
或 previous
返回的最后一个元素。以下多态算法使用 set
将一个指定值的所有匹配项替换为另一个。
public static <E> void replace(List<E> list, E val, E newVal) { for (ListIterator<E> it = list.listIterator(); it.hasNext(); ) if (val == null ? it.next() == null : val.equals(it.next())) it.set(newVal); }
本例中唯一的棘手问题是 val
和 it.next
之间的相等性测试。你需要小心特殊情况下 null
的 val
值以防止 NullPointerException
。
add
方法将新元素插入到列表中当前光标位置之前。此方法在以下多态算法中说明,以使用指定列表中包含的值序列替换指定值的所有匹配项。
public static <E> void replace(List<E> list, E val, List<? extends E> newVals) { for (ListIterator<E> it = list.listIterator(); it.hasNext(); ){ if (val == null ? it.next() == null : val.equals(it.next())) { it.remove(); for (E e : newVals) it.add(e); } } }
range-view
操作,subList(int fromIndex, int toIndex)
,返回此列表部分的 List
视图,其索引范围从 fromIndex
(包括) 到 toIndex
(不包含)。half-open range (半开范围) 反映了典型的 for
循环。
for (int i = fromIndex; i < toIndex; i++) { ... }
正如术语 view (视图) 所暗示的那样,subList
被调用返回的 List
是 List
的备份,所以前者的变化反映在后者中。
此方法消除了对显式范围操作(对于数组通常存在的排序)的需要。任何期望 List
的操作都可以通过传递 subList
视图而不是整个 List
来用作范围操作。例如,以下习惯用法从 List
中移除一系列元素。
list.subList(fromIndex, toIndex).clear();
可以构造类似的习惯用法以搜索范围中的元素。
int i = list.subList(fromIndex, toIndex).indexOf(o); int j = list.subList(fromIndex, toIndex).lastIndexOf(o);
请注意,前面的习惯用法返回 subList
中找到的元素的索引,而不是后备 List
中的索引。
在 List
上运行的任何多态算法,例如 replace
和 shuffle
示例,都适用于通过 subList
返回的 List
。
这是一个多态算法,其实现使用 subList
来处理来自牌组的牌。也就是说,它返回一个新的 List
(“hand”),它包含从指定的 List
(“deck”)的末尾获取的指定数量的元素。手中返回的元素将从牌组中移除。
public static <E> List<E> dealHand(List<E> deck, int n) { int deckSize = deck.size(); List<E> handView = deck.subList(deckSize - n, deckSize); List<E> hand = new ArrayList<E>(handView); handView.clear(); return hand; }
请注意,此算法会从牌组的 尾端 中移除手牌。对于许多常见的 List
实现,例如 ArrayList
,从列表末尾移除元素的性能明显优于从开头移除元素的性能。
以下是 一个程序
,它使用 dealHand
方法结合 Collections.shuffle
从正常 52 张牌组中生成手牌。该程序采用两个命令行参数:(1)要处理的手牌组数和(2)每组手牌数。
import java.util.*; public class Deal { public static void main(String[] args) { if (args.length < 2) { System.out.println("Usage: Deal hands cards"); return; } int numHands = Integer.parseInt(args[0]); int cardsPerHand = Integer.parseInt(args[1]); // Make a normal 52-card deck. String[] suit = new String[] { "spades", "hearts", "diamonds", "clubs" }; String[] rank = new String[] { "ace", "2", "3", "4", "5", "6", "7", "8", "9", "10", "jack", "queen", "king" }; List<String> deck = new ArrayList<String>(); for (int i = 0; i < suit.length; i++) for (int j = 0; j < rank.length; j++) deck.add(rank[j] + " of " + suit[i]); // Shuffle the deck. Collections.shuffle(deck); if (numHands * cardsPerHand > deck.size()) { System.out.println("Not enough cards."); return; } for (int i = 0; i < numHands; i++) System.out.println(dealHand(deck, cardsPerHand)); } public static <E> List<E> dealHand(List<E> deck, int n) { int deckSize = deck.size(); List<E> handView = deck.subList(deckSize - n, deckSize); List<E> hand = new ArrayList<E>(handView); handView.clear(); return hand; } }
运行程序会产生如下输出。
% java Deal 4 5 [8 of hearts, jack of spades, 3 of spades, 4 of spades, king of diamonds] [4 of diamonds, ace of clubs, 6 of clubs, jack of hearts, queen of hearts] [7 of spades, 5 of spades, 2 of diamonds, queen of diamonds, 9 of clubs] [8 of spades, 6 of diamonds, ace of spades, 3 of hearts, ace of hearts]
虽然 subList
操作非常强大,但在使用它时必须小心。除了操作返回的 List
之外,如果以任何方式向后备 List
添加或移除元素,则 subList
返回的 List
的语义将变为未定义。因此,强烈建议你仅将 subList
返回的 List
用作瞬态对象 在后备 List
上执行一个或一系列范围操作。使用 subList
实例的时间越长,通过直接修改后备 List
或通过另一个 subList
对象来破坏它的可能性就越大。请注意,修改子列表的子列表并继续使用原始子列表(尽管不是并发)是合法的。
Collections
类中的大多数多态算法专门应用于 List
。拥有所有这些算法可以很容易地操作列表。以下是这些算法的摘要,这些算法在 Algorithms 部分中有更详细的描述。
sort
使用合并排序算法对 List
进行排序,该算法提供快速,稳定的排序。(A stable sort (稳定排序) 是不重新排序相同元素的排序。)shuffle
随机置换 List
中的元素。reverse
颠倒 List
中元素的顺序。rotate
将 List
中的所有元素旋转指定的距离。swap
将元素交换到 List
中的指定位置。replaceAll
将所有的匹配项用一个指定值替换为另一个。fill
用指定的值覆盖 List
中的每个元素。copy
将源 List
复制到目标 List
。binarySearch
使用二分搜索算法搜索有序 List
中的元素。indexOfSubList
返回 List
中的第一个与另一个列表相等的子列表的索引。lastIndexOfSubList
返回 List
中最后一个与另一个列表相等的子列表的索引。