《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
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 提供了收集多种容器类型的方式,同时允许用户自定义收集器。