Java 中文官方教程 2022 版(二十七)

2024-05-24 15:22:14 浏览数 (1)

对象排序

原文:docs.oracle.com/javase/tutorial/collections/interfaces/order.html

一个List l可以按以下方式排序。

代码语言:javascript复制
Collections.sort(l);

如果List包含String元素,则将按字母顺序对其进行排序。如果包含Date元素,则将按时间顺序对其进行排序。这是如何发生的呢?StringDate都实现了Comparable接口。Comparable实现为一个类提供了*自然排序*,允许该类的对象自动排序。下表总结了一些更重要的实现Comparable`接口的 Java 平台类。

实现 Comparable 接口的类

自然排序

Byte

有符号数值

Character

无符号数值

Long

有符号数值

Integer

有符号数值

Short

有符号数值

Double

有符号数值

Float

有符号数值

BigInteger

有符号数值

BigDecimal

有符号数值

Boolean

Boolean.FALSE < Boolean.TRUE

File

基于路径名的系统相关字典序

String

字典序

Date

时间顺序

CollationKey

区域特定字典序

如果你尝试对一个不实现Comparable接口的列表进行排序,Collections.sort(list)将抛出一个ClassCastException。同样,如果你尝试使用comparator对无法相互比较的列表进行排序,Collections.sort(list, comparator)将抛出ClassCastException。可以相互比较的元素被称为可相互比较的。尽管不同类型的元素可能是可相互比较的,但这里列出的类中没有一个允许跨类比较。

如果你只想对可比较元素的列表进行排序或创建排序的集合,那么关于Comparable接口,这就是你真正需要知道的全部内容。如果你想要实现自己的Comparable类型,那么下一节将对你感兴趣。

编写自己的可比较类型

Comparable接口包含以下方法。

代码语言:javascript复制
public interface Comparable<T> {
    public int compareTo(T o);
}

compareTo方法比较接收对象与指定对象,并根据接收对象是小于、等于还是大于指定对象返回负整数、0 或正整数。如果指定对象无法与接收对象比较,则该方法会抛出ClassCastException

下面的类代表一个人的名字,实现了Comparableexamples/Name.java

代码语言:javascript复制
import java.util.*;

public class Name implements Comparable<Name> {
    private final String firstName, lastName;

    public Name(String firstName, String lastName) {
        if (firstName == null || lastName == null)
            throw new NullPointerException();
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String firstName() { return firstName; }
    public String lastName()  { return lastName;  }

    public boolean equals(Object o) {
        if (!(o instanceof Name))
            return false;
        Name n = (Name) o;
        return n.firstName.equals(firstName) && n.lastName.equals(lastName);
    }

    public int hashCode() {
        return 31*firstName.hashCode()   lastName.hashCode();
    }

    public String toString() {
	return firstName   " "   lastName;
    }

    public int compareTo(Name n) {
        int lastCmp = lastName.compareTo(n.lastName);
        return (lastCmp != 0 ? lastCmp : firstName.compareTo(n.firstName));
    }
}

为了保持前面的示例简洁,该类有一定的限制:它不支持中间名,要求同时有名和姓,而且在任何方面都没有国际化。尽管如此,它阐明了以下重要点:

  • Name 对象是不可变的。其他条件相同的情况下,不可变类型是最好的选择,特别是对于将用作 Set 中的元素或 Map 中的键的对象。如果在集合中修改它们的元素或键,这些集合将会中断。
  • 构造函数检查其参数是否为 null。这确保所有的 Name 对象都是格式良好的,以便其他方法永远不会抛出 NullPointerException
  • hashCode 方法被重新定义。这对于重新定义 equals 方法的任何类都是必不可少的。(相等的对象必须具有相等的哈希码。)
  • equals 方法在指定对象为 null 或不合适类型时返回 falsecompareTo 方法在这些情况下会抛出运行时异常。这两种行为都是各自方法的一般契约所要求的。
  • toString 方法已被重新定义,以便以人类可读的形式打印 Name。这总是一个好主意,特别是对于将要放入集合中的对象。各种集合类型的 toString 方法依赖于它们的元素、键和值的 toString 方法。

由于本节是关于元素排序的,让我们再谈一下 NamecompareTo 方法。它实现了标准的名称排序算法,其中姓氏优先于名字。这正是你在自然排序中想要的。如果自然排序是不自然的,那将会非常令人困惑!

看一下 compareTo 的实现方式,因为它是相当典型的。首先,你比较对象的最重要部分(在这种情况下是姓)。通常情况下,你可以直接使用部分类型的自然排序。在这种情况下,部分是一个 String,自然(词典)排序正是所需的。如果比较结果不是零,表示相等,你就完成了:只需返回结果。如果最重要的部分相等,你继续比较下一个最重要的部分。在这种情况下,只有两个部分——名和姓。如果有更多的部分,你会按照明显的方式继续,比较部分直到找到两个不相等的部分或者你正在比较最不重要的部分,此时你会返回比较的结果。

为了展示它是如何工作的,这里是一个构建名称列表并对其进行排序的程序。

代码语言:javascript复制
import java.util.*;

public class NameSort {
    public static void main(String[] args) {
        Name nameArray[] = {
            new Name("John", "Smith"),
            new Name("Karl", "Ng"),
            new Name("Jeff", "Smith"),
            new Name("Tom", "Rich")
        };

        List<Name> names = Arrays.asList(nameArray);
        Collections.sort(names);
        System.out.println(names);
    }
}

如果你运行这个程序,这是它打印的内容。

代码语言:javascript复制
[Karl Ng, Tom Rich, Jeff Smith, John Smith]

compareTo方法的行为有四个限制,我们现在不会详细讨论,因为它们相当技术性和乏味,并且最好留在 API 文档中。所有实现Comparable的类都必须遵守这些限制,因此如果您正在编写实现它的类,请阅读Comparable的文档。尝试对违反这些限制的对象列表进行排序会导致未定义的行为。从技术上讲,这些限制确保自然排序是实现它的类的对象上的全序;这是确保排序是明确定义的必要条件。

比较器

如果您想按照除自然排序之外的顺序对一些对象进行排序怎么办?或者如果您想对一些不实现Comparable接口的对象进行排序怎么办?要做到这两点,您需要提供一个Comparator — 一个封装排序的对象。与Comparable接口一样,Comparator接口由一个方法组成。

代码语言:javascript复制
public interface Comparator<T> {
    int compare(T o1, T o2);
}

compare方法比较其两个参数,根据第一个参数是否小于、等于或大于第二个参数返回负整数、0 或正整数。如果任一参数的类型对于Comparator不合适,则compare方法会抛出ClassCastException

大部分关于Comparable的内容也适用于Comparator。编写compare方法几乎与编写compareTo方法相同,只是前者将两个对象作为参数传递。compare方法必须遵守与ComparablecompareTo方法相同的四个技术限制,原因是Comparator必须对其比较的对象引入一个全序。

假设您有一个名为Employee的类,如下所示。

代码语言:javascript复制
public class Employee implements Comparable<Employee> {
    public Name name()     { ... }
    public int number()    { ... }
    public Date hireDate() { ... }
       ...
}

假设Employee实例的自然排序是根据员工姓名(如前面的示例中定义的)的Name排序。不幸的是,老板要求按资历顺序列出员工名单。这意味着我们需要做一些工作,但不多。以下程序将生成所需的列表。

代码语言:javascript复制
import java.util.*;
public class EmpSort {
    static final Comparator<Employee> SENIORITY_ORDER = 
                                        new Comparator<Employee>() {
            public int compare(Employee e1, Employee e2) {
                return e2.hireDate().compareTo(e1.hireDate());
            }
    };

    // Employee database
    static final Collection<Employee> employees = ... ;

    public static void main(String[] args) {
        List<Employee> e = new ArrayList<Employee>(employees);
        Collections.sort(e, SENIORITY_ORDER);
        System.out.println(e);
    }
}

程序中的Comparator相当简单。它依赖于应用于hireDate访问器方法返回的值的Date的自然排序。请注意,Comparator将其第二个参数的入职日期传递给其第一个参数,而不是反过来。原因是最近入职的员工资历最低;按照入职日期排序会将列表按照逆资历顺序排列。有时人们用来实现这种效果的另一种技术是保持参数顺序,但对比较结果取反。

代码语言:javascript复制
// Don't do this!!
return -r1.hireDate().compareTo(r2.hireDate());

你应该始终使用前一种技术而不是后一种,因为后一种不能保证有效。原因是compareTo方法如果其参数小于调用它的对象,则可以返回任何负的int。有一个负的int在取反后仍然是负的,尽管这看起来很奇怪。

代码语言:javascript复制
-Integer.MIN_VALUE == Integer.MIN_VALUE

前面程序中的Comparator用于对List进行排序很好,但它有一个缺陷:它不能用于对已排序的集合(如TreeSet)进行排序,因为它生成的排序与equals不兼容。这意味着这个Comparator将把equals方法不认为相等的对象等同起来。特别是,任何在同一天入职的两名员工将被视为相等。当你对List进行排序时,这并不重要;但当你使用Comparator对已排序的集合进行排序时,这是致命的。如果你使用这个Comparator将多名在同一天入职的员工插入TreeSet,只有第一个会被添加到集合中;第二个将被视为重复元素并被忽略。

要解决这个问题,只需微调Comparator,使其生成一个与equals兼容的排序。换句话说,调整它使得当使用compare进行比较时,只有那些在使用equals进行比较时也被视为相等的元素才被视为相等。做法是执行一个两部分比较(如对Name),其中第一部分是我们感兴趣的部分——在这种情况下是入职日期——第二部分是一个唯一标识对象的属性。在这里,员工编号是显而易见的属性。这是产生的Comparator

代码语言:javascript复制
static final Comparator<Employee> SENIORITY_ORDER = 
                                        new Comparator<Employee>() {
    public int compare(Employee e1, Employee e2) {
        int dateCmp = e2.hireDate().compareTo(e1.hireDate());
        if (dateCmp != 0)
            return dateCmp;

        return (e1.number() < e2.number() ? -1 :
               (e1.number() == e2.number() ? 0 : 1));
    }
};

最后一点:你可能会想要用更简单的方式替换Comparator中的最后一个return语句:

代码语言:javascript复制
return e1.number() - e2.number();

不要这样做,除非你绝对确定没有人会有负的员工编号!这个技巧通常不起作用,因为有符号整数类型不足以表示任意两个有符号整数的差值。如果i是一个很大的正整数,而j是一个很大的负整数,i - j会溢出并返回一个负整数。所得到的comparator违反了我们一直谈论的四个技术限制之一(传递性),并产生可怕的、微妙的错误。这不仅仅是理论上的担忧;人们会因此受到伤害。

SortedSet 接口

原文:docs.oracle.com/javase/tutorial/collections/interfaces/sorted-set.html

SortedSet 是一个按升序维护其元素的集合,根据元素的自然顺序或在 SortedSet 创建时提供的 Comparator 进行排序。除了正常的 Set 操作外,SortedSet 接口还提供以下操作:

  • Range view — 允许对排序集合进行任意范围操作
  • Endpoints — 返回排序集合中的第一个或最后一个元素
  • Comparator access — 返回用于对集合进行排序的 Comparator(如果有的话)

SortedSet 接口的代码如下。

代码语言:javascript复制
public interface SortedSet<E> extends Set<E> {
    // Range-view
    SortedSet<E> subSet(E fromElement, E toElement);
    SortedSet<E> headSet(E toElement);
    SortedSet<E> tailSet(E fromElement);

    // Endpoints
    E first();
    E last();

    // Comparator access
    Comparator<? super E> comparator();
}

集合操作

SortedSetSet 继承的操作在排序集合和普通集合上的行为完全相同,但有两个例外:

  • iterator 操作返回的 Iterator 按顺序遍历排序集合。
  • toArray 返回的数组按顺序包含了排序后的集合元素。

尽管接口不保证,但 Java 平台的 SortedSet 实现的 toString 方法返回一个包含排序集合所有元素的字符串,按顺序排列。

标准构造函数

按照惯例,所有通用的 Collection 实现都提供一个标准的转换构造函数,接受一个 CollectionSortedSet 实现也不例外。在 TreeSet 中,这个构造函数创建一个根据元素的自然顺序排序的实例。这可能是一个错误。最好动态检查指定的集合是否是 SortedSet 实例,如果是,则根据相同的标准(比较器或自然顺序)对新的 TreeSet 进行排序。因为 TreeSet 采取了它的方法,它还提供一个接受 SortedSet 的构造函数,并返回一个包含相同元素并根据相同标准排序的新 TreeSet。请注意,参数的编译时类型,而不是运行时类型,决定调用这两个构造函数中的哪一个(以及是否保留排序标准)。

SortedSet 实现通常还提供一个构造函数,接受一个 Comparator 并返回一个根据指定 Comparator 排序的空集合。如果传递 null 给这个构造函数,它将返回一个根据元素的自然顺序排序的集合。

范围视图操作

range-view操作在某种程度上类似于List接口提供的操作,但有一个重大区别。排序集的范围视图即使在直接修改支持的排序集的情况下仍然有效。这是因为排序集的范围视图的端点是元素空间中的绝对点,而不是备份集合中的特定元素,这对于列表是成立的。排序集的range-view实际上只是窗口,显示在元素空间的指定部分中集合的任何部分。对排序集的range-view的更改会写回到支持的排序集中,反之亦然。因此,长时间使用排序集上的range-view是可以的,不像列表上的range-view那样。

排序集提供三种range-view操作。第一种是subSet,类似于subList,需要两个端点。端点不是索引,而是对象,并且必须与排序集中的元素可比,使用SetComparator或其元素的自然排序,取决于Set用于对自身排序的方式。与subList类似,范围是半开的,包括其低端点但不包括高端点。

因此,下面这行代码告诉你在名为dictionary的字符串SortedSet中包含多少个介于"doorbell""pickle"之间的单词,包括"doorbell"但不包括"pickle"

代码语言:javascript复制
int count = dictionary.subSet("doorbell", "pickle").size();

以类似的方式,以下一行代码可以删除所有以字母f开头的元素。

代码语言:javascript复制
dictionary.subSet("f", "g").clear();

类似的技巧可以用来打印一个表格,告诉你每个字母开头的单词有多少个。

代码语言:javascript复制
for (char ch = 'a'; ch <= 'z'; ) {
    String from = String.valueOf(ch  );
    String to = String.valueOf(ch);
    System.out.println(from   ": "   dictionary.subSet(from, to).size());
}

假设你想查看一个闭区间,其中包含两个端点,而不是一个开区间。如果元素类型允许计算元素空间中给定值的后继,只需请求从lowEndpointsuccessor(highEndpoint)subSet。虽然这并不是完全明显的,但在String的自然排序中,字符串s的后继是s "",也就是在s后附加一个null字符。

因此,以下一行代码告诉你在字典中包含多少个介于"doorbell""pickle"之间的单词,包括doorbell pickle

代码语言:javascript复制
count = dictionary.subSet("doorbell", "pickle").size();

类似的技巧可以用来查看一个开区间,其中不包含任何端点。从lowEndpointhighEndpoint的开区间视图是从successor(lowEndpoint)highEndpoint的半开区间。使用以下代码计算介于"doorbell""pickle"之间的单词数量,不包括两者。

代码语言:javascript复制
count = dictionary.subSet("doorbell", "pickle").size();

SortedSet 接口包含两个更多的 range-view 操作 — headSettailSet,两者都接受一个 Object 参数。前者返回一个视图,显示了支持 SortedSet 的初始部分,直到但不包括指定的对象。后者返回一个视图,显示了支持 SortedSet 的最终部分,从指定的对象开始,一直到支持 SortedSet 的末尾。因此,以下代码允许您将字典视为两个不相交的 a-mn-z)。

代码语言:javascript复制
SortedSet<String> volume1 = dictionary.headSet("n");
SortedSet<String> volume2 = dictionary.tailSet("n");

端点操作

SortedSet 接口包含返回排序集合中第一个和最后一个元素的操作,分别称为 firstlast。除了它们的明显用途外,last 还允许解决 SortedSet 接口中的一个缺陷。您希望对 SortedSet 进行的一件事是进入 Set 的内部并向前或向后迭代。从内部向前迭代很容易:只需获取一个 tailSet 并对其进行迭代。不幸的是,向后迭代没有简单的方法。

以下习语获取了元素空间中小于指定对象 o 的第一个元素。

代码语言:javascript复制
Object predecessor = ss.headSet(o).last();

这是从排序集合内部的某一点向后移动一个元素的好方法。可以重复应用它来向后迭代,但这非常低效,每返回一个元素都需要查找。

比较器访问器

SortedSet 接口包含一个名为 comparator 的访问器方法,返回用于对集合进行排序的 Comparator,如果集合根据其元素的 自然顺序 进行排序,则返回 null。提供此方法是为了可以将排序集合复制到具有相同顺序的新排序集合中。它被描述为 SortedSet 构造函数使用的 先前。

排序地图接口

原文:docs.oracle.com/javase/tutorial/collections/interfaces/sorted-map.html

一个SortedMap是一个维护其条目按升序排列的Map,根据键的自然顺序排序,或者根据在创建SortedMap时提供的Comparator排序。自然排序和Comparator在对象排序部分讨论。SortedMap接口提供了常规Map操作以及以下操作:

  • Range view — 在排序地图上执行任意范围操作
  • Endpoints — 返回排序地图中的第一个或最后一个键
  • Comparator access — 返回用于对地图进行排序的Comparator(如果有的话)。

以下接口是SortedSetMap模拟。

代码语言:javascript复制
public interface SortedMap<K, V> extends Map<K, V>{
    Comparator<? super K> comparator();
    SortedMap<K, V> subMap(K fromKey, K toKey);
    SortedMap<K, V> headMap(K toKey);
    SortedMap<K, V> tailMap(K fromKey);
    K firstKey();
    K lastKey();
}

地图操作

SortedMapMap继承的操作在排序地图和普通地图上的行为相同,有两个例外:

  • 任何排序地图的Collection视图上的iterator操作返回的Iterator按顺序遍历集合。
  • Collection视图的toArray操作返回的数组按顺序包含键、值或条目。

虽然接口不能保证,但 Java 平台所有SortedMap实现中Collection视图的toString方法返回一个字符串,其中包含视图中的所有元素,按顺序排列。

标准构造函数

按照惯例,所有通用Map实现都提供一个标准转换构造函数,接受一个MapSortedMap实现也不例外。在TreeMap中,这个构造函数创建一个根据其键的自然顺序排序其条目的实例。这可能是一个错误。最好动态检查指定的Map实例是否是SortedMap,如果是,则根据相同的标准(比较器或自然顺序)对新地图进行排序。因为TreeMap采取了它的方法,它还提供了一个接受SortedMap的构造函数,并返回一个包含与给定SortedMap相同映射的新TreeMap,根据相同标准排序。请注意,参数的编译时类型,而不是运行时类型,决定了是否优先调用SortedMap构造函数而不是普通的map构造函数。

按照惯例,SortedMap实现还提供一个接受Comparator的构造函数,并返回根据指定Comparator排序的空地图。如果将null传递给此构造函数,则返回一个根据其键的自然顺序对其映射进行排序的Map

与 SortedSet 的比较

因为这个接口是SortedSet的精确Map模拟,所以在 SortedSet 接口部分中的所有习语和代码示例都适用于SortedMap,只需进行微不足道的修改。

接口总结

原文:docs.oracle.com/javase/tutorial/collections/interfaces/summary.html

核心集合接口是 Java 集合框架的基础。

Java 集合框架层次结构由两个不同的接口树组成:

  • 第一个树以Collection接口开始,该接口提供了所有集合使用的基本功能,如addremove方法。它的子接口——SetListQueue——提供了更专门化的集合。
  • Set接口不允许重复元素。这对于存储诸如一副牌或学生记录之类的集合非常有用。Set接口有一个子接口,SortedSet,用于对集合中的元素进行排序。
  • List接口提供了一个有序的集合,用于需要精确控制每个元素插入位置的情况。您可以通过它们的确切位置从List中检索元素。
  • Queue接口允许额外的插入、提取和检查操作。Queue中的元素通常按照 FIFO 顺序排序。
  • Deque接口允许在两端进行插入、删除和检查操作。Deque中的元素可以同时用于 LIFO 和 FIFO。
  • 第二个树以Map接口开始,类似于Hashtable将键和值进行映射。
  • Map的子接口SortedMap将其键值对按升序或按Comparator指定的顺序维护。

这些接口允许集合在不考虑其表示细节的情况下进行操作。

问题和练习:接口

原文:docs.oracle.com/javase/tutorial/collections/interfaces/QandE/questions.html

问题

  1. 在本课程的开始,您了解到核心集合接口被组织成两个不同的继承树。特别是一个接口被认为不是真正的Collection,因此位于自己的树的顶部。这个接口的名称是什么?
  2. 集合框架中的每个接口都使用<E>语法声明,告诉您它是泛型的。当您声明一个Collection实例时,指定它将包含的对象类型有什么优势?
  3. 什么接口代表不允许重复元素的集合?
  4. 什么接口形成了集合层次结构的根?
  5. 什么接口代表可能包含重复元素的有序集合?
  6. 什么接口代表在处理之前保存元素的集合?
  7. 什么接口代表将键映射到值的类型?
  8. 什么接口代表双端队列?
  9. 列出遍历List元素的三种不同方法。
  10. 真或假:聚合操作是修改基础集合的变异操作。

练习

  1. 编写一个以随机顺序打印其参数的程序。不要复制参数数组。演示如何使用流和传统的增强 for 语句打印元素。
  2. FindDups示例,并修改它以使用SortedSet而不是Set。指定一个Comparator,以便在排序和识别集合元素时忽略大小写。
  3. 编写一个方法,接受一个List<String>并对每个元素应用String.trim
  4. 考虑四个核心接口,SetListQueueMap。对于以下四个任务中的每一个,指定哪个核心接口最适合,并解释如何使用它来实现任务。
    1. Whimsical Toys Inc(WTI)需要记录所有员工的姓名。每个月,将从这些记录中随机选择一个员工以获得免费玩具。
    2. WTI 已决定每个新产品都将以员工的名字命名,但只使用名字的第一个字母,并且每个名字只能使用一次。准备一个独特的名字列表。
    3. WTI 决定只想使用最受欢迎的名字来命名其玩具。统计每个名字的员工数量。
    4. WTI 为当地的长曲棍球队购买季票,将由员工共享。为这项受欢迎的运动创建一个等待名单。

检查你的答案。

课程:聚合操作

原文:docs.oracle.com/javase/tutorial/collections/streams/index.html

注意:要更好地理解本节中的概念,请查看 Lambda 表达式和方法引用部分。

您使用集合做什么?您不仅仅将对象存储在集合中并将其留在那里。在大多数情况下,您使用集合来检索其中存储的项目。

再次考虑 Lambda 表达式部分中描述的场景。假设您正在创建一个社交网络应用。您希望创建一个功能,使管理员能够对满足某些条件的社交网络应用成员执行任何操作,例如发送消息。

与之前一样,假设这个社交网络应用的成员由以下Person类表示:

代码语言:javascript复制
public class Person {

    public enum Sex {
        MALE, FEMALE
    }

    String name;
    LocalDate birthday;
    Sex gender;
    String emailAddress;

    // ...

    public int getAge() {
        // ...
    }

    public String getName() {
        // ...
    }
}

以下示例使用 for-each 循环打印集合roster中包含的所有成员的名称:

代码语言:javascript复制
for (Person p : roster) {
    System.out.println(p.getName());
}

以下示例使用聚合操作forEach打印出集合roster中包含的所有成员:

代码语言:javascript复制
roster
    .stream()
    .forEach(e -> System.out.println(e.getName());

尽管在此示例中,使用聚合操作的版本比使用 for-each 循环的版本更长,但您将看到,对于更复杂的任务,使用批量数据操作的版本将更简洁。

下面涵盖了以下主题:

  • 管道和流
  • 聚合操作和迭代器之间的区别

在示例BulkDataOperationsExamples中找到本节描述的代码片段。

管道和流

管道是一系列聚合操作。以下示例使用由聚合操作filterforEach组成的管道打印集合roster中包含的男性成员:

代码语言:javascript复制
roster
    .stream()
    .filter(e -> e.getGender() == Person.Sex.MALE)
    .forEach(e -> System.out.println(e.getName()));

将此示例与使用 for-each 循环打印集合roster中的男性成员的示例进行比较:

代码语言:javascript复制
for (Person p : roster) {
    if (p.getGender() == Person.Sex.MALE) {
        System.out.println(p.getName());
    }
}

一个管道包含以下组件:

  • 源:这可以是集合、数组、生成器函数或 I/O 通道。在此示例中,源是集合roster
  • 零个或多个中间操作。中间操作,如filter,会生成一个新流。 是元素的序列。与集合不同,它不是存储元素的数据结构。相反,流通过管道从源头传递值。此示例通过调用stream方法从集合roster创建流。 filter操作返回一个包含与其谓词(该操作的参数)匹配的元素的新流。在这个示例中,谓词是 lambda 表达式e -> e.getGender() == Person.Sex.MALE。如果对象egender字段的值为Person.Sex.MALE,则返回布尔值true。因此,在这个示例中,filter操作返回一个包含集合roster中所有男性成员的流。
  • 终端操作。终端操作,比如forEach,产生一个非流结果,比如一个原始值(比如一个双精度值)、一个集合,或者在forEach的情况下,根本没有值。在这个示例中,forEach操作的参数是 lambda 表达式e -> System.out.println(e.getName()),它在对象e上调用getName方法。(Java 运行时和编译器推断出对象e的类型是Person。)

以下示例计算了集合roster中所有男性成员的平均年龄,使用了由filtermapToIntaverage聚合操作组成的流水线:

代码语言:javascript复制
double average = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

mapToInt操作返回一个新的IntStream类型的流(这是一个只包含整数值的流)。该操作将其参数中指定的函数应用于特定流中的每个元素。在这个示例中,函数是Person::getAge,这是一个返回成员年龄的方法引用。(或者,你可以使用 lambda 表达式e -> e.getAge()。)因此,在这个示例中,mapToInt操作返回一个包含集合roster中所有男性成员年龄的流。

average操作计算IntStream类型流中包含的元素的平均值。它返回一个OptionalDouble类型的对象。如果流不包含任何元素,则average操作返回一个空的OptionalDouble实例,并调用getAsDouble方法会抛出NoSuchElementException。JDK 包含许多像average这样返回通过组合流内容得到的一个值的终端操作。这些操作被称为归约操作;更多信息请参见归约部分。

聚合操作和迭代器之间的区别

聚合操作,如forEach,看起来像迭代器。然而,它们有几个根本的区别:

  • 它们使用内部迭代:聚合操作不包含像next这样的方法来指示它们处理集合的下一个元素。通过内部委托,您的应用程序确定迭代的集合,但 JDK 确定如何迭代集合。通过外部迭代,您的应用程序确定要迭代的集合以及如何迭代它。然而,外部迭代只能按顺序迭代集合的元素。内部迭代没有这种限制。它可以更容易地利用并行计算,这涉及将问题分解为子问题,同时解决这些问题,然后将子问题的解决方案合并为结果。有关更多信息,请参见并行性部分。
  • 它们从流中处理元素:聚合操作从流中处理元素,而不是直接从集合中处理。因此,它们也被称为流操作
  • 它们支持行为作为参数:您可以为大多数聚合操作指定 lambda 表达式作为参数。这使您可以自定义特定聚合操作的行为。

缩减

原文:docs.oracle.com/javase/tutorial/collections/streams/reduction.html

章节聚合操作描述了以下操作流水线,它计算roster集合中所有男性成员的平均年龄:

代码语言:javascript复制
double average = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

JDK 包含许多终端操作(如averagesumminmaxcount),它们通过组合流的内容返回一个值。这些操作称为缩减操作。JDK 还包含返回集合而不是单个值的缩减操作。许多缩减操作执行特定任务,比如找到值的平均值或将元素分组到类别中。然而,JDK 为您提供了通用的缩减操作reducecollect,本节将详细描述这些操作。

本节涵盖以下主题:

  • Stream.reduce 方法
  • Stream.collect 方法

你可以在示例ReductionExamples中找到本节中描述的代码片段。

Stream.reduce 方法

Stream.reduce 方法是一个通用的缩减操作。考虑以下流水线,它计算roster集合中男性成员年龄的总和。它使用Stream.sum 缩减操作:

代码语言:javascript复制
Integer totalAge = roster
    .stream()
    .mapToInt(Person::getAge)
    .sum();

将此与以下使用Stream.reduce操作计算相同值的流水线进行比较:

代码语言:javascript复制
Integer totalAgeReduce = roster
   .stream()
   .map(Person::getAge)
   .reduce(
       0,
       (a, b) -> a   b);

在这个例子中,reduce 操作接受两个参数:

identity:身份元素既是缩减的初始值,也是如果流中没有元素时的默认结果。在这个例子中,身份元素是0;这是年龄总和的初始值,也是如果roster集合中不存在成员时的默认值。

accumulator: 累加器函数接受两个参数:减少的部分结果(在这个例子中,到目前为止所有处理过的整数的总和)和流的下一个元素(在这个例子中,一个整数)。它返回一个新的部分结果。在这个例子中,累加器函数是一个 lambda 表达式,它将两个Integer值相加并返回一个Integer值:

代码语言:javascript复制
(a, b) -> a   b

reduce操作总是返回一个新值。然而,累加器函数在处理流的每个元素时也返回一个新值。假设你想将流的元素减少到一个更复杂的对象,比如一个集合。这可能会影响你的应用程序的性能。如果你的reduce操作涉及将元素添加到一个集合中,那么每次累加器函数处理一个元素时,它都会创建一个包含该元素的新集合,这是低效的。更新现有集合会更有效。你可以使用Stream.collect方法来实现这一点,下一节将介绍。

collect 方法

reduce方法不同,它在处理元素时总是创建一个新值,collect方法修改或改变了现有值。

考虑如何在流中找到值的平均值。你需要两个数据:值的总数和这些值的总和。然而,像reduce方法和所有其他减少方法一样,collect方法只返回一个值。你可以创建一个包含成员变量来跟踪值的总数和这些值总和的新数据类型,比如下面的类Averager

代码语言:javascript复制
class Averager implements IntConsumer
{
    private int total = 0;
    private int count = 0;

    public double average() {
        return count > 0 ? ((double) total)/count : 0;
    }

    public void accept(int i) { total  = i; count  ; }
    public void combine(Averager other) {
        total  = other.total;
        count  = other.count;
    }
}

以下管道使用Averager类和collect方法来计算所有男性成员的平均年龄:

代码语言:javascript复制
Averager averageCollect = roster.stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(Person::getAge)
    .collect(Averager::new, Averager::accept, Averager::combine);

System.out.println("Average age of male members: "  
    averageCollect.average());

这个例子中的collect操作接受三个参数:

  • supplier: 供应商是一个工厂函数;它构造新实例。对于collect操作,它创建结果容器的实例。在这个例子中,它是Averager类的一个新实例。
  • accumulator: 累加器函数将流元素合并到结果容器中。在这个例子中,它通过将count变量加一并将流元素的值(代表男性成员年龄的整数)加到total成员变量中,修改了Averager结果容器。
  • combiner:合并器函数接受两个结果容器并合并它们的内容。在这个例子中,它通过增加另一个Averager实例的count成员变量的值到Averager结果容器的count变量,并将另一个Averager实例的total成员变量的值加到total成员变量中来修改Averager结果容器。

请注意以下内容:

  • 供应商是一个 lambda 表达式(或方法引用),与reduce操作中的单位元素等值相对。
  • 累加器和合并器函数不返回值。
  • 您可以在并行流中使用collect操作;有关更多信息,请参阅并行性部分。(如果您在并行流中运行collect方法,那么当合并器函数创建一个新对象时,例如在这个例子中创建一个Averager对象时,JDK 会创建一个新线程。因此,您不必担心同步。)

尽管 JDK 为您提供了average操作来计算流中元素的平均值,但如果您需要从流的元素计算多个值,可以使用collect操作和自定义类。

collect操作最适合集合。以下示例使用collect操作将男性成员的姓名放入集合中:

代码语言:javascript复制
List<String> namesOfMaleMembersCollect = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(p -> p.getName())
    .collect(Collectors.toList());

此版本的collect操作接受一个类型为Collector的参数。这个类封装了在collect操作中用作参数的函数,该操作需要三个参数(供应商、累加器和合并器函数)。

Collectors类包含许多有用的归约操作,例如将元素累积到集合中并根据各种标准对元素进行总结。这些归约操作返回Collector类的实例,因此您可以将它们作为collect操作的参数使用。

此示例使用Collectors.toList操作,将流元素累积到一个新的List实例中。与Collectors类中的大多数操作一样,toList操作符返回一个Collector实例,而不是一个集合。

以下示例按性别对集合roster的成员进行分组:

代码语言:javascript复制
Map<Person.Sex, List<Person>> byGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(Person::getGender));

groupingBy 操作返回一个映射,其键是应用作为其参数指定的 lambda 表达式(称为分类函数)的结果值。在这个例子中,返回的映射包含两个键,Person.Sex.MALEPerson.Sex.FEMALE。键对应的值是包含通过分类函数处理时对应于键值的流元素的 List 实例。例如,对应于键 Person.Sex.MALE 的值是一个包含所有男性成员的 List 实例。

以下示例按性别检索集合 roster 中每个成员的名称并按性别分组:

代码语言:javascript复制
Map<Person.Sex, List<String>> namesByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.mapping(
                    Person::getName,
                    Collectors.toList())));

在这个例子中,groupingBy 操作需要两个参数,一个分类函数和一个 Collector 实例。Collector 参数称为下游收集器。这是 Java 运行时应用于另一个收集器结果的收集器。因此,这个 groupingBy 操作使您能够对 groupingBy 操作符创建的 List 值应用 collect 方法。这个例子应用了收集器 mapping,它将映射函数 Person::getName 应用于流的每个元素。因此,结果流只包含成员的名称。包含一个或多个下游收集器的管道,像这个例子一样,称为多级减少

以下示例检索每个性别成员的总年龄:

代码语言:javascript复制
Map<Person.Sex, Integer> totalAgeByGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(
                Person::getGender,                      
                Collectors.reducing(
                    0,
                    Person::getAge,
                    Integer::sum)));

reducing 操作需要三个参数:

  • identity:类似于 Stream.reduce 操作,身份元素既是减少的初始值,也是如果流中没有元素时的默认结果。在这个例子中,身份元素是 0;这是年龄总和的初始值,如果没有成员存在,则是默认值。
  • mapperreducing 操作将此映射函数应用于所有流元素。在这个例子中,映射器检索每个成员的年龄。
  • operation:操作函数用于减少映射值。在这个例子中,操作函数添加 Integer 值。

以下示例检索每个性别成员的平均年龄:

代码语言:javascript复制
Map<Person.Sex, Double> averageAgeByGender = roster
    .stream()
    .collect(
        Collectors.groupingBy(
            Person::getGender,                      
            Collectors.averagingInt(Person::getAge)));

并行性

原文:docs.oracle.com/javase/tutorial/collections/streams/parallelism.html

并行计算涉及将问题分解为子问题,同时解决这些问题(并行执行,每个子问题在单独的线程中运行),然后将这些子问题的解决方案合并。Java SE 提供了分支/合并框架,它使您能够更轻松地在应用程序中实现并行计算。然而,使用此框架时,您必须指定如何将问题细分(分区)。使用聚合操作,Java 运行时为您执行此分区和解决方案的合并。

在使用集合的应用程序中实现并行性的一个困难在于集合不是线程安全的,这意味着多个线程不能在不引入线程干扰或内存一致性错误的情况下操作集合。集合框架提供了同步包装器,它可以为任意集合添加自动同步,使其线程安全。然而,同步会引入线程争用。你应该避免线程争用,因为它会阻止线程并行运行。聚合操作和并行流使你能够在非线程安全的集合上实现并行性,前提是在操作集合时不修改它。

请注意,并行性并不自动比串行执行操作更快,尽管在有足够数据和处理器核心的情况下可能会更快。虽然聚合操作使您更容易实现并行性,但确定您的应用程序是否适合并行性仍然是您的责任。

本节涵盖以下主题:

  • 并行执行流
  • 并发归约
  • 顺序
  • 副作用
    • 懒惰性
    • 干扰
    • 有状态的 Lambda 表达式

你可以在示例ParallelismExamples中找到本节中描述的代码摘录。

并行执行流

你可以串行或并行执行流。当流并行执行时,Java 运行时将流分成多个子流。聚合操作并行迭代和处理这些子流,然后将结果合并。

当您创建一个流时,除非另有说明,它总是一个串行流。要创建一个并行流,请调用操作Collection.parallelStream。或者,调用操作BaseStream.parallel。例如,以下语句计算所有男性成员的平均年龄:

代码语言:javascript复制
double average = roster
    .parallelStream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

并发减少

再次考虑以下示例(在减少部分中描述),该示例按性别对成员进行分组。此示例调用collect操作,将集合roster减少为Map

代码语言:javascript复制
Map<Person.Sex, List<Person>> byGender =
    roster
        .stream()
        .collect(
            Collectors.groupingBy(Person::getGender));

以下是并行等价的:

代码语言:javascript复制
ConcurrentMap<Person.Sex, List<Person>> byGender =
    roster
        .parallelStream()
        .collect(
            Collectors.groupingByConcurrent(Person::getGender));

这被称为并发减少。如果包含collect操作的特定管道满足以下所有条件,则 Java 运行时执行并发减少:

  • 流是并行的。
  • collect操作的参数,收集器,具有特征Collector.Characteristics.CONCURRENT。要确定收集器的特征,请调用Collector.characteristics方法。
  • 流要么是无序的,要么收集器具有特征Collector.Characteristics.UNORDERED。要确保流是无序的,请调用BaseStream.unordered操作。

注意:此示例返回一个ConcurrentMap的实例,而不是Map,并调用groupingByConcurrent操作,而不是groupingBy。(有关ConcurrentMap的更多信息,请参见并发集合部分。)与操作groupingByConcurrent不同,操作groupingBy在并行流中表现不佳。(这是因为它通过键合并两个映射,这在计算上是昂贵的。)同样,操作Collectors.toConcurrentMap在并行流中的性能优于操作Collectors.toMap

排序

流水线处理流的元素的顺序取决于流是串行执行还是并行执行、流的来源以及中间操作。例如,考虑以下示例,该示例多次使用forEach操作打印ArrayList实例的元素:

代码语言:javascript复制
Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8 };
List<Integer> listOfIntegers =
    new ArrayList<>(Arrays.asList(intArray));

System.out.println("listOfIntegers:");
listOfIntegers
    .stream()
    .forEach(e -> System.out.print(e   " "));
System.out.println("");

System.out.println("listOfIntegers sorted in reverse order:");
Comparator<Integer> normal = Integer::compare;
Comparator<Integer> reversed = normal.reversed(); 
Collections.sort(listOfIntegers, reversed);  
listOfIntegers
    .stream()
    .forEach(e -> System.out.print(e   " "));
System.out.println("");

System.out.println("Parallel stream");
listOfIntegers
    .parallelStream()
    .forEach(e -> System.out.print(e   " "));
System.out.println("");

System.out.println("Another parallel stream:");
listOfIntegers
    .parallelStream()
    .forEach(e -> System.out.print(e   " "));
System.out.println("");

System.out.println("With forEachOrdered:");
listOfIntegers
    .parallelStream()
    .forEachOrdered(e -> System.out.print(e   " "));
System.out.println("");

该示例包含五个流水线。它打印类似以下的输出:

代码语言:javascript复制
listOfIntegers:
1 2 3 4 5 6 7 8
listOfIntegers sorted in reverse order:
8 7 6 5 4 3 2 1
Parallel stream:
3 4 1 6 2 5 7 8
Another parallel stream:
6 3 1 5 7 8 4 2
With forEachOrdered:
8 7 6 5 4 3 2 1

该示例执行以下操作:

  • 第一个流水线按照它们添加到列表中的顺序打印listOfIntegers列表的元素。
  • 第二个流水线在使用Collections.sort方法对listOfIntegers进行排序后打印元素。
  • 第三和第四个流水线以一种看似随机的顺序打印列表的元素。请记住,流操作在处理流的元素时使用内部迭代。因此,当您并行执行流时,除非流操作另有规定,否则 Java 编译器和运行时会确定处理流元素的顺序,以最大化并行计算的好处。
  • 第五个流水线使用forEachOrdered方法,该方法按照其来源指定的顺序处理流的元素,无论您是串行还是并行执行流。请注意,如果您在并行流中使用类似forEachOrdered的操作,可能会丧失并行性的好处。

副作用

如果一个方法或表达式除了返回或产生一个值外,还修改了计算机的状态,那么它就具有副作用。例如,可变归约(使用collect操作的操作;有关更多信息,请参见 Reduction 部分)以及调用System.out.println方法进行调试。JDK 在流水线中处理某些副作用很好。特别是,collect方法被设计为以并行安全的方式执行具有副作用的最常见流操作。forEachpeek等操作是为了副作用而设计的;一个返回 void 的 Lambda 表达式,比如调用System.out.println的 Lambda 表达式,除了具有副作用外什么也做不了。即便如此,你应该谨慎使用forEachpeek操作;如果你在并行流中使用其中一个操作,那么 Java 运行时可能会从多个线程同时调用你指定为参数的 Lambda 表达式。此外,永远不要在filtermap等操作中传递具有副作用的 Lambda 表达式作为参数。接下来的章节讨论干扰和有状态的 Lambda 表达式,它们都可能是副作用的来源,并且可能返回不一致或不可预测的结果,特别是在并行流中。然而,首先讨论懒惰的概念,因为它对干扰有直接影响。

懒惰

所有中间操作都是懒惰的。如果一个表达式、方法或算法只有在需要时才会被评估,那么它就是懒惰的(如果算法立即被评估或处理,则是急切的)。中间操作是懒惰的,因为它们直到终端操作开始才开始处理流的内容。懒惰地处理流使得 Java 编译器和运行时能够优化它们处理流的方式。例如,在像filter-mapToInt-average这样的流水线中,average操作可以从mapToInt操作创建的流中获取前几个整数,而这些整数是从filter操作获取的。average操作会重复这个过程,直到它从流中获取了所有需要的元素,然后计算平均值。

干扰

流操作中的 Lambda 表达式不应该干扰。当流的源在流水线处理流时被修改时就会发生干扰。例如,下面的代码尝试连接List listOfStrings 中包含的字符串。然而,它会抛出ConcurrentModificationException

代码语言:javascript复制
try {
    List<String> listOfStrings =
        new ArrayList<>(Arrays.asList("one", "two"));

    // This will fail as the peek operation will attempt to add the
    // string "three" to the source after the terminal operation has
    // commenced. 

    String concatenatedString = listOfStrings
        .stream()

        // Don't do this! Interference occurs here.
        .peek(s -> listOfStrings.add("three"))

        .reduce((a, b) -> a   " "   b)
        .get();

    System.out.println("Concatenated string: "   concatenatedString);

} catch (Exception e) {
    System.out.println("Exception caught: "   e.toString());
}

此示例使用reduce操作将listOfStrings 中包含的字符串连接成一个Optional<String> 值,reduce是一个终端操作。然而,此处的管道调用了中间操作peek,它试图向listOfStrings添加一个新元素。请记住,所有中间操作都是惰性的。这意味着在此示例中,管道在调用操作get时开始执行,并在get操作完成时结束执行。peek操作的参数在管道执行过程中尝试修改流源,这会导致 Java 运行时抛出ConcurrentModificationException

有状态的 Lambda 表达式

避免在流操作中将有状态的 lambda 表达式用作参数。有状态的 lambda 表达式是指其结果取决于在管道执行过程中可能发生变化的任何状态。以下示例使用map中间操作将List listOfIntegers 中的元素添加到新的List实例中。它分别使用串行流和并行流执行两次:

代码语言:javascript复制
List<Integer> serialStorage = new ArrayList<>();

System.out.println("Serial stream:");
listOfIntegers
    .stream()

    // Don't do this! It uses a stateful lambda expression.
    .map(e -> { serialStorage.add(e); return e; })

    .forEachOrdered(e -> System.out.print(e   " "));
System.out.println("");

serialStorage
    .stream()
    .forEachOrdered(e -> System.out.print(e   " "));
System.out.println("");

System.out.println("Parallel stream:");
List<Integer> parallelStorage = Collections.synchronizedList(
    new ArrayList<>());
listOfIntegers
    .parallelStream()

    // Don't do this! It uses a stateful lambda expression.
    .map(e -> { parallelStorage.add(e); return e; })

    .forEachOrdered(e -> System.out.print(e   " "));
System.out.println("");

parallelStorage
    .stream()
    .forEachOrdered(e -> System.out.print(e   " "));
System.out.println("");

Lambda 表达式e -> { parallelStorage.add(e); return e; }是一个有状态的 lambda 表达式。其结果可能每次运行代码时都会有所不同。此示例打印如下内容:

代码语言:javascript复制
Serial stream:
8 7 6 5 4 3 2 1
8 7 6 5 4 3 2 1
Parallel stream:
8 7 6 5 4 3 2 1
1 3 6 2 4 5 8 7

操作forEachOrdered按照流指定的顺序处理元素,无论流是串行还是并行执行。然而,当流并行执行时,map操作处理由 Java 运行时和编译器指定的流元素。因此,lambda 表达式e -> { parallelStorage.add(e); return e; }List parallelStorage添加元素的顺序可能每次运行代码时都会有所不同。为了获得确定性和可预测的结果,请确保流操作中的 lambda 表达式参数不是有状态的。

注意:此示例调用了方法synchronizedList,以使List parallelStorage 线程安全。请记住,集合不是线程安全的。这意味着多个线程不应同时访问特定集合。假设在创建parallelStorage时未调用方法synchronizedList

代码语言:javascript复制
List<Integer> parallelStorage = new ArrayList<>();

该示例行为不稳定,因为多个线程访问和修改parallelStorage而没有像同步这样的机制来安排特定线程何时可以访问List实例。因此,该示例可能打印类似以下内容的输出:

代码语言:javascript复制
Parallel stream:
8 7 6 5 4 3 2 1
null 3 5 4 7 8 1 2

问题和练习:聚合操作

原文:docs.oracle.com/javase/tutorial/collections/streams/QandE/questions.html

问题

一系列的聚合操作被称为 ___。

每个流水线包含零个或多个 ___ 操作。

每个流水线以一个 ___ 操作结束。

什么样的操作以另一个流作为输出?

描述forEach聚合操作与增强的for语句或迭代器之间的一个区别。

真或假:流类似于集合,因为它是一个存储元素的数据结构。

在这段代码中识别中间操作和终端操作:

代码语言:javascript复制
double average = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .mapToInt(Person::getAge)
    .average()
    .getAsDouble();

代码p -> p.getGender() == Person.Sex.MALE是什么的一个例子?

代码Person::getAge是什么的一个例子?

将流的内容组合并返回一个值的终端操作被称为什么?

Stream.reduce方法和Stream.collect方法之间的一个重要区别是什么?

如果你想处理一个包含姓名的流,提取男性姓名,并将它们存储在一个新的List中,那么Stream.reduceStream.collect是最合适的操作?

真或假:聚合操作使得可以在非线程安全的集合中实现并行性。

流通常是串行的,除非另有规定。如何请求以并行方式处理流?

练习

将以下增强的for语句编写为使用 lambda 表达式的流水线。提示:使用filter中间操作和forEach终端操作。

代码语言:javascript复制
for (Person p : roster) {
    if (p.getGender() == Person.Sex.MALE) {
        System.out.println(p.getName());
    }
}

将以下代码转换为一个使用 lambda 表达式和聚合操作而不是嵌套for循环的新实现。提示:创建一个依次调用filtersortedcollect操作的流水线。

代码语言:javascript复制
List<Album> favs = new ArrayList<>();
for (Album a : albums) {
    boolean hasFavorite = false;
    for (Track t : a.tracks) {
        if (t.rating >= 4) {
            hasFavorite = true;
            break;
        }
    }
    if (hasFavorite)
        favs.add(a);
}
Collections.sort(favs, new Comparator<Album>() {
                           public int compare(Album a1, Album a2) {
                               return a1.name.compareTo(a2.name);
                           }});

检查你的答案。

教训:实现

原文:docs.oracle.com/javase/tutorial/collections/implementations/index.html

实现是用于存储集合的数据对象,实现了接口部分中描述的接口。本课程描述了以下类型的实现:

  • 通用实现是最常用的实现,设计用于日常使用。它们在标题为通用实现的表中总结。
  • 特殊用途实现设计用于特殊情况,并显示非标准性能特征、使用限制或行为。
  • 并发实现旨在支持高并发性,通常以牺牲单线程性能为代价。这些实现是java.util.concurrent包的一部分。
  • 包装实现通常与其他类型的实现结合使用,通常是通用实现,以提供附加或受限功能。
  • 便利实现是迷你实现,通常通过静态工厂方法提供,为特殊集合提供方便、高效的替代方案(例如,单例集合)。
  • 抽象实现是骨架实现,有助于构建自定义实现,稍后在自定义集合实现部分中描述。这是一个高级主题,不是特别困难,但相对较少的人会需要这样做。

通用实现总结如下表所示。

通用实现

接口

哈希表实现

可调整大小数组实现

树实现

链表实现

哈希表 链表实现

Set

HashSet

TreeSet

LinkedHashSet

List

ArrayList

LinkedList

Queue

Deque

ArrayDeque

LinkedList

Map

HashMap

TreeMap

LinkedHashMap

正如您从表中所看到的,Java 集合框架提供了几个通用实现的SetListMap接口。在每种情况下,一个实现——HashSetArrayListHashMap——显然是大多数应用程序中要使用的实现,其他条件相等。请注意,SortedSetSortedMap接口在表中没有行。每个接口都有一个实现(TreeSetTreeMap),并列在SetMap行中。有两个通用的Queue实现——LinkedList,也是List实现,和PriorityQueue,在表中被省略。这两个实现提供非常不同的语义:LinkedList提供 FIFO 语义,而PriorityQueue根据其值对元素进行排序。

每个通用实现都提供其接口中包含的所有可选操作。所有允许null元素、键和值。没有同步(线程安全)。所有都有快速失败迭代器,在迭代期间检测到非法并发修改,并快速干净地失败,而不是在未来的某个不确定时间冒险出现任意、非确定性的行为。所有都是Serializable,并支持公共clone方法。

这些实现不同步的事实代表了与过去的断裂:传统集合VectorHashtable是同步的。采取当前方法是因为在同步没有好处时集合经常被使用。这些用途包括单线程使用、只读使用以及作为执行自身同步的较大数据对象的一部分使用。一般来说,良好的 API 设计实践是不让用户为他们不使用的功能付费。此外,不必要的同步可能在某些情况下导致死锁。

如果你需要线程安全的集合,包装器实现部分描述的同步包装器允许任何集合转换为同步集合。因此,同步对于通用实现是可选的,而对于传统实现是强制的。此外,java.util.concurrent包提供了BlockingQueue接口的并发实现,它扩展了Queue,以及ConcurrentMap接口的并发实现,它扩展了Map。这些实现比单纯的同步实现具有更高的并发性。

通常情况下,你应该考虑接口,而不是实现。这就是为什么本节中没有编程示例。在很大程度上,实现的选择只影响性能。如接口部分所述,首选的风格是在创建Collection时选择一个实现,并立即将新集合分配给相应接口类型的变量(或将集合传递给期望接口类型参数的方法)。通过这种方式,程序不会依赖于给定实现中添加的任何方法,使程序员可以自由更改实现,只要性能或行为细节需要。

接下来的部分简要讨论了实现。使用诸如常数时间对数线性n log(n)和二次等词语描述了实现的性能,以指代执行操作的时间复杂度的渐近上界。所有这些都是相当复杂的术语,如果你不知道它们的含义也没关系。如果你对更多信息感兴趣,请参考任何一本优秀的算法教材。要记住的一件事是,这种性能指标有其局限性。有时,名义上更慢的实现可能更快。如果有疑问,就要测量性能!

集合实现

原文:docs.oracle.com/javase/tutorial/collections/implementations/set.html

Set实现分为通用实现和特殊实现两类。

通用集合实现

有三种通用的Set实现 — HashSet, TreeSet, 和 LinkedHashSet。在这三种中选择哪一种通常很简单。HashSetTreeSet快得多(大部分操作的时间复杂度是常数时间对数时间),但不提供排序保证。如果你需要使用SortedSet接口中的操作,或者需要按值排序的迭代,使用TreeSet;否则,使用HashSet。很可能大部分时间你会使用HashSet

LinkedHashSet在某种意义上介于HashSetTreeSet之间。作为一个带有链表的哈希表,它提供了插入顺序的迭代(最近插入的到最近插入的)并且运行速度几乎和HashSet一样快。LinkedHashSet实现避免了HashSet提供的未指定、通常混乱的排序,而又不会增加TreeSet的成本。

关于HashSet值得注意的一点是,迭代在条目数和桶数(容量)的总和上是线性的。因此,选择一个初始容量太高会浪费空间和时间。另一方面,选择一个初始容量太低会浪费时间,因为每次强制增加容量时都需要复制数据结构。如果你不指定初始容量,默认值是 16。过去,选择一个质数作为初始容量有一些优势。但现在不再成立。在内部,容量总是向上舍入为 2 的幂。初始容量是通过使用int构造函数指定的。以下代码行分配了一个初始容量为 64 的HashSet

代码语言:javascript复制
Set<String> s = new HashSet<String>(64);

HashSet类还有一个称为负载因子的调整参数。如果你非常关心你的HashSet的空间消耗,阅读HashSet文档以获取更多信息。否则,只需接受默认值;这几乎总是正确的做法。

如果你接受默认的负载因子但想指定一个初始容量,选择一个大约是你期望集合增长到两倍大小的数字。如果你的猜测完全错误,可能会浪费一点空间、时间或两者,但这不太可能成为一个大问题。

LinkedHashSet具有与HashSet相同的调整参数,但迭代时间不受容量影响。TreeSet没有调整参数。

特殊用途的 Set 实现

有两个特殊用途的Set实现 — EnumSetCopyOnWriteArraySet

EnumSet是用于枚举类型的高性能Set实现。枚举集合的所有成员必须是相同的枚举类型。在内部,它由一个位向量表示,通常是一个long。枚举集合支持对枚举类型范围的迭代。例如,给定一周中的工作日的枚举声明,你可以迭代工作日。EnumSet类提供了一个静态工厂,使其易于使用。

代码语言:javascript复制
    for (Day d : EnumSet.range(Day.MONDAY, Day.FRIDAY))
        System.out.println(d);

枚举集合也为传统位标志提供了丰富的、类型安全的替代品。

代码语言:javascript复制
    EnumSet.of(Style.BOLD, Style.ITALIC)

CopyOnWriteArraySet是一个由写时复制数组支持的Set实现。所有的变动操作,比如addsetremove,都是通过创建数组的新副本来实现的;永远不需要加锁。即使在元素插入和删除的同时进行迭代也是安全的。与大多数Set实现不同,addremovecontains方法所需的时间与集合大小成正比。这种实现适用于很少修改但频繁迭代的集合。它非常适合维护必须防止重复的事件处理程序列表。

列表实现

原文:docs.oracle.com/javase/tutorial/collections/implementations/list.html

List实现分为通用和特殊用途的实现。

通用列表实现

有两种通用的List实现——ArrayListLinkedList。大多数情况下,你可能会使用ArrayList,它提供常数时间的位置访问,并且非常快速。它不必为List中的每个元素分配一个节点对象,并且在需要同时移动多个元素时可以利用System.arraycopy。将ArrayList视为没有同步开销的Vector

如果你经常在List的开头添加元素或者迭代List以删除其内部的元素,你应该考虑使用LinkedList。这些操作在LinkedList中需要常数时间,在ArrayList中需要线性时间。但你会在性能上付出很大的代价。在LinkedList中,位置访问需要线性时间,在ArrayList中需要常数时间。此外,LinkedList的常数因子要糟糕得多。如果你认为你想使用LinkedList,在做出选择之前用LinkedListArrayList测量你的应用程序的性能;ArrayList通常更快。

ArrayList有一个调整参数——初始容量,指的是ArrayList在增长之前可以容纳的元素数量。LinkedList没有调整参数,但有七个可选操作,其中之一是clone。另外六个是addFirstgetFirstremoveFirstaddLastgetLastremoveLastLinkedList还实现了Queue接口。

特殊用途的列表实现

CopyOnWriteArrayList是一个由写入时复制数组支持的List实现。这种实现与CopyOnWriteArraySet类似。即使在迭代期间,也不需要同步,并且迭代器永远不会抛出ConcurrentModificationException。这种实现非常适合维护事件处理程序列表,其中变化不频繁,遍历频繁且可能耗时。

如果你需要同步,Vector比使用Collections.synchronizedList同步的ArrayList稍快一些。但Vector有很多遗留操作,所以一定要小心,始终使用List接口操作Vector,否则你将无法在以后替换实现。

如果你的List大小是固定的 — 也就是说,你永远不会使用removeadd或者除了containsAll之外的任何批量操作 — 那么你有第三个选项,绝对值得考虑。查看方便实现部分的Arrays.asList获取更多信息。

Map 实现

原文:docs.oracle.com/javase/tutorial/collections/implementations/map.html

Map实现分为通用、特殊和并发实现。

通用 Map 实现

三种通用Map实现是HashMapTreeMapLinkedHashMap。如果需要SortedMap操作或基于键排序的Collection-view 迭代,请使用TreeMap;如果希望最大速度且不关心迭代顺序,请使用HashMap;如果希望接近HashMap性能且插入顺序迭代,请使用LinkedHashMap。在这方面,Map的情况类似于Set。同样,Set Implementations 部分中的其他所有内容也适用于Map实现。

LinkedHashMap提供了两个LinkedHashSet不可用的功能。当您创建一个LinkedHashMap时,您可以根据键访问而不是插入对其进行排序。换句话说,仅查找与键关联的值会将该键移到地图的末尾。此外,LinkedHashMap提供了removeEldestEntry方法,可以被覆盖以在向地图添加新映射时自动实施删除过时映射的策略。这使得实现自定义缓存非常容易。

例如,这个覆盖将允许地图增长到多达 100 个条目,然后每次添加新条目时都会删除最老的条目,保持 100 个条目的稳定状态。

代码语言:javascript复制
private static final int MAX_ENTRIES = 100;

protected boolean removeEldestEntry(Map.Entry eldest) {
    return size() > MAX_ENTRIES;
}

特殊用途 Map 实现

有三种特殊用途的 Map 实现 — EnumMapWeakHashMapIdentityHashMapEnumMap,内部实现为array,是用于枚举键的高性能Map实现。此实现将Map接口的丰富性和安全性与接近数组的速度结合在一起。如果要将枚举映射到值,应始终优先使用EnumMap而不是数组。

WeakHashMapMap接口的一个实现,只存储对其键的弱引用。只存储弱引用允许在其键不再在WeakHashMap之外被引用时,键值对可以被垃圾回收。这个类提供了利用弱引用功能的最简单方法。它对于实现“注册表样”数据结构非常有用,其中一个条目的实用性在其键不再被任何线程引用时消失。

IdentityHashMap 是基于哈希表的基于身份的Map实现。这个类对于保持拓扑结构的对象图转换非常有用,比如序列化或深拷贝。为了执行这样的转换,你需要维护一个基于身份的“节点表”,用于跟踪哪些对象已经被看到。基于身份的映射也用于在动态调试器和类似系统中维护对象到元信息的映射。最后,基于身份的映射对于阻止由于故意扭曲的equals方法而导致的“欺骗攻击”非常有用,因为IdentityHashMap永远不会在其键上调用equals方法。这个实现的一个额外好处是它很快。

并发映射实现

java.util.concurrent 包含了ConcurrentMap 接口,它通过原子的putIfAbsentremovereplace方法扩展了Map,以及该接口的ConcurrentHashMap 实现。

ConcurrentHashMap 是一个高度并发、高性能的哈希表实现。在执行检索操作时,此实现永远不会阻塞,并允许客户端选择更新的并发级别。它旨在作为Hashtable的一个可替换项:除了实现ConcurrentMap外,它还支持所有Hashtable特有的传统方法。再次强调,如果你不需要传统操作,请小心使用ConcurrentMap接口来操作它。

队列实现

原文:docs.oracle.com/javase/tutorial/collections/implementations/queue.html

Queue 实现分为通用和并发实现。

通用队列实现

如前一节所述,LinkedList 实现了 Queue 接口,为 addpoll 等提供先进先出(FIFO)队列操作。

PriorityQueue 类是基于 数据结构的优先队列。此队列根据在构造时指定的顺序对元素进行排序,可以是元素的自然顺序或由显式 Comparator 强加的顺序。

队列检索操作 — pollremovepeekelement — 访问队列头部的元素。队列的 头部 是相对于指定顺序的最小元素。如果多个元素具有最小值,则头部是这些元素之一;平局将被任意打破。

PriorityQueue 及其迭代器实现了 CollectionIterator 接口的所有可选方法。在 iterator 方法中提供的迭代器不能保证以任何特定顺序遍历 PriorityQueue 的元素。对于有序遍历,请考虑使用 Arrays.sort(pq.toArray())

并发队列实现

java.util.concurrent 包含一组同步的 Queue 接口和类。BlockingQueue 扩展了 Queue,具有在检索元素时等待队列变得非空以及在存储元素时等待队列中有空间可用的操作。该接口由以下类实现:

  • LinkedBlockingQueue — 由链表节点支持的可选有界 FIFO 阻塞队列
  • ArrayBlockingQueue — 由数组支持的有界 FIFO 阻塞队列
  • PriorityBlockingQueue — 由堆支持的无界阻塞优先级队列
  • DelayQueue — 由堆支持的基于时间的调度队列
  • SynchronousQueue — 使用 BlockingQueue 接口的简单会合机制

在 JDK 7 中,TransferQueue是一个专门的BlockingQueue,其中向队列添加元素的代码可以选择等待(阻塞),直到另一个线程中的代码检索元素。TransferQueue只有一个实现:

  • LinkedTransferQueue — 基于链表节点的无界TransferQueue

Deque 实现

原文:docs.oracle.com/javase/tutorial/collections/implementations/deque.html

Deque 接口,发音为*“deck”*, 代表双端队列。Deque 接口可以被实现为各种类型的CollectionsDeque 接口的实现被分为通用和并发实现。

通用 Deque 实现

通用实现包括LinkedListArrayDeque 类。Deque 接口支持在两端插入、删除和检索元素。ArrayDeque 类是Deque 接口的可调整大小数组实现,而LinkedList 类是列表实现。

Deque 接口中的基本插入、删除和检索操作有addFirstaddLastremoveFirstremoveLastgetFirstgetLastaddFirst 方法在头部添加元素,而addLast 方法在Deque 实例的尾部添加元素。

LinkedList 实现比ArrayDeque 实现更灵活。LinkedList 实现了所有可选的列表操作。LinkedList 实现允许null元素,但ArrayDeque 实现不允许。

就效率而言,ArrayDeque 在两端的添加和删除操作上比LinkedList 更高效。在迭代过程中,LinkedList 实现中最好的操作是删除当前元素。LinkedList 实现不是理想的迭代结构。

LinkedList 实现比ArrayDeque 实现消耗更多内存。对于ArrayDeque 实例的遍历,可以使用以下任意一种:

foreach

foreach 是一种快速且适用于各种列表的方法。

代码语言:javascript复制
ArrayDeque<String> aDeque = new ArrayDeque<String>();

. . .
for (String str : aDeque) {
    System.out.println(str);
}
迭代器

Iterator 可用于所有类型的数据的所有类型列表的正向遍历。

代码语言:javascript复制
ArrayDeque<String> aDeque = new ArrayDeque<String>();
. . .
for (Iterator<String> iter = aDeque.iterator(); iter.hasNext();  ) {
    System.out.println(iter.next());
}

ArrayDeque 类在本教程中用于实现Deque 接口。本教程中使用的示例完整代码在ArrayDequeSample中可用。LinkedListArrayDeque 类都不支持多线程并发访问。

并发 Deque 实现

LinkedBlockingDeque 类是Deque 接口的并发实现。如果双端队列为空,则takeFirsttakeLast 等方法会等待直到元素变为可用,然后检索并移除相同的元素。

包装器实现

原文:docs.oracle.com/javase/tutorial/collections/implementations/wrapper.html

包装器实现将所有真正的工作委托给指定的集合,但在该集合提供的功能之上添加额外的功能。对于设计模式爱好者,这是装饰者模式的一个例子。虽然这可能看起来有点奇特,但实际上非常简单。

这些实现是匿名的;而不是提供一个公共类,库提供了一个静态工厂方法。所有这些实现都在 Collections 类中,该类仅包含静态方法。

同步包装器

同步包装器为任意集合添加了自动同步(线程安全)。六个核心集合接口 — Collection, Set, List, Map, SortedSet, 和 SortedMap — 都有一个静态工厂方法。

代码语言:javascript复制
public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);

这些方法中的每一个都返回由指定集合支持的同步(线程安全)Collection。为了保证串行访问,所有对支持集合的访问必须通过返回的集合完成。确保这一点的简单方法是不保留对支持集合的引用。使用以下技巧创建同步集合。

代码语言:javascript复制
List<Type> list = Collections.synchronizedList(new ArrayList<Type>());

以这种方式创建的集合与通常同步的集合(如 Vector)一样线程安全。

面对并发访问时,在迭代时,用户必须手动对返回的集合进行同步。原因是迭代是通过对集合的多次调用完成的,这些调用必须组合成单个原子操作。以下是迭代包装同步集合的惯用法。

代码语言:javascript复制
Collection<Type> c = Collections.synchronizedCollection(myCollection);
synchronized(c) {
    for (Type e : c)
        foo(e);
}

如果使用显式迭代器,则必须在 synchronized 块内调用 iterator 方法。不遵循此建议可能导致不确定的行为。在同步 MapCollection 视图上进行迭代的惯用法类似。在迭代任何 Collection 视图时,用户必须同步在同步的 Map 上,而不是在 Collection 视图本身上进行同步,如下例所示。

代码语言:javascript复制
Map<KeyType, ValType> m = Collections.synchronizedMap(new HashMap<KeyType, ValType>());
    ...
Set<KeyType> s = m.keySet();
    ...
// Synchronizing on m, not s!
synchronized(m) {
    while (KeyType k : s)
        foo(k);
}

使用包装器实现的一个小缺点是,您无法执行包装实现的任何非接口操作。因此,在前面的List示例中,您无法在包装的ArrayList上调用ArrayListensureCapacity操作。

不可修改的包装器

与添加功能到包装集合的同步包装器不同,不可修改的包装器会剥夺功能。特别是,它们剥夺了通过拦截所有可能修改集合的操作并抛出UnsupportedOperationException来修改集合的能力。不可修改的包装器有两个主要用途,如下所示:

  • 使集合在构建后变为不可变。在这种情况下,最好不要保留对支持集合的引用。这绝对保证了不可变性。
  • 允许某些客户端对您的数据结构进行只读访问。您保留对支持集合的引用,但分发对包装器的引用。这样,客户端可以查看但不能修改,而您保持完全访问权限。

与同步包装器一样,每个六个核心Collection接口都有一个静态工厂方法。

代码语言:javascript复制
public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
public static <T> Set<T> unmodifiableSet(Set<? extends T> s);
public static <T> List<T> unmodifiableList(List<? extends T> list);
public static <K,V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);
public static <T> SortedSet<T> unmodifiableSortedSet(SortedSet<? extends T> s);
public static <K,V> SortedMap<K, V> unmodifiableSortedMap(SortedMap<K, ? extends V> m);

检查接口包装器

Collections.checked 接口包装器用于与泛型集合一起使用。这些实现返回指定集合的动态类型安全视图,如果客户端尝试添加错误类型的元素,则会抛出ClassCastException。语言中的泛型机制提供了编译时(静态)类型检查,但有可能击败此机制。动态类型安全视图完全消除了这种可能性。

便利实现

原文:docs.oracle.com/javase/tutorial/collections/implementations/convenience.html

这一部分描述了几个迷你实现,当你不需要它们的全部功能时,它们可能比通用实现更方便、更高效。本节中的所有实现都是通过静态工厂方法而不是public类提供的。

数组的列表视图

Arrays.asList方法返回其数组参数的List视图。对List的更改会写入数组,反之亦然。集合的大小与数组相同且不可更改。如果在List上调用addremove方法,将导致UnsupportedOperationException

这种实现的正常用法是作为基于数组和基于集合的 API 之间的桥梁。它允许你将数组传递给期望CollectionList的方法。然而,这种实现还有另一个用途。如果你需要一个固定大小的List,它比任何通用List实现更高效。这就是惯用法。

代码语言:javascript复制
List<String> list = Arrays.asList(new String[size]);

注意,不会保留对支持数组的引用。

不可变多副本列表

有时你会需要一个由多个相同元素副本组成的不可变ListCollections.nCopies方法返回这样一个列表。这种实现有两个主要用途。第一个是初始化一个新创建的List;例如,假设你想要一个最初由 1,000 个null元素组成的ArrayList。下面的咒语就能实现。

代码语言:javascript复制
List<Type> list = new ArrayList<Type>(Collections.nCopies(1000, (Type)null));

当然,每个元素的初始值不一定是null。第二个主要用途是扩展现有的List。例如,假设你想要向List<String>的末尾添加 69 个字符串"fruit bat"的副本。不清楚为什么你想要做这样的事情,但让我们假设你确实想要。下面是如何做到的。

代码语言:javascript复制
lovablePets.addAll(Collections.nCopies(69, "fruit bat"));

通过使用同时接受索引和CollectionaddAll形式,你可以将新元素添加到List的中间而不是末尾。

不可变单例集合

有时你会需要一个不可变的单例Set,它由一个指定的元素组成。Collections.singleton方法返回这样一个Set。这种实现的一个用途是从Collection中删除所有指定元素的出现。

代码语言:javascript复制
c.removeAll(Collections.singleton(e));

一个相关的惯用法是从Map中删除映射到指定值的所有元素。例如,假设你有一个将人员映射到他们的工作领域的Mapjob — 并且假设你想要消除所有律师。下面的一行代码就能完成任务。

代码语言:javascript复制
job.values().removeAll(Collections.singleton(LAWYER));

这种实现的另一个用途是为一个写成接受值集合的方法提供单个输入值。

空 Set、List 和 Map 常量

Collections 类提供了返回空SetListMap的方法 — emptySetemptyListemptyMap。这些常量的主要用途是作为不提供任何值时作为值的集合的输入方法的输入,就像这个例子中一样。

代码语言:javascript复制
tourist.declarePurchases(Collections.emptySet());

实现摘要

原文:docs.oracle.com/javase/tutorial/collections/implementations/summary.html

实现是用于存储集合的数据对象,这些对象实现了接口课程中描述的接口。

Java 集合框架提供了几个核心接口的通用实现:

  • 对于Set接口,HashSet是最常用的实现。
  • 对于List接口,ArrayList是最常用的实现。
  • 对于Map接口,HashMap是最常用的实现。
  • 对于Queue接口,LinkedList是最常用的实现。
  • 对于Deque接口,ArrayDeque是最常用的实现。

每个通用实现提供其接口中包含的所有可选操作。

Java 集合框架还提供了几个特殊用途的实现,用于需要非标准性能、使用限制或其他异常行为的情况。

java.util.concurrent包含几个集合实现,这些实现是线程安全的,但不受单个排他锁的控制。

Collections类(与Collection接口相对),提供了在集合上操作或返回集合的静态方法,这些方法被称为包装器实现。

最后,还有几个便利实现,当您不需要它们的全部功能时,这些便利实现可能比通用实现更有效。这些便利实现通过静态工厂方法提供。

问题和练习:实现方式

原文:docs.oracle.com/javase/tutorial/collections/implementations/QandE/questions.html

问题

  1. 你计划编写一个程序,使用几个基本的集合接口:SetListQueueMap。你不确定哪种实现方式最适合,所以决定在了解程序在实际环境中如何运行之前,先使用通用的实现方式。这些实现方式是哪些?
  2. 如果你需要一个提供按值排序迭代的Set实现,应该使用哪个类?
  3. 你使用哪个类来访问包装器实现?

练习

  1. 编写一个程序,将由第一个命令行参数指定的文本文件读入一个List中。然后,程序应该打印文件中的随机行,打印的行数由第二个命令行参数指定。编写程序时,应一次性分配正确大小的集合,而不是在读取文件时逐渐扩展。提示:要确定文件中的行数,可以使用java.io.File.length来获取文件的大小,然后除以平均行的假定大小。

检查你的答案。

教训:算法

原文:docs.oracle.com/javase/tutorial/collections/algorithms/index.html

这里描述的多态算法是 Java 平台提供的可重用功能块。所有这些功能块都来自Collections类,并且都采用静态方法的形式,其第一个参数是要执行操作的集合。Java 平台提供的绝大多数算法都是在List实例上操作的,但其中有一些是在任意Collection实例上操作的。本节简要描述了以下算法:

  • 排序
  • 洗牌
  • 常规数据操作
  • 搜索
  • 组合
  • 查找极值

排序

sort算法重新排列一个List,使其元素按照一种排序关系升序排列。提供了两种形式的操作。简单形式接受一个List,并根据其元素的自然排序对其进行排序。如果您对自然排序的概念不熟悉,请阅读对象排序部分。

sort操作使用了一个稍微优化的归并排序算法,速度快且稳定:

  • 快速:它保证在n log(n)时间内运行,并在几乎排序好的列表上运行得更快。经验测试表明它与高度优化的快速排序一样快。快速排序通常被认为比归并排序更快,但不稳定且不能保证n log(n)性能。
  • 稳定:它不会重新排序相等的元素。这一点对于在不同属性上重复对同一列表进行排序很重要。如果邮件程序的用户按邮件日期对收件箱进行排序,然后按发件人对其进行排序,用户自然期望来自同一发件人的现在连续的邮件列表仍然按邮件日期排序。只有第二次排序是稳定的才能保证这一点。

以下简单程序按字典顺序打印出其参数。

代码语言:javascript复制
import java.util.*;

public class Sort {
    public static void main(String[] args) {
        List<String> list = Arrays.asList(args);
        Collections.sort(list);
        System.out.println(list);
    }
}

让我们运行程序。

代码语言:javascript复制
% java Sort i walk the line

产生了以下输出。

代码语言:javascript复制
[i, line, the, walk]

该程序仅用于向您展示算法确实像看起来那样易于使用。

第二种sort形式除了List之外还需要一个Comparator,并使用Comparator对元素进行排序。假设你想要按照我们之前示例中的字谜分组的大小倒序打印出来,即最大的字谜组首先显示。接下来的示例展示了如何借助sort方法的第二种形式实现这一目标。

请记住,变位词组以List实例的形式存储在Map中的值中。修改后的打印代码通过Map的值视图迭代,将通过最小大小测试的每个List放入一个ListList中。然后,该代码对此List进行排序,使用一个期望List实例的Comparator,并实现逆大小排序。最后,该代码对排序后的List进行迭代,打印其元素(变位词组)。以下代码替换了Anagrams示例中main方法末尾的打印代码。

代码语言:javascript复制
// Make a List of all anagram groups above size threshold.
List<List<String>> winners = new ArrayList<List<String>>();
for (List<String> l : m.values())
    if (l.size() >= minGroupSize)
        winners.add(l);

// Sort anagram groups according to size
Collections.sort(winners, new Comparator<List<String>>() {
    public int compare(List<String> o1, List<String> o2) {
        return o2.size() - o1.size();
    }});

// Print anagram groups.
for (List<String> l : winners)
    System.out.println(l.size()   ": "   l);

在与 The Map Interface 部分相同的字典上运行程序,具有相同的最小变位词组大小(八),产生以下输出。

代码语言:javascript复制
12: [apers, apres, asper, pares, parse, pears, prase,
       presa, rapes, reaps, spare, spear]
11: [alerts, alters, artels, estral, laster, ratels,
       salter, slater, staler, stelar, talers]
10: [least, setal, slate, stale, steal, stela, taels,
       tales, teals, tesla]
9: [estrin, inerts, insert, inters, niters, nitres,
       sinter, triens, trines]
9: [capers, crapes, escarp, pacers, parsec, recaps,
       scrape, secpar, spacer]
9: [palest, palets, pastel, petals, plates, pleats,
       septal, staple, tepals]
9: [anestri, antsier, nastier, ratines, retains, retinas,
       retsina, stainer, stearin]
8: [lapse, leaps, pales, peals, pleas, salep, sepal, spale]
8: [aspers, parses, passer, prases, repass, spares,
       sparse, spears]
8: [enters, nester, renest, rentes, resent, tenser,
       ternes,��treens]
8: [arles, earls, lares, laser, lears, rales, reals, seral]
8: [earings, erasing, gainers, reagins, regains, reginas,
       searing, seringa]
8: [peris, piers, pries, prise, ripes, speir, spier, spire]
8: [ates, east, eats, etas, sate, seat, seta, teas]
8: [carets, cartes, caster, caters, crates, reacts,
       recast,��traces]

洗牌

shuffle算法与sort的作用相反,破坏了List中可能存在的任何顺序痕迹。换句话说,该算法根据来自随机源的输入重新排序List,以使所有可能的排列以相等的概率发生,假设有一个公平的随机源。该算法在实现游戏机会时非常有用。例如,它可以用来洗牌代表一副牌的Card对象的List。此外,它还用于生成测试用例。

此操作有两种形式:一种接受一个List并使用默认的随机源,另一种要求调用者提供一个Random对象作为随机源。该算法的代码被用作List部分的示例。

常规数据操作

Collections类提供了五种用于在List对象上进行常规数据操作的算法,所有这些算法都非常简单:

  • reverse — 颠倒List中元素的顺序。
  • fill — 用指定值覆盖List中的每个元素。此操作对重新初始化List非常有用。
  • copy — 接受两个参数,目标List和源List,并将源的元素复制到目标中,覆盖其内容。目标List的长度必须至少与源相同。如果目标更长,则目标中剩余的元素不受影响。
  • swap — 交换List中指定位置的元素。
  • addAll — 将所有指定的元素添加到Collection中。要添加的元素可以逐个指定,也可以作为数组指定。

搜索

binarySearch算法在排序的List中搜索指定的元素。该算法有两种形式。第一种接受一个List和一个要搜索的元素(“搜索键”)。这种形式假定List根据其元素的自然顺序按升序排序。第二种形式除了List和搜索键外还接受一个Comparator,并假定List根据指定的Comparator按升序排序。在调用binarySearch之前,可以使用sort算法对List进行排序。

两种形式的返回值相同。如果List包含搜索键,则返回其索引。如果没有,则返回值为(-(插入点) - 1),其中插入点是将值插入List的位置,或者大于该值的第一个元素的索引,或者list.size()如果List中的所有元素都小于指定值。这个确实丑陋的公式保证了返回值仅当找到搜索键时为>= 0。这基本上是一个将布尔值(found)和整数值(index)组合成单个int返回值的技巧。

下面的习语可用于binarySearch操作的两种形式,查找指定的搜索键,并在适当位置插入它(如果尚未存在)。

代码语言:javascript复制
int pos = Collections.binarySearch(list, key);
if (pos < 0)
   l.add(-pos-1, key);

组成

频率和不相交算法测试一个或多个Collections的组成方面:

  • frequency — 计算指定元素在指定集合中出现的次数
  • disjoint — 确定两个Collections是否不相交;即它们是否没有共同元素。

查找极值

minmax算法分别返回指定Collection中包含的最小和最大元素。这两个操作有两种形式。简单形式只接受一个Collection,根据元素的自然顺序返回最小(或最大)元素。第二种形式除了Collection外还接受一个Comparator,根据指定的Comparator返回最小(或最大)元素。

教训:自定义集合实现

原文:docs.oracle.com/javase/tutorial/collections/custom-implementations/index.html

许多程序员永远不需要实现自己的Collection类。您可以通过使用本章前面描述的实现来走得很远。但是,总有一天您可能想要编写自己的实现。借助 Java 平台提供的抽象实现,这样做相当容易。在讨论如何编写实现之前,让我们讨论一下为什么您可能想要编写一个。

编写实现的原因

以下列表说明了您可能想要实现的自定义Collection类型。这并不是详尽无遗的:

  • 持久性:所有内置的Collection实现都驻留在主内存中,并在程序退出时消失。如果您希望在下次程序启动时仍然存在的集合,可以通过在外部数据库上构建一个薄层来实现它。这样的集合可能可以被多个程序同时访问。
  • 特定应用:这是一个非常广泛的类别。一个例子是包含实时遥测数据的不可修改的Map。键可以表示位置,值可以根据get操作从这些位置的传感器中读取。
  • 高性能,特定用途:许多数据结构利用受限使用来提供比通用实现更好的性能。例如,考虑一个包含长时间相同元素值的List。这样的列表在文本处理中经常出现,可以进行运行长度编码 — 运行可以表示为包含重复元素和连续重复次数的单个对象。这个例子很有趣,因为它在性能方面进行了权衡:它需要更少的空间但比ArrayList需要更多的时间。
  • 高性能,通用用途:Java 集合框架的设计者试图为每个接口提供最佳的通用实现,但是可以使用许多许多数据结构,并且每天都会有新的数据结构被发明。也许您可以想出更快的解决方案!
  • 增强功能:假设您需要一个高效的包实现(也称为multiset):一个Collection,它在允许重复元素的同时提供常数时间的包含性检查。在HashMap之上实现这样一个集合是相当简单的。
  • 便利性:您可能希望提供超出 Java 平台提供的便利的其他实现。例如,您可能经常需要表示一系列Integer的连续范围的List实例。
  • 适配器:假设您正在使用具有自己的特定集合 API 的旧 API。您可以编写一个适配器实现,使这些集合能够在 Java 集合框架中运行。适配器实现是一个薄膜,包装一种类型的对象,并通过将对后者类型的操作转换为对前者类型的操作来使其行为类似于另一种类型的对象。

如何编写自定义实现

编写自定义实现出人意料地容易。Java 集合框架提供了专门设计用于促进自定义实现的抽象实现。我们将从以下Arrays.asList实现的示例开始。

代码语言:javascript复制
public static <T> List<T> asList(T[] a) {
    return new MyArrayList<T>(a);
}

private static class MyArrayList<T> extends AbstractList<T> {

    private final T[] a;

    MyArrayList(T[] array) {
        a = array;
    }

    public T get(int index) {
        return a[index];
    }

    public T set(int index, T element) {
        T oldValue = a[index];
        a[index] = element;
        return oldValue;
    }

    public int size() {
        return a.length;
    }
}

信不信由你,这与包含在java.util.Arrays中的实现非常接近。就是这么简单!您提供一个构造函数和getsetsize方法,AbstractList会处理其余的所有事情。您将获得ListIterator,批量操作,搜索操作,哈希码计算,比较和字符串表示。

假设您想让实现变得更快一点。抽象实现的 API 文档准确描述了每个方法的实现方式,因此您将知道要重写哪些方法以获得所需的性能。前面的实现性能很好,但可以稍微改进一下。特别是,toArray方法会遍历List,一次复制一个元素。鉴于内部表示,仅克隆数组会更快更合理。

代码语言:javascript复制
public Object[] toArray() {
    return (Object[]) a.clone();
}

添加此覆盖和其他几个类似的覆盖后,此实现与java.util.Arrays中找到的实现完全相同。为了充分披露,使用其他抽象实现会有点困难,因为您将不得不编写自己的迭代器,但仍然不是那么困难。

以下列表总结了抽象实现:

  • AbstractCollection — 既不是Set也不是ListCollection。至少,您必须提供iteratorsize方法。
  • AbstractSet — 一个Set;使用方式与AbstractCollection相同。
  • AbstractList — 由随机访问数据存储支持的List。至少,您必须提供位置访问方法(get,可选的setremoveadd)以及size方法。抽象类负责listIterator(和iterator)。
  • AbstractSequentialList — 由顺序访问数据存储支持的List,例如链表。至少,你必须提供listIteratorsize方法。抽象类负责处理位置访问方法。(这与AbstractList相反。)
  • AbstractQueue — 至少,你必须提供offerpeekpollsize方法以及支持removeiterator
  • AbstractMap — 一个Map。至少你必须提供entrySet视图。通常使用AbstractSet类来实现。如果Map是可修改的,你还必须提供put方法。

编写自定义实现的过程如下:

  1. 从上述列表中选择适当的抽象实现类。
  2. 为类的所有抽象方法提供实现。如果你的自定义集合是可修改的,你还必须重写一个或多个具体方法。抽象实现类的 API 文档将告诉你哪些方法需要重写。
  3. 测试并且,如果需要,调试实现。现在你有一个可工作的自定义集合实现。
  4. 如果你关心性能,阅读抽象实现类的 API 文档,了解你要继承的所有方法的实现。如果有任何方法看起来太慢,就重写它们。如果你重写任何方法,请确保在重写之前和之后测量方法的性能。调整性能的努力程度应该取决于实现的使用程度以及其使用对性能的关键性。 (通常最好省略此步骤。)

课程:互操作性

原文:docs.oracle.com/javase/tutorial/collections/interoperability/index.html

在这一部分,您将学习互操作性的以下两个方面:

  • 兼容性:本小节描述了如何使集合能够与早于将Collection添加到 Java 平台的旧 API 一起工作。
  • API 设计:本小节描述了如何设计新的 API,以便它们能够与其他 API 无缝互操作。

兼容性

原文:docs.oracle.com/javase/tutorial/collections/interoperability/compatibility.html

Java 集合框架旨在确保核心集合接口与早期 Java 平台版本中用于表示集合的类型之间的完全互操作性:VectorHashtable,数组,以及Enumeration。在本节中,您将学习如何将旧集合转换为 Java 集合框架集合,反之亦然。

向上兼容性

假设你正在使用一个返回传统集合的 API 以及另一个 API,需要对象实现集合接口。为了使这两个 API 顺利互操作,你必须将传统集合转换为现代集合。幸运的是,Java 集合框架使这变得容易。

假设旧的 API 返回一个对象数组,而新的 API 需要一个Collection。集合框架有一个方便的实现,允许将对象数组视为List。你可以使用Arrays.asList将数组传递给任何需要CollectionList的方法。

代码语言:javascript复制
Foo[] result = oldMethod(arg);
newMethod(Arrays.asList(result));

如果旧的 API 返回一个VectorHashtable,你根本不需要做任何工作,因为Vector已经被改装为实现List接口,而Hashtable已经被改装为实现Map。因此,Vector可以直接传递给任何需要CollectionList的方法。

代码语言:javascript复制
Vector result = oldMethod(arg);
newMethod(result);

类似地,Hashtable可以直接传递给任何需要Map的方法。

代码语言:javascript复制
Hashtable result = oldMethod(arg);
newMethod(result);

较少情况下,一个 API 可能返回一个代表对象集合的EnumerationCollections.list方法将Enumeration转换为Collection

代码语言:javascript复制
Enumeration e = oldMethod(arg);
newMethod(Collections.list(e));

向后兼容性

假设你正在使用一个返回现代集合的 API 以及另一个 API,需要你传递传统集合。为了使这两个 API 顺利互操作,你必须将现代集合转换为旧集合。同样,Java 集合框架使这变得容易。

假设新的 API 返回一个Collection,而旧的 API 需要一个Object数组。正如你可能知道的那样,Collection接口包含一个专门设计用于这种情况的toArray方法。

代码语言:javascript复制
Collection c = newMethod();
oldMethod(c.toArray());

如果旧的 API 需要一个String数组(或其他类型)而不是一个Object数组怎么办?你只需使用toArray的另一种形式 — 需要一个数组作为输入的形式。

代码语言:javascript复制
Collection c = newMethod();
oldMethod((String[]) c.toArray(new String[0]));

如果旧的 API 需要一个Vector,标准集合构造函数会派上用场。

代码语言:javascript复制
Collection c = newMethod();
oldMethod(new Vector(c));

需要Hashtable的旧 API 的情况类似处理。

代码语言:javascript复制
Map m = newMethod();
oldMethod(new Hashtable(m));

最后,如果旧的 API 需要一个Enumeration怎么办?这种情况并不常见,但偶尔会发生,Collections.enumeration 方法就是为了处理这种情况而提供的。这是一个静态工厂方法,接受一个Collection并返回该Collection中元素的Enumeration

代码语言:javascript复制
Collection c = newMethod();
oldMethod(Collections.enumeration(c));

API 设计

原文:docs.oracle.com/javase/tutorial/collections/interoperability/api-design.html

在这个简短但重要的部分,你将学到一些简单的准则,让你的 API 能够与遵循这些准则的所有其他 API 无缝交互。实质上,这些规则定义了在集合世界中成为一个好“公民”所需的条件。

参数

如果你的 API 包含一个需要在输入时传入集合的方法,那么声明相关参数类型为集合接口类型至关重要。永远不要使用实现类型,因为这违背了基于接口的集合框架的目的,即允许集合在不考虑实现细节的情况下进行操作。

此外,你应该始终使用最不具体的类型。例如,如果Collection足够了,就不要要求一个List或者一个Set。并不是说你在输入时永远不应该要求一个List或者Set;如果一个方法依赖于这些接口的属性,那么这样做是正确的。例如,Java 平台提供的许多算法在输入时需要一个List,因为它们依赖于列表是有序的这一事实。然而,作为一般规则,最好在输入时使用最通用的类型:CollectionMap


注意: 永远不要定义自己的临时collection类,并要求在输入时使用这些类的对象。这样做会使你失去 Java 集合框架提供的所有好处。


返回值

对于返回值,你可以比输入参数更加灵活。可以返回任何实现或扩展集合接口之一的类型的对象。这可以是接口之一,也可以是扩展或实现这些接口的特殊用途类型。

例如,可以想象一个图像处理包,称为ImageList,它返回实现List的新类的对象。除了List操作外,ImageList还可以支持任何看起来有用的特定应用操作。例如,它可能提供一个indexImage操作,返回一个包含ImageList中每个图形缩略图的图像。需要注意的是,即使 API 在输出时提供ImageList实例,它也应该接受任意的Collection(或者也许是List)实例作为输入。

从某种意义上说,返回值应该具有与输入参数相反的行为:最好返回最具体的适用集合接口,而不是最一般的。例如,如果你确定总是返回一个SortedMap,你应该给相关方法返回类型为SortedMap,而不是MapSortedMap 实例比普通的Map实例更耗时构建,也更强大。考虑到你的模块已经投入了时间来构建SortedMap,让用户访问其增强功能是明智的。此外,用户将能够将返回的对象传递给需要SortedMap的方法,以及接受任何Map的方法。

传统 API

目前有很多 API 定义了自己的临时集合类型。虽然这很不幸,但考虑到 Java 平台的前两个主要版本中没有集合框架,这是现实。假设你拥有其中一个这样的 API;以下是你可以采取的措施。

如果可能的话,更新你的传统集合类型以实现标准集合接口之一。然后,你返回的所有集合将与其他基于集合的 API 无缝地进行交互。如果这是不可能的(例如,因为一个或多个现有类型签名与标准集合接口冲突),定义一个适配器类,包装你的传统集合对象之一,使其能够作为标准集合运行。(Adapter 类是自定义实现的一个示例。)

如果可能的话,通过新的调用来更新你的 API,遵循输入指南以接受标准集合接口的对象。这样的调用可以与接受传统集合类型的调用共存。如果这是不可能的,为你的传统类型提供一个构造函数或静态工厂,接受一个标准接口的对象,并返回包含相同元素(或映射)的传统集合。这两种方法中的任何一种都将允许用户将任意集合传递给你的 API。

教程:日期时间

原文:docs.oracle.com/javase/tutorial/datetime/index.html

日期时间包,java.time,在 Java SE 8 发布中引入,提供了一个全面的日期和时间模型,并在JSR 310: 日期和时间 API下开发。虽然java.time基于国际标准化组织(ISO)日历系统,但也支持常用的全球日历。

这个教程涵盖了使用基于 ISO 的类来表示日期和时间以及操作日期和时间值的基础知识。

教训:日期时间概述

原文:docs.oracle.com/javase/tutorial/datetime/overview/index.html

时间似乎是一个简单的主题;即使是一只廉价的手表也可以提供相当准确的日期和时间。然而,仔细观察后,你会意识到时间的微妙复杂性和许多影响你对时间理解的因素。例如,将一个月加到 1 月 31 日的结果对于闰年和其他年份是不同的。时区也增加了复杂性。例如,一个国家可能会在短时间内进入和退出夏令时,或者一年内多次进入和退出夏令时,或者在某一年完全跳过夏令时。

日期时间 API 使用ISO-8601中定义的日历系统作为默认日历。这个日历基于格里高利历系统,在全球范围内被用作代表日期和时间的事实标准。日期时间 API 中的核心类的名称如LocalDateTimeZonedDateTimeOffsetDateTime。所有这些类都使用 ISO 日历系统。如果你想使用另一个日历系统,比如伊斯兰历或泰国佛历,java.time.chrono包允许你使用其中一个预定义的日历系统。或者你也可以创建自己的日历系统。

日期时间 API 使用Unicode 通用区域数据存储库 (CLDR)。这个存储库支持世界上的语言,并包含可用的最大的区域数据集合。这个存储库中的信息已被本地化到数百种语言。日期时间 API 还使用时区数据库 (TZDB)。这个数据库提供自 1970 年以来全球每个时区变更的信息,以及自该概念引入以来主要时区的历史。

日期时间设计原则

原文:docs.oracle.com/javase/tutorial/datetime/overview/design.html

日期时间 API 是根据几个设计原则开发的。

清晰

API 中的方法被明确定义,其行为清晰可预期。例如,使用null参数值调用日期时间方法通常会触发NullPointerException

流畅

日期时间 API 提供了流畅的接口,使代码易于阅读。因为大多数方法不允许带有null值的参数,并且不返回null值,方法调用可以链接在一起,结果代码可以快速理解。例如:

代码语言:javascript复制
LocalDate today = LocalDate.now();
LocalDate payday = today.with(TemporalAdjusters.lastDayOfMonth()).minusDays(2);

不可变

日期时间 API 中的大多数类创建的对象是不可变的,这意味着在对象创建后,它是不能被修改的。要修改不可变对象的值,必须构建一个修改后的原始副本作为新对象。这也意味着日期时间 API 在定义上是线程安全的。这影响了 API,大多数用于创建日期或时间对象的方法都以offromwith为前缀,而不是构造函数,并且没有set方法。例如:

代码语言:javascript复制
LocalDate dateOfBirth = LocalDate.of(2012, Month.MAY, 14);
LocalDate firstBirthday = dateOfBirth.plusYears(1);

可扩展

日期时间 API 在尽可能的地方是可扩展的。例如,您可以定义自己的时间调整器和查询,或构建自己的日历系统。

日期时间包

原文:docs.oracle.com/javase/tutorial/datetime/overview/packages.html

日期时间 API 由主要包java.time和四个子包组成:

java.time

该 API 的核心用于表示日期和时间。它包括了日期、时间、日期和时间的组合、时区、瞬间、持续时间和时钟的类。这些类基于 ISO-8601 中定义的日历系统,是不可变的和线程安全的。

java.time.chrono

用于表示除默认 ISO-8601 之外的日历系统的 API。您也可以定义自己的日历系统。本教程不会详细介绍这个包。

java.time.format

用于格式化和解析日期和时间的类。

java.time.temporal

扩展 API,主要用于框架和库的编写者,允许日期和时间类之间的互操作,查询和调整。在这个包中定义了字段(TemporalFieldChronoField)和单位(TemporalUnitChronoUnit)。

java.time.zone

支持时区、时区偏移和时区规则的类。如果涉及时区,大多数开发人员只需要使用ZonedDateTimeZoneIdZoneOffset

0 人点赞