Java8新特性Lambda表达式&Stream流&方法引用最全集锦

6

7

7

++++++++

17

12

20

++++++++

12.27872414236691

11.732085449736195

12.196509449817267

++++++++

为了消除冗余代码,我创建了一个泛型方法 show(Stream<T> stream) (在讲解泛型之前就使用这个特性,确实有点作弊,但是回报是值得的)。类型参数 T 可以是任何类型,所以这个方法对 IntegerLongDouble 类型都生效。但是 Random 类只能生成基本类型 intlongdouble 的流。幸运的是, boxed() 流操作将会自动地把基本类型包装成为对应的装箱类型,从而使得 show() 能够接受流。

我们可以使用 Random 为任意对象集合创建 Supplier。如下是一个文本文件提供字符串对象的例子。

Cheese.dat 文件内容:

// streams/Cheese.dat

Not much of a cheese shop really, is it?

Finest in the district, sir.

And what leads you to that conclusion?

Well, it’s so clean.

It’s certainly uncontaminated by cheese.

我们通过 File 类将 Cheese.dat 文件的所有行读取到 List<String> 中。代码示例:

// streams/RandomWords.java

import java.util.*;

import java.util.stream.*;

import java.util.function.*;

import java.io.*;

import java.nio.file.*;

public class RandomWords implements Supplier {

List words = new ArrayList<>();

Random rand = new Random(47);

RandomWords(String fname) throws IOException {

List lines = Files.readAllLines(Paths.get(fname));

// 略过第一行

for (String line : lines.subList(1, lines.size())) {

for (String word : line.split(“[ .?,]+”))

words.add(word.toLowerCase());

}

}

public String get() {

return words.get(rand.nextInt(words.size()));

}

@Override

public String toString() {

return words.stream()

.collect(Collectors.joining(" "));

}

public static void main(String[] args) throws Exception {

System.out.println(

Stream.generate(new RandomWords(“Cheese.dat”))

.limit(10)

.collect(Collectors.joining(" ")));

}

}

输出结果:

it shop sir the much cheese by conclusion district is

在这里你可以看到更为复杂的 split() 运用。在构造器中,每一行都被 split() 通过空格或者被方括号包裹的任意标点符号进行分割。在结束方括号后面的 + 代表 + 前面的东西可以出现一次或者多次。

我们注意到在构造函数中循环体使用命令式编程(外部迭代)。在以后的例子中,你甚至会看到我们如何消除这一点。这种旧的形式虽不是特别糟糕,但使用流会让人感觉更好。

toString() 和主方法中你看到了 collect() 收集操作,它根据参数来组合所有流中的元素。

当你使用 Collectors.joining(),你将会得到一个 String 类型的结果,每个元素都根据 joining() 的参数来进行分割。还有许多不同的 Collectors 用于产生不同的结果。

在主方法中,我们提前看到了 Stream.generate() 的用法,它可以把任意 Supplier<T> 用于生成 T 类型的流。

int 类型的范围

IntStream 类提供了 range() 方法用于生成整型序列的流。编写循环时,这个方法会更加便利:

// streams/Ranges.java

import static java.util.stream.IntStream.*;

public class Ranges {

public static void main(String[] args) {

// 传统方法:

int result = 0;

for (int i = 10; i < 20; i++)

result += i;

System.out.println(result);

// for-in 循环:

result = 0;

for (int i : range(10, 20).toArray())

result += i;

System.out.println(result);

// 使用流:

System.out.println(range(10, 20).sum());

}

}

输出结果:

145

145

145

在主方法中的第一种方式是我们传统编写 for 循环的方式;第二种方式,我们使用 range() 创建了流并将其转化为数组,然后在 for-in 代码块中使用。但是,如果你能像第三种方法那样全程使用流是更好的。我们对范围中的数字进行求和。在流中可以很方便的使用 sum() 操作求和。

注意 IntStream.range() 相比 onjava.Range.range() 拥有更多的限制。这是由于其可选的第三个参数,后者允许步长大于 1,并且可以从大到小来生成。

实用小功能 repeat() 可以用来替换简单的 for 循环。代码示例:

// onjava/Repeat.java

package onjava;

import static java.util.stream.IntStream.*;

public class Repeat {

public static void repeat(int n, Runnable action) {

range(0, n).forEach(i -> action.run());

}

}

其产生的循环更加清晰:

// streams/Looping.java

import static onjava.Repeat.*;

public class Looping {

static void hi() {

System.out.println(“Hi!”);

}

public static void main(String[] args) {

repeat(3, () -> System.out.println(“Looping!”));

repeat(2, Looping::hi);

}

}

输出结果:

Looping!

Looping!

Looping!

Hi!

Hi!

原则上,在代码中包含并解释 repeat() 并不值得。诚然它是一个相当透明的工具,但结果取决于你的团队和公司的运作方式。

generate()

参照 RandomWords.javaStream.generate() 搭配 Supplier<T> 使用的例子。代码示例:

// streams/Generator.java

import java.util.*;

import java.util.function.*;

import java.util.stream.*;

public class Generator implements Supplier {

Random rand = new Random(47);

char[] letters = “ABCDEFGHIJKLMNOPQRSTUVWXYZ”.toCharArray();

public String get() {

return “” + letters[rand.nextInt(letters.length)];

}

public static void main(String[] args) {

String word = Stream.generate(new Generator())

.limit(30)

.collect(Collectors.joining());

System.out.println(word);

}

}

