Java 8 对核心类库的改进主要包括集合类的 API 和新引入的流 (Stream)。流使程序员得以站在更高的抽象层次上对集合进行操作.
1.从外部迭代到内部迭代
使用 for 循环计算来自伦敦的艺术家人数:
int count = 0;
for (Artist artist : allArtists) {
if (artist.isFrom("London")) {
count++;
}
}
改进一(外部迭代), 使用迭代器计算来自伦敦的艺术家人数:
int count = 0;
Iterator<Artist> iterator = allArtists.iterator();
while(iterator.hasNext()) {
Artist artist = iterator.next();
if (artist.isFrom("London")) {
count++;
}
}
改进二(内部迭代), 利用stream计算来自伦敦的艺术家人数:
long count = allArtists.stream()
.filter(artist -> artist.isFrom("London"))
.count();
Stream 是用函数式编程方式在集合类上进行复杂操作的工具.
上述代码可被分解为两步更简单的操作:
- 找出所有来自伦敦的艺术家;
- 计算他们的人数.
每种操作都对应 Stream 接口的一个方法. 为了找出来自伦敦的艺术家, 需要对 Stream 对象进行过滤:filter. 过滤在这里是指"只保留通过某项测试的对象". 测试由一个函数完成, 根据艺术家是否来自伦敦, 该函数返回true或者false. 由于Stream API的函数式编程风格, 我们并没有改变集合的内容, 而是描述出 Stream 里的内容. count() 方法计算给定 Stream 里包含多少个对象.
2.实现机制
上述整个过程被分解为两种更简单的操作: 过滤和计数, 看似有化简为繁之嫌--两次操作(filter和count)是否需要两次循环? 事实上类库设计精妙, 只需对艺术家列表迭代一次.
通常, 在 Java 中调用一个方法,计算机会随即执行操作:比如, System.out.println ("Hello World"); 会在终端上输出一条信息. 这种方式叫做及早求值. 而Stream对象则则使用了另一个概念叫做惰性求值, 直到真正需要时才进行计算.
//只过滤不计数
allArtists.stream()
.filter(artist -> artist.isFrom("London"));
这行代码并未做什么实际性的工作, filter 只刻画出了 Stream, 但没有产生新的集合.
判断一个操作是惰性求值还是及早求值很简单, 只需看它的返回值: 如果返回值是 Stream, 那么是惰性求值; 如果返回值是另一个值或为空, 那么就是及早求值.
3.常用的流操作
3.1 collect(toList())
collect(toList()) 方法由 Stream 里的值生成一个列表,是一个及早求值操作.
List<String> collected = Stream.of("a", "b", "c").collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);
3.2 map
如果有一个函数可以将一种类型的值转换成另外一种类型, map 操作就可以使用该函数, 将一个流中的值转换成一个新的流.
//使用 map 操作将字符串转换为大写形式
List<String> collected = Stream.of("a", "b", "hello")
.map(string -> string.toUpperCase())
.collect(Collectors.toList());
assertEquals(Arrays.asList("A", "B", "HELLO"), collected);
3.3 filter
遍历数据并检查其中的元素时,可尝试使用 Stream 中提供的新方法 filter.
List<String> beginningWithNumbers
= Stream.of("a", "1abc", "abc1")
.filter(value -> Character.isDigit(value.charAt(0)))
.collect(Collectors.toList());
assertEquals(Arrays.asList("1abc"), beginningWithNumbers);
filter 接受一个函数作为参数, 该函数用 Lambda 表达式表示, 其返回值必须是 true 或者 false, 该 Lambda 表达式的函数接口正是之前介绍过的 Predicate.
3.4 flatMap
flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream.
//假设有一个包含多个列表的流, 现在希望得到所有数字的序列.
List<Integer> together = Stream.of(Arrays.asList(1, 2), Arrays.asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(Collectors.toList());
assertEquals(Arrays.asList(1, 2, 3, 4), together);
3.5 max和min
Stream上常用的操作之一是求最大值和最小值. Stream API中的max和min操作足以解决这一问题.
//查找长度最小的元素
List<String> tracks = Arrays.asList("Bakai", "Violets for Your Furs","Time Was");
String shortestTrack = tracks.stream()
.min(Comparator.comparing(track -> track.length()))
.get();
assertEquals(tracks.get(1), shortestTrack);
3.6 reduce
reduce 操作可以实现从一组值中生成一个值. 在上述例子中用到的 count、min 和 max 方 法, 因为常用而被纳入标准库中. 事实上, 这些方法都是 reduce 操作.
下例展示这一过程, Lambda 表达式就是 reducer, 它执行求和操作, 有两个参数: 传入 Stream 中的当前元素和 acc. 将两个参数相加, acc 是累加器, 保存着当前的累加结果.
//使用 reduce 求和
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element);
assertEquals(6, count);
4.重构遗留代码
为了进一步阐释如何重构遗留代码, 本节将举例说明如何将一段使用循环进行集合操作的代码, 重构成基于 Stream 的操作.
下面是跟专辑相关的一组基础类:
package com.chyun.java8.lambda.base;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import static java.util.stream.Collectors.toList;
/**
* Domain class for a popular music artist.
*
* @author Richard Warburton
*/
public final class Artist {
private String name;
private List<Artist> members;
private String nationality;
public Artist(String name, String nationality) {
this(name, Collections.emptyList(), nationality);
}
public Artist(String name, List<Artist> members, String nationality) {
Objects.requireNonNull(name);
Objects.requireNonNull(members);
Objects.requireNonNull(nationality);
this.name = name;
this.members = new ArrayList<>(members);
this.nationality = nationality;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @return the members
*/
public Stream<Artist> getMembers() {
return members.stream();
}
/**
* @return the nationality
*/
public String getNationality() {
return nationality;
}
public boolean isSolo() {
return members.isEmpty();
}
public boolean isFrom(String nationality) {
return this.nationality.equals(nationality);
}
@Override
public String toString() {
return getName();
}
public Artist copy() {
List<Artist> members = getMembers().map(Artist::copy).collect(toList());
return new Artist(name, members, nationality);
}
}
package com.chyun.java8.lambda.base;
import java.util.stream.Stream;
import static java.util.stream.Stream.concat;
public interface Performance {
public String getName();
public Stream<Artist> getMusicians();
// TODO: test
public default Stream<Artist> getAllMusicians() {
return getMusicians().flatMap(artist -> {
return concat(Stream.of(artist), artist.getMembers());
});
}
}
package com.chyun.java8.lambda.base;
public final class Track {
private final String name;
private final int length;
public Track(String name, int length) {
this.name = name;
this.length = length;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @return the length of the track in milliseconds.
*/
public int getLength() {
return length;
}
public Track copy() {
return new Track(name, length);
}
}
package com.chyun.java8.lambda.base;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import static java.util.Collections.unmodifiableList;
import static java.util.stream.Collectors.toList;
/**
*
* @author richard
*/
public final class Album implements Performance {
private String name;
private List<Track> tracks;
private List<Artist> musicians;
public Album(String name, List<Track> tracks, List<Artist> musicians) {
Objects.requireNonNull(name);
Objects.requireNonNull(tracks);
Objects.requireNonNull(musicians);
this.name = name;
this.tracks = new ArrayList<>(tracks);
this.musicians = new ArrayList<>(musicians);
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @return the tracks
*/
public Stream<Track> getTracks() {
return tracks.stream();
}
/**
* Used in imperative code examples that need to iterate over a list
*/
public List<Track> getTrackList() {
return unmodifiableList(tracks);
}
/**
* @return the musicians
*/
public Stream<Artist> getMusicians() {
return musicians.stream();
}
/**
* Used in imperative code examples that need to iterate over a list
*/
public List<Artist> getMusicianList() {
return unmodifiableList(musicians);
}
public Artist getMainMusician() {
return musicians.get(0);
}
public Album copy() {
List<Track> tracks = getTracks().map(Track::copy).collect(toList());
List<Artist> musicians = getMusicians().map(Artist::copy).collect(toList());
return new Album(name, tracks, musicians);
}
}
假定选定一组专辑, 找出其中所有长度大于 1 分钟的曲目名称.
//遗留代码, 使用for循环
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
for(Album album : albums) {
for (Track track : album.getTrackList()) {
if (track.getLength() > 60) {
String name = track.getName();
trackNames.add(name);
}
}
}
return trackNames;
}
第一步要修改的是 for 循环. 首先使用 Stream 的 forEach 方法替换掉 for 循环.
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.forEach(album -> {
album.getTracks()
.forEach(track -> {
if (track.getLength() > 60) {
String name = track.getName();
trackNames.add(name);
}
});
});
return trackNames;
}
第二步, 将内部的 forEach 方法用Stream代替.
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.forEach(album -> {
album.getTracks()
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.forEach(name -> trackNames.add(name));
});
return trackNames;
}
第三步,用flatMap替换第一个foreach.
public Set<String> findLongTracks(List<Album> albums) {
Set<String> trackNames = new HashSet<>();
albums.stream()
.flatMap(album -> album.getTracks())
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.forEach(name -> trackNames.add(name));
return trackNames;
}
第四步, 用collect()方法替换最后的forEach.
public Set<String> findLongTracks(List<Album> albums) {
return albums.stream()
.flatMap(album -> album.getTracks())
.filter(track -> track.getLength() > 60)
.map(track -> track.getName())
.collect(Collectors.toSet());
}
5.练习
a.编写一个求和函数, 计算流中所有数之和;
public int addUp(Stream<Integer> numbers) {
return numbers.reduce(0, (acc, x) -> acc + x);
}
b.编写一个函数, 接受艺术家列表作为参数, 返回一个字符串列表, 其中包含艺术家的姓名和国籍;
public static List<String> getNamesAndOrigins(List<Artist> artists) {
return artists.stream().flatMap(artist -> Stream.of(artist.getName(), artist.getNationality()))
.collect(Collectors.toList());
}
c.修改如下代码,将外部迭代转换成内部迭代;
int totalMembers = 0;
for (Artist artist : artists) {
Stream<Artist> members = artist.getMembers();
totalMembers += members.count();
}
artists.stream().map(artist -> artist.getMembers().count()).reduce(0L, Long::sum).intValue();
d.在一个字符串列表中, 找出包含最多小写字母的字符串.
public static Optional<String> mostLowercaseString(List<String> strings) {
return strings.stream().max(Comparator.comparing(string -> (int) string.chars().filter(Character::isLowerCase).count()));
}
e.只用 reduce 和 Lambda 表达式写出实现 Stream 上的 map 操作的代码, 如果不想返回 Stream, 可以返回一个 List.
public static <I, O> List<O> map(Stream<I> stream, Function<I, O> mapper) {
return stream.reduce(new ArrayList<O>(), (acc, x) -> {
List<O> newAcc = new ArrayList<>(acc);
newAcc.add(mapper.apply(x));
return newAcc;
}, (List<O> left, List<O> right) -> {
List<O> newleft = new ArrayList<>(left);
newleft.addAll(right);
return newleft;
});
}
此处关于reduce可以参考https://segmentfault.com/q/1010000004944450.
f.只用 reduce 和 Lambda 表达式写出实现 Stream 上的 filter 操作的代码, 如果不想返回 Stream, 可以返回一个 List.
public static <O> List<O> filter(Stream<O> stream, Predicate<O> mapper) {
return stream.reduce(new ArrayList<O>(), (acc, x) -> {
if (mapper.test(x)) {
List<O> newAcc = new ArrayList<>(acc);
newAcc.add(x);
return newAcc;
}
return acc;
}, (List<O> left, List<O> right) -> {
List<O> newleft = new ArrayList<>(left);
newleft.addAll(right);
return newleft;
});
}
网友评论