Java 8 Lambda 表达式详解

简介

Lambda 表达式,也可称为闭包,它是推动 Java 8 发布的最重要新特性。Lambda 表达式可以取代大部分的匿名内部类,写出更优雅的 Java 代码,可以极大地优化代码结构。JDK 1.8 也提供了大量的内置函数式接口供我们使用,使得 Lambda 表达式的运用更加方便、高效。

Lambda 表达式的加入,使得 Java 拥有了函数式编程的能力。在其它语言中,Lambda 表达式的类型是一个函数;但在 Java 中,Lambda 表达式被表示为对象,因此它们必须绑定到被称为函数式(Functional Interface)接口的特定对象类型。

Lambda 表达式并不能取代所有的匿名内部类,只能用来取代函数式接口的简写。

函数式接口

函数式接口只包含一个抽象方法的接口。函数式接口有时候被称为 SAM 类型,意思是单抽象方法(Single Abstract Method)。

一般来说,这个抽象方法指明了接口的目标用途。因此,函数式接口通常表示单个动作。

同时,Java 8 引入了一个新的注解: @FunctionalInterface ,用于指明该接口类型声明是根据 Java 语言规范定义的函数式接口。

可以在任意函数式接口上面使用 @FunctionalInterface 来标识它是一个函数式接口,但是该注解不是强制的。

Java 8 新增了 java.util.funcion 包,里面包含常用的函数接口,这是 Lambda 表达式的基础,Java 集合框架也新增部分接口,以便与 Lambda 表达式对接。

JDK 1.8 中有另一个新特性:default, 被 default 修饰的方法会有默认实现,不是必须被实现的方法,所以不影响 Lambda 表达式的使用。

语法

Java 中的 Lambda 表达式通常使用语法是 () -> {},其中 () 用来描述参数列表,{} 用来描述方法体,-> 为 lambda 运算符,读作(goes to)Lambda 表达式的语法格式如下:

(parameters) -> expression
或
(parameters) ->{ statements; }

如果方法体为表达式,则该表达式的值作为返回值返回;如果方法体为代码块,则必须用 {} 来包裹起来,且需要一个 return 返回值,但若函数式接口里面方法返回值是 void,则无需返回值。

Lambda 表达式的简单示例如下:

// 1. 不需要参数,返回值为 5  
() -> 5  

// 2. 接收一个参数(数字类型),返回其2倍的值  
x -> 2 * x  

// 3. 接受2个参数(数字),并返回他们的差值  
(x, y) -> x – y  

// 4. 接收2个int型整数,返回他们的和  
(int x, int y) -> x + y  

// 5. 接受一个 String 对象,并在控制台打印,不返回任何值(看起来像是返回void)  
(String s) -> System.out.print(s)

Lambda 表达式的结构如下:

  • Lambda 表达式可以具有零个、一个或多个参数。
  • Lambda 表达式的参数类型可以显式声明,也可以由编译器自动根据上下文推断。例如:(int a)(a)效果相同。
  • Lambda 表达式的参数需要用圆括号()括起来,用逗号分隔。例如:(a, b)(int a, int b)(String a, int b, float c)
  • Lambda 表达式中空圆括号()用于表示参数集为空。例如:() -> 42
  • Lambda 表达式中当有且仅有一个参数,且其类型可推导时,如果不显式指明类型,则圆括号()可以省略。例如:a -> return a*a
  • Lambda 表达式的主体可以包含零条、一条或多条语句。
  • 如果 Lambda 表达式的主体只有一条语句,则花括号{}可以省略,且匿名函数的返回类型要与表达式的返回值类型一致。
  • 如果 Lambda 表达式的主体有一条以上的语句必须包含在花括号{}(形成代码块)中,且匿名函数的返回类型要与表达式的返回值类型一致,若没有返回则为空。

方法引用

方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。

当 Lambda 表达式中只是执行一个方法调用时,不使用 Lambda 表达式,直接通过方法引用的形式可读性更高一些。方法引用是一种更简洁易懂的 Lambda 表达式。

Java 中的方法引用通过 :: 操作符实现。使用一个方法的引用时,目标引用放在 :: 之前,目标引用提供的方法名称放在 :: 之后,即 目标引用::方法:: 是域操作符,也可以称作定界符、分隔符。

常见的方法引用示例如下:

方法引用 等价的 Lambda 表达式
String::valueOf x -> String.valueOf(x)
Object::toString x -> x.toString()
x::toString () -> x.toString()
ArrayList::new () -> new ArrayList<>()

