zjjfly's blog

Java,Clojure,Scala...

0%

快学Java8 第三章

延迟执行

所有的lambda表达式都是延迟执行的.如果你希望立刻执行,就没必要使用lambda表达式了.延迟执行的原因有很多,例如:

  • 在另一个线程中运行
  • 多次运行代码
  • 在某个算法的正确时间点上运行代码(例如排序算法中的比较操作)
  • 只有在需要的时候运行代码

当你使用lambda的时候,要考虑一下希望达到什么效果.来看一个简单的例子.假如你要记录一个事件

1
logger.info("x: "+x+",y: "+y);

如果日志级别设置成忽略INFO消息的话,这个字符串还是会被计算并传递给info方法,然后再确定是否打印日志.为什么不能先确定是否要打印,然后再把字符串拼接起来呢?这种情景就需要使用lambda了.惯用的办法是将代码包装成一个无参数的lambda表达式:

1
()->"x: "+x = ",y: "+y

现在我们需要编写一个方法,它可以接受lambda表达式,检查它是否应该被执行,如果需要的话才执行.要接受lambda表达式,需要选择一个函数式接口,这里我们选择Supplier<String>.

1
2
3
4
5
public  static void info(Logger logger,Supplier<String> message){
if(logger.isLoggable(Level.INFO)){
logger.info(message.get());
}
}

延迟记录日志消息是一个很好的想法,在Java8的API中,java.util.logging.Loggerinfo方法和其他日志方法都可以接受Supplier<String>作为参数.

lambda表达式的参数

下面的方法接受一个action,并对其多次重复执行这个action.

1
2
3
4
5
private static void repeat(int n, IntConsumer action) {
for (int i = 0; i < n; i++) {
action.accept(i);
}
}

为什么使用IntConsumer而不是Runnable对象呢,因为我们可能需要告诉action它发生在哪次迭代.

1
repeat(10, i -> System.out.println("Countdown: "+(9-i)));

另一个例子是时间监听器:

1
button.setOnAction(event -> action);

event对象包含了action可能需要的信息.
一般来说,在设计方法的时候会希望把它需要的所有信息作为参数传递进去.但是,如果这些参数很少用到,那么可以考虑第二个版本,不强制用户传递那些不需要的参数.

1
2
3
4
5
private static void repeat(int n, Runnable action) {
for (int i = 0; i < n; i++) {
action.run();
}
}

选择一个函数式接口

在大多数情况下,函数类型是结构化的.例如把两个字符串映射为一个整数的函数,需要使用一个类似于Function2<String,String,Integer>或者(String,Stirng)->int类型.不过在Java中,你需要使用Comparator<String>这样的函数式接口来表明函数的目的,在编程语言的理论中,这被称为名义类型.
当然,在很多情况下,我们希望接受任意的函数,而不是某种特定语义的函数.因此,Java8提供很多一般的函数类型,我们要尽可能使用它们.

假如你想要编写一个匹配特定条件的文件,应当使用描述性的`java.io.FileFilter`类,还是一个`Predicate`呢?强烈推荐使用`Predicate`.只有一种情况下可以不使用它,那就是你已经有了很多生成`FileFilter`实例的方法.**选择函数式接口的一个原则是选择尽量通用的** 大多数标准的函数式接口都拥有用来生成或组合函数的静态方法.例如,`Predicate.isEqual(a)`同`a::equals`一样(假设a不为null).此外,它们还有用来组合`Predicate`的默认方法`and`,`or`,`negate`.例如`Predicate.is(a).or(Predicate.isEqual(b))`同`x->a.equals(x)||b.equals(x)`一样. 下面列举了专门为原始类型int,long和double提供的函数式接口.用这些函数式接口可以减少自动装箱.
有些时候,**标准库中找不到合适的接口,你才需要自己创建一个函数式接口**.例如你想要修改一种图片的颜色,需要实现一个根据像素在图片中的位置来计算新的颜色,那么你可以这样定义接口:
1
2
3
public interface ColorTransformer{
Color apply(int x,int y,Color colorAtXY);
}

返回函数

Java8中所谓的返回一个函数实际是返回一个函数式接口的对象:

1
2
3
static UnaryOperator<Color> brighten(double factor) {
return c -> c.deriveColor(0, 1, factor, 1);
}

组合函数

如果一个函数的输出是另一个函数的输入,那么可以把这两个函数组合起来.下面是例子:

1
2
3
static <T> UnaryOperator<T> compose(UnaryOperator<T> op1,UnaryOperator<T> op2){
return t->op2.apply(op1.apply(t));
}

延迟和并行操作

如果一个对象要频繁的进行转换操作,类似Stream,那么最好让这些操作延迟执行.如果一个函数式接口经常被调用,那么应该考虑是否可以使用并行来实现.

处理异常

函数式接口通常不允许检查异常,当然,你也可以使用允许检查异常的函数式接口作为参数,如Callable<T>.还有一种方法是使用一个包装器把会抛出检查异常的函数式接口转换成只抛出非检查异常的接口.

1
2
3
4
5
6
7
8
9
10
11
12
13
static <T> Supplier<T> unchecked(Callable<T> f) {
return () -> {
try {
return f.call();
} catch (Exception e) {
//把所有的Exception变为运行时异常抛出
throw new RuntimeException(e);
} catch (Throwable t) {
//所有的Error直接抛出
throw t;
}
};
}

lambda和泛型

如果需要在方法中接受带泛型的函数式接口,需要遵守下面的几条规则:

  • 如果T是函数参数,但不是函数返回值类型,那么在T前面加? super ,类似Scala中的逆变
  • 如果是返回值类型,但不是函数参数类型,那么在R前面加? extends ,类似Scala的协变
  • 如果T即是参数又是返回值类型,那么不要加任何东西.

单子操作

在你设计类型G<T>和函数T->U的时候,想一下定义一个生成G<U>的方法map是否有意义.如果合适,也可以为函数T->G<U>提供flatMap方法.