输出结果:

YNZBRNYGCFOWZNTCQRGSEGZMMJMROE

使用 Random.nextInt() 方法来挑选字母表中的大写字母。Random.nextInt() 的参数代表可以接受的最大的随机数范围,所以使用数组边界是经过深思熟虑的。

如果要创建包含相同对象的流,只需要传递一个生成那些对象的 lambdagenerate() 中:

// streams/Duplicator.java

import java.util.stream.*;

public class Duplicator {

public static void main(String[] args) {

Stream.generate(() -> “duplicate”)

.limit(3)

.forEach(System.out::println);

}

}

输出结果:

duplicate

duplicate

duplicate

如下是在本章之前例子中使用过的 Bubble 类。注意它包含了自己的静态生成器(Static generator)方法。

// streams/Bubble.java

import java.util.function.*;

public class Bubble {

public final int i;

public Bubble(int n) {

i = n;

}

@Override

public String toString() {

return “Bubble(” + i + “)”;

}

private static int count = 0;

public static Bubble bubbler() {

return new Bubble(count++);

}

}

由于 bubbler()Supplier<Bubble> 是接口兼容的,我们可以将其方法引用直接传递给 Stream.generate()

// streams/Bubbles.java

import java.util.stream.*;

public class Bubbles {

public static void main(String[] args) {

Stream.generate(Bubble::bubbler)

.limit(5)

.forEach(System.out::println);

}

}

输出结果:

Bubble(0)

Bubble(1)

Bubble(2)

Bubble(3)

Bubble(4)

这是创建单独工厂类(Separate Factory class)的另一种方式。在很多方面它更加整洁,但是这是一个对于代码组织和品味的问题——你总是可以创建一个完全不同的工厂类。

iterate()

Stream.iterate() 以种子(第一个参数)开头,并将其传给方法(第二个参数)。方法的结果将添加到流,并存储作为第一个参数用于下次调用 iterate(),依次类推。我们可以利用 iterate() 生成一个斐波那契数列。代码示例:

// streams/Fibonacci.java

import java.util.stream.*;

public class Fibonacci {

int x = 1;

Stream numbers() {

return Stream.iterate(0, i -> {

int result = x + i;

x = i;

return result;

});

}

public static void main(String[] args) {

new Fibonacci().numbers()

.skip(20) // 过滤前 20 个

.limit(10) // 然后取 10 个

.forEach(System.out::println);

}

}

输出结果:

6765

10946

17711

28657

46368

75025

121393

196418

317811

514229

斐波那契数列将数列中最后两个元素进行求和以产生下一个元素。iterate() 只能记忆结果,因此我们需要利用一个变量 x 追踪另外一个元素。

在主方法中,我们使用了一个之前没有见过的 skip() 操作。它根据参数丢弃指定数量的流元素。在这里,我们丢弃了前 20 个元素。

流的建造者模式

在建造者设计模式(也称构造器模式)中,首先创建一个 builder 对象,传递给它多个构造器信息,最后执行“构造”。Stream 库提供了这样的 Builder。在这里,我们重新审视文件读取并将其转换成为单词流的过程。代码示例:

// streams/FileToWordsBuilder.java

import java.io.*;

import java.nio.file.*;

import java.util.stream.*;

public class FileToWordsBuilder {

Stream.Builder builder = Stream.builder();

public FileToWordsBuilder(String filePath) throws Exception {

Files.lines(Paths.get(filePath))

.skip(1) // 略过开头的注释行

.forEach(line -> {

for (String w : line.split(“[ .?,]+”))

builder.add(w);

});

}

Stream stream() {

return builder.build();

}

public static void main(String[] args) throws Exception {

new FileToWordsBuilder(“Cheese.dat”)

.stream()

.limit(7)

.map(w -> w + " ")

.forEach(System.out::print);

}

}

输出结果:

Not much of a cheese shop really

注意,构造器会添加文件中的所有单词(除了第一行,它是包含文件路径信息的注释),但是其并没有调用 build()。只要你不调用 stream() 方法,就可以继续向 builder 对象中添加单词。

在该类的更完整形式中,你可以添加一个标志位用于查看 build() 是否被调用,并且可能的话增加一个可以添加更多单词的方法。在 Stream.Builder 调用 build() 方法后继续尝试添加单词会产生一个异常。

Arrays

Arrays 类中含有一个名为 stream() 的静态方法用于把数组转换成为流。我们可以重写 interfaces/Machine.java 中的主方法用于创建一个流,并将 execute() 应用于每一个元素。代码示例:

// streams/Machine2.java

import java.util.*;

import onjava.Operations;

public class Machine2 {

public static void main(String[] args) {

Arrays.stream(new Operations[] {

() -> Operations.show(“Bing”),

() -> Operations.show(“Crack”),

() -> Operations.show(“Twist”),

() -> Operations.show(“Pop”)

}).forEach(Operations::execute);

}

}

输出结果:

Bing

Crack

Twist

Pop

new Operations[] 表达式动态创建了 Operations 对象的数组。

stream() 同样可以产生 IntStreamLongStreamDoubleStream

// streams/ArrayStreams.java

import java.util.*;

import java.util.stream.*;

