什么是映射

一个非常常见的数据处理套路就是从某些对象中选择信息。比如在SQL里,你可以从表中选择一列。Stream API也通过map和flatMap方法提供了类似的工具。

对流中每一个元素应用函数

流支持map方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。例如,下面的代码把方法引用Dish::getName传给了map方法,来提取流中菜肴的名称:

List dishNames = menu.stream()
.map(Dish::getName)
.collect(toList());

因为getName方法返回一个String,所以map方法输出的流的类型就是Stream

让我们看一个稍微不同的例子来巩固一下对map的理解。给定一个单词列表,你想要返回另一个列表,显示每个单词中有几个字母。怎么做呢?你需要对列表中的每个元素应用一个函数。这听起来正好该用map方法去做!应用的函数应该接受一个单词,并返回其长度。你可以像下面这样,给map传递一个方法引用String::length来解决这个问题:

List words = Arrays.asList(“Java 8”, “Lambdas”, “In”, “Action”);
List wordLengths = words.stream()
.map(String::length)
.collect(toList());

现在让我们回到提取菜名的例子。如果你要找出每道菜的名称有多长,怎么做?你可以像下面这样,再链接上一个map:

List dishNameLengths = menu.stream()
.map(Dish::getName)
.map(String::length)
.collect(toList());

流的扁平化flatMap

你已经看到如何使用map方法返回列表中每个单词的长度了。让我们拓展一下:对于一张单词表,如何返回一张列表,列出里面各不相同的字符呢?例如,给定单词列表[“Hello”,”World”],你想要返回列表[“H”,”e”,”l”, “o”,”W”,”r”,”d”]。
你可能会认为这很容易,你可以把每个单词映射成一张字符表,然后调用distinct来过滤重复的字符。第一个版本可能是这样的:

words.stream()
.map(word -> word.split(“”))
.distinct()
.collect(toList());

这个方法的问题在于,传递给map方法的Lambda为每个单词返回了一个String[](String列表)。因此,map返回的流实际上是Stream类型的。你真正想要的是用Stream来表示一个字符流。图5-5说明了这个问题。

image.png
图 5-5 不正确地使用map找出单词列表中各不相同的字符
幸好可以用flatMap来解决这个问题!让我们一步步看看怎么解决它。

  1. 尝试使用map和Arrays.stream()
    首先,你需要一个字符流,而不是数组流。有一个叫作Arrays.stream()的方法可以接受一个数组并产生一个流,例如:

String[] arrayOfWords = {“Goodbye”, “World”};
Stream streamOfwords = Arrays.stream(arrayOfWords);

把它用在前面的那个流水线里,看看会发生什么:

words.stream()
.map(word -> word.split(“”)) ←─将每个单词转换为由其字母构成的数组
.map(Arrays::stream) ←─让每个数组变成一个单独的流
.distinct()
.collect(toList());

当前的解决方案仍然搞不定!这是因为,你现在得到的是一个流的列表(更准确地说是Stream)!的确,你先是把每个单词转换成一个字母数组,然后把每个数组变成了一个独立的流。

  1. 使用flatMap
    你可以像下面这样使用flatMap来解决这个问题:

List uniqueCharacters =
words.stream()
.map(w -> w.split(“”)) ←─将每个单词转换为由其字母构成的数组
.flatMap(Arrays::stream) ←─将各个生成流扁平化为单个流
.distinct()
.collect(Collectors.toList());

使用flatMap方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用map(Arrays::stream)时生成的单个流都被合并起来,即扁平化为一个流。图5-6说明了使用flatMap方法的效果。把它和图5-5中map的效果比较一下。
image.png

图 5-6 使用flatMap找出单词列表中各不相同的字符

一言以蔽之,flatmap方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接起来成为一个流。

测验5.2:映射

