高效开发!Lambda表达式和函数式接口最佳实践

开发 前端
在 java.util.function 包中定义的函数式接口满足了大多数开发者为 lambda 表达式和方法引用提供目标类型的需求。这些接口中的每一个都是通用且抽象的,这使得它们能够轻松适应几乎任何 lambda 表达式。

环境:Spring Boot 3.2.5

1. 简介

Lambda表达式与函数式接口是Java 8引入的重要特性,它们极大地简化了代码编写,提升了代码的可读性和简洁性。Lambda表达式提供了一种简洁的方式来表示匿名函数,而函数式接口则是一种只包含一个抽象方法的接口,非常适合与Lambda表达式结合使用。

在实际开发中,合理运用这些特性可以提高代码的灵活性和复用性。最佳实践中,推荐首先定义清晰且具有描述性的函数式接口,以增强代码的可理解性;其次,在使用Lambda表达式时,尽量保持其简短且功能单一,避免复杂的逻辑嵌套;此外,利用Stream API等函数式编程工具进行集合操作,能够使代码更加流畅、高效。通过遵循这些原则,开发者不仅能够写出更加优雅的代码,还能更好地应对并发编程的需求,提升程序的整体性能。

接下来,我们将详细研究函 数式接口 和 lambda 表达式。

2. 最佳实践

2.1 优先使用标准的函数式接口

在 java.util.function 包中定义的函数式接口满足了大多数开发者为 lambda 表达式和方法引用提供目标类型的需求。这些接口中的每一个都是通用且抽象的,这使得它们能够轻松适应几乎任何 lambda 表达式。我们在创建自定义的函数式接口之前,应该优先查看该包中的定义。

我们先来看下如下接口:

@FunctionalInterface
public interface Foo {


  String xxxooo(String string) ;
}

实用该接口:

public class UseFoo {
  public String pack(String param, Foo foo) {
    return foo.xxxooo(param) ;
  }
}

运行程序应该是如下方式:

Foo foo = param -> String.format("%s other info", param) ;
String ret = new UseFoo().pack("Message ", foo) ;

在这里的Foo接口方法签名需要一个入参然后返回一个参数。而Java 8 已经在 java.util.function 包中提供了这样的接口 Function<T, R>。所以我们没有必要自己在定义,可以将上面的UseFoo修改如下:

public String pack(String param, Function<String, String> foo) {
  return foo.apply(param) ;
}
// 调用
Function<String, String> foo = param -> String.format("%s other info", param) ;

使用与之前定义的基本相同。

2.2 使用@FunctionalInterface注解

使用 @FunctionalInterface 注解接口。乍一看,这个注解似乎没有什么用处。即使没有它,只要接口中只有一个抽象方法,该接口也会被视为函数式接口。

然而,如果在一个大型项目中有多个接口;手动控制所有接口是很困难的。一个原本设计为函数式的接口可能会因为不小心添加了另一个抽象方法而被改变,从而使它不再是一个有效的函数式接口。

通过使用 @FunctionalInterface 注解,编译器会在任何试图破坏函数式接口预定义结构的行为时触发错误。如下示例:

@FunctionalInterface
public interface Foo {
  String xxxooo(String param) ;
}

使用该接口,能防止你再定义其它方法。

2.3 不要在函数式接口中过渡使用默认方法

我们可以轻松地在函数式接口中添加默认方法。只要接口中只有一个抽象方法声明,这样做是符合函数式接口契约的:

@FunctionalInterface
public interface Foo {
  String xxxooo(String param);
  default void defaultMethod() {
    // ...
  }
}

如果它们的抽象方法具有相同的签名,函数式接口可以被其他函数式接口继承:

@FunctionalInterface
public interface Zoo extends Baz, Bar {}
  
@FunctionalInterface
public interface Baz {  
  String xxxooo(String param);  
  default String defaultBaz() {
    return "Baz..." ;
  }    
}
  
@FunctionalInterface
public interface Bar {  
  String xxxooo(String param);  
  default String defaultBar() {
    return "Bar..." ;
  }  
}
public static void main(String[] args) {
  Zoo zoo = param -> String.format("%s extends", param) ;
  System.out.println(zoo.xxxooo("Functional Interface")) ;
}

就像普通的接口一样,如果不同的函数式接口继承了具有相同默认方法的接口,这也可能会带来问题。

修改上面的Baz和Bar接口,添加相同的默认方法:

@FunctionalInterface
public interface Baz { 
  default String print(){
    // ...
  }
}
@FunctionalInterface
public interface Bar { 
  default String print(){
    // ...
  }
}

这样定义后,Zoo接口将编译不通过,重复的默认方法错误。

我们可以通过如下方式,在Zoo接口重写defaultCommon方法,如下示例:

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

所以,我们不应该在函数式接口中定义过多的默认方法。

2.4 使用 Lambda 表达式实例化功能接口

编译器允许我们使用内部类来实例化函数式接口;然而,这样做会导致代码非常冗长。我们应该优先使用 lambda 表达式:

// 是使用上面定义的Zoo接口
Zoo zoo = param -> String.format("%s extends", param) ;
System.out.println(zoo.xxxooo("Functional Interface")) ;

如果是内部类定义那就太不优雅了。

Zoo zoo = new Zoo() {
  public String xxxooo(String param) {
    return String.format("%s extends", param) ;
  }
} ;

现在开发工具都能自动帮你将这里的内部类转换为lambda表达式。

2.5 避免重载带有函数式接口作为参数的方法

public interface Processor {
  String process(Callable<String> c) throws Exception;


  String process(Supplier<String> s);
}


public class ProcessorImpl implements Processor {
  public String process(Callable<String> c) throws Exception {
    return c.call() ;
  }
  public String process(Supplier<String> s) {
    return s.get() ;
  }
}