public class ArrayStreams {

public static void main(String[] args) {

Arrays.stream(new double[] { 3.14159, 2.718, 1.618 })

.forEach(n -> System.out.format("%f ", n));

System.out.println();

Arrays.stream(new int[] { 1, 3, 5 })

.forEach(n -> System.out.format("%d ", n));

System.out.println();

Arrays.stream(new long[] { 11, 22, 44, 66 })

.forEach(n -> System.out.format("%d ", n));

System.out.println();

// 选择一个子域:

Arrays.stream(new int[] { 1, 3, 5, 7, 15, 28, 37 }, 3, 6)

.forEach(n -> System.out.format("%d ", n));

}

}

输出结果:

3.141590 2.718000 1.618000

1 3 5

11 22 44 66

7 15 28

最后一次 stream() 的调用有两个额外的参数。第一个参数告诉 stream() 从数组的哪个位置开始选择元素,第二个参数用于告知在哪里停止。每种不同类型的 stream() 都有类似的操作。

正则表达式

Java 的正则表达式将在字符串这一章节详细介绍。Java 8 在 java.util.regex.Pattern 中增加了一个新的方法 splitAsStream()。这个方法可以根据传入的公式将字符序列转化为流。但是有一个限制,输入只能是 CharSequence,因此不能将流作为 splitAsStream() 的参数。

我们再一次查看将文件处理为单词流的过程。这一次,我们使用流将文件分割为单独的字符串,接着使用正则表达式将字符串转化为单词流。

// streams/FileToWordsRegexp.java

import java.io.*;

import java.nio.file.*;

import java.util.stream.*;

import java.util.regex.Pattern;

public class FileToWordsRegexp {

private String all;

public FileToWordsRegexp(String filePath) throws Exception {

all = Files.lines(Paths.get(filePath))

.skip(1) // First (comment) line

.collect(Collectors.joining(" "));

}

public Stream stream() {

return Pattern

.compile(“[ .,?]+”).splitAsStream(all);

}

public static void

main(String[] args) throws Exception {

FileToWordsRegexp fw = new FileToWordsRegexp(“Cheese.dat”);

fw.stream()

.limit(7)

.map(w -> w + " ")

.forEach(System.out::print);

fw.stream()

.skip(7)

.limit(2)

.map(w -> w + " ")

.forEach(System.out::print);

}

}

输出结果:

Not much of a cheese shop really is it

在构造器中我们读取了文件中的所有内容(跳过第一行注释,并将其转化成为单行字符串)。现在,当你调用 stream() 的时候,可以像往常一样获取一个流,但这次你可以多次调用 stream() 在已存储的字符串中创建一个新的流。这里有个限制,整个文件必须存储在内存中;在大多数情况下这并不是什么问题,但是这损失了流操作非常重要的优势:

  1. 流“不需要存储”。当然它们需要一些内部存储,但是这只是序列的一小部分,和持有整个序列并不相同。

  2. 它们是懒加载计算的。

幸运的是,我们稍后就会知道如何解决这个问题。

中间操作


中间操作用于从一个流中获取对象,并将对象作为另一个流从后端输出,以连接到其他操作。

跟踪和调试

peek() 操作的目的是帮助调试。它允许你无修改地查看流中的元素。代码示例:

// streams/Peeking.java

class Peeking {

public static void main(String[] args) throws Exception {

FileToWords.stream(“Cheese.dat”)

.skip(21)

.limit(4)

.map(w -> w + " ")

.peek(System.out::print)

.map(String::toUpperCase)

.peek(System.out::print)

.map(String::toLowerCase)

.forEach(System.out::print);

}

}

输出结果:

Well WELL well it IT it s S s so SO so

FileToWords 稍后定义,但它的功能实现貌似和之前我们看到的差不多:产生字符串对象的流。之后在其通过管道时调用 peek() 进行处理。

因为 peek() 符合无返回值的 Consumer 函数式接口,所以我们只能观察,无法使用不同的元素来替换流中的对象。

流元素排序

Randoms.java 中,我们熟识了 sorted() 的默认比较器实现。其实它还有另一种形式的实现:传入一个 Comparator 参数。代码示例:

// streams/SortedComparator.java

import java.util.*;

public class SortedComparator {

public static void main(String[] args) throws Exception {

FileToWords.stream(“Cheese.dat”)

.skip(10)

.limit(10)

.sorted(Comparator.reverseOrder())

.map(w -> w + " ")

.forEach(System.out::print);

}

}

输出结果:

you what to the that sir leads in district And

sorted() 预设了一些默认的比较器。这里我们使用的是反转“自然排序”。当然你也可以把 Lambda 函数作为参数传递给 sorted()

移除元素

  • distinct():在 Randoms.java 类中的 distinct() 可用于消除流中的重复元素。相比创建一个 Set 集合,该方法的工作量要少得多。

  • filter(Predicate):过滤操作会保留与传递进去的过滤器函数计算结果为 true 的元素。

在下例中,isPrime() 作为过滤器函数,用于检测质数。

import java.util.stream.*;

import static java.util.stream.LongStream.*;

public class Prime {

public static Boolean isPrime(long n) {

return rangeClosed(2, (long)Math.sqrt(n))

.noneMatch(i -> n % i == 0);

}

public LongStream numbers() {

return iterate(2, i -> i + 1)

.filter(Prime::isPrime);

}

public static void main(String[] args) {

new Prime().numbers()

.limit(10)

.forEach(n -> System.out.format("%d ", n));

System.out.println();

new Prime().numbers()

.skip(90)

.limit(10)

.forEach(n -> System.out.format("%d ", n));

}

}

