本文介绍了Java集合框架。在这里,你将了解什么是集合,以及它们如何使你的工作更轻松,程序更完善。你将了解构成Java集合框架的核心元素--接口、实现、集合操作和算法。
集合介绍
集合--有时被称为容器--只是一个将多个元素组合成一个单元的对象。集合被用来存储、检索、操作和通信聚合数据。通常情况下,它们代表一个自然组的数据对象,如扑克牌(卡片的集合)、邮件文件夹(信件的集合)或电话簿(姓名与电话号码的映射)。如果你使用过Java编程语言--或者几乎任何其他编程语言--你已经熟悉了集合。
什么是集合框架
集合框架是一个统一的架构,用于表示和操作集合。所有的集合框架都包含以下内容:
- 接口:这些是代表集合的抽象数据类型。接口允许对集合的操作独立于其表示的细节。在面向对象的语言中,接口通常表示一个层次结构。
- 实现:这些是集合接口的具体实现。在本质上,它们是可重用的数据结构。
- 算法:这些是在实现集合接口的对象上进行有用的计算的方法,例如搜索和排序。这些算法被认为是多态的:也就是说,同样的方法可以在适当的集合接口的许多不同实现上使用。从本质上讲,算法是可重复使用的功能。
Java集合框架的好处
Java集合框架提供了以下好处:
- 减少编程工作:通过提供有用的数据结构和算法,集合框架使你能够专注于你的程序的重要部分,而不是完成低级的任务。通过促进不相关的API之间的互操作性,Java集合框架使你无需编写适配器对象或转换代码来连接API。
- 提高了程序的速度和质量:这个集合框架为有用的数据结构和算法提供了高性能、高质量的实现。每个接口的各种实现都是可以互换的,所以程序可以通过切换集合的实现来轻松调整。因为你从编写自己的数据结构的苦差事中解脱出来,你将有更多的时间用于提高程序的质量和性能。
- 允许不相关API之间的互操作性:如果我的网络管理API提供了节点名称的集合,并且如果您的GUI工具包需要列标题的集合,我们的API将无缝互操作,即使它们是独立编写的。
- 减少了学习和使用新API的工作量:许多API把集合作为输入,并将其作为输出。在过去,每一个这样的API都有一个小的子API专门用来操作其集合。这些集合子API之间几乎没有一致性,所以你必须从头开始学习每一个,而且在使用它们时很容易犯错。随着标准集合接口的出现,这个问题消失了。
- 减少了设计新API的工作量:设计者和实现者不必在每次创建依赖于集合的API时重复造轮子;相反,他们可以使用标准的集合接口。
- 促进了软件的重复使用:符合标准集合接口的新数据结构在本质上是可以重用的。对实现这些接口的对象进行操作的新算法也是如此。
接口
核心集合接口封装了不同类型的集合,如下图所示。这些接口允许对集合的操作独立于其表示的细节。核心集合接口是Java集合框架的基础。正如你在下图中看到的,核心集合接口形成了一个层次结构。
Set是Collection的一种特殊类型,SortedSet是Set的一种特殊类型,以此类推。还要注意的是,层次结构由两个不同的树组成--Map不是一个真正的集合。
所有的核心集合接口都是通用的。例如,这是集合接口的声明。
public interface Collection<E>...
语法告诉你这个接口是通用的。当你声明一个集合实例时,你可以而且应该指定集合中包含的对象的类型。指定类型允许编译器验证(在编译时)你放入集合中的对象的类型是否正确,从而减少运行时的错误。关于泛型的信息,请参见Generics。 当你了解如何使用这些接口时,你就会知道关于Java集合框架的大部分知识。本章讨论了有效使用这些接口的一般准则,包括何时使用哪个接口。你还将学习每个接口的编程习惯,以帮助你最大限度地利用它。 为了保持核心集合接口的数量可控,Java 平台并没有为每种集合类型的每个变体提供单独的接口。(这些变体可能包括不可变的、固定大小的和只附加的。)相反,每个接口中的修改操作被指定为可选的--一个特定的实现可以选择不支持所有操作。如果一个不支持的操作被调用,一个集合就会抛出一个不支持操作的异常。实现负责记录他们支持哪些可选的操作。所有Java平台的通用实现都支持所有的可选操作。 下面的列表描述了核心收集接口:
- Collection - 集合层次结构的根。一个集合表示一组对象,每个对象被称为集合的元素。集合接口是所有集合实现的基类,用于传递集合,并在需要最大通用性时对其进行操作。有些类型的集合允许重复的元素,而有些则不允许。有些是有序的,有些则是无序的。Java平台没有提供这个接口的任何直接实现,但提供了更具体的子接口的实现,如Set和List。
- Set - 一个不能包含重复元素的集合。这个接口是数学集合抽象的模型,用来表示集合,比如组成扑克牌的牌,组成学生课程表的课程,或者机器上运行的进程。
- List - 一个有序的集合(有时称为序列)。列表可以包含重复的元素。List的用户通常可以精确控制每个元素在列表中的插入位置,并可以通过其整数索引(位置)访问元素。如果你使用过Vector,你就会熟悉List的一般风格。
- Queue - 一个用于在处理前保存多个元素的集合。除了基本的集合操作,队列还提供额外的插入、提取和检查操作。队列通常,但不一定,以先进先出的方式排列元素。其中的例外是优先级队列,它根据提供的比较器或元素的自然排序来排列元素。无论使用何种排序方式,队列的头都是通过调用remove或poll被移除的。在FIFO队列中,所有的新元素都插入队列的尾部。其他种类的队列可能使用不同的放置规则。每个队列的实现都必须指定其排序属性。
- Deque - 一个用于在处理前保存多个元素的集合。除了基本的集合操作外,Deque还提供额外的插入、提取和检查操作。Deque可以作为FIFO(先进先出)和LIFO(后进先出)使用。在一个deque中,所有的新元素都可以在两端插入、提取和删除。
- Map - 一个将键映射到值的对象。一个Map不能包含重复的键;每个键最多可以映射到一个值。
- SortedSet - 一个保持其元素升序排列的集合。提供了几个额外的操作来利用排序的优势。排序集用于自然有序的集合,如单词表和成员卷。
- SortedMap - 一个Map,它以升序键的方式保持其映射。这是与SortedSet类似的Map。SortedMap用于自然排序的键/值对集合。
Collection接口
一个集合表示一组被称为其元素的对象。集合接口被用来传递对象的集合,例如,按照惯例,所有通用的集合实现都有一个构造函数,它需要一个集合参数。这个构造函数被称为转换构造函数,它初始化新的集合以包含指定集合中的所有元素,无论给定集合的子接口或实现类型如何。换句话说,它允许你转换集合的类型。 例如,假设你有一个Collectio c,它可能是一个List,一个Set,或者其他类型的集合。这个习惯法创建了一个新的ArrayList(List接口的实现),包含了c中的所有元素。
List<String> list = new ArrayList<String>(c);
或者--如果你使用JDK 7或更高版本--你可以使用钻石操作符:
List<String> list = new ArrayList<>(c);
集合接口包含执行基本操作的方法,如int size()、boolean isEmpty()、boolean contains(Object element)、boolean add(E element)、boolean remove(Object element),以及Iterator iterator()。 它还包含了对整个集合进行操作的方法,如boolean containsAll(Collection c), boolean addAll(Collection c), boolean removeAll(Collection c), boolean retainAll(Collection c), 和 void clear()。 另外还有用于数组操作的方法(如Object[] toArray()和<T> T[] toArray(T[] a))。 在JDK 8及以后的版本中,Collection接口还公开了Stream<E> stream()和Stream<E> parallelStream()方法,用于从底层集合中获取顺序或并行的流。 集合接口做了你所期望的事情,因为一个集合代表了一组对象。它有一些方法可以告诉你集合中有多少个元素(size, isEmpty),有一些方法可以检查一个给定的对象是否在集合中(contains),有一些方法可以从集合中添加和删除一个元素(add, remove),还有一些方法可以提供一个集合的迭代器(iterator)。 它还包含了对整个集合进行操作的方法,如boolean containsAll(Collection c), boolean addAll(Collection c), boolean removeAll(Collection c), boolean retainAll(Collection<?> c), 和 void clear()。 也存在其他的数组操作方法(如Object[] toArray()和 T[] toArray(T[] a))。 在JDK 8及以后的版本中,Collection接口还公开了Stream stream()和Stream parallelStream()方法,用于从底层集合中获取顺序或并行的流。 集合接口做了你所期望的事情,因为一个集合代表了一组对象。它有一些方法可以告诉你集合中有多少个元素(size, isEmpty),有一些方法可以检查一个给定的对象是否在集合中(contains),有一些方法可以从集合中添加和删除一个元素(add, remove),还有一些方法可以提供一个集合的迭代器(iterator)。 add方法的定义足够宽泛,所以它对那些允许重复的集合和不允许重复的集合都有意义。它保证在调用完成后,集合将包含指定的元素,并且如果集合由于调用而发生变化,则返回true。类似地,remove方法被设计成从集合中移除指定元素的单个实例,假设它一开始就包含该元素,如果集合因此而被修改,则返回true。
遍历集合
有三种方法可以遍历集合:(1)使用聚合操作(2)使用for-each结构(3)使用Iterators。
聚合操作
在JDK 8及以后的版本中,对一个集合进行迭代的首选方法是获得一个流并对其进行聚合操作。聚合操作经常与lambda表达式一起使用,使编程更有表现力,使用更少的代码行。下面的代码依次迭代了一个图形集合,并打印出红色的对象:
myShapesCollection.stream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));
同样,你可以很容易地请求一个并行流,在集合足够大,而且你的计算机有足够的核心的场景下很有帮助:
myShapesCollection.parallelStream()
.filter(e -> e.getColor() == Color.RED)
.forEach(e -> System.out.println(e.getName()));
有许多不同的方法可以用这个API来收集数据。例如,你可能想把一个集合的元素转换成字符串对象,然后把它们连接起来,用逗号分开:
String joined = elements.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
或者是所有员工的工资总和:
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
集合框架一直提供一些所谓的 "批量操作 "作为其API的一部分。这些包括对整个集合进行操作的方法,如containsAll、addAll、removeAll等。不要将这些方法与JDK 8中引入的聚合操作混淆。新的聚合操作与现有的批量操作(containsAll、addAll 等)之间的关键区别在于,旧的版本都会修改底层集合。相比之下,新的聚合操作不会修改底层集合。当使用新的聚合操作和lambda表达式时,你必须注意避免对底层集合的直接修改,以免在将来你的代码从并行流中运行时引入问题。
for-each
for-each结构允许你使用for循环简洁地遍历一个集合或数组。下面的代码使用for-each结构在单独的一行中打印出一个集合的每个元素。
for (Object o : collection)
System.out.println(o);
Iterators
迭代器是一个对象,它使你能够遍历一个集合,并在需要时有选择地从集合中删除元素。你通过调用一个集合的迭代器方法来获得一个迭代器。下面是Iterator的接口。
public interface Iterator<E> {
boolean hasNext();
E next();
void remove(); //optional
}
如果迭代中有更多的元素,hasNext方法返回真,而next方法返回迭代中的下一个元素。remove方法从底层集合中移除由next返回的最后一个元素。remove方法在每次调用next时只能被调用一次,如果违反了这个规则,就会抛出一个异常。注意,Iterator.remove是在迭代过程中修改一个集合的唯一安全方式。 以下场景,使用Iterator代替for-each结构:
- 删除当前元素。for-each结构隐藏了迭代器,所以你不能调用移除。因此,for-each结构不能用于过滤。
- 对多个集合进行并行迭代。 下面的方法向你展示了如何使用迭代器来过滤一个任意的集合--也就是遍历该集合,去除特定的元素。这段简单的代码是多态的,这意味着它对任何集合都有效,而不考虑其实现。
static void filter(Collection<?> c) {
for (Iterator<?> it = c.iterator(); it.hasNext(); )
if (!cond(it.next()))
it.remove();
}
集合接口批量操作
批量操作在整个集合上执行一个操作。你可以使用基本操作来实现这些快速操作,尽管在大多数情况下,这样的实现会降低效率。下面是一些批量操作: containsAll - 如果目标集合包含指定集合中的所有元素,则返回true。 addAll - 将指定集合中的所有元素添加到目标集合中。 removeAll - 从目标集合中移除所有也包含在指定集合中的元素。 retainAll - 从目标集合中删除其不包含在指定集合中的所有元素。也就是说,它只保留目标集合中也包含在指定集合中的元素。 clear - 从集合中删除所有元素。 如果在执行操作的过程中修改了目标Collection,则addAll、removeAll和retainAll方法都返回true。
集合接口数组操作
数组操作允许将集合的内容转换为数组。简单的场景就是新建了一个Object数组。更复杂的场景允许调用者提供数组或选择输出数组的运行时类型。 例如,假设c是一个集合。下面的片段将c的内容转储到一个新分配的Object数组中,其长度与c中元素的数量相同。
Object[] a = c.toArray();
假设已知c只包含字符串。下面的片段将c的内容转储到一个新分配的String数组中,其长度与c中的元素数相同。
String[] a = c.toArray(new String[0]);
Set接口
一个集合是一个不能包含重复元素的集合。它是数学集合抽象的模型。Set接口只包含继承自Collection的方法,并增加了禁止重复元素的限制。Set还对equals和hashCode操作的行为增加了一个更强的约束,允许Set实例被有意义地比较,即使它们的实现类型不同。如果两个Set实例包含相同的元素,它们就是相等的。 Java平台包含三种通用的Set实现:HashSet, TreeSet, 和 LinkedHashSet。HashSet将其元素存储在一个哈希表中,是性能最好的实现;但是它对迭代的顺序没有任何保证。TreeSet将其元素存储在一个红黑树中,根据元素的值来排序;它比HashSet慢得多。LinkedHashSet,它被实现为一个哈希表,有一个贯穿其中的链表,根据元素被插入到集合中的顺序(插入顺序)来排序。LinkedHashSet以小代价解决了HashSet无序的问题。 这里有一个简单而有用的集合例子。假设你有一个集合,c,你想创建另一个包含相同元素的集合,但要消除所有重复的元素。下面的单行代码就可以做到这一点。
Collection<Type> noDups = new HashSet<Type>(c);
或者,如果使用JDK 8或更高版本,你可以使用聚合操作轻松地收集到一个Set:
c.stream().collect(Collectors.toSet()); // no duplicates
下面是一个稍长的例子,它将一个名字集合累积成一个TreeSet:
Set<String> set = people.stream()
.map(Person::getName)
.collect(Collectors.toCollection(TreeSet::new));
下面是第一个例子的一个小变体,它保留了原始集合的顺序,同时删除了重复的元素:
Collection<Type> noDups = new LinkedHashSet<Type>(c);
下面是一个封装了前面例子的泛型方法,它返回一个与传入的泛型类型相同的Set。
public static <E> Set<E> removeDups(Collection<E> c) {
return new LinkedHashSet<E>(c);
}
Set接口基本操作
size操作返回Set中元素的数量。isEmpty方法判断Set是否为空。add方法将指定的元素添加到Set中,如果它还没有出现,并返回一个布尔值,表明该元素是否被添加。同样地,如果指定的元素已经存在,remove方法将其从Set中移除,并返回一个布尔值,表示该元素是否存在。iterator方法返回Set上的一个Iterator。 下面的程序打印出其参数列表中所有不同的词。我们提供了这个程序的两个版本。第一个版本使用JDK 8聚合操作。第二个使用了for-each结构。 使用 JDK 8 聚合操作:
import java.util.*;
import java.util.stream.*;
public class FindDups {
public static void main(String[] args) {
Set<String> distinctWords = Arrays.asList(args).stream()
.collect(Collectors.toSet());
System.out.println(distinctWords.size()+
" distinct words: " +
distinctWords);
}
}
使用for-each结构:
import java.util.*;
public class FindDups {
public static void main(String[] args) {
Set<String> s = new HashSet<String>();
for (String a : args)
s.add(a);
System.out.println(s.size() + " distinct words: " + s);
}
}
现在运行任一版本的程序。
java FindDups i came i saw i left
产生的输出结果如下:
4 distinct words: [left, came, saw, i]
请注意,代码中总是以接口类型(Set)而不是以实现类型来指代集合。这是一个强烈推荐的编程实践,因为它使你能够灵活地改变实现,只需改变构造函数。如果用于存储集合的变量或用于传递集合的参数被声明为集合的实现类型而不是它的接口类型,所有这些变量和参数都必须被改变,以便改变它的实现类型。 此外,也不能保证所产生的程序能够工作。如果程序使用了任何在原实现类型中存在但在新实现类型中不存在的非标准操作,程序将失败。只通过接口来引用集合可以防止你使用任何非标准的操作。 在前面的例子中,Set的实现类型是HashSet,它不保证Set中元素的顺序。如果你想让程序按字母顺序打印单词列表,只需将Set的实现类型从HashSet改为TreeSet。这个微不足道的单行改动使前面例子中的命令行产生以下输出。
java FindDups i came i saw i left
4 distinct words: [came, i, left, saw]
集合接口批量操作
批量操作特别适合于集合;当应用时,它们执行标准的集合数学操作。假设s1和s2是集合。下面是批量运算的作用:
- s1.containsAll(s2) - 如果s2是s1的一个子集,则返回true。(如果集合s1包含s2中的所有元素,那么s2就是s1的子集)。
- s1.addAll(s2) - 将s1转化为s1和s2的联合。(两个集合的并集是包含任何一个集合中的所有元素的集合。)
- s1.retainAll(s2) - 将s1转换为s1和s2的交集。(两个集合的交集是只包含两个集合的共同元素的集合。)
- s1.removeAll(s2) - 将s1转化为s1和s2的(不对称的)集合差。(例如,s1减去s2的集合之差是包含s1中的所有元素但不包含s2中的元素的集合)。 为了无损地计算两个集合的并集、交集或集差(不修改任何一个集合),调用者必须在调用适当的批量操作之前复制一个集合。下面是例子
Set<Type> union = new HashSet<Type>(s1);
union.addAll(s2);
Set<Type> intersection = new HashSet<Type>(s1);
intersection.retainAll(s2);
Set<Type> difference = new HashSet<Type>(s1);
difference.removeAll(s2);
在前面的例子中,Set的实现类型是HashSet,如前所述,它是Java平台中最好的全面的Set实现。然而,任何通用的Set实现都可以被替代。 让我们再来看看FindDups程序。假设你想知道参数列表中哪些词只出现过一次,哪些词出现过多次,但你不希望任何重复的词被重复打印出来。这种效果可以通过生成两个集合来实现--一个包含参数列表中的每个词,另一个只包含重复的词。只出现一次的词是这两个集合的集合差,我们知道如何计算。下面是生成的程序的样子。
import java.util.*;
public class FindDups2 {
public static void main(String[] args) {
Set<String> uniques = new HashSet<String>();
Set<String> dups = new HashSet<String>();
for (String a : args)
if (!uniques.add(a))
dups.add(a);
// Destructive set-difference
uniques.removeAll(dups);
System.out.println("Unique words: " + uniques);
System.out.println("Duplicate words: " + dups);
}
}
当以先前使用的相同参数列表运行时(i came i saw i left),该程序产生了以下输出。
Unique words: [left, saw, came]
Duplicate words: [i]
一个不太常见的集合代数运算是对称集差(包含在两个指定集合中的任何一个但不包含在两个集合中的元素集合)。下面的代码以无损的方式计算两个集合的对称集之差。
Set<Type> symmetricDiff = new HashSet<Type>(s1);
symmetricDiff.addAll(s2);
Set<Type> tmp = new HashSet<Type>(s1);
tmp.retainAll(s2);
symmetricDiff.removeAll(tmp);
集合接口数组操作
与Collection接口一致。
List接口
列表是一个有序的集合(有时称为序列)。列表可能包含重复的元素。除了继承自Collection的操作外,List接口还包括以下操作:
- 位置访问 - 根据元素在列表中的数字位置来操作它们。这包括诸如 get、set、add、addAll 和 remove 等方法。
- 搜索 - 在列表中搜索指定的对象并返回其数字位置。搜索方法包括 indexOf 和 lastIndexOf。
- 迭代 - 扩展了 Iterator 的语义,以利用列表的顺序性。listIterator 方法提供了这种行为。
- Range-view - sublist 方法在列表上执行任意的范围操作。
Java平台包含两种通用的List实现。ArrayList和LinkedList,前者通常是性能更好的实现,后者在某些情况下具有更好的性能。
集合操作
remove操作总是将指定元素的第一次出现从列表中删除。add和addAll操作总是将新元素追加到列表的末尾。因此,下面这个成语将一个列表连接到另一个列表。
list1.addAll(list2);
下面是这个例子的另一种形式,它产生了第三个List,由第二个List附加到第一个List上组成。
List<Type> list3 = new ArrayList<Type>(list1);
list3.addAll(list2);
这里有一个例子(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的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中的两个元素,无论其实现类型如何。下面是另一种多态算法,它使用前面的交换方法。
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);
}
}
事实上,这个程序还可以做得更短更快。数组类有一个名为asList的静态工厂方法,它允许将一个数组看成一个List。这个方法并不复制数组。List中的变化会写到数组中,反之亦然。由此产生的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 会以适当的顺序返回列表中的元素。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的参数。listIterator方法有两种形式。没有参数的形式返回一个位于列表开始位置的 ListIterator;有一个 int 参数的形式返回一个位于指定索引位置的 ListIterator。索引指的是初始调用 next 所返回的元素。对 previous 的初始调用将返回索引为 index-1 的元素。在一个长度为n的列表中,索引有n+1个有效值,从0到n,包括在内。
直观地讲,游标总是在两个元素之间--调用previous会返回的元素和调用next会返回的元素。n+1个有效的索引值对应于元素之间的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()会返回我们将要检查的元素的索引,而我们想返回我们刚刚检查的元素的索引。 迭代器接口提供了移除操作,从集合中移除由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(不包括)的部分视图。对subList返回的列表片段的修改也会影响到原来的列表。 这种方法消除了对显式范围操作(通常存在于数组的那种)的需要。任何期望使用List的操作都可以通过传递一个子List视图而不是整个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中的索引。 这里有一个多态的算法,它的实现使用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("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实例的时间越长,你就越有可能直接或通过另一个subList对象来修改支持的List,从而影响到它。请注意,修改子列表的一个子列表并继续使用原来的子列表是合法的。
public static void main(String[] args) throws IOException {
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
List<Integer> subList1 = list.subList(1, 4);
System.out.println(subList1);
List<Integer> subList = list.subList(1, 2);
System.out.println(subList);
subList1.add(5);
subList1.add(6);
System.out.println(subList1);
System.out.println(subList);//抛出异常 java.util.ConcurrentModificationException subList对应的原list已被sublist1修改。
}
列表算法
- sort - 使用合并排序算法对List进行排序,该算法提供了一个快速、稳定的排序。(稳定的排序是指不对相等的元素进行重新排序)。
- shuffle - 对List中的元素进行随机排列。
- reverse - 颠倒列表中元素的顺序。
- rotate - 将列表中的所有元素按指定距离旋转。
- swap - 在列表中的指定位置交换元素。
- replaceAll - 用一个指定的值替换所有出现的值。
- fill - 用指定的值覆盖列表中的每个元素。
- copy - 将源列表复制到目标列表中。
- binarySearch - 使用二进制搜索算法在一个有序列表中搜索一个元素。
- indexOfSubList - 返回一个列表中第一个与另一个相等的子列表的索引。
- lastIndexOfSubList - 返回一个列表中与另一个列表相等的最后一个子列表的索引。