(1) 给定一个数字列表,如何返回一个由每个数的平方构成的列表呢?例如,给定[1, 2, 3, 4, 5],应该返回[1, 4, 9, 16, 25]。
答案:你可以利用map方法的Lambda,接受一个数字,并返回该数字平方的Lambda来解决这个问题。
List numbers = Arrays.asList(1, 2, 3, 4, 5);
List squares =
numbers.stream()
.map(n -> n * n)
.collect(toList());

(2) 给定两个数字列表,如何返回所有的数对呢?例如,给定列表[1, 2, 3]和列表[3, 4],应该返回[(1, 3), (1, 4), (2, 3), (2, 4), (3, 3), (3, 4)]。为简单起见,你可以用有两个元素的数组来代表数对。
答案:你可以使用两个map来迭代这两个列表,并生成数对。但这样会返回一个Stream>。你需要让生成的流扁平化,以得到一个Stream。这正是flatMap所做的:

List numbers1 = Arrays.asList(1, 2, 3);
List numbers2 = Arrays.asList(3, 4);
List pairs =
numbers1.stream()
.flatMap(i -> numbers2.stream()
.map(j -> new int[]{i, j})
)
.collect(toList());

(3) 如何扩展前一个例子,只返回总和能被3整除的数对呢?例如(2, 4)和(3, 3)是可以的。

答案:你在前面看到了,filter可以配合谓词使用来筛选流中的元素。因为在flatMap操作后,你有了一个代表数对的int[]流,所以你只需要一个谓词来检查总和是否能被3整除就可以了:

List numbers1 = Arrays.asList(1, 2, 3);
List numbers2 = Arrays.asList(3, 4);
List pairs =
numbers1.stream()
.flatMap(i ->
numbers2.stream()
.filter(j -> (i + j) % 3 == 0)
.map(j -> new int[]{i, j})
)
.collect(toList());

其结果是[(2, 4), (3, 3)]。

其它

flat 是平铺的意思,flatMap 即对流中每个元素进行平铺后,形成多个流合在一起

比如现在有 3 个字符串数组:

  1. String[] arr1 = {"AAA", "BBB", "CCC", "DDD", "EEE", "FFF"};
  2. String[] arr2 = {"aaa", "bbb", "ccc"};
  3. String[] arr3 = {"111", "222", "333"};

现在直接转换为 Stream:

    @Test
    public void c_6() {
        String[] arr1 = {"AAA", "BBB", "CCC", "DDD", "EEE", "FFF"};
        String[] arr2 = {"aaa", "bbb", "ccc"};
        String[] arr3 = {"111", "222", "333"};

        List<String[]> list = Stream.of(arr1, arr2, arr3).collect(Collectors.toList());
        list.forEach(System.out::println);
        System.out.println("\narrays list size: " + list.size());
    }

结果:

[Ljava.lang.String;@5f150435
[Ljava.lang.String;@1c53fd30
[Ljava.lang.String;@50cbc42f

arrays list size: 3

是 3 个数组元素。

如果是使用flatMap 方法的效果:

 @Test
    public void c_2343() {
        String[] arr1 = {"AAA", "BBB", "CCC", "DDD", "EEE", "FFF"};
        String[] arr2 = {"aaa", "bbb", "ccc"};
        String[] arr3 = {"111", "222", "333"};

        List<String[]> list = Stream.of(arr1, arr2, arr3).collect(Collectors.toList());

        List<String> mapList = list.stream().flatMap(Arrays::stream).collect(Collectors.toList());
        mapList.forEach(System.out::println);
        System.out.println("\nflatmap list size: " + mapList.size());
        System.out.println();
    }

结果输出:

AAA
BBB
CCC
DDD
EEE
FFF
aaa
bbb
ccc
111
222
333

flatmap list size: 12

元素大小变成了 12,把 3 个数组流中的元素全部平铺合到一个流中了,然后流中总共为 12 个元素。
这里我用的数组类型,如果是多个 List 类型也是可以的,其底层也是数组,只要能将元素转换成流都是可以的。