输出结果:

2 3 5 7 11 13 17 19 23 29

467 479 487 491 499 503 509 521 523 541

rangeClosed() 包含了上限值。如果不能整除,即余数不等于 0,则 noneMatch() 操作返回 true,如果出现任何等于 0 的结果则返回 falsenoneMatch() 操作一旦有失败就会退出。

应用函数到元素

  • map(Function):将函数操作应用在输入流的元素中,并将返回值传递到输出流中。

  • mapToInt(ToIntFunction):操作同上,但结果是 IntStream

  • mapToLong(ToLongFunction):操作同上,但结果是 LongStream

  • mapToDouble(ToDoubleFunction):操作同上,但结果是 DoubleStream

在这里,我们使用 map() 映射多种函数到一个字符串流中。代码示例:

// streams/FunctionMap.java

import java.util.*;

import java.util.stream.*;

import java.util.function.*;

class FunctionMap {

static String[] elements = { “12”, “”, “23”, “45” };

static Stream

testStream() {

return Arrays.stream(elements);

}

static void test(String descr, Function<String, String> func) {

System.out.println(" —( " + descr + " )—");

testStream()

.map(func)

.forEach(System.out::println);

}

public static void main(String[] args) {

test(“add brackets”, s -> “[” + s + “]”);

test(“Increment”, s -> {

try {

return Integer.parseInt(s) + 1 + “”;

}

catch(NumberFormatException e) {

return s;

}

}

);

test(“Replace”, s -> s.replace(“2”, “9”));

test(“Take last digit”, s -> s.length() > 0 ?

s.charAt(s.length() - 1) + “” : s);

}

}

输出结果:

—( add brackets )—

[12]

[]

[23]

[45]

—( Increment )—

13

24

46

—( Replace )—

19

93

45

—( Take last digit )—

2

3

5

在上面的自增示例中,我们使用 Integer.parseInt() 尝试将一个字符串转化为整数。如果字符串不能转化成为整数就会抛出 NumberFormatException 异常,我们只须回过头来将原始字符串放回到输出流中。

在以上例子中,map() 将一个字符串映射为另一个字符串,但是我们完全可以产生和接收类型完全不同的类型,从而改变流的数据类型。下面代码示例:

// streams/FunctionMap2.java

// Different input and output types (不同的输入输出类型)

import java.util.*;

import java.util.stream.*;

class Numbered {

final int n;

Numbered(int n) {

this.n = n;

}

@Override

public String toString() {

return “Numbered(” + n + “)”;

}

}

class FunctionMap2 {

public static void main(String[] args) {

Stream.of(1, 5, 7, 9, 11, 13)

.map(Numbered::new)

.forEach(System.out::println);

}

}

输出结果:

Numbered(1)

Numbered(5)

Numbered(7)

Numbered(9)

Numbered(11)

Numbered(13)

我们将获取到的整数通过构造器 Numbered::new 转化成为 Numbered 类型。

如果使用 Function 返回的结果是数值类型的一种,我们必须使用合适的 mapTo数值类型 进行替代。代码示例:

// streams/FunctionMap3.java

// Producing numeric output streams( 产生数值输出流)

import java.util.*;

import java.util.stream.*;

class FunctionMap3 {

public static void main(String[] args) {

Stream.of(“5”, “7”, “9”)

.mapToInt(Integer::parseInt)

.forEach(n -> System.out.format("%d ", n));

System.out.println();

Stream.of(“17”, “19”, “23”)

.mapToLong(Long::parseLong)

.forEach(n -> System.out.format("%d ", n));

System.out.println();

Stream.of(“17”, “1.9”, “.23”)

.mapToDouble(Double::parseDouble)

.forEach(n -> System.out.format("%f ", n));

}

}

输出结果:

5 7 9

17 19 23

17.000000 1.900000 0.230000

遗憾的是,Java 设计者并没有尽最大努力去消除基本类型。

map() 中组合流

假设现有一个传入的元素流,并且打算对流元素使用 map() 函数。现在你已经找到了一些可爱并独一无二的函数功能,但问题来了:这个函数功能是产生一个流。我们想要产生一个元素流,而实际却产生了一个元素流的流。

flatMap() 做了两件事:

  • 将产生流的函数应用在每个元素上(与 map() 所做的相同)

  • 然后将每个流都扁平化为元素

因而最终产生的仅是元素。

flatMap(Function):当 Function 产生流时使用。

flatMapToInt(Function):当 Function 产生 IntStream 时使用。

flatMapToLong(Function):当 Function 产生 LongStream 时使用。

flatMapToDouble(Function):当 Function 产生 DoubleStream 时使用。

为了弄清其工作原理,我们从传入一个刻意设计的函数给 map() 开始。该函数接受一个整数并产生一个字符串流:

我们天真地希望能够得到字符串流,但实际得到的却是“Head”流的流。

可使用 flatMap() 解决:

从map返回的每个流都会自动扁平为组成它的字符串。

现在从一个整数流开始,然后使用每个整数去创建更多的随机数。

concat()以参数顺序组合两个流。 如此,我们在每个随机 Integer 流的末尾添加一个 -1 作为标记。你可以看到最终流确实是从一组扁平流中创建的。

