1.概览
随着Java 8的广泛使用,开始有人为其新增特性总结最佳实践,在本教程中,我们来讨论一下函数式接口与Lambda表达式。
2.使用标准的函数式接口
java.util.function包的函数式接口,满足了大部分程序员在使用Lambda表达式和方法引用时,对目标类型的需求。这些抽象的接口可以轻松适配大部分Lambda表达式。在创建新的函数表达式前,开发者应该好好研究一下这个包。
假设有一个叫Foo的接口:
@FunctionalInterface
public interface Foo {
String method(String string);
}
和一个UseFoo类,里面有add()的方法,它使用Foo接口作为参数。
public String add(String string, Foo foo) {
return foo.method(string);
}
你可能会这样执行方法:
Foo foo = parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", foo);
仔细检查代码,你会发现Foo仅仅是接受一个参数并返回结果的函数。Java已在java.util.function包中的[Function<T,R>]提供同样接口。
现在我们可以完全删除Foo,并把代码改为:
public String add(String string, Function<String, String> fn) {
return fn.apply(string);
}
然后这样执行方法:
Function<String, String> fn =
parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", fn);
3.使用@FunctionalInterface注解你的函数式接口。在开始时,该注解似乎并无意义——哪怕不加注解,只要接口有且仅有一个抽象方法,它就会被看成函数式接口。
但是假设现在有个大项目,其中包含多个接口,这时就很难把控全局。一个本被设计为函数式接口的接口,或会因被意外加上其他抽象方法,而失去了函数式接口的功能。
而使用@FunctionalInterface注解后,每当编译器发现任何试图破坏函数式接口结构的改动,就会报错。这样一来,其他开发者就能轻松理解该项目的结构。
所以,请这样写:
@FunctionalInterface
public interface Foo {
String method();
}
而非这样:
public interface Foo {
String method();
}
4.不要滥用函数式接口的默认方法
你可以轻而易举地在函数式接口中添加默认方法,只要遵守“接口只含一个抽象方法”的规定,就不会有问题:
@FunctionalInterface
public interface Foo {
String method();
default void defaultMethod() {}
}
如果抽象方法的方法签名一样,函数式接口就可以被其他函数式接口继承。例如:
@FunctionalInterface
public interface FooExtended extends Baz, Bar {}
@FunctionalInterface
public interface Baz {
String method();
default void defaultBaz() {}
}
@FunctionalInterface
public interface Bar {
String method();
default void defaultBar() {}
}
与普通接口一样,使用同一默认方法继承不同的函数式接口会产生许多问题。例如,假设Bar 和 Baz各有一个叫defaultCommon()的默认方法,这样就会发生编译时错误:
interface Foo inherits unrelated defaults for defaultCommon() from types Baz and Bar...
你需要在Foo 接口中,覆盖defaultCommon() 方法才能修复该问题。当然,你也可以为该方法提供自定义实现。但如果你想使用其中一个父类接口的实现(例如,Baz接口),就需要在defaultCommon()方法体中添加如下代码:
Baz.super.defaultCommon();
但要小心,在接口中增加太多默认方法,会带来架构上的混乱。你应把默认方法看成在既要更新已有的接口,又要保持原有兼容性时,一种无可奈何的折衷。
5.使用Lambda表达式实例化函数式接口
编译器允许你使用内部类实例化函数式接口,不过这样会导致代码繁琐,使用Lambda是更好的选择:
Foo foo = parameter -> parameter + " from Foo";
而不是这样:
Foo fooByIC = new Foo() {
@Override
public String method(String string) {
return string + " from Foo";
}
};
Lambda表达式对很多旧的库都有效。例如是Runnable,Comparator之类。但这不等于需要你把旧的代码全部改为Lambda。
6.避免重载参数带有函数式接口的方法
使用不同的方法名去避免冲突;来看看一个例子:
public interface Processor {
String process(Callable<String> c) throws Exception;
String process(Supplier<String> s);
}
public class ProcessorImpl implements Processor {
@Override
public String process(Callable<String> c) throws Exception {
// implementation details
}
@Override
public String process(Supplier<String> s) {
// implementation details
}
}
初看之下貌似并无异样,但只要试图执行ProcessorImpl下面的其中一个方法:
String result = processor.process(() -> "abc");
就会出现如下错误信息:
reference to process is ambiguous
both method process(java.util.concurrent.Callable<java.lang.String>)
in com.baeldung.java8.lambda.tips.ProcessorImpl
and method process(java.util.function.Supplier<java.lang.String>)
in com.baeldung.java8.lambda.tips.ProcessorImpl match
我们可以用两个方法解决这个问题。第一,使用不同的方法名:
String processWithCallable(Callable<String> c) throws Exception;
String processWithSupplier(Supplier<String> s);
第二是手工转型,不推荐这样做。
String result = processor.process((Supplier<String>) () -> "abc");
7.不要把Lambda看成是内部类
之前的例子里,我们使用Lambda替代内部类,但两者有个很大的不同点:域。
在创建内部类时,也创造了一个新的域。你可以在私有域中,新建名称相同的本地变量。你还可以在内部类使用this关键字代指该(内部类的)实例。
例如,类UseFoo有一个实例变量:
private String value = "Enclosing scope value";
然后在这个类写下如下代码并执行:
public String scopeExperiment() {
Foo fooIC = new Foo() {
String value = "Inner class value";
@Override
public String method(String string) {
return this.value;
}
};
String resultIC = fooIC.method("");
Foo fooLambda = parameter -> {
String value = "Lambda value";
return this.value;
};
String resultLambda = fooLambda.method("");
return "Results: resultIC = " + resultIC +
", resultLambda = " + resultLambda;
}
执行scopeExperiment()方法会得到如下结果:
Results: resultIC = Inner class value, resultLambda = Enclosing scope value
如你所见,fooIC中的this.value返回其内部类的本地变量。Lambda的this.value却对Lambda方法体内的值视若无睹,返回了UseFoo类的同名变量值。
8.让Lambda保持简洁易懂
如情况允许,尽可能用单行结构,而非一大块代码。要记住,Lambda是表达式,而非叙述体。虽然结构简单,但Lambda应该清晰明了。
这仅仅是代码风格建议,虽然它并不会大幅提高性能,但这种风格让代码更易阅读,更亲和。
8.1 避免在Lambda方法体内使用代码块
理想情况下,Lambda应该是一行而就。这种结构让它清晰易懂,别人能明白它使用什么数据(在Lambda有参数的情况下),干了什么事情。
如果你使用了代码块,Lambda的功能就变得不那么显而易见。
带着上面思路,看如下代码:
Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
String result = "Something " + parameter;
//many lines of code
return result;
}
而不是:
Foo foo = parameter -> { String result = "Something " + parameter;
//many lines of code
return result;
};
但是,也无需把“Lambda只需一行”视为教条。如果只有两三行代码,或许没必要把这些代码抽出来化为方法。
8.2 避免指定参数类型
在大部分情况下,编译器使用类型判断功能足以得知Lambda的参数类型。因此,可忽略参数中类型。
应该这样:
(a, b) -> a.toLowerCase() + b.toLowerCase();
而不是这样:
(String a, String b) -> a.toLowerCase() + b.toLowerCase();
8.3 单参数时,无需使用括号
根据Lambda语法,只有在多个参数,或者完全没有参数时,才需要使用括号。所以,如果只有一个参数,可大胆的把括号去掉,简化代码。
应该这样:
a -> a.toLowerCase();
而不是这样:
(a) -> a.toLowerCase();
8.4 避免使用大括号和Return
在Lambda的单行方法中,大括号和Return是可选项。为了简洁,可忽略掉。
应该这样:
a -> a.toLowerCase();
而不是这样:
a -> {return a.toLowerCase()};
8.5 使用方法引用
在之前的例子中,Lambda往往只是调用在别处已经实现的方法。如此一来,我们便可以使用Java8的另一个特性:方法引用。
因此,这句Lambda:
a -> a.toLowerCase();
可替换成:
String::toLowerCase;
或许代码短不了多少,但这样更易懂。
9.使用“有效final”变量
在Lambda表达式中,访问非final变量会导致编译错误。但这不等于你要把所有变量都改为final。
根据“有效final”概念,只要某个变量只被赋值一次,它就会看成是final变量。
编译器会控制Lambda内的变量状态,但凡发现任何更改变量的意图,就会抛出编译错误,所以可大胆的在Lambda内使用变量。
例如,以下的代码无法通过编译:
public void method() {
String localVariable = "Local";
Foo foo = parameter -> {
String localVariable = parameter;
return localVariable;
};
}
编译器会告诉你:
Variable 'localVariable' is already defined in the scope.
这个功能会让Lambda执行时变得线程安全。
10.防止变量发生更变
Lambda的其中一个主要用途就是并发计算——这意味着它们在线程安全上能大派用场。
“有效final”特性虽能杜绝大部分问题,但凡事皆有例外。
Lambda方法体内虽无法改变变量的值,但却可改变可变对象的状态。
思考如下代码:
int[] total = new int[1];
Runnable r = () -> total[0]++;
r.run();
这段代码是非法的,虽然total变量属于“有效final”。但在执行Lambda后,它指向的还是同一个引用状态吗?不!
以该段代码为鉴,避免写出会产生不可预料结果的状态更变。
11.结论
在该教程中,我们介绍了一些Java8 Lambda表达式和函数表达式的最佳实践。虽然这些新特性功能强大,但它们也是工具,每个开发者在使用时均需多加注意。