《Java8函数式编程》学习笔记 - 第5章

第五章 高级集合器和收集器

介绍一些高级主题,包括新引入的Collectors类,方法引用。

5.1 方法引用

artist -> artist.getName()
Artist :: getName
标准语法为ClassName::MethodName
虽然这是一个方法,但不需要在后面加括号,
因为这里并不调用该方法。
我们只是提供了和Lambda表达式等价的一种结构,在需要时才会调用。
凡是使用Lambda表达式的地方,就可以用使用方法引用。

例如对构造函数改造成方法引用
(name, nationality) -> new Artist(name, nationality)
Artist::new
创建一个字符串型数组:
String[]::new

5.2 元素顺序

Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
List<Integer> sameOrder = numbers.stream()
                                 .sorted()
                                 .collect(toList());
assertEquals(asList(1, 2, 3, 4), sameOrder);

sorted方法使得原本无序的流映射后成了有序的。
unordered方法可以消除顺序。
但大多数操作都是在有序的流上效率更高。

5.3 使用收集器

从流中生成List,是最自然的数据结构。
但有时候,需要从流生成Map或者Set,或者定制一个类,把需要的结果抽象出来。

仅凭流上方法的签名,就能判断出这是否是一个及早求值的操作。
⇒及早求值的方法,其返回值是流以外的类型。
reduce操作就是一个很好地例子。
⇒从一个流中返回一个值,因此是一个及早求值的操作

有时人们希望做的更多,这就是收集器
一种通用的,从流生成复杂值的结构。只要将它传给collect方法,
所有的流就都可以使用它。

标准类库已经提供了一些有用的收集器,例如本章使用的收集器
都是java.util.stream.Collectors类中静态导入的。

5.3.1 转换成其他集合(toSet,toCollection)

有一些收集器可以生成其他集合。
比如前面已经见过的toList,生成了java.util.List类的实例。
还有toSet和toCollection,分别生成Set和Collection类的实例

如何使用Stream 类库并行处理数据,收集并行操作
的结果需要的Set,和对线程安全没有要求的Set 类是完全不同的。
具体内容在p?

使用一个特定的集合收集值,而且你可以稍后指定该集合的类型。
比如,使用TreeSet的情况,可以使用toCollection

//例5-5 使用toCollection,用定制的集合收集元素
stream.collect(toCollection(TreeSet::new));
5.3.2 转换成值(maxBy...)
//成员最多的乐队
public Optional<Artist> biggestGroup(Stream<Artist> artists) {
    Function<Artist,Integer> getCount =
         artist -> artist.getMembers().size();
    return artists.collect(Collectors.maxBy(Comparator.comparing(getCount)));
}

这里主要学习maxBy的用法。

double d = this.albums.stream()
                .collect(Collectors.averagingInt(
                        album -> album.getTrackList().size()));
int i = this.albums.stream()
                .collect(Collectors.summingInt(
                        album -> album.getTrackList().size()));

这里主要学习averagingInt以及summingInt的用法。

5.3.3 数据分块(partitioningBy)
Map<Boolean, List<Artist>> x =
    allArtists.stream().collect(Collectors.partitioningBy(
        artist -> artist.isFrom("London")));

这里主要学习partitioningBy的用法
返回一个map,true那边是来自伦敦,false那边是不来自伦敦

5.3.4 数据分组(groupingBy)
public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) {
    return albums.collect(groupingBy(album -> album.getMainMusician()));
}

groupingBy跟数据分块类似,只是不再限制true,false,可以使用任意值对数据分组

5.3.5 字符串(joining)
allArtists.stream()
        .map(Artist::getName)
        .collect(Collectors.joining(", ", "[", "]"));

joining的三个参数分别为,分隔符,前缀,后缀

5.3.6 组合收集器
public Map<Artist, Long> numberOfAlbums(Stream<Album> albums) {
    return albums.collect(
        groupingBy(album -> album.getMainMusician(),counting()));
}

groupingBy的第二个参数为第二个收集器

5.3.6 重构和定制收集器
StringBuilder reduced =
                allArtists.stream()
                .map(Artist::getName)
                .reduce(new StringBuilder(), (builder, name) -> {
                    if (builder.length() > 0)
                        builder.append(", ");
                    builder.append(name);
                    return builder;
                    }, (left, right) -> left.append(right));
        reduced.insert(0, "[");
        reduced.append("]");
        String result = reduced.toString();