上面代码看着没撒毛病,但是你通过lambda表达传参时,就出问题了:

ProcessorImpl process = new ProcessorImpl() ;
process.process(() -> "Pack") ;

Eclipse下提示

图片图片

模棱两可的方法调用。解决办法有2种:

  • 定义不同的方法名称
  • 强制转换
ProcessorImpl process = new ProcessorImpl() ;
process.process((Supplier<String>)() -> "Pack") ;

但是不推荐这种方式。

2.6 不要将 Lambda 表达式视为内部类

尽管在前面的例子中,我们基本上是用 Lambda 表达式替换了内部类,但这两个概念在一个重要方面是不同的:作用域。

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

然而,Lambda 表达式则与外部作用域一起工作。我们不能在 Lambda 表达式的主体中隐藏外部作用域中的变量。在这种情况下,this 关键字是对外部实例的引用。

private String value = "Outer class value";


@FunctionalInterface
public interface Foo {
  String fn(String param);
}


public void xxoo() {
  Foo f = new Foo() {
    String value = "Inner class value";


    @Override
    public String fn(String param) {
      return this.value;
    }
  };
  String ret = f.fn("Pack") ;
  System.out.println(ret) ;
  Foo fl = param -> {
    String value = "Lambda value";
    return this.value;
  };
  ret = fl.fn("Pack");


  System.out.println(ret) ;
}

输出结果:

Inner class value
Outer class value

根据运行结果得知,在Lambda中this.value方法的是类中定义的变量,而内部类访问的则是当前内部类的变量。

2.7 避免在 Lambda 表达式的主体中使用代码块

Lambda 表达式应该用一行代码来编写。通过这种方式,Lambda 表达式成为一个自解释的结构,声明了应该对哪些数据执行什么操作。

如果我们有一大段代码,那么 lambda 的功能就不会立即显现出来。

Foo foo = param -> buildString(param) ;
private String buildString(String param) {
  String result = "Something " + param ;
  // ...
  return result ;
}

而不应该是如下代码

Foo foo = param -> { 
  String result = "Something " + param ; 
  // ...
  return result ; 
} ;

注意:如果 lambda的定义有两三行代码,那么将代码提取到另一个方法中也没有什么价值,我们不应该将 "单行 lambda" 完全作为一个规约。

2.8 避免指定参数类型

在大多数情况下,编译器可以通过类型推断来确定 lambda 参数的类型。 因此,为参数添加类型是可选的,可以省略,如下实例:

BiFunction<String, String, String> fun = 
  (String a, String b) -> a.toLowerCase() + b.toLowerCase() ;

这里我们不用声明类型,而是如下方式:

BiFunction<String, String, String> fun = 
  (a, b) -> a.toLowerCase() + b.toLowerCase() ;

这里完全可以通过类型推断确定类型,所以没有必要什么参数的类型。

2.9 单参数不要使用括号

Lambda 语法只要求在多个参数或没有参数时使用括号。

错误示例

Function<String, String> fun = (a) -> a.toLowerCase() ;

正确示例

Function<String, String> fun = a -> a.toLowerCase() ;

只有一个参数时没有必要添加括号

2.10 避免返回语句和括号

理想情况下,Lambda 表达式应该用一行代码来编写。通过这种方式,Lambda 表达式成为一个自解释的结构,声明了应该对哪些数据执行什么操作。

错误示例

Function<String, String> func = a -> {return a.toLowerCase()};

正确示例

Function<String, String> func = a -> a.toLowerCase() ;

这里我们没有必要使用代码块,我们应该时刻注意尽可能的使得 Lambda 表达式只有一行。

2.11 方法引用

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

错误示例

Function<String, String> func = a -> a.toLowerCase();

正确示例

Function<String, String> func = String::toLowerCase;

如果你不懂方法引用,那么这种写法是不是可读性不好了?

2.12 使用"Effectively Final"变量

在 Lambda 表达式内部访问 非final 变量会导致编译时错误,但这并不意味着我们应该将每个目标变量都标记为 final。根据 "Effectively final" 的概念,只要变量仅被赋值一次,编译器就会将其视为 final。

public void xxxooo() {
  String value = "Local" ; // 这里我们可以省去 final 修饰符
  Function<String, String> func = str -> {
    return value ;
  } ;
}

这里我们没有必要在变量value前使用 final 修饰。但是我们不能在代码块中去修改,如下将无法编译通过:

图片 图片

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2022-12-05 09:31:51

接口lambda表达式

2024-03-08 09:45:21

Lambda表达式Stream

2020-10-16 10:07:03

Lambda表达式Java8

2022-12-01 07:38:49

lambda表达式函数式

2009-08-10 10:06:10

.NET Lambda

2009-08-31 17:11:37

Lambda表达式

2009-09-17 09:09:50

Lambda表达式Linq查询

2020-10-16 06:40:25

C++匿名函数

2021-08-31 07:19:41

Lambda表达式C#

2009-09-15 15:18:00

Linq Lambda

2009-09-09 13:01:33

LINQ Lambda

2009-09-11 09:48:27

Linq Lambda

2009-10-12 10:11:08

Lambda表达式编写

2009-08-10 17:11:34

.NET 3.5扩展方Lambda表达式

2024-03-12 08:23:54

JavaLambda函数式编程

2021-08-07 07:21:26

AndroidKotlinLambda

2009-08-27 09:44:59

C# Lambda表达

2009-09-15 17:30:00

Linq Lambda

2009-09-17 10:40:22

Linq Lambda

2009-09-17 09:44:54

Linq Lambda
点赞
收藏

51CTO技术栈公众号