1. 行为参数化
在 Java 8 之前,能作为参数进行传递的只能是一个确切类型的变量,比如:基本数据类型的变量,或者某个对象的引用变量。在面向对象程序设计中,对象主要包含两部分,属性和行为,在此,行为可以理解为就是对象中的方法。
Java 8 中的行为参数化,做到了可以把方法在调用链上进行传递。行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。
对集合进行排序是一个常见的编程任务。同一个集合列表,在不同的需求下可能排序规则不同,将排序行为参数化就可以应对这种变化的需求。
List<Integer> list = new ArrayList<>();
Collections.sort(list, (o1, o2) -> o1.compareTo(o2)); // 升序
Collections.sort(list, (o1, o2) -> o2.compareTo(o1)); // 倒序
2. Lambda表达式
Lambda 表达式可以理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。
匿名
—— 因为它不像普通的方法那样有一个明确的名称。
函数
—— 因为 Lambda 函数不像方法那样属于某个特定的类。但和方法一样,Lambda 有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
传递
—— Lambda 表达式可以作为参数传递给方法或存储在变量中。
简洁
—— 无需像匿名类那样写很多模板代码。
2.1 语法规则
(parameters) -> expression
或
(parameters) -> { statements; }
2.2 函数式接口
一般在函数式接口上使用 Lambda 表达式,函数式接口就是只定义一个抽象方法的接口,比如:Runnable 接口的定义:
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
2.3 函数描述符
函数式接口的抽象方法叫作函数描述符。在使用 Lambda 表达式的时候,Lambda 表达式的签名必须和函数式接口的函数描述符匹配才能正常使用。比如:
() -> void 代表了参数列表为空,且返回 void 的抽象方法。这正是 Runnable 函数式接口中的抽象方法。所以就可以在使用 Runnable 的地方使用这个 Lambda 表达式。
在 Java 8 的 API 中,函数式接口带有 @FunctionalInterface 的标注。这个标注用于表示该接口会设计成一个函数式接口。
2.4 付诸实践
从文件中一次读取一行内容,代码模板:
public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine(); // 功能代码
}
}
如果一次读取两行内容,代码模板:
public static String processFile() throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return br.readLine() + br.readLine(); // 功能代码
}
}
把 processFile 的行为参数化。然后需要一种方法把行为作为参数传递给 processFile ,以便它可以利用BufferedReader 执行不同的行为。
第1步:定义参数化行为
String result = processFile((BufferedReader br) -> br.readLine() + br.readLine());
(BufferedReader br) -> br.readLine() + br.readLine() 就是要定义的行为。
第2步:定义函数式接口
@FunctionalInterface
public interface BufferedReaderProcessor {
String process(BufferedReader b) throws IOException;
}
函数式接口用来传递参数化的行为。
第3步:定义行为如何执行
public static String processFile(BufferedReaderProcessor p) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
return p.process(br); // 功能代码
}
}
第4步:传递 Lambda 表达式
String oneLine = processFile((BufferedReader br) -> br.readLine());
String twoLine = processFile((BufferedReader br) -> br.readLine() + br.readLine());
通过传递不同的 Lambda 重用 processFile 方法,并以不同的方式处理文件。
2.5 内置的函数式接口
上面展示了如何利用函数式接口来传递 Lambda ,但需要自己定义函数式接口才能使用。Java 8 中已经提供了一些常见的函数式接口,可以重用它们来传递不同的 Lambda,如果有现成的函数式接口,那么就不用我们自己定义并创建接口类了。
Predicate 接口
java.util.function.Predicate<T> 接口定义了一个名叫 test 的抽象方法,它接受泛型 T 对象,并返回一个 boolean 值。
@FunctionalInterface
public interface Predicate<T>{
boolean test(T t);
}
Consumer 接口
java.util.function.Consumer<T> 定义了一个名叫accept 的抽象方法,它接受泛型T 的对象,没有返回(void)。
FunctionalInterface
public interface Consumer<T>{
void accept(T t);
}
Function 接口
java.util.function.Function<T, R> 接口定义了一个叫作 apply 的方法,它接受一个泛型 T 的对象,并返回一个泛型 R 的对象。
@FunctionalInterface
public interface Function<T, R>{
R apply(T t);
}
Java 类型要么是引用类型(比如 Byte、Integer、Object、List),要么是原始类型(比如 int、double、byte、char)。但是泛型(比如 Consumer<T> 中的 T )只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此,在 Java 里有一个将原始类型转换为对应的引用类型的机制。这个机制叫作装箱 (boxing)。相反的操作,也就是将引用类型转换为对应的原始类型,叫作拆箱 (unboxing)。Java 还有一个自动装箱机制来帮助程序员执行这一任务:装箱和拆箱操作是自动完成的。
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。Java 8 为我们前面所说的函数式接口带来了专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如:IntPredicate。其他的还有 DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction 等,这里就不一一举例子了。下面是 Java 8 提供的常用的函数式接口:
3. 流式数据处理
Java 8 中提供了流,用来实现对集合的声明式操作,就像数据库语言 SQL 那样,只需要指定具体的需求,比如:查找,分组等。可以把流看成遍历数据集的高级迭代器。此外,流还可以透明地
并行处理,无需写任何多线程代码等。流的使用一般包括三件事:
- 一个数据源(如集合)来执行一个查询;
- 一个中间操作链,形成一条流的流水线;
- 一个终端操作,执行流水线,并能生成结果。
3.1 筛选和切片
3.1.1 用谓词筛选
Streams 接口支持 filter 方法。该操作会接受一个谓词(一个返回 boolean 的函数)作为参数,并返回一个包括所有符合谓词的元素的流。
Arrays.asList("a", "b", "c", "z", "d").stream().filter(s -> s.equals("a")).forEach(System.out::println);
3.1.2 筛选去重元素
流还支持一个叫作 distinct 的方法,它会返回一个元素各异(根据流所生成元素的 hashCode 和 equals 方法实现)的流。
Arrays.asList(1, 2, 1, 3, 3, 2, 4).stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);
3.1.3 截短流
流支持 limit(n) 方法,该方法会返回一个不超过给定长度的流。
Arrays.asList("ab", "bc", "cd").stream().filter(s -> s.length() > 1).limit(2).forEach(System.out.println);
3.1.4 跳过元素
流还支持 skip(n) 方法,返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。
Arrays.asList("a", "b", "c", "d", "e", "f").stream().skip(10).forEach(System.out.println);
3.2 映射
一个非常常见的数据处理需求就是从某些对象中选择属性信息。就像在 SQL 里,可以从表中选择一列。Stream 也通过 map 和 flatMap 方法提供了类似的工具。
3.2.1 对元素应用函数
流支持 map 方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是 “创建一个新版本” 而不是去 “修改” )。
Arrays.asList("aaa", "bb", "c", "zzzz", "dd").stream().map(String::lengh).forEach(System.out.println);
3.2.2 将多个流融合
对字符串数组 ["ab", "bc", "cd", "zd", "dz"] 进行字符去重输出,输出结果为 a b c d z
使用 map 方法:
Stream<String[]> mapStream = Arrays.asList("ab", "bc", "cd", "zd", "dz").stream().map(s -> s.split(""));
mapStream.distinct().forEach(System.out::println);
输出结果:
[Ljava.lang.String;@7ba4f24f
[Ljava.lang.String;@3b9a45b3
[Ljava.lang.String;@7699a589
[Ljava.lang.String;@58372a00
[Ljava.lang.String;@4dd8dc3
未能达到预期结果。
使用 flatMap 方法:
Stream<String> flatMapStream = Arrays.asList("ab", "bc", "cd", "zd", "dz").stream().flatMap(s -> Stream.of(s.split("")));
flatMapStream.distinct().forEach(System.out::println);
输出结果:
a
b
c
d
z
达到预期输出结果。
总结,flatmap 方法可以把一个流中的每个值都换成一个流,然后把所有的流连接起来成为一个流。通过上面 map 方法 和 flatMap 方法能够比较直观的看到这一点。
3.3 查找和匹配
表达数据集中的某些元素是否匹配一个给定的属性。Stream API 通过 allMatch、anyMatch、 noneMatch、findFirst 和 findAny 方法提供了这样的实现。
3.3.1 检查谓词是否至少匹配一个元素
anyMatch 方法可以表达 “流中是否有一个元素能匹配给定的谓词” 。
boolean b = Arrays.asList("a", "b", "c", "d", "z").stream().anyMatch(e -> e.equals("b"));
b 的结果是 true。
3.3.3 检查谓词是否匹配所有元素
allMatch 方法的工作原理和 anyMatch 类似,但它会看看流中的元素是否都能匹配给定的谓词。
boolean b = Arrays.asList("a", "b", "c", "d", "z").stream().allMatch(e -> e.equals("b"));
b 的结果是 false。
和 allMatch 相对的是 noneMatch 。它可以确保流中没有任何元素与给定的谓词匹配。
boolean b = Arrays.asList("a", "b", "c", "d", "z").stream().noneMatch(e -> e.equals("f"));
b 的结果是 true。
anyMatch、allMatch 和 noneMatch 这三个操作都用到了我们所谓的短路,这就是大家熟悉的 Java 中 && 和 || 运算符短路在流中的版本。
3.3.3 查找任意一个元素
findAny 方法将返回当前流中的任意元素。
Optional<String> any = Arrays.asList("a", "b", "c", "d", "z").stream().findAny();
3.3.4 查找第一个元素
有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由 List
或排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个 findFirst
方法,它的工作方式类似于 findany。
Optional<String> first = Arrays.asList("a", "b", "c", "d", "z").stream().findFirst();
为什么会同时有 findFirst 和 findAny 呢?答案是并行。找到第一个元素在并行上限制更多,如果不关心返回的元素是哪个,请使用 findAny,因为它在使用并行流时限制较少。
3.4 归约
把一个流中的元素组合起来,使用 reduce 操作来表达更复杂的需求。此操作需要将流中所有元素反复结合起来,得到一个值,比如一个 Integer。这样的操作可以被归类为归约操作(将流归约成一个值)。
3.4.1 元素求和
int sum = numbers.stream().reduce(0, (a, b) -> a + b);
或
int sum = numbers.stream().reduce(0, Integer::sum);
3.4.2 最大值和最小值
Optional<Integer> max = numbers.stream().reduce(Integer::max);
Optional<Integer> min = numbers.stream().reduce(Integer::min);
3.4.3 map 和 reduce 组合
int count = menu.stream().map(d -> 1).reduce(0 , (a, b) -> a + b);
map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名,因为它很容易并行化。
3.5 构建流
3.5.1 由集合构建流
Stream<String> stream = Arrays.asList("a", "b", "c", "d", "z").stream();
3.5.2 由值创建流
使用静态方法 Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。
Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
3.5.3 由数组创建流
int[] numbers = {2, 3, 5, 7, 11, 13};
IntStream stream = Arrays.stream(numbers);
3.5.4 由文件生成流
计算 data.txt 文件中的单词数量。
long uniqueWords = 0;
try (Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset())) {
uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
.distinct()
.count();
} catch (IOException e) {
}
System.out.println(uniqueWords);
4. 并行流
Stream 接口可以通过调用 parallelStream 方法来把集合转换为并行流,或者对顺序流使用 parallel 方法来把其转化为并行流。并行流是一个把内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,就可以自动把给定操作的工作负荷分配给多核处理器的所有内核,让它们都忙起来。
对顺序流使用 parallel 方法:
Stream.of("AAA", "BBB", "CCC").parallel().forEach(s -> System.out.println("Output:" + s));
对收集源使用 parallelStream 方法:
Arrays.asList("AAA", "BBB", "CCC").parallelStream().forEach(s -> System.out.println("Output:" + s));
5. CompletableFuture:组合式异步编程
5.1 异步执行任务
runAsync 方法接收一个消费者 Consumer 作为参数,Consumer 会交由 ForkJoinPool 池中的某个执行线程(Executor)运行。
CompletableFuture.runAsync(this::sendMessage);
supplyAsync 方法接受一个 Supplier 作为参数,返回一个 CompletableFuture 对象。调用 CompletableFuture 对象的 get 方法可以获取计算结果的值,Supplier 会交由 ForkJoinPool 池中的某个执行线程(Executor)运行。
CompletableFuture<Integer> result = CompletableFuture.supplyAsync(this::calculate);
Integer value = result.get();
5.2 增加回调
CompletableFuture.supplyAsync(this::calculate)
.thenAccept(this::sendMessage);
使用 thenAccept 方法实现 calculate 方法计算结果的回调,接收一个 Consumer 函数式接口。
5.3 链式回调
如果需要将 calculate 的计算结果从一个回调传递到另一个回调,thenAccept 方法做不到,因为它不会返回值。这个时候可以是用 thenApply 方法。
CompletableFuture.supplyAsync(this::calculate)
.thenApply(this::format)
.thenAccept(this::sendMessage);
5.4 thenCompose 方法
假设 calculate 方法返回 CompletionStage,如果继续使用 thenApply 最终会得到嵌套 CompletionStage。
CompletableFuture.supplyAsync(this::calculate)
.thenApply(this::format);
// Returns type CompletionStage<CompletionStage<String>>
上面嵌套的 CompletionStage 是我们不希望的,这个时候可以使用 thenCompose 方法。
CompletableFuture.supplyAsync(this::calculate)
.thenCompose(this::format);
// Returns type CompletionStage<String>
5.5 独立线程执行回调
async 后缀的回调方法可以将回调提交给 ForkJoinPool.commonPool() 来使用独立的线程执行。
假设我们想一次向同一个接收者发送两条消息。
CompletableFuture<String> receiver
= CompletableFuture.supplyAsync(this::findReceiver);
receiver.thenApply(this::sendMsg);
receiver.thenApply(this::sendOtherMsg);
在上面的示例中,sendMsg 和 sendOtherMsg 会使用一个线程执行。sendOtherMsg 必须等待 sendMsg 执行完成后才能执行。
使用 async 后缀的回调方法,sendMsg 和 sendOtherMsg 都作为单独的任务提交给 ForkJoinPool.commonPool()。各自在独立的线程中实现异步回调执行。
CompletableFuture<String> receiver
= CompletableFuture.supplyAsync(this::findReceiver);
receiver.thenApplyAsync(this::sendMsg);
receiver.thenApplyAsync(this::sendOtherMsg);
5.6 异常处理
exceptionally 方法可以实现如果前面的计算因异常而失败,可以对失败进行降级处理,返回失败替代结果。这样,后续回调可以继续以替代结果作为输入。
CompletableFuture.supplyAsync(this::failingMsg)
.exceptionally(ex -> new Result(Status.FAILED))
.thenAccept(this::notify);
5.7 回调取决于多个计算
有时,能够创建一个依赖于两次计算结果的回调会非常有帮助。这时可以使用 thenCombine。thenCombine 允许 BiFunction 根据两个 CompletionStages 的结果注册一个回调。
public static String findUser() {
return "小明";
}
public static String createMessage() {
return "你好!";
}
public static String concat(String user, String content) {
return user + "," + content;
}
public static void main(String[] args) throws Exception {
CompletableFuture<String> user = CompletableFuture.supplyAsync(CompletableFutureTest::findUser);
CompletableFuture<String> message = CompletableFuture.supplyAsync(CompletableFutureTest::createMessage);
CompletableFuture<String> result = user.thenCombine(message, CompletableFutureTest::concat);
System.out.println(result.get());
}
输出结果是:小明,您好!
6. 默认方法
Java 程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现。但是,一旦类库的设计者需要更新接口,向其中加入新的方法,这种方式就会出现问题。
Java 8 中的接口支持在声明方法的同时提供实现,通过两种方式可以完成这种操作。其一,Java 8 允许在接口内声明静态方法。其二,Java 8 引入了一个新功能,叫默认方法,通过默认方法你可以指定接口方法的默认实现。换句话说,接口能提供方法的具体实现。因此,实现接口的类如果不显式地提供该方法的具体实现,就会自动继承默认的实现。比如:Collection 接口中的 stream 方法:
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
默认方法由 default 修饰符修饰,并像类中声明的其他方法一样包含方法体。
参考
《Java 8 实战》
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。