因为 rand.ints() 产生的是一个 IntStream,所以必须使用 flatMap()concat()of() 的特定整数形式。

将文件划分为单词流。

最后使用到的是 FileToWordsRegexp.java,它的问题是需要将整个文件读入行列表中 —— 显然需要存储该列表。而我们真正想要的是创建一个不需要中间存储层的单词流。

下面,我们再使用 flatMap() 来解决这个问题:

stream() 现在是个静态方法,因为它可自己完成整个流创建过程。

注意\\W+ 是一个正则表达式,表示非单词字符,+ 表示可出现一或多次。小写形式的 \\w 表示“单词字符”。

之前遇到的问题是 Pattern.compile().splitAsStream() 产生的结果为流,这意味着当只想要一个简单的单词流时,在传入的行流(stream of lines)上调用 map() 会产生一个单词流的流。

好在 flatMap() 可将元素流的流扁平化为一个简单的元素流。或者,可使用 String.split() 生成一个数组,其可以被 Arrays.stream() 转化成为流:

.flatMap(line -> Arrays.stream(line.split(“\W+”))))

有了真正的、而非 FileToWordsRegexp.java 中基于集合存储的流,我们每次使用都必须从头创建,因为流不能被复用:

System.out.format() 中的 %s 表明参数为 String 类型。

Optional类


若在一个空流取元素会发生什么?我们喜欢为了“happy path”而将流连接起来,并假设流不会被中断。在流中放置 null 就是个很好的中断方法。那么是否有某种对象,可作为流元素的持有者,即使查看的元素不存在也能友好地提示我们(即不会粗暴地抛异常)?

Optional 就可以。一些标准流操作返回 Optional 对象,因为它们并不能保证预期结果一定存在

  • findFirst()

返回一个包含第一个元素的 Optional 对象,若流为空则返回 Optional.empty

  • findAny()

返回包含任意元素的 Optional 对象,若流为空则返回 Optional.empty

  • max()min()

返回一个包含最大值或者最小值的 Optional 对象,若流为空则返回 Optional.empty

reduce() 不再以 identity 形式开头,而是将其返回值包装在 Optional 中。(identity 对象成为其他形式的 reduce() 的默认结果,因此不存在空结果的风险)

对于数字流 IntStreamLongStreamDoubleStreamaverage() 会将结果包装在 Optional 以防止流为空。

以下是对空流进行所有这些操作的简单测试:

class OptionalsFromEmptyStreams {

public static void main(String[] args) {

System.out.println(Stream.empty()

.findFirst());

System.out.println(Stream.empty()

.findAny());

System.out.println(Stream.empty()

.max(String.CASE_INSENSITIVE_ORDER));

System.out.println(Stream.empty()

.min(String.CASE_INSENSITIVE_ORDER));

System.out.println(Stream.empty()

.reduce((s1, s2) -> s1 + s2));

System.out.println(IntStream.empty()

.average());

}

}

Optional.empty

Optional.empty

Optional.empty

Optional.empty

Optional.empty

OptionalDouble.empty

当流为空的时候你会获得一个 Optional.empty 对象,而不是抛异常。OptionaltoString() 方法可以用于展示有用信息。

空流是通过 Stream.<String>empty() 创建的。如果你在没有任何上下文环境的情况下调用 Stream.empty(),Java 并不知道它的数据类型;这个语法解决了这个问题。如果编译器拥有了足够的上下文信息,比如:

Stream s = Stream.empty();

就可以在调用 empty() 时推断类型。

Optional 的两个基本用法:

class OptionalBasics {

static void test(Optional optString) {

if(optString.isPresent())

System.out.println(optString.get());

else

System.out.println(“Nothing inside!”);

}

public static void main(String[] args) {

test(Stream.of(“Epithets”).findFirst());

test(Stream.empty().findFirst());

}

}

Epithets

Nothing inside!

当你接收到 Optional 对象时,应首先调用 isPresent() 检查其中是否包含元素。如果存在,可使用 get() 获取。

便利函数

有许多便利函数可以解包 Optional ,这简化了上述“对所包含的对象的检查和执行操作”的过程:

  • ifPresent(Consumer):当值存在时调用 Consumer,否则什么也不做。

  • orElse(otherObject):如果值存在则直接返回,否则生成 otherObject

  • orElseGet(Supplier):如果值存在则直接返回,否则使用 Supplier 函数生成一个可替代对象。

  • orElseThrow(Supplier):如果值存在直接返回,否则使用 Supplier 函数生成一个异常。

如下是针对不同便利函数的简单演示:

public class Optionals {

static void basics(Optional optString) {

if(optString.isPresent())

System.out.println(optString.get());

else

System.out.println(“Nothing inside!”);

}

static void ifPresent(Optional optString) {

optString.ifPresent(System.out::println);

}

static void orElse(Optional optString) {

System.out.println(optString.orElse(“Nada”));

}

static void orElseGet(Optional optString) {

System.out.println(

optString.orElseGet(() -> “Generated”));

}

static void orElseThrow(Optional optString) {

try {

System.out.println(optString.orElseThrow(

() -> new Exception(“Supplied”)));

} catch(Exception e) {

System.out.println("Caught " + e);

}

}

static void test(String testName, Consumer<Optional> cos) {

System.out.println(" === " + testName + " === ");

cos.accept(Stream.of(“Epithets”).findFirst());

cos.accept(Stream.empty().findFirst());

}

public static void main(String[] args) {

test(“basics”, Optionals::basics);

test(“ifPresent”, Optionals::ifPresent);

test(“orElse”, Optionals::orElse);

test(“orElseGet”, Optionals::orElseGet);

test(“orElseThrow”, Optionals::orElseThrow);

}

}

