Java教程是针对JDK 8编写的。本页面描述的示例和实践不利用后续版本中引入的改进,并可能使用不再可用的技术。
请参阅Java语言更改,了解Java SE 9及其后续版本中更新的语言特性的摘要。
请参阅JDK发行说明,了解有关所有JDK版本的新功能、增强功能以及已删除或弃用选项的信息。
List
是一个有序的Collection
(有时称为序列)。列表可以包含重复元素。除了从Collection
继承的操作之外,List
接口还包括以下操作:
位置访问
—— 根据列表中元素的位置进行操作。这包括get
、set
、add
、addAll
和remove
等方法。搜索
—— 在列表中搜索指定的对象并返回其位置。搜索方法包括indexOf
和lastIndexOf
。迭代
—— 扩展了Iterator
的语义,以利用列表的顺序性。 listIterator
方法提供了这种行为。范围视图
—— sublist
方法对列表执行任意的范围操作。Java平台包含两种通用的List
实现。通常表现更好的是ArrayList
,而在某些情况下性能更好的是LinkedList
。
从Collection
继承的操作都做了你期望它们做的事情,假设你已经熟悉这些操作。如果你对Collection
中的操作不熟悉,现在是阅读集合接口部分的好时机。 remove
操作总是从列表中删除指定元素的第一个出现。 add
和addAll
操作总是将新元素追加到列表的末尾。因此,以下习语将一个列表连接到另一个列表。
list1.addAll(list2);
这是一个非破坏性形式的习语,它生成一个由第二个列表附加到第一个列表的第三个List
。
List<Type> list3 = new ArrayList<Type>(list1); list3.addAll(list2);
请注意,这个习语在非破坏性形式中利用了ArrayList
的标准转换构造函数。
这是一个示例(JDK 8及更高版本),它将一些名称聚合成一个List
:
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
类中,它使用指定的随机源随机排列指定的列表。这是一个有点微妙的算法:它从底部向上遍历列表,重复将随机选择的元素交换到当前位置。与大多数简单的洗牌尝试不同,它是公平的(所有排列的可能性均等,假设随机源是无偏的)且快速的(仅需要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
(使用默认的随机源),你将得到与前一个程序相同行为的下面这个简短的程序
。
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
的列表中,index
有n+1
个有效值,从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。这意味着两个边界情况的行为:(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
之间的等式测试。你需要特殊处理val
为null
的情况,以防止出现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); } } }
subList(int fromIndex, int toIndex)
方法返回一个此列表部分的视图,其索引范围从fromIndex
(包括)到toIndex
(不包括)。这个半开范围与典型的for
循环相对应。
for (int i = fromIndex; i < toIndex; i++) { ... }
正如视图这个术语所暗示的,返回的List
是由调用subList
的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
(称为“手牌”),其中包含从指定List
(称为“牌堆”)末尾取出的指定数量的元素。返回的手牌中的元素从牌堆中移除。
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("用法:Deal hands cards"); return; } int numHands = Integer.parseInt(args[0]); int cardsPerHand = Integer.parseInt(args[1]); // 创建一副普通的52张牌的牌堆。 String[] suit = new String[] { "黑桃", "红心", "方块", "梅花" }; String[] rank = new String[] { "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K" }; 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]); // 洗牌。 Collections.shuffle(deck); if (numHands * cardsPerHand > deck.size()) { System.out.println("牌不够了。"); 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, 黑桃J, 黑桃3, 黑桃4, 方块K] [方块4, 梅花A, 梅花6, 红心J, 红心Q] [黑桃7, 黑桃5, 方块2, 方块Q, 梅花9] [黑桃8, 方块6, 黑桃A, 红心3, 红心A]
尽管subList
操作非常强大,但在使用它时需要小心。如果通过除了返回的List
之外的任何方式向支持的List
添加或删除元素,subList
返回的List
的语义将变得不确定。因此,强烈建议您仅将subList
返回的List
用作临时对象,用于对支持的List
执行一个或一系列的范围操作。您使用subList
实例的时间越长,您通过直接修改支持的List
或通过另一个subList
对象修改它的可能性就越大。请注意,修改子列表的子列表并继续使用原始子列表是合法的(但不能同时进行)。
Collections
类中的大多数多态算法专门适用于List
。拥有所有这些算法使得操作列表非常容易。以下是这些算法的概述,更详细的说明请参见算法部分。
sort
- 使用归并排序算法对List
进行排序,提供了快速、稳定的排序。(稳定的排序是指不重新排序相等元素的排序)shuffle
- 随机排列List
中的元素。reverse
- 反转List
中元素的顺序。rotate
- 将List
中的所有元素按指定的距离旋转。swap
- 在List
的指定位置交换元素。replaceAll
- 用另一个指定的值替换List
中的所有出现。fill
- 用指定的值覆盖List
中的每个元素。copy
- 将源List
复制到目标List
。binarySearch
- 使用二分搜索算法在有序List
中搜索元素。indexOfSubList
- 返回一个List
中第一个与另一个List
相等的子列表的索引。lastIndexOfSubList
- 返回一个List
中最后一个与另一个List
相等的子列表的索引。