1. 行为参数化
在 Java 8 之前,能作为参数进行传递的只能是一个确切类型的变量,比如:基本数据类型的变量,或者某个对象的引用变量。在面向对象程序设计中,对象主要包含两部分,属性和行为,在此,行为可以理解为就是对象中的方法。
Java 8 中的行为参数化,做到了可以把方法在调用链上进行传递。行为参数化是一个很有用的模式,它能够轻松地适应不断变化的需求。
对集合进行排序是一个常见的编程任务。同一个集合列表,在不同的需求下可能排序规则不同,将排序行为参数化就可以应对这种变化的需求。
List 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
@FunctionalInterface
public interface Predicate{
boolean test(T t);
}
Consumer 接口
java.util.function.Consumer
FunctionalInterface
public interface Consumer{
void accept(T t);
}
Function 接口
java.util.function.Function
@FunctionalInterface
public interface Function{
R apply(T t);
}
Java 类型要么是引用类型(比如 Byte、Integer、Object、List),要么是原始类型(比如 int、double、byte、char)。但是泛型(比如 Consumer
但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。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 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 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 any = Arrays.asList("a", "b", "c", "d", "z").stream().findAny();
3.3.4 查找第一个元素
有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由 List
或排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。为此有一个 findFirst
方法,它的工作方式类似于 findany。
Optional 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 max = numbers.stream().reduce(Integer::max);
Optional 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 stream = Arrays.asList("a", "b", "c", "d", "z").stream();
3.5.2 由值创建流
使用静态方法 Stream.of,通过显式值创建一个流。它可以接受任意数量的参数。
Stream 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 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 异步执行任务
supplyAsync 方法接受一个生产者(Supplier)作为参数,返回一个 CompletableFuture 对象,该对象完成异步执行后会读取生产者方法的执行结果。生产者方法会交由 ForkJoinPool 池中的某个执行线程(Executor)运行,也可以使用 supplyAsync 方法的重载版本,传递第二个参数指定不同的执行线程执行生产者方法。
CompletableFuture.supplyAsync(this::sendMsg);
默认都使用固定数目的线程,具体线程数取决 Runtime.getRuntime().availableProcessors() 的返回值。
CompletableFuture.supplyAsync(this::sendMsg, Executors.newFixedThreadPool(10));
5.2 增加回调
CompletableFuture.supplyAsync(this::sendMsg)
.thenAccept(this::notify);
使用 thenAccept 方法实现异步任务的回调,接收一个 Consumer 函数式接口。
5.3 链式回调
如果想继续将值从一个回调传递到另一个回调,thenAccept 方法做不到,因为不会返回值。这个时候可以是用 thenApply 方法。
CompletableFuture.supplyAsync(this::findReceiver)
.thenApply(this::sendMsg)
.thenAccept(this::notify);
5.4 thenCompose 方法
假设我们有一个 sendMsgAsync 返回 CompletionStage,如果继续使用 thenApply 最终会得到嵌套 CompletionStage。
CompletableFuture.supplyAsync(this::findReceiver)
.thenApply(this::sendMsgAsync);
// Returns type CompletionStage>
上面嵌套的 CompletionStage 是我们不希望的,这个时候可以使用 thenCompose 方法。
CompletableFuture.supplyAsync(this::findReceiver)
.thenCompose(this::sendMsgAsync);
// Returns type CompletionStage
5.5 独立线程执行回调
async 后缀的回调方法可以将回调提交给 ForkJoinPool.commonPool() 来使用独立的线程执行,而不是使用与前任相同的线程。
假设我们想一次向同一个接收者发送两条消息。
CompletableFuture receiver
= CompletableFuture.supplyAsync(this::findReceiver);
receiver.thenApply(this::sendMsg);
receiver.thenApply(this::sendOtherMsg);
在上面的示例中,所有内容都将在同一个线程上执行。这导致最后一条消息等待第一条消息完成。
通过使用 async 后缀,每条消息都作为单独的任务提交给 ForkJoinPool.commonPool()。这样可以实现在计算完成后异步执行两个回调。当有多个回调依赖于同一个计算时,异步版本会很方便。
CompletableFuture receiver
= CompletableFuture.supplyAsync(this::findReceiver);
receiver.thenApplyAsync(this::sendMsg);
receiver.thenApplyAsync(this::sendMsg);
5.6 异常处理
exceptionally 方法可以实现如果前面的计算因异常而失败,可以对失败进行降级处理,返回失败替代结果。这样,后续回调可以继续以替代结果作为输入。
CompletableFuture.supplyAsync(this::failingMsg)
.exceptionally(ex -> new Result(Status.FAILED))
.thenAccept(this::notify);
5.7 回调取决于多个计算
有时,能够创建一个依赖于两次计算结果的回调会非常有帮助。这时可以使用 thenCombine。thenCombine 允许 BiFunction 根据两个 CompletionStages 的结果注册一个回调。
CompletableFuture to =
CompletableFuture.supplyAsync(this::findReceiver);
CompletableFuture text =
CompletableFuture.supplyAsync(this::createContent);
to.thenCombine(text, this::sendMsg);
6. 默认方法
Java 程序的接口是将相关方法按照约定组合到一起的方式。实现接口的类必须为接口中定义的每个方法提供一个实现,或者从父类中继承它的实现。但是,一旦类库的设计者需要更新接口,向其中加入新的方法,这种方式就会出现问题。
Java 8 中的接口支持在声明方法的同时提供实现,通过两种方式可以完成这种操作。其一,Java 8 允许在接口内声明静态方法。其二,Java 8 引入了一个新功能,叫默认方法,通过默认方法你可以指定接口方法的默认实现。换句话说,接口能提供方法的具体实现。因此,实现接口的类如果不显式地提供该方法的具体实现,就会自动继承默认的实现。比如:Collection 接口中的 stream 方法:
default Stream stream() {
return StreamSupport.stream(spliterator(), false);
}
默认方法由 default 修饰符修饰,并像类中声明的其他方法一样包含方法体。
参考
《Java 8 实战》
https://www.deadcoderising.co...