Lambda 表达式与函数式接口

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

匿名内部类

什么是匿名内部类

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

public class NormalNamedClass {
    public static void main(String[] args) {

        Student student = new Student();
        student.eat();
    }
}

interface Person {
    void eat();
}

class Student implements Person {
    @Override
    public void eat() {
        System.out.println("eat something.");
    }
}
复制代码

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

class AnonymousClass {
    public static void main(String[] args) {
        Person person = new Person(){
            @Override
            public void eat() {
                System.out.println("eat something.");
            }
        };
        person.eat();
    }
}

interface Person {
    void eat();
}
复制代码

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

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

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

匿名内部类的作用

主要是用来简化代码

Lambda 表达式

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

Anonymous new Person() can be replaced with lambda

class AnonymousClass {
    public static void main(String[] args) {
        Person person = new Person(){
            @Override
            public void eat() {
                System.out.println("eat something.");
            }
        };
        person.eat();
    }
}

interface Person {
    void eat();
}
复制代码

优化后的代码如下:

class AnonymousClass {
    public static void main(String[] args) {
        Person person = () -> System.out.println("eat something.");
        person.eat();
    }
}

interface Person {
    void eat();
}
复制代码

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

() -> 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。

函数式接口

什么是函数式接口

先来看一份示例代码:

public class TestLambda {
    public static void main(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
public interface Predicate<T> {
    boolean test(T t);
    // 其他 default 方法省略
}
复制代码

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

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

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

@FunctionalInterface
public interface Person {
    void eat();
    void sing();
}
复制代码

上述代码会报如下错误:

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

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

常用函数式接口

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

Consumer<T>: 接受一个输入并且无返回结果
public class TestConsumer {
    public static void main(String[] args) {
        Consumer<String> consumer = s -> System.out.println("accept:" + s);
        consumer.accept("egg");
    }
}

// 输出 accept egg.
复制代码
Function<T,R>: 接受一个输入并且返回一个结果
public class TestFunction {
    public static void main(String[] args) {
        Function<String, String> function = (s) -> "function input with:" + s;
        String result = function.apply("egg");
        System.out.println(result);
    }
}
复制代码
Supplier<T>: 没有参数返回但返回一个结果
public class TestSupplier {
    public static void main(String[] args) {
        Supplier<String> supplier = () -> "egg";
        String result = supplier.get();
        System.out.println(result);
    }
}
复制代码
Predicate<T>: 接受一个输入并且返回一个布尔值结果
public class TestPredicate {
    public static void main(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 中一些常用方法的定义:

public interface Stream<T> {
    void forEach(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 表达式而生。

猜你喜欢

转载自juejin.im/post/5f203f636fb9a07e7c3bbdc4