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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容