=== basics ===

Epithets

Nothing inside!

=== ifPresent ===

Epithets

=== orElse ===

Epithets

Nada

=== orElseGet ===

Epithets

Generated

=== orElseThrow ===

Epithets

Caught java.lang.Exception: Supplied

test() 通过传入所有方法都适用的 Consumer 来避免重复代码。

orElseThrow() 通过 catch 关键字来捕获抛出的异常。

创建 Optional

当我们在自己的代码中加入 Optional 时,可以使用下面 3 个静态方法:

  • empty():生成一个空 Optional

  • of(value):将一个非空值包装到 Optional 里。

  • ofNullable(value):针对一个可能为空的值,为空时自动生成 Optional.empty,否则将值包装在 Optional 中。

代码示例:

class CreatingOptionals {

static void test(String testName, Optional opt) {

System.out.println(" === " + testName + " === ");

System.out.println(opt.orElse(“Null”));

}

public static void main(String[] args) {

test(“empty”, Optional.empty());

test(“of”, Optional.of(“Howdy”));

try {

test(“of”, Optional.of(null));

} catch(Exception e) {

System.out.println(e);

}

test(“ofNullable”, Optional.ofNullable(“Hi”));

test(“ofNullable”, Optional.ofNullable(null));

}

}

=== empty ===

Null

=== of ===

Howdy

java.lang.NullPointerException

=== ofNullable ===

Hi

=== ofNullable ===

Null

我们不能通过传递 nullof() 来创建 Optional 对象。最安全的方法是, 使用 ofNullable() 来优雅地处理 null

Optional 对象操作

当我们的流管道生成了 Optional 对象,如下方法可使得 Optional 的后续能做更多操作:

  • filter(Predicate):将 Predicate 应用于 Optional 中的内容并返回结果。当 Optional 不满足 Predicate 时返回空。如果 Optional 为空,则直接返回。

  • map(Function):如果 Optional 不为空,应用 FunctionOptional 中的内容,并返回结果。否则直接返回 Optional.empty

  • flatMap(Function):同 map(),但是提供的映射函数将结果包装在 Optional 对象中,因此 flatMap() 不会在最后进行任何包装。

以上方法都不适用于数值型 Optional

一般来说,流的 filter() 会在 Predicate 返回 false 时移除流元素。

Optional.filter() 在失败时不会删除 Optional,而是将其保留下来,并转化为空。

class OptionalFilter {

static String[] elements = {

“Foo”, “”, “Bar”, “Baz”, “Bingo”

};

static Stream testStream() {

return Arrays.stream(elements);

}

static void test(String descr, Predicate pred) {

System.out.println(" —( " + descr + " )—");

for(int i = 0; i <= elements.length; i++) {

System.out.println(

testStream()

.skip(i)

.findFirst()

.filter(pred));

}

}

public static void main(String[] args) {

test(“true”, str -> true);

test(“false”, str -> false);

test(“str != “””, str -> str != “”);

test(“str.length() == 3”, str -> str.length() == 3);

test(“startsWith(“B”)”,

str -> str.startsWith(“B”));

}

}

即使输出看起来像流,特别是 test() 中的 for 循环。每一次的 for 循环时重新启动流,然后根据 for 循环的索引跳过指定个数的元素,这就是它最终在流中的每个连续元素上的结果。接下来调用 findFirst() 获取剩余元素中的第一个元素,结果会包装在 Optional 中。

注意,不同于普通 for 循环,这里的索引值范围并不是 i < elements.length, 而是 i <= elements.length。所以最后一个元素实际上超出了流。方便的是,这将自动成为 Optional.empty

map() 一样 , Optional.map() 应用于函数。它仅在 Optional 不为空时才应用映射函数,并将 Optional 的内容提取到映射函数。代码示例:

class OptionalMap {

static String[] elements = {“12”, “”, “23”, “45”};

static Stream testStream() {

return Arrays.stream(elements);

}

static void test(String descr, Function<String, String> func) {

System.out.println(" —( " + descr + " )—");

for (int i = 0; i <= elements.length; i++) {

System.out.println(

testStream()

.skip(i)

.findFirst() // Produces an Optional

.map(func));

}

}

public static void main(String[] args) {

// If Optional is not empty, map() first extracts

// the contents which it then passes

// to the function:

test(“Add brackets”, s -> “[” + s + “]”);

test(“Increment”, s -> {

try {

return Integer.parseInt(s) + 1 + “”;

} catch (NumberFormatException e) {

return s;

}

});

test(“Replace”, s -> s.replace(“2”, “9”));

test(“Take last digit”, s -> s.length() > 0 ?

s.charAt(s.length() - 1) + “” : s);

}

// After the function is finished, map() wraps the

// result in an Optional before returning it:

}

映射函数的返回结果会自动包装成为 OptionalOptional.empty 会被直接跳过。

OptionalflatMap() 应用于已生成 Optional 的映射函数,所以 flatMap() 不会像 map() 那样将结果封装在 Optional 中。代码示例:

// streams/OptionalFlatMap.java

