关于Lambda表达式与函数式接口的技巧与最佳实践

2019-07-13  本文已影响0人  梨涡贱笑

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表达式和函数表达式的最佳实践。虽然这些新特性功能强大,但它们也是工具,每个开发者在使用时均需多加注意。

上一篇下一篇

猜你喜欢

热点阅读