Lambda Expressions and Functional Interfaces: Tips and Best Practices – Lambda表达式和功能接口 提示和最佳实践

最后修改: 2016年 1月 9日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Now that Java 8 has reached wide usage, patterns and best practices have begun to emerge for some of its headlining features. In this tutorial, we’ll take a closer look at functional interfaces and lambda expressions.

现在,Java 8已经得到了广泛的应用,它的一些主要功能的模式和最佳实践已经开始出现了。在本教程中,我们将仔细研究功能接口和lambda表达式。

2. Prefer Standard Functional Interfaces

2.倾向于标准功能接口

Functional interfaces, which are gathered in the java.util.function package, satisfy most developers’ needs in providing target types for lambda expressions and method references. Each of these interfaces is general and abstract, making them easy to adapt to almost any lambda expression. Developers should explore this package before creating new functional interfaces.

聚集在java.util.function包中的功能接口,满足了大多数开发者为lambda表达式和方法引用提供目标类型的需求。这些接口中的每一个都是通用的和抽象的,使得它们很容易适应几乎所有的lambda表达式。开发者在创建新的函数接口之前,应该探索这个包。

Let’s consider an interface Foo:

让我们考虑一个接口 Foo

@FunctionalInterface
public interface Foo {
    String method(String string);
}

In addition, we have a method add() in some class UseFoo, which takes this interface as a parameter:

此外,我们有一个方法add()在某个类UseFoo中,它把这个接口作为一个参数。

public String add(String string, Foo foo) {
    return foo.method(string);
}

To execute it, we would write:

为了执行它,我们会写。

Foo foo = parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", foo);

If we look closer, we’ll see that Foo is nothing more than a function that accepts one argument and produces a result. Java 8 already provides such an interface in Function<T,R> from the java.util.function package.

如果我们仔细观察,我们会发现Foo只不过是一个接受一个参数并产生一个结果的函数。Java 8已经在Function<T,R>中提供了这样一个接口,它来自java.util.function包。

Now we can remove interface Foo completely and change our code to:

现在我们可以完全删除接口Foo,将我们的代码改为。

public String add(String string, Function<String, String> fn) {
    return fn.apply(string);
}

To execute this, we can write:

为了执行这个,我们可以写。

Function<String, String> fn = 
  parameter -> parameter + " from lambda";
String result = useFoo.add("Message ", fn);

3. Use the @FunctionalInterface Annotation

3.使用@FunctionalInterface 注释

Now let’s annotate our functional interfaces with @FunctionalInterface. At first, this annotation seems to be useless. Even without it, our interface will be treated as functional as long as it has just one abstract method.

现在让我们用@FunctionalInterface来注释我们的函数式接口。 起初,这个注释似乎没有用。即使没有它,只要我们的接口只有一个抽象方法,就会被视为功能性接口。

However, let’s imagine a big project with several interfaces; it’s hard to control everything manually. An interface, which was designed to be functional, could accidentally be changed by adding another abstract method/methods, rendering it unusable as a functional interface.

然而,让我们想象一个有多个接口的大项目;很难手动控制所有的东西。一个被设计成功能性的接口,可能会不小心被添加另一个抽象方法/方法而改变,从而导致它无法作为一个功能性接口使用。

By using the @FunctionalInterface annotation, the compiler will trigger an error in response to any attempt to break the predefined structure of a functional interface. It is also a very handy tool to make our application architecture easier to understand for other developers.

通过使用@FunctionalInterface注解,编译器将对任何试图破坏功能接口预定义结构的行为触发一个错误。这也是一个非常方便的工具,可以使我们的应用架构更容易被其他开发者理解。

So we can use this:

所以我们可以用这个。

@FunctionalInterface
public interface Foo {
    String method();
}

Instead of just:

而不仅仅是。

public interface Foo {
    String method();
}

4. Don’t Overuse Default Methods in Functional Interfaces

4.不要在功能接口中过度使用默认方法

We can easily add default methods to the functional interface. This is acceptable to the functional interface contract as long as there is only one abstract method declaration:

我们可以很容易地在功能接口中添加默认方法。只要只有一个抽象方法声明,这对函数式接口合同来说是可以接受的。

@FunctionalInterface
public interface Foo {
    String method(String string);
    default void defaultMethod() {}
}

Functional interfaces can be extended by other functional interfaces if their abstract methods have the same signature:

如果功能接口的抽象方法具有相同的签名,则可以由其他功能接口扩展。

@FunctionalInterface
public interface FooExtended extends Baz, Bar {}
	