方法引用的唯一用途是支持 Lambda 的简写。方法引用提高了代码的可读性,也使逻辑更加清晰。

静态方法引用

静态方法引用的语法格式如下:

ClassName::staticMethodName

静态方法引用比较容易理解,和静态方法调用相比,只是把 . 换为 ::。在目标类型兼容的任何地方,都可以使用静态方法引用。代码示例如下:

String::valueOf   等价于lambda表达式  (s) -> String.valueOf(s) 

Math::pow         等价于lambda表达式  (x, y) -> Math.pow(x, y)

实例方法引用

实例方法引用的语法与静态方法的语法类似,只不过这里使用对象引用而不是类名。实例方法引用又分以下三种类型:

  • 实例上的实例方法引用
    实例上的实例方法引用的语法格式如下:

    instanceReference::methodName
    

    对于具体(或者任意)对象的实例方法引用,在实例方法名称和其所属类型名称间加上分隔符。

  • 超类上的实例方法引用
    超类上的实例方法引用的语法格式如下:

    super::methodName
    

    通过使用 thissuper,可以引用方法的超类版本。

  • 类型上的实例方法引用
    类型上的实例方法引用的语法格式如下:

    ClassName::methodName
    

    类型的实例方法是泛型的,就需要在 :: 分隔符前提供类型参数,或者多数情况下利用目标类型推导出其类型。

代码示例如下:

String::toUpperCase   等价于lambda表达式  s -> s.toUpperCase()

s::toUpperCase        等价于lambda表达式  () -> s.toUpperCase()

this :: equals        等价于lambda表达式  x -> this.equals(x)

构造方法引用

构造方法引用也可以称作构造器引用,又分构造方法引用和数组构造方法引用。

  • 构造方法引用
    构造方法引用的语法格式如下:

    Class::new
    
  • 数组构造方法引用
    数组构造方法引用的语法格式如下:

    TypeName[]::new
    

构造函数本质上是静态方法,只是方法名字比较特殊,使用的是 new 关键字。

代码示例如下:

String::new   等价于lambda表达式  () -> new String()

String[]::new        等价于lambda表达式  x -> new String[x]

方法引用和Lambda表达式的区别

不要认为方法引用和 Lambda 表达式是完全等价的;

  • 方法引用会对 :: 符号前的子表达式进行预求值,如果发现值为 null,会立即抛出 NPE
  • lambda 表达式只有在真正运行 lambda 主体时,才会抛出 NPE
  • 如果需要用到 Java FunctionInterface 的缓求值特性,使用 lambda 表达式,而不要使用方法引用,否则有提前抛出 NPE 的风险。

Lambda表达式和匿名类之间的区别

Lambda 表达式与匿名内部类存在如下相同点:

  • Lambda 表达式与匿名内部类一样,都可以直接访问 effectively final 的局部变量,以及外部类的成员变量(包括实例变量和类变量)。
  • Lambda 表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口继承得到的默认方法。

Lambda 表达式与匿名内部类主要存在如下区别:

  • 匿名内部类可以为任意接口创建实例(不管接口包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可),但 Lambda 表达式只能为函数式接口创建实例。
  • 匿名内部类可以为抽象类、甚至普通类创建实例,但 Lambda 表达式只能为函数式接口创建实例。
  • 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但 Lambda 表达式的代码块不允许调用接口中定义的默认方法。
  • 匿名内部类的 this 关键字解析为匿名类,但 Lambda 表达式的 this 关键字解析为包含写入 Lambda 的类。
  • 匿名内部类编译后仍然是一个类,编译器会自动为该类取名,但 Lambda 表达式通过 invokedynamic 指令实现,Lambda 表达式不会产生新的类。

评论
 上一篇
Gradle 缓存目录结构说明 Gradle 缓存目录结构说明
Gradle 缓存策略Gradle 依赖的版本分为正式版本、快照版本、动态版本: 正式版本:有明确指明版本号,比如 implementation 'androidx.appcompat:appcompat:1.1.0'。
2020-02-07
下一篇 
Kotlin 操作符重载 Kotlin 操作符重载
Kotlin 允许我们为自己的类型提供预定义的一组操作符的实现。这些操作符具有固定的符号表示(如 + 或 *)和固定的优先级。为实现这样的操作符,我们为相应的类型(即二元操作符左侧的类型和一元操作符的参数类型)提供了一个固定名字的成员函数或
2020-01-16
  目录