Contents

Lambda表达式和功能接口:技巧和最佳实践

1. 概述

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

2. 首选标准功能接口

功能接口,集合在**java.util.function **包中,满足了大多数开发人员为 lambda 表达式和方法引用提供目标类型的需求。这些接口中的每一个都是通用和抽象的,使它们易于适应几乎任何 lambda 表达式。开发人员应该在创建新的功能接口之前探索这个包。 让我们考虑一个接口Foo

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

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

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 8 已经在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*注解,编译器将触发错误以响应任何破坏功能接口的预定义结构的尝试。它也是一个非常方便的工具,可以让其他开发人员更容易理解我们的应用程序架构。 所以我们可以使用这个:

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

而不仅仅是:

public interface Foo {
    String method();
}

4. 不要在函数式接口中过度使用默认方法

我们可以轻松地将默认方法添加到功能接口。只要只有一个抽象方法声明,函数式接口契约就可以接受:

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

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

@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() {}
}

与常规接口一样,使用相同的默认方法扩展不同的功能接口可能会出现问题。 例如,让我们将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() {}
}

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

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

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

@FunctionalInterface
public interface FooExtended extends Baz, Bar {
    @Override
    default String defaultCommon() {
        return Bar.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 表达式方法可用于旧库中的任何合适的接口。**可用于RunnableComparator等接口;但是,这并不意味着我们应该审查整个旧代码库并更改所有内容。

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.blogdemo.java8.lambda.tips.ProcessorImpl 
and method process(java.util.function.Supplier<java.lang.String>) 
in com.blogdemo.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作为对其实例的引用。 然而,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()方法,我们将得到以下结果:结果:resultIC = 内部类值,resultLambda = 封闭范围值 正如我们所见,通过调用IC 中的this.value,我们可以从它的实例中访问一个局部变量。在 lambda 的情况下,this.value调用让我们可以访问在UseFoo类中定义的变量值,但不能访问在 lambda 主体中定义的变量值。

8. 保持 Lambda 表达式简短且不言自明

如果可能,我们应该使用一行结构而不是一大块代码。请记住,**lambdas 应该是一个表达式,而不是叙述。**尽管语法简洁,lambdas 应该专门表达它们提供的功能。 这主要是风格上的建议,因为性能不会发生巨大变化。然而,总的来说,这样的代码更容易理解和使用。 这可以通过多种方式实现;让我们仔细看看。

8.1. 避免 Lambda 正文中的代码块

在理想情况下,lambdas 应该写在一行代码中。使用这种方法,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”规则用作教条。如果我们在 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. 避免返回语句和大括号

大括号返回语句在单行 lambda 主体中是可选的。这意味着为了清晰和简洁可以省略它们。 我们做得到:

a -> a.toLowerCase();

而不是这个:

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

8.5.使用方法参考

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

a -> a.toLowerCase();

我们可以将其替换为:

String::toLowerCase;

这并不总是更短,但它使代码更具可读性。

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

在 lambda 表达式中访问非 final 变量会导致编译时错误,但这并不意味着我们应该将每个目标变量都标记为final 根据“有效最终 ”概念,编译器将每个变量视为final变量 ,只要它只分配一次。 在 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 的主要用途之一是用于并行计算,这意味着它们在线程安全方面非常有用。

“有效最终”范式在这里有很大帮助,但并非在所有情况下都如此。Lambda 不能从封闭范围更改对象的值。但是在可变对象变量的情况下,可以在 lambda 表达式中更改状态。 考虑以下代码:

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

这段代码是合法的,因为变量仍然是“有效的最终变量”,但它引用的对象在执行 lambda 后是否会具有相同的状态?不! 保留此示例作为提醒,以避免可能导致意外突变的代码。