@FunctionalInterface
public interface Baz {	
    String method(String string);	
    default String defaultBaz() {}		
}
	
@FunctionalInterface
public interface Bar {	
    String method(String string);	
    default String defaultBar() {}	
}

Just as with regular interfaces, extending different functional interfaces with the same default method can be problematic.

就像普通接口一样,用相同的默认方法扩展不同的功能接口可能会有问题

For example, let’s add the defaultCommon() method to the Bar and Baz interfaces:

例如,让我们将defaultCommon()方法添加到BarBaz接口。

@FunctionalInterface
public interface Baz {
    String method(String string);
    default String defaultBaz() {}
    default String defaultCommon(){}
}

@FunctionalInterface
public interface Bar {
    String method(String string);
    default String defaultBar() {}
    default String defaultCommon() {}
}

In this case, we’ll get a compile-time error:

在这种情况下,我们会得到一个编译时错误。

interface FooExtended inherits unrelated defaults for defaultCommon() from types Baz and Bar...

To fix this, the defaultCommon() method should be overridden in the FooExtended interface. We can provide a custom implementation of this method; however, we can also reuse the implementation from the parent interface:

为了解决这个问题,defaultCommon()方法应该在FooExtended接口中被覆盖。我们可以为这个方法提供一个自定义的实现;但是,我们也可以重新使用来自父接口的实现

@FunctionalInterface
public interface FooExtended extends Baz, Bar {
    @Override
    default String defaultCommon() {
        return Bar.super.defaultCommon();
    }
}

It’s important to note that we have to be careful. Adding too many default methods to the interface is not a very good architectural decision. This should be considered a compromise, only to be used when required for upgrading existing interfaces without breaking backward compatibility.

需要注意的是,我们必须要小心谨慎。为接口添加过多的默认方法并不是一个很好的架构决策。这应该被认为是一种妥协,只有在需要升级现有接口而不破坏后向兼容性时才会使用。

5. Instantiate Functional Interfaces With Lambda Expressions

5.用Lambda表达式实例化功能接口

The compiler will allow us to use an inner class to instantiate a functional interface; however, this can lead to very verbose code. We should prefer to use lambda expressions:

编译器会允许我们使用一个内部类来实例化一个功能接口;然而,这可能会导致非常冗长的代码。我们应该更倾向于使用lambda表达式。

Foo foo = parameter -> parameter + " from Foo";

Over an inner class:

在一个内部类之上。

Foo fooByIC = new Foo() {
    @Override
    public String method(String string) {
        return string + " from Foo";
    }
};

The lambda expression approach can be used for any suitable interface from old libraries. It is usable for interfaces like Runnable, Comparator, and so on; however, this doesn’t mean that we should review our whole older code base and change everything.

lambda表达式方法可用于旧库中任何合适的接口。它可用于RunnableComparator等接口;h然而,这并不意味着我们应该审查我们整个旧代码库并改变一切。

6. Avoid Overloading Methods With Functional Interfaces as Parameters

6.避免用功能接口作为参数的重载方法

We should use methods with different names to avoid collisions:

我们应该使用不同名称的方法来避免碰撞。

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
    }
}

At first glance, this seems reasonable, but any attempt to execute either of the ProcessorImpl‘s methods:

乍一看,这似乎是合理的,但任何试图执行ProcessorImpl的方法的行为。

String result = processor.process(() -> "abc");

Ends with an error with the following message:

以一个错误结束,其信息如下。

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

To solve this problem, we have two options. The first option is to use methods with different names:

为了解决这个问题,我们有两个选择。第一个选项是使用不同名称的方法:

String processWithCallable(Callable<String> c) throws Exception;

String processWithSupplier(Supplier<String> s);

The second option is to perform casting manually, which is not preferred:

第二种选择是手动执行铸造,这并不是首选。

String result = processor.process((Supplier<String>) () -> "abc");

7. Don’t Treat Lambda Expressions as Inner Classes

7.不要将Lambda表达式视为内类

Despite our previous example, where we essentially substituted inner class by a lambda expression, the two concepts are different in an important way: scope.

尽管我们之前的例子中,我们基本上是用lambda表达式代替了内层类,但这两个概念在一个重要方面是不同的:范围。

When we use an inner class, it creates a new scope. We can hide local variables from the enclosing scope by instantiating new local variables with the same names. We can also use the keyword this inside our inner class as a reference to its instance.

当我们使用一个内层类时,它会创建一个新的作用域。我们可以通过实例化相同名称的新的局部变量来将局部变量从包围的作用域中隐藏。我们还可以在我们的内类中使用关键字this作为对其实例的引用。