StringCombiner combined =
    artists.stream()
        .map(Artist::getName)
        .reduce(new StringCombiner(", ", "[", "]"),
            StringCombiner::add,
            StringCombiner::merge);
String result = combined.toString();

如何理解上述代码,首先要理解reduce的三种override方法

  • Optional<T> reduce(BinaryOperator<T> accumulator);
  • T reduce(T identity, BinaryOperator<T> accumulator);
  • <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

第一个变形,接受一个函数接口BinaryOperator

  • 变形1,未定义初始值,从而第一次执行的时候第一个参数的值是Stream的第一个元素,
    第二个参数是Stream的第二个元素
  • 变形2,定义了初始值,从而第一次执行的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素

  • 变形3,如果使用了parallelStream,reduce操作是并发进行的,
    为了避免竞争,每个reduce线程都会有独立的result
    combiner的作用在于合并每个线程的result得到最终结果

ArrayList<Integer> accResult_ = Stream.of(1, 2, 3, 4)
    .reduce(new ArrayList<Integer>(),
        new BiFunction<ArrayList<Integer>, Integer, ArrayList<Integer>>() {
            @Override
            public ArrayList<Integer> apply(ArrayList<Integer> acc, Integer item) {
                acc.add(item);
                System.out.println("item: " + item);
                System.out.println("acc+ : " + acc);
                System.out.println("BiFunction");
                return acc;
            }
        }, new BinaryOperator<ArrayList<Integer>>() {
            @Override
            public ArrayList<Integer> apply(ArrayList<Integer> acc, 
                ArrayList<Integer> item) {
                System.out.println("BinaryOperator");
                acc.addAll(item);
                System.out.println("item: " + item);
                System.out.println("acc+ : " + acc);
                System.out.println("--------");
                return acc;
            }
        });
System.out.println("accResult_: " + accResult_);

传递给第一个参数是ArrayList,在第二个函数参数中打印了“BiFunction”,
而在第三个参数接口中打印了函数接口中打印了”BinaryOperator“.
可是,看打印结果,只是打印了“BiFunction”,而没有打印”BinaryOperator“,
说明第三个函数参数并没有执行。这里我们知道了该变形可以返回任意类型的数据。
对于第三个函数参数,为什么没有执行,刚开始的时候也是没有看懂到底是啥意思呢,
而且其参数必须为返回的数据类型?看了好几遍文档也是一头雾水。
在 java8 reduce方法中的第三个参数combiner有什么作用?这里找到了答案,
Stream是支持并发操作的,为了避免竞争,对于reduce线程都会有独立的result,
combiner的作用在于合并每个线程的result得到最终结果。
这也说明了了第三个函数参数的数据类型必须为返回数据类型了。

5.3.8 对收集器的归一化处理

如果想为自己领域内的类定制一个收集器,不妨考虑一下其他替代方案。
最容易想到的方案是构建若干个集合对象,作为参数传给领域内类的构造函数
如果领域内的类包含多种集合,这种方式又简单又适用。

另外可以使用reducing收集器,它为流上的归一操作提供了统一实现。

5.4 一些细节

Java8对Map类的一个改变。
引入了一个新方法,computeIfAbsent,
该方法接收一个Lambda表达式,值不存在时使用该Lambda表达式计算新值。

//例5-32使用computeIfAbsent 缓存
public Artist getArtist(String name) {
    return artistCache.computeIfAbsent(name, this::readArtistFromDB);
}
//例5-33 一种丑陋的迭代Map 的方式
Map<Artist, Integer> countOfAlbums = new HashMap<>();
for(Map.Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {
    Artist artist = entry.getKey();
    List<Album> albums = entry.getValue();
    countOfAlbums.put(artist, albums.size());
}

统计每一个艺术家专辑的数量,从Map<Artist,List> -> Map<Artist, Integer>
Java8中的forEach,接受一个BiConsumer对象作为参数,
用这个重写的代码如下

5-34 使用内部迭代遍历Map 里的值
Map<Artist, Integer> countOfAlbums = new HashMap<>();
albumsByArtist.forEach((artist, albums) -> {
    countOfAlbums.put(artist, albums.size());});

5.5 要点回顾

  • 方法引用是一种引用方法的轻量级语法,形如:ClassName::methodName。
  • 收集器可用来计算流的最终值,是 reduce 方法的模拟。
  • Java 8 提供了收集多种容器类型的方式,同时允许用户自定义收集器。