列表(List)是有序的集合(有时称为序列(sequence))。 列表可能包含重复的元素。 除了从Collection继承的操作外,List接口还包括以下操作:
位置访问(Positional access)—根据元素在列表中的数字位置处理元素。这包括方法,例如get,set,add,addAll和remove。搜索(Search)—在列表中搜索指定的对象并返回其数字位置。搜索方法包括indexOf和lastIndexOf。迭代(Iteration)—扩展Iterator语义以利用列表的顺序性质。listIterator方法提供了这种行为。范围视图(Range-view)—sublist方法对列表执行任意范围的操作。
Java平台包含两个泛型List实现。 ArrayList,这通常是性能较好的实现,并且在某些情况下LinkedList可以提供更好的性能。
集合操作
假设您已经熟悉了Collection,那么从Collection继承的所有操作都可以完成您期望的操作。如果您对Collection不熟悉,那么现在是阅读Collection接口部分的好时机。删除操作始终从列表中删除第一次出现的指定元素。add和addAll操作始终将新元素追加到列表的末尾。因此,以下习语将一个列表连接到另一个列表。
list1.addAll(list2);
这是此惯用语的一种非破坏性形式,它产生第三个List,其中第二个List附加在第一个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实现,因为它没有实现(可选)添加和删除操作:数组不可调整大小。 利用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完全类似。前一个操作引用了(隐式)光标之前的元素,而后一个操作引用了光标之后的元素。上一个操作将光标向后移动,而下一个操作将光标向前移动。
这是用于向后遍历列表的标准用法。
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个有效值,从0到n(含)。
从直觉上讲,游标始终位于两个元素之间——一个元素将由对上一个的调用返回,而一个元素将由对下一个的调用返回。n+1个有效索引值对应于元素之间的n+1个间隙,从第一个元素之前的间隙到最后一个元素之后的间隙。下图显示了包含四个元素的列表中五个可能的光标位置。
五个可能的光标位置
可以将对next和previous的调用混在一起,但是您必须小心一点。对previous的第一次调用返回与对next的最后一次调用相同的元素。类似地,在对前一个调用的一系列调用之后,对next的第一次调用返回与对previous的最后调用相同的元素。
毫不奇怪,nextIndex方法返回将由对next的后续调用返回的元素的索引,而previousIndex返回由对next的后续调用将返回的元素的索引。这些调用通常用于报告发现某物的位置或记录ListIterator的位置,以便可以创建另一个具有相同位置的ListIterator。
因此,nextIndex返回的数字总是比previousIndex返回的数字大1也就不足为奇了。这暗示了两种边界情况的行为:(1)当游标在初始元素返回-1之前时,对previousIndex的调用;(2)当游标在最后一个元素返回list.size()之后时,对nextIndex的调用 。为了使所有这些具体,下面是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 foundreturn -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)返回此列表部分的List视图,其索引范围从fromIndex(包括)到toIndex(不包括)。这个半开范围反映了典型的for循环。
for (int i = fromIndex; i < toIndex; i++) {...}
顾名思义,该返回的List由调用subList的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;}
请注意,此算法从牌组的末端移开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或从后备List中删除元素,则subList返回的List的语义将变得不确定。因此,强烈建议您仅将subList返回的List用作瞬态对象——对后备List执行一个或一系列范围操作。使用subList实例的时间越长,通过直接或通过另一个subList对象修改后备List损害它的可能性就越大。请注意,修改子列表的子列表并继续使用原始子列表是合法的(尽管不能同时使用)。
列表算法
Collections类中的大多数多态算法专门适用于List。拥有所有这些算法可轻松处理列表。这是这些算法的摘要,在“算法”部分中有更详细的描述。
sort—使用合并排序算法对List进行排序,该算法可提供快速,稳定的排序。(稳定排序是一种不会对相等元素进行重新排序的排序。)shuffle—随机排列List中的元素。reverse—反转List中元素的顺序。rotate—将List所有元素旋转指定距离。swap—交换元素List中指定位置的元素。replaceAll—将所有出现的一个指定值替换为另一个。fill— 用指定的值覆盖List中的每个元素。copy-将源List复制到目标List。binarySearch— 使用二进制搜索算法在有序列表中搜索元素。indexOfSubList—返回一个等于另一个List的第一个子列表的索引。lastIndexOfSubList—返回一个等于另一个List的最后一个子列表的索引。