Lambda expressions, however, work with enclosing scope. We can’t hide variables from the enclosing scope inside the lambda’s body. In this case, the keyword this is a reference to an enclosing instance.

然而,Lambda表达式是在封闭的范围内工作的。我们不能在lambda的主体中隐藏来自包围范围的变量。在这种情况下,关键字this是对一个封闭实例的引用。

For example, in the class UseFoo, we have an instance variable value:

例如,在类UseFoo中,我们有一个实例变量value:

private String value = "Enclosing scope value";

Then in some method of this class, place the following code and execute this method:

然后在这个类的某个方法中,放置以下代码并执行这个方法。

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;
}

If we execute the scopeExperiment() method, we’ll get the following result: Results: resultIC = Inner class value, resultLambda = Enclosing scope value

如果我们执行scopeExperiment()方法,我们会得到以下结果。结果: resultIC = 内部类值, resultLambda = 包围范围值

As we can see, by calling this.value in IC, we can access a local variable from its instance. In the case of the lambda, this.value call gives us access to the variable value, which is defined in the UseFoo class, but not to the variable value defined inside the lambda’s body.

我们可以看到,通过在IC中调用this.value,我们可以从其实例中访问一个局部变量。在lambda的例子中,this.value的调用让我们可以访问变量value,它被定义在UseFoo类中,但不能访问定义在lambda主体中的变量value

8. Keep Lambda Expressions Short and Self-explanatory

8.保持Lambda表达式的简短和自明性

If possible, we should use one line constructions instead of a large block of code. Remember, lambdas should be an expression, not a narrative. Despite its concise syntax, lambdas should specifically express the functionality they provide.

如果可能的话,我们应该使用单行结构而不是大块的代码。记住,lambdas应该是一个表达式,而不是一个叙述式。尽管它的语法简洁,lambdas应该具体表达它们所提供的功能。

This is mainly stylistic advice, as performance will not change drastically. In general, however, it is much easier to understand and to work with such code.

这主要是风格上的建议,因为性能不会有大的改变。然而,一般来说,这样的代码更容易理解和操作。

This can be achieved in many ways; let’s have a closer look.

这可以通过许多方式实现;让我们仔细看看。

8.1. Avoid Blocks of Code in Lambda’s Body

8.1.避免Lambda主体中的代码块

In an ideal situation, lambdas should be written in one line of code. With this approach, the lambda is a self-explanatory construction, which declares what action should be executed with what data (in the case of lambdas with parameters).

在理想情况下,lambdas应该用一行代码来写。通过这种方法,lambda是一个不言自明的结构,它声明了应该用什么数据执行什么动作(在带参数的lambdas的情况下)。

If we have a large block of code, the lambda’s functionality is not immediately clear.

如果我们有一个大的代码块,lambda的功能就不会立即明确。

With this in mind, do the following:

考虑到这一点,请做以下工作。

Foo foo = parameter -> buildString(parameter);
private String buildString(String parameter) {
    String result = "Something " + parameter;
    //many lines of code
    return result;
}

Instead of:

而不是。

Foo foo = parameter -> { String result = "Something " + parameter; 
    //many lines of code 
    return result; 
};

It is important to note, we shouldn’t use this “one-line lambda” rule as dogma. If we have two or three lines in lambda’s definition, it may not be valuable to extract that code into another method.

需要注意的是,我们不应该把这个 “单行lambda “规则当作教条来使用。如果我们在lambda的定义中有两行或三行,那么将这些代码提取到另一个方法中可能没有价值。

8.2. Avoid Specifying Parameter Types

8.2.避免指定参数类型

A compiler, in most cases, is able to resolve the type of lambda parameters with the help of type inference. Consequently, adding a type to the parameters is optional and can be omitted.

在大多数情况下,编译器能够在类型推理的帮助下解决lambda参数的类型。因此,给参数添加类型是可选的,可以省略。

We can do this:

我们可以这样做。

(a, b) -> a.toLowerCase() + b.toLowerCase();

Instead of this:

而不是这样。

(String a, String b) -> a.toLowerCase() + b.toLowerCase();

8.3. Avoid Parentheses Around a Single Parameter

8.3.避免在单个参数周围加括号

Lambda syntax only requires parentheses around more than one parameter, or when there is no parameter at all. That’s why it’s safe to make our code a little bit shorter, and to exclude parentheses when there is only one parameter.

