1.2 什么是函数式编程
每个人对函数式编程的理解不尽相同。但其核心是:在思考问题时,使用不可变值和函 数,函数对一个值进行处理,映射成另一个值。
第二章 Lambda表达式
2.2 辨别Lambda表达式
Runnable noArguments = () -> System.out.println("Hello World”);
ActionListener oneArgument = event -> System.out.println("button clicked”);
Runnable multiStatement = () -> {
System.out.print("Hello");
System.out.println(" World");
};
BinaryOperator<Long> add = (x, y) -> x + y;
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
上述例子还隐含了另外一层意思:Lambda 表达式的类型依赖于上下文环境,是由编译器 推断出来的。
2.3 引用值,而不是变量
final String name = getUserName(); button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
System.out.println("hi " + name);
}
});
Java 8虽然放松了这一限制,可以引用非final变量,但是该变量在既成事实上必须是 final
//这里编译不会通过
String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));
这种行为也解释了为什么 Lambda 表达式也被称为闭包。未赋值的变量与周边环境隔离起 来,进而被绑定到一个特定的值
总而言之, lambda 引用的是值 而非变量
2.4 函数接口
使用只有一个方法的接口来表示某特定方法并反复使用,是很早就有的习惯。使用 Swing 编写过用户界面的人对这种方式都不陌生,这里无需再标新立 异,Lambda 表达式也使用同样的技巧,并将这种接口称为函数接口
2.5 类型推断
javac 根据 Lambda 表达式上下文信息 就能推断出参数的正确类型。程序依然要经过类型检查来保证运行的安全性,但不用再显 式声明类型罢了。这就是所谓的类型推断。
BinaryOperator<Long> add = (x, y) -> x + y;
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;
2.6 要点回顾
- Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。
- Lambda 表达式的常见结构:BinaryOperator add = (x, y) → x + y。
- 函数接口指仅具有单个抽象方法的接口,用来表示Lambda表达式的类型。
3.1 从外部迭代到内部迭代
3.2 实现机制
1、惰性求值法
如下,不会出现打印结果
allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName());
return artist.isFrom("London");
}
);
2、及早求值法
如下,会打印结果
long count = allArtists.stream()
.filter(artist -> {
System.out.println(artist.getName());
return artist.isFrom("London");
}).count();
使用这些操作的理 想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,这正是 它的合理之处。计数的示例也是这样运行的
3.3 常用的流操作
3.3.1 collect(toList())
List<String> collected = Stream.of("a", "b", "c") .collect(Collectors.toList());
assertEquals(Arrays.asList("a", "b", "c"), collected);
这个例子也展示了本节中所有示例代码的通用格式。首先由列表生成一个 Stream ,然后 进行一些 Stream 上的操作,继而是 collect 操作,由 Stream 生成列表,最后使用断言 判断结果是否和预期一致。
3.3.2 map
使用普通的方式将数组中的数据改成大写
List<String> collected = new ArrayList<>();
for (String string : asList("a", "b", "hello")) {
String uppercaseString = string.toUpperCase();
collected.add(uppercaseString);
}
assertEquals(asList("A", "B", "HELLO"), collected);
使用stream.map
List<String> collected = Stream.of("a", "b", "hello").map(string -> string.toUpperCase()).collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);
看源码map的传参是function,正好适用将一个值变为另外一个值的场景
3.3.3 filter
filter的传参是Predicate接口,传入一个值,返回一个boolean判断,适用于筛选的场景
3.3.4 flatmap
flatMap 方法可用 Stream 替换值,然后将多个 Stream 连接成一个 Stream
List list = Stream.of(Arrays.asList("a,b,c"),Arrays.asList("d,e,f")).flatMap(strings -> {
return strings.stream().map(string-> string.toUpperCase());
}).collect(Collectors.toList());
System.out.println(list);
flatMap 方法的相关函数接口和 map 方法的一样,都是 Function 接口,只是方法的返回值 限定为 Stream 类型罢了。
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);
3.3.5 max和min
List<Track> tracks = asList(new Track("Bakai", 524),
new Track("Violets for Your Furs", 378),
new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
.min(Comparator.comparing(track -> track.getLength()))
.get();
assertEquals(tracks.get(1), shortestTrack);
这里的stream调用 max 和 min 会获得一个 Optional对象,调用get才会获得具体的值
comparing的源码,最终返回的是一个Comparator函数
public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
Function<? super T, ? extends U> keyExtractor)
{
Objects.requireNonNull(keyExtractor);
return (Comparator<T> & Serializable)
(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}
3.3.7 reduce
使用 reduce 求和
int count = Stream.of(1, 2, 3)
.reduce(0, (acc, element) -> acc + element);
assertEquals(6, count);
3.4 重构遗留代码
/**
* 唱片筛选(参考函数式编程 3。4 重构遗留代码)
*/
public class AlbumScreen {
/**
* 专辑
*/
static class Album{
public Album(String albumName, List<Track> trackList) {
this.albumName = albumName;
this.trackList = trackList;
}
//专辑名称
private String albumName;
//曲目列表
private List<Track> trackList;
public String getAlbumName() {
return albumName;
}
public void setAlbumName(String albumName) {
this.albumName = albumName;
}
public List<Track> getTrackList() {
return trackList;
}
public void setTrackList(List<Track> trackList) {
this.trackList = trackList;
}
}
/**
* 曲目
*/
static class Track{
public Track(String trackName,Long seconds) {
this.trackName = trackName;
this.seconds = seconds;
}
//曲目名称
private String trackName;
//时长 秒
private Long seconds;
public Long getSeconds() {
return seconds;
}
public void setSeconds(Long seconds) {
this.seconds = seconds;
}
public String getTrackName() {
return trackName;
}
public void setTrackName(String trackName) {
this.trackName = trackName;
}
}
/**
* 筛选时长大于60秒以上的曲目
* @param albumList
* @return
*/
public static Set<String> screen(List<Album> albumList){
Set<String> trackNames = new HashSet<>();
for(Album album : albumList) {
for (Track track : album.getTrackList()) {
if (track.seconds > 60) {
String name = track.getTrackName();
trackNames.add(name);
}
}
}
return trackNames;
}
/**
* 筛选时长大于60秒以上的曲目(lambda版)
* @param albumList
* @return
*/
public static Set<String> screenLambda(List<Album> albumList){
return albumList.stream().flatMap(tracks -> tracks.trackList.stream()) //flatmap 将多个流合并成一个
.filter(track -> track.seconds>60) //filter 进行筛选
.map(track -> track.trackName) //map 通过一个值获取另外一个值,这里根据曲目对象获取名称
.collect(Collectors.toSet()); //创建 set
}
public static void main(String[] args){
List<Track> trackList1 =
Stream.of(new Track("燃烧我的卡路里",180L)
,new Track("我已经爱上你",59L)
,new Track("好汉歌",100L))
.collect(Collectors.toList());
List<Track> trackList2 =
Stream.of(new Track("一百万个可能",90L)
,new Track("答案",30L)
,new Track("一个人去巴黎",120L))
.collect(Collectors.toList());
List<Album> albumList =
Arrays.asList(new Album("火箭队",trackList1),new Album("银河队",trackList2));
System.out.println(screen(albumList));
System.out.println(screenLambda(albumList));
}
}
3.8 要点回顾
内部迭代将更多控制权交给了集合类。
和Iterator类似,Stream是一种内部迭代方式。
将Lambda表达式和Stream上的方法结合起来,可以完成很多常见的集合操作。
4 类库
4.1 在代码中使用lambda表达式
使用 isDebugEnabled 方法降低日志性能开销
Logger logger = new Logger(); if (logger.isDebugEnabled()) {
logger.debug("Look at this: " + expensiveOperation());
}
//使用lambda表达式简化日志
Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());
public void debug(Supplier<String> message) { if (isDebugEnabled()) {
debug(message.get());
}
}
4.2 基本类型
由于装箱类型是对象,因此在内存中存在额外开销。比如,整型在内存中占用 4 字节,整型对象却要占用 16 字节。这一情况在数组上更加严重,整型数组中的每个元素 只占用基本类型的内存,而整型对象数组中,每个元素都是内存中的一个指针,指向 Java 堆中的某个对象。在最坏的情况下,同样大小的数组,Integer[] 要比 int[] 多占用 6 倍 内存。
为了减小这些性能开销,Stream 类的某些方法对基本类型和装箱类型做了区分。图 4-1 所 示的高阶函数mapToLong和其他类似函数即为该方面的一个尝试
//Stream 中的源码
LongStream mapToLong(ToLongFunction<? super T> mapper);
//LongStream 中的源码
<U> Stream<U> mapToObj(LongFunction<? extends U> mapper);
4.3 重载解析
Lambda 表达式作为参数时,其类型由它的目标类型推导得出,推导过程遵循 如下规则:
? 如果只有一个可能的目标类型,由相应函数接口里的参数类型推导得出;
? 如果有多个可能的目标类型,由最具体的类型推导得出;
? 如果有多个可能的目标类型且最具体的类型不明确,则需人为指定类型。
4.4 @FunctionalInterface
该注释会强制 javac 检查一个接口是否符合函数接口的标准。如果该注释添加给一个枚举 类型、类或另一个注释,或者接口包含不止一个抽象方法,javac 就会报错。重构代码时, 使用它能很容易发现问题。
4.6 默认方法
因为接口的改造,接口方法的增加,会导致用旧的jdk编译的类有不兼容的问题,所以采用了default关键字,接口提供一个默认的实现方法
默认方法示例:forEach 实现方式
default void forEach(Consumer<? super T> action) {
for(Tt:this){
action.accept(t);
}
}
和类不同,接口没有成员变量,因此默认方法只能通过调用子类的方法来修改子类本身, 避免了对子类的实现做出各种假设。
4.7 多重继承
public interface Jukebox {
public default String rock() { return "... all over the world!";
}
}
public interface Carriage {
public default String rock() { return "... from side to side";
}
}
public class MusicalCarriage
implements Carriage, Jukebox {
@Override
public String rock() {
return Carriage.super.rock();
}
}
javac 并不明确应该继承哪个接口中的方法,因此编译器会报错:class Musical Carriage inherits unrelated defaults for rock() from types Carriage and Jukebox。当然,在类 中实现 rock 方法就能解决这个问题
三定律
如果对默认方法的工作原理,特别是在多重继承下的行为还没有把握,如下三条简单的定 律可以帮助大家。
- 类胜于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义 的方法。
- 子类胜于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法, 那么子类中定义的方法胜出。
- 没有规则三。如果上面两条规则不适用,子类要么需要实现该方法,要么将该方法声明 为抽象方法。
其中第一条规则是为了让代码向后兼容。
4.9 接口的静态方法
Stream 是个接口, Stream.of是接口的静态方法。这也是Java 8中添加的一个新的语言特性,旨在帮助编写 类库的开发人员,但对于日常应用程序的开发人员也同样适用。
4.10 Optional
Optional 是为核心类库新设计的一个数据类型,用来替换 null 值。
使用 Optional 对象有两个目的:首先,Optional 对象鼓励程序员适时检查 变量是否为空,以避免代码缺陷;其次,它将一个类的 API 中可能为空的值文档化,这比 阅读实现代码要简单很多。
//创建某个值的 Optional 对象
Optional<String> a = Optional.of("a");
assertEquals("a", a.get());
//创建一个空的 Optional 对象,并检查其是否有值
Optional emptyOptional = Optional.empty();
Optional alsoEmpty = Optional.ofNullable(null);
assertFalse(emptyOptional.isPresent());
//使用 orElse 和 orElseGet 方法
assertEquals("b", emptyOptional.orElse("b"));
assertEquals("c", emptyOptional.orElseGet(() -> "c"));
4.11 要点回顾
? 使用为基本类型定制的Lambda表达式和Stream,如IntStream可以显著提升系统性能。
? 默认方法是指接口中定义的包含方法体的方法,方法名有default关键字做前缀。
? 在一个值可能为空的建模情况下,使用Optional对象能替代使用null值。