import java.util.Arrays;

import java.util.Optional;

import java.util.function.Function;

import java.util.stream.Stream;

class OptionalFlatMap {

static String[] elements = {“12”, “”, “23”, “45”};

static Stream testStream() {

return Arrays.stream(elements);

}

static void test(String descr,

Function<String, Optional> func) {

System.out.println(" —( " + descr + " )—");

for (int i = 0; i <= elements.length; i++) {

System.out.println(

testStream()

.skip(i)

.findFirst()

.flatMap(func));

}

}

public static void main(String[] args) {

test(“Add brackets”,

s -> Optional.of(“[” + s + “]”));

test(“Increment”, s -> {

try {

return Optional.of(

Integer.parseInt(s) + 1 + “”);

} catch (NumberFormatException e) {

return Optional.of(s);

}

});

test(“Replace”,

s -> Optional.of(s.replace(“2”, “9”)));

test(“Take last digit”,

s -> Optional.of(s.length() > 0 ?

s.charAt(s.length() - 1) + “”
s));

}

}

map()flatMap() 将提取非空 Optional 的内容并将其应用在映射函数。唯一的区别就是 flatMap() 不会把结果包装在 Optional 中,因为映射函数已经被包装过了。在如上示例中,我们已经在每一个映射函数中显式地完成了包装,但是很显然 Optional.flatMap() 是为那些自己已经生成 Optional 的函数而设计的。

Optional 流

假设你的生成器可能产生 null 值,那么当用它来创建流时,你会想到用 Optional 包装元素:

使用这个流时,必须清楚如何解包 Optional

输出结果:

由于每种情况都需要定义“空值”的含义,所以通常我们要为每个应用程序采用不同的方法。

终端操作


以下操作将会获取流的最终结果。至此我们无法再继续往后传递流。可以说,终端操作总是我们在流管道中所做的最后一件事。

数组

  • toArray():将流转换成适当类型的数组

  • toArray(generator):在特殊情况下,生成自定义类型的数组

假设需复用流产生的随机数:

这样每次调用 rands() 的时候可以重复获取相同的整数流。

循环

  • forEach(Consumer)常见如 System.out::println 作为 Consumer 函数。

  • forEachOrdered(Consumer): 保证 forEach 按照原始流顺序操作。

第一种形式:无序操作,仅在引入并行流时才有意义。 parallel():可实现多处理器并行操作。实现原理为将流分割为多个(通常数目为 CPU 核心数)并在不同处理器上分别执行操作。因为我们采用的是内部迭代,而不是外部迭代,所以这是可能实现的。

下例引入 parallel() 来帮助理解 forEachOrdered(Consumer) 的作用和使用场景:

// streams/ForEach.java

import java.util.*;

import java.util.stream.*;

import static streams.RandInts.*;

public class ForEach {

static final int SZ = 14;

public static void main(String[] args) {

rands().limit(SZ)

.forEach(n -> System.out.format("%d ", n));

System.out.println();

rands().limit(SZ)

.parallel()

.forEach(n -> System.out.format("%d ", n));

System.out.println();

rands().limit(SZ)

.parallel()

.forEachOrdered(n -> System.out.format("%d ", n));

}

}

为了方便测试不同大小的数组,我们抽离出了 SZ 变量。结果很有趣:在第一个流中,未使用 parallel() ,所以 rands() 按照元素迭代出现的顺序显示结果;在第二个流中,引入parallel() ,即便流很小,输出的结果顺序也和前面不一样。这是由于多处理器并行操作的原因。多次运行测试,结果均不同。多处理器并行操作带来的非确定性因素造成了这样的结果。

在最后一个流中,同时使用了 parallel()forEachOrdered() 来强制保持原始流顺序。因此,对非并行流使用 forEachOrdered() 是没有任何影响的。

集合

  • collect(Collector):使用 Collector 收集流元素到结果集合中。

  • collect(Supplier, BiConsumer, BiConsumer):同上,第一个参数 Supplier 创建了一个新结果集合,第二个参数 BiConsumer 将下一个元素包含到结果中,第三个参数 BiConsumer 用于将两个值组合起来。

假设我们现在为保证元素有序,将元素存储在 TreeSetCollectors 没有特定的 toTreeSet(),但可以通过将集合的构造器引用传递给 Collectors.toCollection(),从而构建任意类型的集合。

比如,将一个文件中的单词收集到 TreeSet

我们也可以在流中生成 Map。代码示例:

// streams/MapCollector.java

import java.util.*;

import java.util.stream.*;

class Pair {

public final Character c;

public final Integer i;

Pair(Character c, Integer i) {

this.c = c;

this.i = i;

}

public Character getC() { return c; }

public Integer getI() { return i; }

@Override

public String toString() {

return “Pair(” + c + ", " + i + “)”;

}

}

class RandomPair {

Random rand = new Random(47);

// An infinite iterator of random capital letters:

Iterator capChars = rand.ints(65,91)

.mapToObj(i -> (char)i)

.iterator();

public Stream stream() {

return rand.ints(100, 1000).distinct()

.mapToObj(i -> new Pair(capChars.next(), i));

}

}

public class MapCollector {

public static void main(String[] args) {

Map<Integer, Character> map =

new RandomPair().stream()

.limit(8)

.collect(

Collectors.toMap(Pair::getI, Pair::getC));

System.out.println(map);

}

}

