认识Lambda表达式
Lambda有很多种形式,每一种形式的应用场景也略微不一样,下面对不同形式的Lambda表达式进行介绍。
ini
// 示例1
Runnable noArguments=()-> System.out.println("Hello World");
// 示例2
ActionListener oneArgument=event-> System.out.println("Hello World");
// 示例3
Runnable multiStatement=()-> {
System.out.print("Hello");
System.out.println(" World");
};
// 示例4
BinaryOperator<Long> add=(x, y)-> x+y;
// 示例5
BinaryOperator<Long> addExplicit=(Long x, Long y)-> x+y;
- 示例1,Lambda表达式不包含参数,使用空括号()表示没有参数。
- 示例2,Lambda表达式包含且只包含一个参数,可省略参数的括号。
- 示例3,Lambda表达式的主体不仅可以是一个表达式,而且也可以是一段代码块,使用大括号
{ }
将代码块括起来。 - 示例4,这行代码并不是将两个数字相加,而是创建了一个函数,用来计算两个数字相加的结果。
- 示例5,显式声明参数类型,此时就需要使用小括号将参数括起来,多个参数的情况也是如此。
Lambda引用的是值,而不是变量
Lambda表达式对内部引用的变量是有要求的,内部引用的变量必须被声明为final类型或者在既成事实上必须是final的。
将变量声明为final,意味着不能为其重复赋值。同时也意味着在使用final变量时,实际上是在使用赋给该变量的一个特定的值。
ini
final String name=getUserName();
button.addActionListener(event-> System.out.println("hi "+name));
Java 8放松了这一限制 , 可以引用非final
变量,但是该变量在既成事实上必须是final
。虽然无需将变量声明为final
,但在Lambda表达式中,也无法用作非终态变量(非终态变量即变量的值还会改变)。如果坚持用作非终态变量,编译器就会报错,报错信息:Variable used in lambda expression should be final or effectively final(在lambda表达式中使用的变量应该是final的或者实际上是final的)
ini
String name=getUserName();
// 修改非final变量,会报错
name="alan";
button.addActionListener(event-> System.out.println("hi "+name));
rust
final String name=getUserName();
// 引用非final变量不做修改,不会报错
button.addActionListener(event-> System.out.println("hi "+name));
函数接口
函数接口是只有一个抽象方法的接口,用作Lambda表达式的类型。
以Predicate<T>
接口为例,接口内定义了很多个方法,但只有一个(test(T t)
)是抽象方法,其他的都是默认方法或者静态方法,所以它仍然被定义为一个函数式接口。
scss
......
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
default Predicate<T> and(Predicate<? super T> other) {
Objects.requireNonNull(other);
return (t) -> test(t) && other.test(t);
}
......
}
类型推断
我们在编写Lambda表达式的时候,可以省略Lambda表达式中的所有参数类型,javac根据Lambda表达式上下文信息就能推断出参数的正确类型。
下面我们以JDK中两个具体的函数式接口,来介绍一下Java的推断系统是如何实现Lambda的类型推断的。
Predicate
接口
csharp
public interface Predicate<T> {
boolean test(T t);
}
Predicate
接口,接受一个对象,返回一个布尔值。
ini
Predicate<Integer> atLeast5=x-> x > 5;
atLeast5
是一个Lambda表达式,表达式x > 5
是Lambda表达式的主体,表达式x > 5
的返回值就是Lambda表达式主体的值。
对于表达式Predicate<Integer> atLeast5=x-> x > 5
,其实现了Predicate
接口,因此Predicate接口的单一参数被推断为Integer
类型(Integer
会被作为泛型参数传入Predicate
中)。代码编译时,javac
还会检查Lambda表达式的返回值是不是boolean
,boolean
也正是Predicate
方法的返回类型。
BinaryOperator
接口
ini
BinaryOperator<Long> addLongs=(x, y)-> x+y;
BinaryOperator
接口:接受两个参数,返回一个值,参数和值的类型均相同。
类型推断系统,根据BinaryOperator<Long> addLongs
,可以推断出来x,y都是Long
类型,同时 返回值x+y
的类型也是Long
类型
ini
BinaryOperator add=(x, y)-> x+y;
编译器报错:Operator '+' cannot be applied to 'java.lang.Object', 'java.lang.Object'
编译器报这个错是因为上面的例子中并没有给出变量add
的任何泛型信息,给出的是原始类型的定义。因此,编译器认为参数和返回值都是java.lang.Object
实例。
如何理解下面的问题
变量在既成事实上必须是final
,Lambda表达式引用的是值,而不是变量?
从两方面来解释这句话
- "既成事实上的final":在Java中,当你声明一个变量为final时,意味着这个变量只能被赋值一次,一旦赋值后就不能再改变。这是一个强制的规则。然而,也有另外一种情况,即一个变量在定义之后,只被赋值一次,并且没有修改过。虽然该变量在语法上并没有标记为final,但实际上,由于它的值从未改变过,因此我们可以将其看作是"既成事实上的final"。
- "Lambda表达式引用的是值,而不是变量":在Java的Lambda表达式中,如果你在lambda里面引用一个外部变量,那么这个变量必须是effectively final(这个变量的值在初始化后就不能再被改变,即终态变量) 。这是因为Lambda可能会在并发的环境中执行,所以引用的变量的值需要是稳定的,以防止数据的不一致性。 也就是说,Lambda引用的其实是变量初始化后的值。
因此,结合这两条,我们可以理解为在Lambda表达式中只能引用一次赋值并且之后未被修改过的变量,即所谓的"既成事实上的final"。
如何使用BinaryOperator?
BinaryOperator
是 Java 8 中引入的一个函数式接口,它接受两个输入参数,返回一个结果,通常用于代表一种特定的操作,比如加法、乘法等。
arduino
public class Main {
public static void main(String[] args) {
BinaryOperator<Long> addLongs = (x, y) -> x + y;
Long result = addLongs.apply(2L, 3L);
// 输出 5
System.out.println("Result of addition is: " + result);
}
}
在这个代码中:我们首先定义了一个 BinaryOperator addLongs
,然后通过调用它的 apply
方法,将参数 2L
和 3L
传给这个 BinaryOperator
。这个 BinaryOperator
的作用是将两个参数相加,所以最后返回的结果是 5。打印结果显示:"Result of addition is: 5"
。