Lambda 表达式与函数式接口

Java8 中引入了很多的新特性,包括接口的默认方法、函数式接口、Lambda 表达式等。今天就来聊聊用的比较多的特性: Lambda 表达式。Lambda 表达式并不是多么新的技术,它的本质是匿名内部类,在了解 Lambda 表达式之前先来看看匿名内部类。

匿名内部类

什么是匿名内部类

程序中大部分情况下用到的类都是有名字的,比如下面代码中的 Student 类,可以反复使用。

publicclassNormalNamedClass{

publicstaticvoidmain(String[] args){

Student student = new Student();

student.eat();

}

}

interfacePerson{

voideat();

}

classStudentimplementsPerson{

@Override

publicvoideat(){

System.out.println("eat something.");

}

}

如果上述 Student 类只会被使用一次,而为其单独定义一个类显得比较麻烦,使用匿名内部类简化如下:

classAnonymousClass{

publicstaticvoidmain(String[] args){

Person person = new Person(){

@Override

publicvoideat(){

System.out.println("eat something.");

}

};

person.eat();

}

}

interfacePerson{

voideat();

}

上述代码 Person person = new Person(){...} 包含两个步骤:

  • 定义了一个匿名内部类,该类实现 Person 接口。
  • new 一个匿名内部类的对象,并赋值给 person。

匿名内部类的本质是一个 带具体实现的 继承某个父类或者父接口的 匿名的 子类

匿名内部类的作用

主要是用来简化代码

Lambda 表达式

Lambda 表达式的本质是一个匿名内部类。将下面代码拷贝到 Intellij IDEA 中,idea 会提示可以进一步对改代码进行优化。

Anonymous new Person() can be replaced with lambda

classAnonymousClass{

publicstaticvoidmain(String[] args){

Person person = new Person(){

@Override

publicvoideat(){

System.out.println("eat something.");

}

};

person.eat();

}

}

interfacePerson{

voideat();

}

优化后的代码如下:

classAnonymousClass{

publicstaticvoidmain(String[] args){

Person person = () -> System.out.println("eat something.");

person.eat();

}

}

interfacePerson{

voideat();

}

匿名内部类被简化成一行代码:

() -> System.out.println("eat something.");

这甚至连一个方法都不像,别说像个类了。其实这就是 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 表达式为了简化代码,无所不用其极。完全打破了 Java 方法在我们脑海中的固有格式:

  • 一个名称
  • 返回类型
  • 参数列表
  • 主体

Lambda 表达式只保留了最后两项,最后两项有时候还省去了圆括号、大括号和 return。

函数式接口

什么是函数式接口

先来看一份示例代码:

publicclassTestLambda{

publicstaticvoidmain(String[] args){

List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);

list = list.stream().filter(item-> item % 2 == 0).collect(Collectors.toList());

System.out.println(list);

}

}

这份代码是筛选出 list 中为偶数的数字,非常简洁简单。stream() 将 list 转成流,filter 将满足条件的数据筛选出来继续流入下一个节点。 item-> item % 2 == 0是个简化后的 lambda 表达式,为什么可以写在 filter() 参数里面呢?点击去可以看到 filter 的定义:

Stream<T> filter(Predicate<? super T> predicate);

再点 Predicate 进去,可以看到 Predicate 是一个接口:

@FunctionalInterface

publicinterfacePredicate<T> {

booleantest(T t);

// 其他 default 方法省略

}

Predicate 中有很多 default 方法和一个抽象方法,并加了 @FunctionalInterface 注解,是一个函数式接口。函数式接口定义如下:

函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。

函数式接口加不加 @FunctionalInterface 注解都不影响将某个接口作为函数式接口使用,但加上该注解,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。如下:

@FunctionalInterface

publicinterfacePerson{

voideat();

voidsing();

}

上述代码会报如下错误:

为什么引入函数式接口的概念

作为纯面向对象语言 Java 方法的参数只有两种类型:基本类型和对象引用类型。没有办法把函数作为参数传递到方法中去,必须被包装在接口中,这样就不会破坏 Java 世界中一切皆对象的原则。item-> item % 2 == 0 看似传了一个方法到 filter 方法中,实际是通过各种推导传的是一个匿名内部类(接口)到其中。可以说函数式接口为 Lambda 表达式而生。

常用函数式接口

Java.util.function 中包含了很多函数式接口,用来支持 Java 的函数式编程。但其中最基本的只有 Consumer/Supplier/Function<T,R>/Predicate 四种,包中的其他的函数式接口都是该四种类型的扩展。

Consumer<T>: 接受一个输入并且无返回结果

publicclassTestConsumer{

publicstaticvoidmain(String[] args){

Consumer<String> consumer = s -> System.out.println("accept:" + s);

consumer.accept("egg");

}

}

// 输出 accept egg.

Function<T,R>: 接受一个输入并且返回一个结果

publicclassTestFunction{

publicstaticvoidmain(String[] args){

Function<String, String> function = (s) -> "function input with:" + s;

String result = function.apply("egg");

System.out.println(result);

}

}

Supplier<T>: 没有参数返回但返回一个结果

publicclassTestSupplier{

publicstaticvoidmain(String[] args){

Supplier<String> supplier = () -> "egg";

String result = supplier.get();

System.out.println(result);

}

}

Predicate<T>: 接受一个输入并且返回一个布尔值结果

publicclassTestPredicate{

publicstaticvoidmain(String[] args){

Predicate<String> predicate = (s) -> s.equalsIgnoreCase("egg");

System.out.println(predicate.test("egg"));

System.out.println(predicate.test("agg"));

}

}

Stream 中的函数式接口

筛选偶数的例子使用到了 Stream 的 filter 方法,其实他还定义了很多方法来进行各种流操作,来看看 Stream 中一些常用方法的定义:

publicinterfaceStream<T> {

voidforEach(Consumer<? super T> action);

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Stream<T> filter(Predicate<? super T> predicate);

Optional<T> min(Comparator<? super T> comparator);

...

}

方法比较多,不一一列举。可以看到 lambda 表达式大量使用函数式接口作为参数。而这些接口很多都使用了常用的函数式接口。

总结

匿名内部类是为了简化代码,Lambda 表达式是为了进一步简化代码,函数式接口是为了 Lambada 表达式而生。

以上是 Lambda 表达式与函数式接口 的全部内容, 来源链接: utcz.com/a/36256.html

回到顶部