Lambda语法只需要在一个以上的参数周围加上小括号,或者在没有参数的时候。这就是为什么让我们的代码变得更短一点,并且在只有一个参数的时候排除小括号是安全的。

So we can do this:

所以我们可以这样做。

a -> a.toLowerCase();

Instead of this:

而不是这样。

(a) -> a.toLowerCase();

8.4. Avoid Return Statement and Braces

8.4.避免使用返回语句和大括号

Braces and return statements are optional in one-line lambda bodies. This means that they can be omitted for clarity and conciseness.

Bracesreturn语句在单行lambda体中是可选的。这意味着为了清晰和简洁,可以省略它们。

We can do this:

我们可以这样做。

a -> a.toLowerCase();

Instead of this:

而不是这样。

a -> {return a.toLowerCase()};

8.5. Use Method References

8.5.使用方法引用

Very often, even in our previous examples, lambda expressions just call methods which are already implemented elsewhere. In this situation, it is very useful to use another Java 8 feature, method references.

很多时候,甚至在我们之前的例子中,lambda表达式只是调用已经在其他地方实现的方法。在这种情况下,使用另一个Java 8的特性,方法引用是非常有用的。

The lambda expression would be:

lambda表达式将是。

a -> a.toLowerCase();

We could substitute it with:

我们可以用以下内容代替。

String::toLowerCase;

This is not always shorter, but it makes the code more readable.

这并不总是更短,但它使代码更容易阅读。

9. Use “Effectively Final” Variables

9.使用 “有效的最终 “变量

Accessing a non-final variable inside lambda expressions will cause a compile-time error, but that doesn’t mean that we should mark every target variable as final.

在lambda表达式内部访问非最终变量将导致编译时错误,b但这并不意味着我们应该将每个目标变量都标记为最终变量。

According to the “effectively final” concept, a compiler treats every variable as final as long as it is assigned only once.

根据”effectively final“的概念,编译器将每个变量视为final,只要它只被分配一次。

It’s safe to use such variables inside lambdas because the compiler will control their state and trigger a compile-time error immediately after any attempt to change them.

在lambdas中使用这样的变量是安全的,因为编译器会控制它们的状态,并在任何试图改变它们的行为后立即触发一个编译时错误。

For example, the following code will not compile:

例如,以下代码将无法编译。

public void method() {
    String localVariable = "Local";
    Foo foo = parameter -> {
        String localVariable = parameter;
        return localVariable;
    };
}

The compiler will inform us that:

编译器会通知我们。

Variable 'localVariable' is already defined in the scope.

This approach should simplify the process of making lambda execution thread-safe.

这种方法应该可以简化使lambda的执行成为线程安全的过程。

10. Protect Object Variables From Mutation

10.保护对象变量免受突变影响

One of the main purposes of lambdas is use in parallel computing, which means that they’re really helpful when it comes to thread-safety.

lambdas的主要目的之一是用于并行计算,这意味着当涉及到线程安全时,它们真的很有用。

The “effectively final” paradigm helps a lot here, but not in every case. Lambdas can’t change a value of an object from enclosing scope. But in the case of mutable object variables, a state could be changed inside lambda expressions.

“有效最终 “的范式在这里有很大的帮助,但不是在所有的情况下。lambdas不能从包围的范围内改变一个对象的值。但是在可变对象变量的情况下,一个状态可以在lambda表达式中被改变。

Consider the following code:

请考虑以下代码。

int[] total = new int[1];
Runnable r = () -> total[0]++;
r.run();

This code is legal, as total variable remains “effectively final,” but will the object it references have the same state after execution of the lambda? No!

这段代码是合法的,因为变量仍然是 “有效的最终”,但是它所引用的对象在执行lambda之后会有相同的状态吗?不!是的。

Keep this example as a reminder to avoid code that can cause unexpected mutations.

保留这个例子作为提醒,以避免可能导致意外突变的代码。

11. Conclusion

11.结论

In this article, we explored some of the best practices and pitfalls in Java 8’s lambda expressions and functional interfaces. Despite the utility and power of these new features, they are just tools. Every developer should pay attention while using them.

在这篇文章中,我们探讨了Java 8的lambda表达式和功能接口中的一些最佳实践和陷阱。尽管这些新功能很实用,很强大,但它们只是工具而已。每个开发人员在使用它们时都应该注意。

The complete source code for the example is available in this GitHub project. This is a Maven and Eclipse project, so it can be imported and used as is.

该示例的完整源代码可在这个GitHub项目中找到。这是一个Maven和Eclipse项目,因此可以按原样导入和使用。