美文网首页
关于Lambda表达式与函数式接口的技巧与最佳实践

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

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

    相关文章

      网友评论

          本文标题:关于Lambda表达式与函数式接口的技巧与最佳实践

          本文链接:https://www.haomeiwen.com/subject/vowpkctx.html