Oracle 专业认证 JavaSE8 编程测验(三)
五、Lambda 内置函数式接口
| 认证目标 |
|---|
| 使用 java.util.function 包中包含的内置接口,如谓词、使用者、函数和供应商 |
| 开发使用函数式接口原始版本的代码 |
| 开发使用函数式接口二进制版本的代码 |
| 开发使用一元运算符接口的代码 |
java.util.function有许多内置接口。Java 库中的其他包(特别是java.util.stream包)使用这个包中定义的接口。对于 OCPJP 8 考试,你应该熟悉使用本软件包中提供的关键接口。
正如我们之前讨论的(在第三章中),一个函数式接口声明了一个抽象方法(但是除此之外,它可以有任意数量的默认或静态方法)。函数式接口对于创建 lambda 表达式很有用。整个java.util.function包由功能接口组成。
在定义您自己的功能接口之前,请根据您的需要考虑使用
java.util.function包中定义的现成的功能接口。如果您正在寻找的 lambda 函数的签名在该库中提供的任何函数式接口中都不可用,您可以定义自己的函数式接口。
使用内置的功能接口
| 认证目标 |
|---|
| 使用 java.util.function 包中包含的内置接口,如谓词、使用者、函数和供应商 |
在本节中,让我们讨论一下java.util.function包中包含的四个重要的内置接口:Predicate、Consumer、Function和Supplier。参见表 5-1 和图 5-1 获得这些功能接口的概述。
图 5-1。
Abstract method declarations in key functional interfaces in java.util.function package
表 5-1。
Key Functional Interfaces in java.util.function Package
| 功能接口 | 简要描述 | 多畜共牧 |
|---|---|---|
Predicate<T> | 检查条件并返回一个布尔值作为结果 | 在java.util.stream.Stream中的filter()方法中,用于删除流中与给定条件(即谓词)不匹配的元素作为参数。 |
Consumer<T> | 接受参数但不返回任何值的操作 | 在集合中的forEach()方法和在java.util.stream.Stream中;此方法用于遍历集合或流中的所有元素。 |
Function<T, R> | 接受参数并返回结果的函数 | 在java.util.stream.Stream的map()方法中,对传递的值进行转换或操作,并返回结果。 |
Supplier<T> | 向调用者返回值的操作(返回值可以是相同或不同的值) | 在java.util.stream.Stream中的generate()方法创建一个无限元素流。 |
谓词接口
在代码中,我们经常需要使用检查条件并返回布尔值的函数。考虑下面的代码段:
Stream.of("hello", "world")
.filter(str -> str.startsWith("h"))
.forEach(System.out::println);
这段代码只是在控制台上打印“hello”。只有当传递的字符串以“h”开头时,filter()方法才返回 true,因此它从流中“过滤掉”字符串“world ”,因为该字符串不是以“h”开头的。在这段代码中,filter()方法将一个Predicate作为参数。这里是Predicate功能界面:
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
// other methods elided
}
名为test()的抽象方法接受一个参数并返回true或false(图 5-2 )。
图 5-2。
A Predicate takes an argument of type T and returns a boolean value as the result
A
Predicate<T>“确认”某事为true或false:它接受一个T类型的参数,并返回一个boolean值。你可以在一个Predicate对象上调用test()方法。
这个函数式接口还定义了名为and()和or()的默认方法,它们接受一个Predicate并返回一个Predicate。这些方法的行为类似于& &和||操作符。方法negate()返回一个Predicate,它的行为类似于!操作员。它们有什么用?这里有一个程序说明了在Predicate接口中and()方法的使用(清单 5-1 )。
Listing 5-1. PredicateTest.java
import java.util.function.Predicate;
public class PredicateTest {
public static void main(String []args) {
Predicate<String> nullCheck = arg -> arg != null;
Predicate<String> emptyCheck = arg -> arg.length() > 0;
Predicate<String> nullAndEmptyCheck = nullCheck.and(emptyCheck);
String helloStr = "hello";
System.out.println(nullAndEmptyCheck.test(helloStr));
String nullStr = null;
System.out.println(nullAndEmptyCheck.test(nullStr));
}
}
该程序打印:
true
false
在这个程序中,对象nullCheck是一个Predicate,如果给定的String参数不是null,它将返回true。如果给定的字符串不为空,emptyCheck谓词返回 true。nullAndEmptyCheck谓词通过使用Predicate中提供的名为and()的默认方法来组合nullCheck和emptyCheck谓词。由于helloStr在第一次调用nullAndEmptyCheck.test(helloStr)中指向字符串“hello”,且该字符串不为空,所以返回true。然而,在下一个调用中,nullStr为空,因此调用nullAndEmptyCheck.test(nullStr)返回false。
再举一个使用Predicate s 的例子,这里有一段代码使用了 Java 8 的Collection接口中添加的removeIf()方法(清单 5-2 )。
Listing 5-2. RemoveIfMethod.java
import java.util.List;
import java.util.ArrayList;
public class RemoveIfMethod {
public static void main(String []args) {
List<String> greeting = new ArrayList<>();
greeting.add("hello");
greeting.add("world");
greeting.removeIf(str -> !str.startsWith("h"));
greeting.forEach(System.out::println);
}
}
它在控制台中打印“hello”。在Collection接口(ArrayList的超级接口)中定义的默认方法removeIf()以一个Predicate作为参数:
default boolean removeIf(Predicate<? super E> filter)
在对removeIf()方法的调用中,我们传递了一个 lambda 表达式,它与在Predicate接口中声明的抽象方法boolean test(T t)相匹配:
greeting.removeIf(str -> !str.startsWith("h"));
结果,ArrayList对象问候语中的字符串“world”被删除,因此控制台中只显示“hello”。在这段代码中,我们使用了!操作员。除此之外,使用在Predicate中定义的等价的negate()方法怎么样?是的,这是可能的,下面是更改后的代码:
greeting.removeIf(((Predicate<String>) str -> str.startsWith("h")).negate());
当您执行清单 5-2 中的程序时,程序会打印“hello”。注意我们是如何在这个表达式中执行显式类型转换(到Predicate<String>)的。没有这种显式的类型转换——就像在((str -> str.startsWith("h")).negate())中一样——编译器不能执行类型推断来确定匹配的函数式接口,因此会报告一个错误。
消费者界面
有许多方法接受一个参数,根据该参数执行一些操作,但不向它们的调用者返回任何东西——它们是消费者方法。考虑下面的代码段:
Stream.of("hello", "world")
.forEach(System.out::println);
这段代码通过使用在Stream接口中定义的forEach()方法打印单词“hello”和“world ”,它们是流的一部分。该方法在java.util.stream.Stream界面中声明如下:
void forEach(Consumer<? super T> action);
forEach()将Consumer的一个实例作为参数。Consumer函数式接口声明了一个名为accept()的抽象方法(图 5-3 ):
图 5-3。
A Consumer takes an argument of type T and returns nothing
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
// the default andThen method elided
}
accept()方法“消费”一个对象,不返回任何东西(void)。
A
Consumer<T>“消费”某物:它接受一个参数(泛型类型T)并且不返回任何东西(void)。你可以在一个Consumer对象上调用accept()方法。
下面是一个使用Consumer接口的例子:
Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
printUpperCase.accept("hello");
// prints: HELLO
在这段代码中,lambda 表达式接受给定的字符串,转换成大写字母,并将其打印到控制台。我们将实际的参数“hello”传递给accept()方法。
现在,让我们回到对forEach()的讨论:调用forEach(System.out::println)是如何工作的?
System类有一个名为out的静态变量,它的类型是PrintStream。PrintStream类定义了重载的println方法;其中一个重载方法有签名void println(String)。在调用forEach(System.out::println)中,我们传递的是println的方法引用,即System.out::println。该方法引用匹配Consumer接口中抽象方法的签名,即void accept(T)。因此,方法引用System.out::println用于实现函数式接口Consumer,代码将字符串“hello”和“world”打印到控制台。清单 5-3 将代码Stream.of("hello", "world").forEach(System.out::println);分成三个不同的语句,只是为了展示它是如何工作的。
Listing 5-3. ConsumerUse.java
import java.util.stream.Stream;
import java.util.function.Consumer;
class ConsumerUse {
public static void main(String []args) {
Stream<String> strings = Stream.of("hello", "world");
Consumer<String> printString = System.out::println;
strings.forEach(printString);
}
}
该程序打印:
hello
world
Consumer也有一个名为andThen()的默认方法;它允许链接对Consumer对象的调用。
功能界面
考虑这个在java.util.stream.Stream接口中使用map()方法的例子(列表 5-4 ):
Listing 5-4. FunctionUse.java
import java.util.Arrays;
public class FunctionUse {
public static void main(String []args) {
Arrays.stream("4, -9, 16".split(", "))
.map(Integer::parseInt)
.map(i -> (i < 0) ? -i : i)
.forEach(System.out::println);
}
}
该程序打印:
4
9
16
这个程序通过拆分字符串“4,-9,16”来创建一个String流。方法引用Integer::parseInt被传递给map()方法——这个调用为流中的每个元素返回一个Integer对象。在流中对map()方法的第二次调用中,我们使用了 lambda 函数(i -> (i < 0) ? -i : i)来产生一个非负整数列表(或者,我们可以使用Math::abs方法)。我们在这里使用的map()方法将一个Function作为参数(这个例子是为了说明Function接口的用处)。最后,使用forEach()方法打印结果整数。
Function接口定义了一个名为apply()的抽象方法,它接受一个泛型类型T的参数并返回一个泛型类型R的对象(图 5-4 ):
图 5-4。
A Function<T, R> takes an argument of type T and returns a value of type R
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// other methods elided
}
Function接口也有默认的方法,比如compose(), andThen()和identity()。
A
Function<T, R>“操作”某物并返回某物:它接受一个参数(泛型类型T)并返回一个对象(泛型类型R)。你可以在一个Function对象上调用apply()方法。
下面是一个使用Function的简单例子:
Function<String, Integer> strLength = str -> str.length();
System.out.println(strLength.apply("supercalifragilisticexpialidocious"));
// prints: 34
这段代码获取一个字符串并返回它的长度。对于调用strLength.apply,我们传递字符串“supercalifragilisticexpialidocious"。作为调用apply()的结果,我们得到字符串 34 的长度作为结果。
让我们将清单 5-4 中的早期程序改为使用andThen()方法(清单 5-5 )。
Listing 5-5. CombineFunctions.java
import java.util.Arrays;
import java.util.function.Function;
public class CombineFunctions {
public static void main(String []args) {
Function<String, Integer> parseInt = Integer::``parseInt
Function<Integer, Integer> absInt = Math::``abs
Function<String, Integer> parseAndAbsInt = parseInt.andThen(absInt);
Arrays.``stream
.map(parseAndAbsInt)
.forEach(System.``out
}
}
该程序在单独的行中打印 4、9 和 16:与清单 5-4 的输出相同,但是在Stream中对map()方法进行了一次调用。因为Integer::parseInt()将一个字符串作为参数,解析它以返回一个Integer,我们声明了Function<String, Integer>类型的parseInt()方法。Math::abs方法接受一个整数并返回一个整数,因此我们将其声明为类型Function<Integer, Integer>。因为parseAndAbsInt接受一个String作为参数并返回一个整数作为结果,我们声明它的类型为Function<String, Integer>。
Function界面中的andThen()和compose()方法有什么区别?andThen()方法在调用当前的Function后应用传递的参数(如本例所示)。compose()方法在调用当前Function之前调用参数,如:
Function<String, Integer> parseAndAbsInt = absInt.compose(parseInt);
Function中的identity()函数只是返回传递的参数,不做任何事情!那它有什么用?它有时用于测试——当你写了一段使用了Function的代码并想检查它是否工作时,你可以调用identity(),因为它不做任何事情。这里有一个例子:
Arrays.stream("4, -9, 16".split(", "))
.map(Function.identity())
.forEach(System.out::println);
在这段代码中,map(Function.identity())什么也不做;它只是将流中的元素传递给调用forEach(System.out::println)。因此,代码按原样打印元素,即值 4、-9 和 16 在单独的行中。
供应商界面
在程序中,我们经常需要使用一种不接受任何输入但返回一些输出的方法。考虑以下生成Boolean值的程序(列表 5-6 ):
Listing 5-6. GenerateBooleans.java
import java.util.stream.Stream;
import java.util.Random;
class GenerateBooleans {
public static void main(String []args) {
Random random = new Random();
Stream.generate(random::nextBoolean)
.limit(2)
.forEach(System.out::println);
}
}
这个程序随机打印两个布尔值,例如,“真”和“假”。Stream接口中的generate()方法是一个静态成员,以一个Supplier作为参数;
static <T> Stream<T> generate(Supplier<T> s)
这里,您正在传递在java.util.Random类中定义的nextBoolean的方法引用。它返回一个随机选择的布尔值:
boolean nextBoolean()
您可以将nextBoolean的方法引用传递给Stream的generate()方法,因为它匹配Supplier接口中的抽象方法,即T get()图 5-5 。
图 5-5。
A Supplier takes no arguments and returns a value of type T
@FunctionalInterface
public interface Supplier<T> {
T get();
// no other methods in this interface
}
A
Supplier<T>“supplies”什么都不带,只返回一些东西:它没有参数,返回一个对象(通用类型T)。你可以在一个Supplier对象上调用get()方法。
下面是一个简单的例子,它不需要任何输入就可以返回值:
Supplier<String> currentDateTime = () -> LocalDateTime.now().toString();
System.out.println(currentDateTime.get());
我们已经在java.time.LocalDateTime上调用了now()方法(我们将在第八章中讨论 java 日期和时间 API)。当我们执行它时,它打印:2015-10-16T12:40:55.164。当然,如果您尝试这段代码,您将得到不同的输出。这里我们用的是Supplier<String>。lambda 表达式不接受任何输出,而是以String格式返回当前日期/时间。当我们在currentDateTime变量上调用get()方法时,我们正在调用 lambda。
构造函数引用
考虑以下代码:
Supplier<String> newString = String::new;
System.out.println(newString.get());
// prints an empty string (nothing) to the console and then a newline character
这段代码使用了构造函数引用。此代码相当于:
Supplier<String> newString = () -> new String();
System.out.println(newString.get());
通过使用::new的方法引用,这个 lambda 表达式得到了简化,如String::new所示。如何使用带参数的构造函数?例如,考虑构造函数Integer(String):这个Integer构造函数接受一个String作为参数,并用该字符串中给定的值创建一个Integer对象。下面是如何使用该构造函数:
Function<String, Integer> anotherInteger = Integer::new;
System.out.println(anotherInteger.apply("100"));
// this code prints: 100
我们不能在这里使用Supplier,因为Suppliers不接受任何参数。Functions接受参数,这里的返回类型是Integer,因此我们可以使用Function<String, Integer>。
函数式接口的原始版本
| 认证目标 |
|---|
| 开发使用函数式接口原始版本的代码 |
内置接口Predicate、Consumer、Function和Supplier作用于引用类型对象。对于原始类型,这些功能接口的int、long和double类型有可用的专门化。考虑对类型为T的对象进行操作的Predicate,即它是Predicate<T>。int、long和double对Predicate的专门化分别是IntPredicate、LongPredicate和DoublePredicate。
由于泛型的限制,您不能在函数式接口Predicate、Consumer、Function和Supplier中使用基元类型值。但是你可以使用包装器类型,比如Integer和Double来实现这些功能接口。当您试图在这些函数式接口中使用基本类型时,会导致隐式的自动装箱和取消装箱,例如,int值被转换为Integer对象,反之亦然。事实上,您甚至经常意识不到您正在使用带有这些功能接口的包装器类型。然而,当我们使用包装器类型时,性能会受到影响:想象一下在一个流中装箱和解箱几百万个整数。为了避免这个性能问题,您可以使用这些函数式接口的相关原语版本。
谓词接口的原始版本
考虑这个例子:
IntStream.range(1, 10).filter(i -> (i % 2) == 0).forEach(System.out::println);
这里的filter()方法将一个IntPredicate作为参数,因为底层流是一个IntStream。下面是显式使用IntPredicate的等价代码:
IntPredicate evenNums = i -> (i % 2) == 0;
IntStream.range(1, 10).filter(evenNums).forEach(System.out::println);
表 5-2 列出了java.util.function包中提供的Predicate接口的原始版本。
表 5-2。
Primitive Versions of Predicate Interface
| 功能接口 | 抽象方法 | 简要描述 |
|---|---|---|
IntPredicate | boolean test(int value) | 评估作为int传递的条件,并返回一个boolean值作为结果 |
LongPredicate | boolean test(long value) | 评估作为long传递的条件,并返回一个boolean值作为结果 |
DoublePredicate | boolean test(double value) | 评估作为double传递的条件,并返回一个boolean值作为结果 |
函数式接口的原始版本
下面是一个将Stream用于原始类型 integers 的例子:
AtomicInteger ints = new AtomicInteger(0);
Stream.generate(ints::incrementAndGet).limit(10).forEach(System.out::println);
// prints integers from 1 to 10 on the console
这段代码调用在类java.util.concurrent.atomic.AtomicInteger中定义的int incrementAndGet()方法。注意,这个方法返回一个int,而不是一个Integer。尽管如此,我们还是可以将它与Stream一起使用,因为它隐含了与int的包装器类型Integer之间的自动装箱和取消装箱。这种装箱和拆箱完全没有必要。相反,你可以使用IntStream接口;它的generator()方法以一个IntSupplier作为参数。经过这一更改,下面是等效的代码:
AtomicInteger ints = new AtomicInteger(0);
IntStream.generate(ints::incrementAndGet).limit(10).forEach(System.out::println);
// prints integers from 1 to 10 on the console
因为他的代码使用了IntStream,generate()方法取了一个IntSupplier,所以没有隐式的装箱和拆箱;因此这段代码执行得更快,因为它不会生成不必要的临时Integer对象。
再举一个例子,下面是我们之前看到的使用Math.abs()方法的一段代码:
Function<Integer, Integer> absInt = Math::abs;
你可以使用Function的int特殊化来替换它,称为IntFunction:
IntFunction absInt = Math::abs;
根据参数和返回类型的不同,有许多版本的Function接口原语类型(见表 5-3 )。
表 5-3。
Primitive Versions of Function Interface
| 功能接口 | 抽象方法 | 简要描述 |
|---|---|---|
IntFunction<R> | R apply(int value) | 对传递的int参数进行操作,并返回泛型类型的值R |
LongFunction<R> | R apply(long value) | 对传递的long参数进行操作,并返回泛型类型的值R |
DoubleFunction<R> | R apply(double value) | 对传递的double参数进行操作,并返回泛型类型的值R |
ToIntFunction<T> | int applyAsInt(T value) | 对传递的泛型类型参数T进行操作,并返回一个int值 |
ToLongFunction<T> | long applyAsLong(T value) | 对传递的泛型类型参数T进行操作,并返回一个long值 |
ToDoubleFunction<T> | double applyAsDouble(T value) | 对传递的泛型类型参数T进行操作,并返回一个double值 |
IntToLongFunction | long applyAsLong(int value) | 对传递的int类型参数进行操作,并返回一个long值 |
IntToDoubleFunction | double applyAsDouble(int value) | 对传递的int类型参数进行操作,并返回一个double值 |
LongToIntFunction | int applyAsInt(long value) | 对传递的long类型参数进行操作,并返回一个int值 |
LongToDoubleFunction | double applyAsLong(long value) | 对传递的long类型参数进行操作,并返回一个double值 |
DoubleToIntFunction | int applyAsInt(double value) | 对传递的double类型参数进行操作,并返回一个int值 |
DoubleToLongFunction | long applyAsLong(double value) | 对传递的double类型参数进行操作,并返回一个long值 |
消费者界面的原始版本
根据参数的种类,有许多版本的Consumer接口原语类型可用(见表 5-4 )。
表 5-4。
Primitive Versions of Consumer Interface
| 功能接口 | 抽象方法 | 简要描述 |
|---|---|---|
IntConsumer | void accept(int value) | 对给定的int参数进行操作,不返回任何内容 |
LongConsumer | void accept(long value) | 对给定的long参数进行操作,不返回任何内容 |
DoubleConsumer | void accept(double value) | 对给定的double参数进行操作,不返回任何内容 |
ObjIntConsumer<T> | void accept(T t, int value) | 对给定的泛型类型参数T和int进行操作,不返回任何内容 |
ObjLongConsumer<T> | void accept(T t, long value) | 对给定的泛型类型参数T和long进行操作,不返回任何内容 |
ObjDoubleConsumer<T> | void accept(T t, double value) | 对给定的泛型类型参数T和double进行操作,不返回任何内容 |
供应商接口的原始版本
Supplier的原始版本是分别返回boolean、int、long、double的BooleanSupplier、IntSupplier、LongSupplier、DoubleSupplier(见表 5-5 )。
表 5-5。
Primitive Versions of Supplier Interface
| 功能接口 | 抽象方法 | 简要描述 |
|---|---|---|
BooleanSupplier | boolean getAsBoolean() | 不接受任何参数并返回一个boolean值 |
IntSupplier | int getAsInt() | 不接受任何参数并返回一个int值 |
LongSupplier | long getAsLong() | 不接受任何参数并返回一个long值 |
DoubleSupplier | double getAsDouble() | 不接受任何参数并返回一个double值 |
功能接口的原语版本仅适用于
int、long,和double(除了Supplier的这三种类型外,还有boolean类型)。如果您需要一个接受或返回其他原语类型char、byte或short的函数式接口,该怎么办?你必须使用隐式转换到相关的int专门化。同样,当你使用float时,你可以使用double类型的专门化。
功能接口的二进制版本
| 认证目标 |
|---|
| 开发使用函数式接口二进制版本的代码 |
函数式接口Predicate、Consumer和Function具有带一个参数的抽象方法。例如,考虑一下Function接口:
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
// other methods elided
}
抽象方法apply()接受一个参数(泛型类型T)。下面是二进制版本的Function界面:
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
// other methods elided
}
A
BiFunction类似于Function,但不同之处在于它接受两个参数:接受泛型类型T和U的参数,并返回泛型类型R的对象。你可以在一个BiFunction对象上调用apply()方法。
前缀“Bi”表示采用“两个”参数的版本。与Function的BiFunction相同,还有Predicate的BiPredicate和Consumer的BiConsumer需要两个参数(见表 5-6 )。Supplier怎么样?因为Supplier中的抽象方法不接受任何参数,所以没有等价的BiSupplier可用。
表 5-6。
Binary Versions of Functional Interfaces
| 功能接口 | 抽象方法 | 简要描述 |
|---|---|---|
BiPredicate<T, U> | boolean test(T t, U u) | 检查参数是否与条件匹配,并返回一个boolean值作为结果 |
BiConsumer<T, U> | void accept(T t, U u) | 消耗两个参数但不返回任何内容的操作 |
BiFunction<T, U, R> | R apply(T t, U u) | 接受两个参数并返回结果的函数 |
双功能接口
下面是一个使用BiFunction接口的例子:
BiFunction<String, String, String> concatStr = (x, y) -> x + y;
System.out.println(concatStr.apply("hello ", "world"));
// prints: hello world
在此示例中,参数和返回类型是相同的类型,但它们可以不同,如:
BiFunction<Double, Double, Integer> compareDoubles = Double::compare;
System.out.println(compareDoubles.apply(10.0, 10.0));
// prints: 0
在这种情况下,参数类型是类型double,返回类型是integer。当传递的双精度值相等时,Double类中的compare方法返回 0,因此我们得到这个代码段的输出 0。
考虑到在java.util.function包中有大量可用的函数式接口,为给定的上下文找到合适的函数式接口可能很棘手。例如,在我们之前的例子中,我们使用了BiFunction<Double, Double, Integer>。相反,我们可以使用函数式接口ToIntBiFunction,因为它返回一个int。
双预测接口
考虑下面的代码段:
BiPredicate<List<Integer>, Integer> listContains = List::contains;
List aList = Arrays.asList(10, 20, 30);
System.out.println(listContains.test(aList, 20));
// prints: true
这段代码展示了如何使用BiPredicate。List中的contains()方法将一个元素作为参数,并检查底层列表是否包含该元素。因为它接受一个参数并返回一个Integer,所以我们可以使用一个BiPredicate。为什么不用BiFunction<T, U, Boolean>?是的,代码可以工作,但是更好的选择是等价的BiPredicate<T, U>,因为BiPredicate返回一个boolean值。
双消费者界面
考虑这段代码:
BiConsumer<List<Integer>, Integer> listAddElement = List::add;
List aList = new ArrayList();
listAddElement.accept(aList, 10);
System.out.println(aList);
// prints: [10]
这段代码展示了如何使用BiConsumer。类似于在前面的例子中为BiPredicate使用List::contains方法引用,这个例子展示了如何使用这个接口使用BiConsumer来调用List中的add()方法。
一元运算符接口
| 认证目标 |
|---|
| 开发使用一元运算符接口的代码 |
考虑下面的例子。
List<Integer> ell = Arrays.asList(-11, 22, 33, -44, 55);
System.out.println("Before: " + ell);
ell.replaceAll(Math::abs);
System.out.println("After: " + ell);
该代码打印:
Before: [-11, 22, 33, -44, 55]
After: [11, 22, 33, 44, 55]
这段代码使用 Java 8 中引入的replaceAll()方法来替换给定List中的元素。replaceAll()方法将UnaryOperator作为唯一的参数:
void replaceAll(UnaryOperator<T> operator)
将replaceAll()方法与Math::abs方法一起传递给它。
Math对abs()方法有四个重载方法:
abs(int)
abs(long)
abs(double)
abs(float)
因为类型是Integer,所以通过类型推断选择重载的方法abs(int)。
UnaryOperator是一个函数式接口,它扩展了Function接口,可以使用Function接口中声明的apply()方法;此外,它从Function接口继承了默认功能compose()和andThen()。类似于UnaryOperator扩展了Function接口,还有一个BinaryOperator扩展了BiFunction接口。
接口IntUnaryOperator、LongUnaryOperator和DoubleUnaryOperator的原始类型版本也作为java.util.function包的一部分提供。
Java . util . function 包只包含函数式接口。这个包里只有四个核心接口:
Predicate、Consumer、Function和Supplier。剩下的接口都是原语版本,二进制版本,以及衍生接口比如UnaryOperator接口。这些接口的主要区别在于它们声明的抽象方法的签名。您需要根据上下文和您的需求选择合适的功能接口。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
使用 java.util.function 包中包含的内置接口,如谓词、使用者、函数和供应商
- 内置函数式接口
Predicate、Consumer、Function和Supplier的区别主要在于它们声明的抽象方法的签名。 - A
Predicate测试给定的条件并返回true或false;因此,它有一个名为“test”的抽象方法,该方法接受泛型类型T的参数并返回类型boolean。 - A
Consumer“消费”一个对象,不返回任何东西;因此,它有一个名为“accept”的抽象方法,该方法接受泛型类型T的参数,并具有返回类型void。 - A
Function“操作”参数并返回结果;因此,它有一个名为“apply”的抽象方法,该方法接受泛型类型T的参数,并具有泛型返回类型R。 - 一个
Supplier“物资”什么都不拿,却要回报一些东西;因此,它有一个名为“get”的抽象方法,该方法不带参数,返回一个泛型类型T。 - 在
Iterable(由集合类实现)方法中定义的forEach()方法接受一个Consumer<T>。
开发使用函数式接口原始版本的代码
- 内置接口
Predicate、Consumer、Function和Supplier作用于引用类型对象。对于基本类型,这些函数式接口的int、long和double类型都有专门化。 - 当
Stream接口用于基本类型时,会导致不必要的基本类型到它们的包装器类型的装箱和拆箱。这会导致代码变慢,并浪费内存,因为会创建不必要的包装对象。因此,只要有可能,最好使用功能接口Predicate、Consumer、Function和Supplier的原始类型专门化。 - 功能接口
Predicate、Consumer、Function、Supplier的原语版本仅适用于int、long和double类型(除了Supplier的这三种类型外,还有boolean类型)。当你需要使用char、byte或short类型时,你必须使用到相关int版本的隐式转换;同样,当需要使用float时,可以使用double类型的版本。
开发使用函数式接口二进制版本的代码
- 功能接口
BiPredicate、BiConsumer、BiFunction分别是Predicate、Consumer、Function的二进制版本。对于Supplier没有等价的二进制,因为它不接受任何参数。前缀“Bi”表示采用“两个”参数的版本。
开发使用一元运算符接口的代码
UnaryOperator是一个功能接口,它扩展了Function接口。UnaryOperator的原始类型专门化为IntUnaryOperator、LongUnaryOperator和DoubleUnaryOperator,分别代表int、long和double类型。
Question TimeWhich of the following are functional interfaces? (Select ALL that apply) java.util.stream.Stream java.util.function.Consumer java.util.function.Supplier java.util.function.Predicate java.util.function.Function<T, R> Choose the correct option based on this program: import java.util.function.Predicate; public class PredicateTest { public static void main(String []args) { Predicate<String> notNull = ((Predicate<String>)(arg -> arg == null)).negate(); // #1 System.out.println(notNull.test(null)); } } This program results in a compiler error in line marked with the comment #1 This program prints: true This program prints: false This program crashes by throwing NullPointerException Choose the correct option based on this program: import java.util.function.Function; public class AndThen { public static void main(String []args) { Function<Integer, Integer> negate = (i -> -i), square = (i -> i * i), negateSquare = negate.compose(square); System. out .println(negateSquare.apply(10)); } } This program results in a compiler error This program prints: -100 This program prints: 100 This program prints: -10 This program prints: 10 Which one of the following functional interfaces can you assign the method reference Integer::parseInt? Note that the static method parseInt() in Integer class takes a String and returns an int, as in: int parseInt(String s) BiPredicate<String, Integer> Function<Integer, String> Function<String, Integer> Predicate Consumer<Integer, String> Consumer<String, Integer> Choose the correct option based on this program: import java.util.function.BiFunction; public class StringCompare { public static void main(String args[]){ BiFunction<String, String, Boolean> compareString = (x, y) -> x.equals(y); System.out.println(compareString.apply("Java8","Java8")); // #1 } } This program results in a compiler error in line marked with #1 This program prints: true This program prints: false This program prints: (x, y) -> x.equals(y) This program prints: ("Java8", "Java8") -> "Java8".equals("Java8") Which one of the following abstract methods does not take any argument but returns a value? The accept() method in java.util.function.Consumer<T> interface The get() method in java.util.function.Supplier<T> interface The test() method in java.util.function.Predicate<T> interface The apply() method in java.util.function.Function<T, R> interface Choose the correct option based on this program: import java.util.function.Predicate; public class PredUse { public static void main(String args[]){ Predicate<String> predContains = "I am going to write OCP8 exam"::contains; checkString(predContains, "OCPJP"); } static void checkString(Predicate<String> predicate, String str) { System.out.println(predicate.test(str) ? "contains" : "doesn't contain"); } } This program results in a compiler error for code within main() method This program results in a compiler error for code within checkString() method This program prints: contains This program prints: doesn’t contain Choose the correct option based on this program: import java.util.function.ObjIntConsumer; class ConsumerUse { public static void main(String []args) { ObjIntConsumer<String> charAt = (str, i) -> str.charAt(i); // #1 System. out .println(charAt.accept("java", 2)); // #2 } } This program results in a compiler error for the line marked with comment #1 This program results in a compiler error for the line marked with comment #2 This program prints: a This program prints: v This program prints: 2
答案:
B, C, D, and E The interface java.util.stream.Stream<T> is not a functional interface–it has numerous abstract methods. The other four options are functional interfaces. The functional interface java.util.function.Consumer<T> has an abstract method with the signature void accept(T t); The functional interface java.util.function.Supplier<T> has an abstract method with the signature T get(); The functional interface java.util.function.Predicate<T> has an abstract method with the signature boolean test(T t); The functional interface java.util.function.Function<T, R> has an abstract method with the signature R apply(T t); C. This program prints: false The expression ((Predicate<String>)(arg -> arg == null)) is a valid cast to the type (Predicate<String>) for the lambda expression (arg -> arg == null). Hence, it does not result in a compiler error. The negate function in Predicate interface turns true to false and false to true. Hence, given null, the notNull.test(null) results in returning the value false. B. This program prints: -100 The negate.compose(square) calls square before calling negate. Hence, for the given value 10, square results in 100, and when negated, it becomes -100. C. Function<String, Integer> The parseInt() method takes a String and returns a value, hence we need to use the Function interface because it matches the signature of the abstract method R apply(T t). In Function<T, R>, the first type argument is the argument type and the second one is the return type. Given that parseInt takes a String as the argument and returns an int (that can be wrapped in an Integer), we can assign it to Function<String, Integer>. B. This program prints: true The BiFunction interface takes two type arguments–they are of types String in this program. The return type is Boolean. BiFunction functional interface has abstract method named apply(). Since the signature of String’s equals() method matches that of the signature of the abstract method apply(), this program compiles fine. When executed, the strings “Java8” and “Java8” are equal; hence, the evaluation returns true that is printed on the console. B. The get() method in java.util.function.Supplier<T> interface The signature of get() method in java.util.function.Supplier<T> interface is: T get(). D. This program prints: doesn’t contain You can create method references for object as well, so the code within main() compiles without errors. The code within checkString() method is also correct and hence it also compiles without errors. The string “OCPJP” is not present in the string “I am going to write OCP8 exam” and hence this program prints “doesn’t contain” on the console. D. This program results in a compiler error for the line marked with comment #2 ObjIntConsumer operates on the given String argument str and int argument i and returns nothing. Though the charAt method is declared to return the char at given index i, the accept method in ObjIntConsumer has return type void. Since System.out.println expects an argument to be passed, the call charAt.accept("java", 2) results in a compiler error because accept() method returns void.
六、Java 流 API
| 认证目标 |
|---|
| 开发代码以使用 peek()和 map()方法从对象中提取数据,包括 map()方法的原始版本 |
| 使用流类的搜索方法搜索数据,包括 findFirst、findAny、anyMatch、allMatch、noneMatch |
| 开发使用可选类的代码 |
| 开发使用流数据方法和计算方法的代码 |
| 使用流 API 对集合进行排序 |
| 使用 collect 方法将结果保存到集合中,使用 Collectors 类将数据分组/分区 |
| 在流 API 中使用 flatMap()方法 |
在本章中,我们将讨论 Java 8 中对 Java 库最重要的补充:stream API。流 API 是java.util.stream包的一部分。本章的重点是这个包中的关键接口:Stream<T>接口(及其原始类型版本)。在这一章中,我们还将讨论诸如Optional和Collectors这样的职业。
我们已经在第四章(泛型和集合)中介绍了 stream API。stream API 广泛使用了内置的函数式接口,这些接口是我们在前一章讨论的java.util.function包的一部分(第五章)。所以,我们假设你在看本章之前已经看过这两章了。
从流中提取数据
| 认证目标 |
|---|
| 开发代码以使用 peek()和 map()方法从对象中提取数据,包括 map()方法的原始版本 |
让我们从一个简单的例子开始:
long count = Stream.of(1, 2, 3, 4, 5).map(i -> i * i).count();
System.out.printf("The stream has %d elements", count);
这段代码打印了:
The stream has 5 elements
该流中的map()操作将作为参数传递的给定 lambda 函数应用于流的元素。在这种情况下,它对流中的元素求平方。count()方法返回值 5——您在一个变量中捕获它并在控制台上打印出来。但是你如何检查在这段代码中应用中间操作map()的结果呢?为此,您可以使用peek()方法:
long count = Stream.of(1, 2, 3, 4, 5)
.map(i -> i * i)
.peek(i -> System.out.printf("%d ", i))
.count();
System.out.printf("%nThe stream has %d elements", count);
这段代码打印出来
1 4 9 16 25
The stream has 5 elements
这个例子还说明了如何将中间操作链接在一起。这是可能的,因为中间操作返回流。
现在,让我们在调用map()方法之前添加另一个peek()方法,以了解它是如何工作的:
Stream.of(1, 2, 3, 4, 5)
.peek(i -> System.out.printf("%d ", i))
.map(i -> i * i)
.peek(i -> System.out.printf("%d ", i))
.count();
这段代码打印出来
1 1 2 4 3 9 4 16 5 25
从这个输出可以看出,流管道正在逐个处理元素。每个元素都映射到它的正方形。peek()方法帮助我们理解流中正在处理什么,而不分发它。
peek()方法主要用于调试目的。它有助于我们理解元素在管道中是如何转换的。不要在产品代码中使用它。
你可以在Stream<T>的原始版本中使用map()和peek()方法;然后下面的代码片段使用了一个DoubleStream:
DoubleStream.``of
.map(Math::``sqrt
.peek(System.``out
.sum();
此代码在控制台上的不同行中打印出 1.0、2.0 和 3.0。图 6-1 直观地显示了该流管道中的源、中间操作和终端操作。
图 6-1。
A stream pipeline with source, intermediate operations and terminal operation
从流中搜索数据
| 认证目标 |
|---|
| 使用流类的搜索方法搜索数据,包括 findFirst、findAny、anyMatch、allMatch、noneMatch |
在Stream界面中,以单词Match结尾的方法和以单词find开头的方法对于从流中搜索数据很有用(表 6-1 )。如果您在流中寻找匹配给定条件的元素,您可以使用匹配操作,例如anyMatch()、allMatch()和noneMatch()。这些方法返回一个布尔值。对于搜索操作findFirst()和findAny(),匹配元素可能不在Stream中,所以它们返回Optional<T>(我们将在下一节讨论Optional<T>)。
表 6-1。
Important Match and Find Methods in the Stream Interface
| 方法名称 | 简短描述 |
|---|---|
boolean anyMatch(Predicate<? super T> check) | 如果流中有任何元素匹配给定的谓词,则返回 true。如果流为空或者没有匹配的元素,则返回 false。 |
boolean allMatch(Predicate<? super T> check) | 仅当流中的所有元素都匹配给定的谓词时,才返回 true。如果流是空的,不计算谓词,则返回 true! |
boolean noneMatch(Predicate<? super T> check) | 仅当流中没有任何元素与给定谓词匹配时,才返回 true。如果流是空的,不计算谓词,则返回 true! |
Optional<T> findFirst() | 返回流中的第一个元素;如果流中没有元素,它返回一个空的Optional<T>对象。 |
Optional<T> findAny() | 从流中返回一个元素;如果流中没有元素,它返回一个空的Optional<T>对象。 |
与流为空时返回 false 的 anyMatch()方法不同,allMatch()和 noneMatch()方法在流为空时返回 true!
下面是一个简单的程序,演示了如何使用anyMatch()、allMatch()和noneMatch()方法(清单 6-1 )。
Listing 6-1. MatchUse.java
import java.util.stream.IntStream;
public class MatchUse {
public static void main(String []args) {
// Average temperatures in Concordia, Antarctica in a week in October 2015
boolean anyMatch
= IntStream.of(-56, -57, -55, -52, -48, -51, -49).anyMatch(temp -> temp > 0);
System.out.println("anyMatch(temp -> temp > 0): " + anyMatch);
boolean allMatch
= IntStream.of(-56, -57, -55, -52, -48, -51, -49).allMatch(temp -> temp > 0);
System.out.println("allMatch(temp -> temp > 0): " + allMatch);
boolean noneMatch
= IntStream.of(-56, -57, -55, -52, -48, -51, -49).noneMatch(temp -> temp > 0);
System.out.println("noneMatch(temp -> temp > 0): " + noneMatch);
}
}
该程序打印:
anyMatch(temp -> temp > 0): false
allMatch(temp -> temp > 0): false
noneMatch(temp -> temp > 0): true
因为所有给定的温度都是负值,anyMatch()和allMatch()方法返回 false,而noneMatch()返回 true。
findFirst()和findAny()方法对于在流中搜索元素很有用。这里有一个使用findFirst()方法的程序(列表 6-2 )。
Listing 6-2. FindFirstUse1.java
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;
public class FindFirstUse1 {
public static void main(String []args) {
Method[] methods = Stream.class.getMethods();
Optional<String> methodName = Arrays.stream(methods)
.map(method -> method.getName())
.filter(name -> name.endsWith("Match"))
.sorted()
.findFirst();
System.out.println("Result: " + methodName.orElse("No suitable method found"));
}
}
该程序打印:
Result: allMatch
在这个程序中,我们使用反射获得了Stream本身的方法列表。然后,使用map()方法,我们获得方法名的列表,检查名称是否以字符串“Match”结尾,对这些方法进行排序,并返回第一个找到的方法。如果我们正在寻找任何以“匹配”结尾的方法名,那么我们可以使用findAny()方法来代替。
为什么
java.util.function包同时有findFirst()和findAny()方法?在并行流中,findAny()比findFirst()用起来更快(我们在第十一章中讨论并行流)。
清单 6-3 有一个流,它有许多温度值,都是双精度值。使用findFirst(),我们寻找任何大于 0 的温度。程序将打印什么?
Listing 6-3. FindFirstUse2.java
import java.util.OptionalDouble;
import java.util.stream.DoubleStream;
public class FindFirstUse2 {
public static void main(String []args) {
OptionalDouble temperature = DoubleStream.of(-10.1, -5.4, 6.0, -3.4, 8.9, 2.2)
.filter(temp -> temp > 0)
.findFirst();
System.out.println("First matching temperature > 0 is " + temperature.getAsDouble());
}
}
该程序打印:
First matching temperature > 0 is 6.0
在这个双精度值流中,filter()方法过滤元素10.1和-5.4,因为条件temp > 0为假。对于元素 6.0,filter()方法评估条件为真,并且findFirst()返回该元素。注意,这个流管道中的其余元素被忽略了:元素 8.9 和 2.2 也满足条件temp > 0,但是流管道被关闭,因为findFirst()方法已经返回值 6.0。换句话说,像findFirst()这样的搜索方法就是短路。一旦确定了结果,就不会处理流中的其余元素。
用于搜索元素的“匹配”和“查找”方法本质上是“短路”。什么是短路?一旦找到结果,评估就停止(并且不评估其余部分)。您已经熟悉了运算符& &和||“短路”的名称。例如,在表达式((s!= null) & & (s.length() > 0)),如果字符串 s 为 null,则条件(s!= null)计算结果为 false 因此,表达式的结果为 false。在这种情况下,不计算剩余的表达式(s.length() > 0)。
在清单 6-2 和 6-3 中,我们使用了Optional和OptionalDouble类;现在让我们来讨论这两个类。
选修课
| 认证目标 |
|---|
| 开发使用可选类的代码 |
类java.util.Optional是值的持有者,该值可以是null。在java.util.stream包的类中有许多方法返回Optional值。现在让我们看一个例子。
考虑这种方法:
public static void selectHighestTemperature(Stream<Double> temperatures) {
System.out.println(temperatures.max(Double::compareTo));
}
下面是对此方法的调用:
selectHighestTemperature(Stream.of(24.5, 23.6, 27.9, 21.1, 23.5, 25.5, 28.3));
该代码打印:
Optional[28.3]
Stream中的max()方法将一个Comparator作为参数,并返回一个Optional<T>:
Optional<T> max(Comparator<? super T> comparator);
为什么用Optional<T>而不是返回类型T?这是因为max()方法可能无法找到最大值——例如,考虑一个空流:
selectHighestTemperature(Stream.of());
现在,这段代码打印出来:
Optional.empty
要从Optional获取值,可以使用isPresent()和get()方法,如下所示:
public static void selectHighestTemperature(Stream<Double> temperatures) {
Optional<Double> max = temperatures.max(Double::compareTo);
if(max.isPresent()) {
System.out.println(max.get());
}
}
编写一个if条件是繁琐的(并且不是函数式的),所以可以使用ifPresent方法来编写简化的代码:
max.ifPresent(System.out::println);
Optional中的这个ifPresent()方法以一个Consumer<T>作为参数。你也可以使用像orElse()和orElseThrow()这样的方法,我们将在讨论如何创建Optional对象之后讨论这些方法。
创建可选对象
有许多方法可以创建Optional对象。创建Optional对象的一种方法是在Optional类中使用工厂方法,如下所示:
Optional<String> empty = Optional.empty();
也可以在Optional类中使用of():
Optional<String> nonEmptyOptional = Optional.of("abracadabra");
但是,您不能将null传递给Optional.of()方法,如:
Optional<String> nullStr = Optional.of(null);
System.out.println(nullStr);
// crashes with a NullPointerException
这将导致抛出一个NullPointerException。如果你想创建一个有null值的Optional对象,那么你可以使用ofNullable()方法:
Optional<String> nullableStr = Optional.ofNullable(null);
System.out.println(nullableStr);
// prints: Optional.empty
图 6-2 将nonEmptyOptional、nullStr和nullableStr所指向的Optional<String>对象形象化表示。
图 6-2。
Representation of three Optional objects
可选流
你也可以把Optional看作一个可以有零个元素或者一个元素的流。所以你可以在这个流上应用诸如map()、filter()和flatMap()操作的方法!怎么有用?下面是一个例子(列表 6-4 ):
Listing 6-4. OptionalStream.java
import java.util.Optional;
public class OptionalStream {
public static void main(String []args) {
Optional<String> string = Optional.of(" abracadabra ");
string.map(String::trim).ifPresent(System.out::println);
}
}
该程序打印:
abracadabra
当这些操作失败时,您可以使用orElse()或orElseThrow()方法(例如,底层的Optional具有空值),如下所示:
Optional<String> string = Optional.ofNullable(null);
System.out.println(string.map(String::length).orElse(-1));
这段代码输出-1,因为变量string是保存null的Optional变量,因此orElse()方法执行并返回-1。或者,您可以使用orElseThrow()方法抛出一个异常:
Optional<String> string = Optional.ofNullable(null);
System.out.println(string.map(String::length).orElseThrow(IllegalArgumentException::new));
这段代码抛出了一个IllegalArgumentException。当你处理从一个函数返回的Optional对象,而你不知道Optional对象包含什么的时候,在一个Optional对象上调用map()、flatMap()或filter()这样的方法是很有用的。
可选的原始版本
在我们之前讨论的代码中,我们同时使用了Stream<Double>和Optional<Double>类型:
public static void selectHighestTemperature(Stream<Double> temperatures) {
Optional<Double> max = temperatures.max(Double::compareTo);
if(max.isPresent()) {
System.out.println(max.get());
}
}
最好用DoubleStream和OptionalDouble,分别是Stream<T>和Optional<T>的double的原语类型版本。(另外两个可用的原语类型版本用于int和long,分别命名为OptionalInt和OptionalLong。)所以,这段代码可以重写为:
public static void selectHighestTemperature(DoubleStream temperatures) {
OptionalDouble max = temperatures.max();
max.ifPresent(System.out::println);
}
当用下面的调用调用时,
selectHighestTemperature(DoubleStream.of(24.5, 23.6, 27.9, 21.1, 23.5, 25.5, 28.3));
我们得到控制台上正确显示的最大值:
28.3
类似于返回Optional<T>的max()方法Stream<T>,DoubleStream中的max()方法返回一个OptionalDouble。
流数据方法和计算方法
| 认证目标 |
|---|
| 开发使用流数据方法和计算方法的代码 |
Stream<T>接口有数据和计算方法count()、min()和max()。min()和max()方法将一个Comparator对象作为参数并返回一个Optional<T>。这里有一个使用这些方法的例子(清单 6-5 )。
Listing 6-5. WordsCalculation.java
import java.util.Arrays;
public class WordsCalculation {
public static void main(String []args) {
String[] string = "you never know what you have until you clean your room".split(" ");
System.out.println(Arrays.stream(string).min(String::compareTo).get());
}
}
该程序打印:
clean
因为min()方法需要一种方法来比较流中的元素,所以我们在这个程序中传递了String::compareTo方法引用。由于min()返回一个Optional<T>,我们使用了get()方法来获得结果字符串。由于String::compareTo按字典顺序比较两个字符串,我们得到单词“clean”作为结果。
下面是修改后的代码片段,它不是按字典顺序而是根据字符串的长度来比较字符串:
Comparator<String> lengthCompare = (str1, str2) -> str1.length() - str2.length();
System.out.println(Arrays.stream(string).min(lengthCompare).get());
有了这个改变,程序打印出“you ”,因为它是给定的string中长度最小的单词。
在Stream<T>接口的原始版本中提供了额外的数据和计算方法,如sum()和average()。表 6-2 列出了我们在本节讨论的IntStream接口中的重要方法。
表 6-2。
Important Data and Calculation Methods in IntStream Interface
| 方法 | 简短描述 |
|---|---|
int sum() | 返回流中元素的总和;如果流为空,则为 0。 |
long count() | 返回流中元素的数量;如果流为空,则为 0。 |
OptionalDouble average() | 返回流中元素的平均值;如果流为空,则为空的OptionalDouble值。 |
OptionalInt min() | 返回流中的最小整数值;如果流为空,则为空的OptionalInt值。 |
OptionalInt max() | 返回流中的最大整数值;如果流为空,则为空的OptionalInt值。 |
IntSummaryStatistics summaryStatistics() | 返回一个具有sum、count、average、min和max值的IntSummaryStatistics对象。 |
LongStream和DoubleStream接口的方法与本表中为IntStream列出的方法相似(表 6-2 )。下面是一个使用它们的简单程序(清单 6-6 )。
Listing 6-6. WordStatistics.java
import java.util.IntSummaryStatistics;
import java.util.regex.Pattern;
public class WordStatistics {
public static void main(String []args) {
String limerick = "There was a young lady named Bright " +
"who traveled much faster than light " +
"She set out one day " +
"in a relative way " +
"and came back the previous night ";
IntSummaryStatistics wordStatistics =
Pattern.compile(" ")
.splitAsStream(limerick)
.mapToInt(word -> word.length())
.summaryStatistics();
System.out.printf(" Number of words = %d \n Sum of the length of the words = %d \n" +
" Minimum word size = %d \n Maximum word size %d \n " +
" Average word size = %f \n", wordStatistics.getCount(),
wordStatistics.getSum(), wordStatistics.getMin(),
wordStatistics.getMax(), wordStatistics.getAverage());
}
}
该程序打印:
Number of words = 28
Sum of the length of the words = 115
Minimum word size = 1
Maximum word size 8
Average word size = 4.107143
在使用Pattern类中的splitAsStream()方法将单词分割成一个流之后,这个程序调用mapToInt()方法将单词转换成它们的长度。为什么用mapToInt()而不是map()的方法?map()方法返回一个Stream,但是我们想要在流中的底层元素上执行计算。接口没有执行计算的方法,但是它的原始类型版本有数据和计算方法。因此,我们称返回IntStream的mapToInt()方法:IntStream有许多有用的数据和计算方法(列于表 6-2 )。我们已经在IntStream上调用了summaryStatistics()方法。最后,我们在返回的IntSummaryStatistics对象上调用了各种方法,如sum()和average()来总结对给定打油诗中使用的单词的计算。
也可以直接调用IntStream中提供的sum()和average()等方法,如:
IntStream.of(10, 20, 30, 40).sum();
这些方法比使用reduce()方法的等效方法更简洁:
IntStream.of(10, 20, 30, 40).reduce(0, ((sum, val) -> sum + val));
为什么 stream API 提供了reduce()方法,而我们可以使用像sum()这样更简洁,更方便使用,也更易读的方法?
答案是reduce()是一个一般化的方法:当你想对 stream 元素执行重复操作来计算一个结果时,可以使用它。考虑 10 的阶乘。我们没有像IntStream中的sum()那样的方法可以帮助我们将所有的值相乘。因此,我们可以在这种情况下使用reduce()方法:
// factorial of 5
System.out.println(IntStream.rangeClosed(1, 5).reduce((x, y) -> (x * y)).getAsInt());
// prints: 120
实际上,IntStream的sum()方法是通过调用reduce()方法(在IntPipeline类中)在内部实现的:
@Override
public final int sum() {
return reduce(0, Integer::sum);
}
在这种情况下,sum()方法通过将方法引用Integer::sum作为第二个参数传递给reduce()方法来实现。
归约操作(又名“归约器”)可以是隐式的,也可以是显式的。
IntStream中的sum()、min()和max()等方法就是隐式归约器的例子。当我们在代码中直接使用reduce()方法时,我们使用的是显式归约器。我们可以将隐式归约器转换为等价的显式归约器。
使用流 API 对集合进行排序
| 认证目标 |
|---|
| 使用流 API 对集合进行排序 |
在第四章(关于泛型和集合)中,我们讨论了如何使用Comparator和Comparable接口对集合进行排序。流简化了对集合进行排序的任务。下面是一个用字典式比较对字符串进行排序的程序(清单 6-7 )。
Listing 6-7. SortingCollection.java
import java.util.Arrays;
import java.util.List;
public class SortingCollection {
public static void main(String []args) {
List words =
Arrays.``asList
words.stream().distinct().sorted().forEach(System.out::println);
}
}
该程序打印:
brain
but
follow
heart
take
with
you
your
在这段代码中,words是一个类型为List的集合。我们首先使用stream()方法从列表中获取一个流,然后调用distinct()方法删除重复项(单词“your”在集合中重复出现)。之后,我们称之为sorted()法。
sorted()方法按照元素的“自然顺序”对它们进行排序;sorted()方法要求流中的元素实现Comparable接口。如何以其他顺序对元素进行排序?为此,您可以调用以Comparator作为参数的重载排序方法:
Stream<T> sorted(Comparator<? super T> comparator)
这里(清单 6-8 )是早期程序(在清单 6-7 中)的修改版本,它根据字符串的长度对元素进行排序。
Listing 6-8. SortByLength.java
import java.util.Arrays;
import java.util.List;
import java.util.Comparator;
public class SortByLength {
public static void main(String []args) {
List words =
Arrays.asList("follow your heart but take your brain with you".split(" "));
Comparator<String> lengthCompare = (str1, str2) -> str1.length() - str2.length();
words.stream().distinct().sorted(lengthCompare).forEach(System.out::println);
}
}
该程序打印:
but
you
your
take
with
heart
brain
follow
在这个输出中,单词根据单词的长度进行排序。“心”这个词出现在“脑”之前,尽管它们的长度相同。那么,如果我们想先把单词按长度排序,然后再把同样长度的单词按自然顺序排序呢?为此,您可以使用Comparator接口中提供的thenComparing()默认方法(列表 6-9 )。
Listing 6-9. SortByLengthThenNatural.java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class SortByLengthThenNatural {
public static void main(String []args) {
List words =
Arrays.``asList
Comparator<String> lengthCompare = (str1, str2) -> str1.length() - str2.length();
words.stream()
.distinct()
.sorted(lengthCompare.thenComparing(String::compareTo))
.forEach(System.``out
}
}
该程序打印:
but
you
take
with
your
brain
heart
follow
如果我们想颠倒这个顺序呢?幸运的是,在 Java 8 中,Comparator接口已经通过许多有用的默认和静态方法得到了增强。添加的一个这样的方法是reversed(),您可以利用它(清单 6-10 )。
Listing 6-10. SortByLengthThenNaturalReversed.java
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class SortByLengthThenNaturalReversed {
public static void main(String []args) {
List words =
Arrays.asList("follow your heart but take your brain with you".split(" "));
Comparator<String> lengthCompare = (str1, str2) -> str1.length() - str2.length();
words.stream()
.distinct()
.sorted(lengthCompare.thenComparing(String::compareTo).reversed())
.forEach(System.out::println);
}
}
该程序打印:
follow
heart
brain
your
with
take
you
but
将结果保存到集合
| 认证目标 |
|---|
| 使用 collect 方法将结果保存到集合中,使用 Collectors 类将数据分组/分区 |
Collectors类具有支持将元素收集到集合中的任务的方法。您可以使用toList()、toSet()、toMap()和toCollection()等方法从流中创建一个集合。下面是一个简单的例子,它从一个流中创建一个List并返回它(清单 6-11 )。这段代码使用了Stream的collect()方法和Collectors类的toList()方法。
Listing 6-11. CollectorsToList.java
import java.util.stream.Collectors;
import java.util.regex.Pattern;
import java.util.List;
public class CollectorsToList {
public static void main(String []args) {
String frenchCounting = "un:deux:trois:quatre";
List gmailList = Pattern.compile(":")
.splitAsStream(frenchCounting)
.collect(Collectors.toList());
gmailList.forEach(System.out::println);
}
}
The collect() method in Stream takes a Collector as an argument:
<R, A> R collect(Collector<? super T, A, R> collector);
在这段代码中,我们使用了 Collectors 类中的toList()方法将流中的元素收集到一个列表中。
这里有一个使用Collectors.toSet()方法的例子(列表 6-12 ):
Listing 6-12. CollectorsToSet.java
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class CollectorsToSet {
public static void main(String []args) {
String []roseQuote = "a rose is a rose is a rose".split(" ");
Set words = Arrays.stream(roseQuote).collect(Collectors.toSet());
words.forEach(System.out::println);
}
}
该程序打印:
a
rose
is
这段代码将字符串中的给定句子转换成单词流。在collect()方法中调用的Collectors.toSet()方法将单词收集到一个Set中。由于 a Set删除了重复项,这个程序只将单词“a”、“rose”和“is”打印到控制台。
就像Lists和Sets一样,你也可以从流中创建Maps。下面是一个从字符串流中创建一个Map的程序(列表 6-13 )。
Listing 6-13. CollectorsToMap.java
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class CollectorsToMap {
public static void main(String []args) {
Map<String, Integer> nameLength = Stream.of("Arnold", "Alois", "Schwarzenegger")
.collect(Collectors.toMap(name -> name, name -> name.length()));
nameLength.forEach((name, len) -> System.out.printf("%s - %d \n", name, len));
}
}
该程序打印:
Alois - 5
Schwarzenegger - 14
Arnold - 6
Collectors.toMap()方法有两个参数——第一个是键,第二个是值。这里,我们使用流本身中的元素作为键,字符串的长度作为值。你有没有注意到字符串“阿诺德”、“阿洛伊斯”和“施瓦辛格”在流中的顺序没有保留?这是因为Map没有保持元素的插入顺序。
在这段代码中,注意我们使用了name -> name:
Collectors.toMap(name -> name, name -> name.length())
我们可以通过传递Function.identity()来简化它,比如:
Collectors.toMap(Function.identity(), name -> name.length())
回想一下Function接口中的identity()方法返回它接收到的参数(在第五章中讨论)。
如果您想使用一个特定的集合——比如说TreeSet——来聚合来自collect()方法的元素,该怎么办呢?为此,您可以使用Collections.toCollection()方法,并将TreeSet的构造函数引用作为参数传递(清单 6-14 )。
Listing 6-14. CollectorsToTreeSet.java
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
public class CollectorsToTreeSet {
public static void main(String []args) {
String []roseQuote = "a rose is a rose is a rose".split(" ");
Set words = Arrays.stream(roseQuote).collect(Collectors.toCollection(TreeSet::new));
words.forEach(System.out::println);
}
}
该程序打印:
a
is
rose
记住,TreeSet对元素排序,因此输出是有序的。
您还可以根据某些标准对流中的元素进行分组(清单 6-15 )。
Listing 6-15. GroupStringsByLength.java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class GroupStringsByLength {
public static void main(String []args) {
String []string= "you never know what you have until you clean your room".split(" ");
Stream<String> distinctWords = Arrays.stream(string).distinct();
Map<Integer, List<String>> wordGroups =
distinctWords.collect(Collectors.groupingBy(String::length));
wordGroups.forEach(
(count, words) -> {
System.out.printf("word(s) of length %d %n", count);
words.forEach(System.out::println);
});
}
}
该程序打印:
word(s) of length 3
you
word(s) of length 4
know
what
have
your
room
word(s) of length 5
never
until
clean
Collectors类中的groupingBy()方法将一个Function作为参数。它使用函数的结果返回一个Map。Map对象由匹配元素的Function和List返回的值组成。
如果你想把较长的单词和较小的单词分开呢?为此,您可以使用Collectors类中的partitioningBy()方法(清单 6-16 )。分区方法以一个Predicate作为参数。
Listing 6-16. PartitionStrings.java
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class PartitionStrings {
public static void main(String []args) {
String []string= "you never know what you have until you clean your room".split(" ");
Stream<String> distinctWords = Arrays.``stream
Map<Boolean, List<String>> wordBlocks =
distinctWords.collect(Collectors.``partitioningBy
System.``out
System.``out
}
}
该程序打印:
Short words (len <= 4): [you, know, what, have, your, room]
Long words (len > 4): [never, until, clean]
在partitioningBy()方法中,我们已经给出了条件str -> str.length() > 4。现在,结果将被分成两部分:一部分包含针对此条件评估为 true 的元素,另一部分评估为 false。在这种情况下,我们使用了partitioningBy()方法将单词分为小单词(带有length <= 4的单词)和长单词(带有length > 4的单词)。
方法
groupingBy()和partitioningBy()有什么不同?groupingBy()方法采用一个分类函数(类型为Function),并基于分类函数返回输入元素及其匹配条目(并将结果组织在一个Map<K, List<T>>)。partitioningBy()方法将一个Predicate作为参数,并根据给定的Predicate将条目分类为真和假(并将结果组织在一个Map<Boolean, List<T>>中)。
在流中使用平面图方法
| 认证目标 |
|---|
| 在流 API 中使用 flatMap()方法 |
在前面的程序中,我们在调用split()方法后发现字符串中有不同的单词:
String []string= "you never know what you have until you clean your room".split(" ");
Stream<String> distinctWords = Arrays.``stream
如果我们想在句子中找到不同的(独特的)字符呢?这个代码怎么样,有用吗?
String []string= "you never know what you have until you clean your room".split(" ");
Arrays.stream(string)
.map(word -> word.split(""))
.distinct()
.forEach(System.out::print);
这段代码打印出类似这样的乱码:
Ljava.lang.String;@5f184fc6[Ljava.lang.String;@3feba861[Ljava.lang.String;@5b480cf9[
为什么呢?因为word.split()返回一个String[]并且distinct()删除重复的引用。因为流中的元素是类型String[],所以forEach() prints 调用默认的toString()实现来打印一些人类不可读的东西。
解决这个问题的一个方法是在word.split("")上再次使用Arrays.stream(),并将结果流转换成单独的条目(即“展平”流),如:flatMap(word -> Arrays.stream(word.split("")))。经过这一修改,下面是打印句子中唯一字符的程序(清单 6-17 )。
Listing 6-17. UniqueCharacters.java
import java.util.Arrays;
public class UniqueCharacters {
public static void main(String []args) {
String []string= "you never know what you have until you clean your room".split(" ");
Arrays.stream(string)
.flatMap(word -> Arrays.stream(word.split("")))
.distinct()
.forEach(System.out::print);
}
}
该程序可以正确打印:
younevrkwhatilcm
让我们讨论一个例子,它清楚地说明了map()和flatMap()方法之间的区别(清单 6-18 和 6-19 )。
Listing 6-18. UsingMap.java
import java.util.Arrays;
import java.util.List;
public class UsingMap {
public static void main(String []args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
integers.stream()
.map(i -> i * i)
.forEach(System.out::println);
}
}
该程序打印:
1
4
9
16
25
在这个程序中,我们有一个值为 1 到 5 的List<Integer>。既然有了Integer个元素,就可以直接调用map()方法,将元素转换成它们的平方值(见图 6-3 )。
图 6-3。
The map() method transforms elements in a stream
现在,如果我们有一个List<Integer>的List,事情就变得困难了,如清单 6-19 所示。
Listing 6-19. UsingFlatMap.java
import java.util.Arrays;
import java.util.List;
public class UsingFlatMap {
public static void main(String []args) {
List<List<Integer>> intsOfInts = Arrays.asList(
Arrays.asList(1, 3, 5),
Arrays.asList(2, 4));
intsOfInts.stream()
.flatMap(ints -> ints.stream())
.sorted()
.map(i -> i * i)
.forEach(System.out::println);
}
}
该程序的输出与之前的程序相同(列表 6-18 )。它还打印值 1 到 5 的平方。
在这个程序中,我们有一个变量intsOfInts,它是List<Integer>的一个List。当你在intsOfInts上调用stream()方法时,元素的类型会是什么?会是List<Integer>。我们如何处理List<Integer>中的元素?为此,一种方法是对它的每个元素调用stream()方法。为了将这些流转换成Integer元素,我们调用flatMap()方法。在调用了flatMap()之后,我们有了一个Integer的流。我们现在可以执行像sorted()和map()这样的操作来处理或转换这些元素。图 6-4 直观显示了Stream中map()与flatMap()方法的区别。
图 6-4。
The flatMap() method flattens the streams
flatMap()方法像map()方法一样对元素进行操作。然而,flatMap()将每个元素映射到一个平面流中得到的流变平。
摘要
让我们简要回顾一下本章中每个认证目标的要点。请在参加考试之前阅读它。
开发代码以使用 peek()和 map()方法从对象中提取数据,包括 map()方法的原始版本
peek()方法对于调试很有用:它帮助我们理解元素在管道中是如何转换的。- 您可以使用
map()方法转换(或者只是提取)流中的元素
使用流类的搜索方法搜索数据,包括 findFirst、findAny、anyMatch、allMatch、noneMatch
- 您可以使用
allMatch()、noneMatch()和anyMatch()方法匹配流中的给定谓词。与流为空时返回 false 的anyMatch()方法不同,allMatch()和noneMatch()方法在流为空时返回 true。 - 您可以使用
findFirst()和findAny()方法在流中寻找元素。在并行流的情况下,findAny()方法比findFirst()方法更快。 - “
match”和“find”方法“短路”:一旦找到结果,求值就停止,流的其余部分不求值。
开发使用可选类的代码
- 当流中没有条目并且调用了 max 等操作时,Java 8 中采用的(更好的)方法是返回
Optional值,而不是返回null或抛出异常。 int、long和double的Optional<T>的原始类型版本分别为OptionalInteger、OptionalLong和OptionalDouble。
开发使用流数据方法和计算方法的代码
Stream<T>接口有数据和计算方法count()、min()和max();当调用这些min()和max()方法时,需要传递一个Comparator对象作为参数。- 流接口的原始类型版本有以下数据和计算方法:
count()、sum()、average()、min()、max()。 IntStream、LongStream和DoubleStream中的summaryStatistics()方法具有计算流中元素的计数、总和、平均值、最小值和最大值的方法。
使用流 API 对集合进行排序
- 对集合进行排序的一种方式是从集合中获取一个流,并对该流调用
sorted()方法。sorted()方法按照自然顺序对流中的元素进行排序(它要求流元素实现Comparable接口)。 - 当您想要对流中的元素进行排序而不是自然顺序时,您可以将一个
Comparator对象传递给sorted()方法。 - 在 Java 8 中,
Comparator接口已经通过许多有用的静态或默认方法得到了增强,比如thenComparing()和reversed()方法。
使用 collect 方法将结果保存到集合中,使用 Collectors 类将数据分组/分区
Collectors类的collect()方法具有支持将元素收集到集合中的任务的方法。Collectors类提供了诸如toList()、toSet()、toMap()和toCollection()之类的方法来从流中创建集合。- 您可以使用
Collectors.groupingBy()方法将流中的元素分组,并将分组标准(以Function的形式给出)作为参数传递。 - 您可以使用
Collectors类中的partition()方法,根据条件(作为Predicate给出)来分离流中的元素。。
使用流 API 的 flatMap()方法
Stream中的flatMap()方法将每个元素映射到一个平面流中所产生的流变平。
Question TimeChoose the best option based on this code segment: "abracadabra".chars().distinct().peek(ch -> System. out .printf("%c ", ch)).sorted(); It prints: “a b c d r” It prints: “a b r c d” It crashes by throwing a java.util.IllegalFormatConversionException This code segment terminates normally without printing any output in the console Choose the best option based on this program: import java.util.function.IntPredicate; import java.util.stream.IntStream; public class MatchUse { public static void main(String []args) { IntStream temperatures = IntStream.of(-5, -6, -7, -5, 2, -8, -9); IntPredicate positiveTemperature = temp -> temp > 0; // #1 if(temperatures.anyMatch(positiveTemperature)) { // #2 int temp = temperatures .filter(positiveTemperature) .findAny() .getAsInt(); // #3 System.out.println(temp); } } } This program results in a compiler error in line marked with comment #1 This program results in a compiler error in line marked with comment #2 This program results in a compiler error in line marked with comment #3 This program prints: 2 This program crashes by throwing java.lang.IllegalStateException Choose the best option based on this program: import java.util.stream.Stream; public class AllMatch { public static void main(String []args) { boolean result = Stream.of("do", "re", "mi", "fa", "so", "la", "ti") .filter(str -> str.length() > 5) // #1 .peek(System.out::println) // #2 .allMatch(str -> str.length() > 5); // #3 System.out.println(result); } } This program results in a compiler error in line marked with comment #1 This program results in a compiler error in line marked with comment #2 This program results in a compiler error in line marked with comment #3 This program prints: false This program prints the strings “do”, “re”, “mi”, “fa”, “so”, “la”, “ti”, and “false” in separate lines This program prints: true Choose the best option based on this program: import java.util.*; class Sort { public static void main(String []args) { List<String> strings = Arrays.asList("eeny ", "meeny ", "miny ", "mo "); Collections.sort(strings, (str1, str2) -> str2.compareTo(str1)); strings.forEach(string -> System.out.print(string)); } } Compiler error: improper lambda function definition This program prints: eeny meeny miny mo This program prints: mo miny meeny eeny This program will compile fine, and when run, will crash by throwing a runtime exception. Choose the best option based on this program: import java.util.regex.Pattern; import java.util.stream.Stream; public class SumUse { public static void main(String []args) { Stream<String> words = Pattern.compile(" ").splitAsStream("a bb ccc"); System.out.println(words.map(word -> word.length()).sum()); } } Compiler error: Cannot find symbol “sum” in interface Stream This program prints: 3 This program prints: 5 This program prints: 6 This program crashes by throwing java.lang.IllegalStateException Choose the best option based on this program: import java.util.OptionalInt; import java.util.stream.IntStream; public class FindMax { public static void main(String args[]) { maxMarks(IntStream.of(52,60,99,80,76)); // #1 } public static void maxMarks(IntStream marks) { OptionalInt max = marks.max(); // #2 if(max.ifPresent()) { // #3 System.out.print(max.getAsInt()); } } } This program results in a compiler error in line marked with comment #1 This program results in a compiler error in line marked with comment #2 This program results in a compiler error in line marked with comment #3 This program prints: 99 Choose the best option based on this program: import java.util.Optional; import java.util.stream.Stream; public class StringToUpper { public static void main(String args[]){ Stream.of("eeny ","meeny ",null).forEach(StringToUpper::toUpper); } private static void toUpper(String str) { Optional <String> string = Optional.ofNullable(str); System.out.print(string.map(String::toUpperCase).orElse("dummy")); } } This program prints: EENY MEENY dummy This program prints: EENY MEENY DUMMY This program prints: EENY MEENY null This program prints: Optional[EENY] Optional[MEENY] Optional[dummy] This program prints: Optional[EENY] Optional[MEENY] Optional[DUMMY]
答案:
D. This code segment terminates normally without printing any output in the console. A stream pipeline is lazily evaluated. Since there is no terminal operation provided (such as count(), forEach(), reduce(), or collect()), this pipeline is not evaluated and hence the peek() method does not print any output to the console. E. This program crashes by throwing java.lang.IllegalStateException A stream once used–i.e., once “consumed”–cannot be used again. In this program, anyMatch() is a terminal operation. Hence, once anyMatch() is called, the stream in temperatures is considered “used” or “consumed”. Hence, calling findAny() terminal operation on temperatures results in the program throwing java.lang.IllegalStateException. F. This program prints: true The predicate str -> str.length() > 5 returns false for all the elements because the length of each string is 2. Hence, the filter() method results in an empty stream and the peek() method does not print anything. The allMatch() method returns true for an empty stream and does not evalute the given predicate. Hence this program prints true. C. This program prints: mo miny meeny eeny This is a proper definition of a lambda expression. Since the second argument of Collections.sort() method takes the functional interface Comparator and a matching lambda expression is passed in this code. Note that second argument is compared with the first argument in the lambda expression (str1, str2) -> str2.compareTo(str1). For this reason, the comparison is performed in descending order. A. Compiler error: Cannot find symbol “sum” in interface Stream<Integer> Data and calculation methods such as sum() and average() are not available in the Stream<T> interface; they are available only in the primitive type versions IntStream, LongStream, and DoubleStream. To create an IntStream, one solution is to use mapToInt() method instead of map() method in this program. If mapToInt() were used, this program would compile without errors, and when executed, it will print 6 to the console. C. This program results in a compiler error in line marked with comment #3 The ifPresent() method in Optional takes a Consumer<T> as the argument. This program uses ifPresent() without passing an argument and hence it results in a compiler error. If the method isPresent() were used instead of ifPresent() in this program, it will compile cleanly and print 99 on the console. A. This program prints: EENY MEENY dummy Note that the variable string points to Optional.ofNullable(str). When the element null is encountered in the stream, it cannot be converted to uppercase and hence the orElse() method executes to return the string “dummy”. In this program, if Optional.of(str) were used instead of Optional.ofNullable(str) the program would have resulted in throwing a NullPointerException.