输出结果:

{688=W, 309=C, 293=B, 761=N, 858=N, 668=G, 622=F, 751=N}

Pair 只是一个基础的数据对象。RandomPair 创建了随机生成的 Pair 对象流。在 Java 中,我们不能直接以某种方式组合两个流。所以这里创建了一个整数流,并且使用 mapToObj() 将其转化成为 Pair 流。 capChars 随机生成的大写字母迭代器从流开始,然后 iterator() 允许我们在 stream() 中使用它。就我所知,这是组合多个流以生成新的对象流的唯一方法。

在这里,我们只使用最简单形式的 Collectors.toMap(),这个方法值需要一个可以从流中获取键值对的函数。还有其他重载形式,其中一种形式是在遇到键值冲突时,需要一个函数来处理这种情况。

大多数情况下,java.util.stream.Collectors 中预设的 Collector 就能满足我们的要求。

还可以使用第二种形式的 collect()

// streams/SpecialCollector.java

import java.util.*;

import java.util.stream.*;

public class SpecialCollector {

public static void main(String[] args) throws Exception {

ArrayList words =

FileToWords.stream(“Cheese.dat”)

.collect(ArrayList::new,

ArrayList::add,

ArrayList::addAll);

words.stream()

.filter(s -> s.equals(“cheese”))

.forEach(System.out::println);

}

}

输出结果:

cheese

cheese

在这里, ArrayList 的方法已经执行了你所需要的操作,但是似乎更有可能的是,如果你必须使用这种形式的 collect(),则必须自己创建特殊的定义。

对List根据一个或多个字段分组

项目中遇到了需要对list进行分组的场景,根据List中entity的某字段或者多个字段进行分组,形成Map,然后根据map进行相关的业务操作。

根据一个字段进行分组

public class ListGroupBy {

public static void main(String[] args) {

List scoreList = new ArrayList<>();

scoreList.add(new Score().setStudentId(“001”).setScoreYear(“2018”).setScore(100.0));

scoreList.add(new Score().setStudentId(“001”).setScoreYear(“2019”).setScore(59.5));

scoreList.add(new Score().setStudentId(“001”).setScoreYear(“2019”).setScore(99.0));

scoreList.add(new Score().setStudentId(“002”).setScoreYear(“2018”).setScore(99.6));

//根据scoreYear字段进行分组

Map<String, List> map = scoreList.stream().collect(

Collectors.groupingBy(

score -> score.getScoreYear()

));

System.out.println(JSONUtil.toJsonPrettyStr(map));

}

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
)

.limit(8)

.collect(

Collectors.toMap(Pair::getI, Pair::getC));

System.out.println(map);

}

}

输出结果:

{688=W, 309=C, 293=B, 761=N, 858=N, 668=G, 622=F, 751=N}

Pair 只是一个基础的数据对象。RandomPair 创建了随机生成的 Pair 对象流。在 Java 中,我们不能直接以某种方式组合两个流。所以这里创建了一个整数流,并且使用 mapToObj() 将其转化成为 Pair 流。 capChars 随机生成的大写字母迭代器从流开始,然后 iterator() 允许我们在 stream() 中使用它。就我所知,这是组合多个流以生成新的对象流的唯一方法。

在这里,我们只使用最简单形式的 Collectors.toMap(),这个方法值需要一个可以从流中获取键值对的函数。还有其他重载形式,其中一种形式是在遇到键值冲突时,需要一个函数来处理这种情况。

大多数情况下,java.util.stream.Collectors 中预设的 Collector 就能满足我们的要求。

还可以使用第二种形式的 collect()

// streams/SpecialCollector.java

import java.util.*;

import java.util.stream.*;

public class SpecialCollector {

public static void main(String[] args) throws Exception {

ArrayList words =

FileToWords.stream(“Cheese.dat”)

.collect(ArrayList::new,

ArrayList::add,

ArrayList::addAll);

words.stream()

.filter(s -> s.equals(“cheese”))

.forEach(System.out::println);

}

}

输出结果:

cheese

cheese

在这里, ArrayList 的方法已经执行了你所需要的操作,但是似乎更有可能的是,如果你必须使用这种形式的 collect(),则必须自己创建特殊的定义。

对List根据一个或多个字段分组

项目中遇到了需要对list进行分组的场景,根据List中entity的某字段或者多个字段进行分组,形成Map,然后根据map进行相关的业务操作。

根据一个字段进行分组

public class ListGroupBy {

public static void main(String[] args) {

List scoreList = new ArrayList<>();

scoreList.add(new Score().setStudentId(“001”).setScoreYear(“2018”).setScore(100.0));

scoreList.add(new Score().setStudentId(“001”).setScoreYear(“2019”).setScore(59.5));

scoreList.add(new Score().setStudentId(“001”).setScoreYear(“2019”).setScore(99.0));

scoreList.add(new Score().setStudentId(“002”).setScoreYear(“2018”).setScore(99.6));

//根据scoreYear字段进行分组

Map<String, List> map = scoreList.stream().collect(

Collectors.groupingBy(

score -> score.getScoreYear()

));

System.out.println(JSONUtil.toJsonPrettyStr(map));

}

}

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-RigvFvlg-1715710817959)]

[外链图片转存中…(img-E6lv6UQA-1715710817959)]

[外链图片转存中…(img-sORtNLVE-1715710817959)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 30
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值