java8 函数式接口

2021/09/02 JAVA8 共 5039 字,约 15 分钟
闷骚的程序员

1. 函数式接口

1.1 什么是函数式接口?

函数式接口:有且仅有一个抽象方法的接口。
其意味着,一个接口中有且仅有一个抽象方法,只有方法定义,没有方法体;其他的方法都是已经提供了实现的默认方法或者静态方法。
函数式接口是在java8对接口默认方法和静态方法的基础上提供的新的接口类别。
因为Object类是所有类的父类,因此,如果一个接口中定义了Object类中可以被覆盖的方法,那么即便这个方法在接口中定义是抽象的,也不被认为是一个抽象方法,因为它实际上继承自Object类,已经有明确的方法实现。
(1)函数式接口只能有一个抽象方法;
(2)函数式接口其他的方法可以是默认方法或者静态方法;
(3)只要是Object类中能够被复写的方法都不被算作接口的抽象方法。

下面我们定义一个函数式接口:

package zeh.myjavase.code42java8.demo02;

interface EatDemo02 {
    //没有返回值的抽象方法
    public abstract void eat(String name);

    //==========================下面三种类型的方法都不是抽象方法==========================================================
    //java8中,默认方法,必须实现。
    default void defaultMethod() {
        System.out.println("接口中默认方法");
    }

    default void defaultMethod2() {
        System.out.println("接口中默认方法");
    }

    //java8中,静态方法必须实现
    public static void staticMethod() {
        System.out.println("接口中静态方法");
    }

    public static void staticMethod2() {
        System.out.println("接口中静态方法");
    }

    //java8中,object中的public方法,虽然是抽象的,但实际继承的是object中该方法的实现。
    @Override
    public abstract boolean equals(Object obj);
}

1.2 为何使用函数式接口?

java8实际上从编译器、从jdk实现、从字节码实现、和jvm解释器上都做了很多的升级和扩展。其中对于接口就做了语法的提升。
和java8之前相比,一个接口中允许定义default方法和static方法了,那么现在又了解到,一个接口还可以被设计成函数式接口。
只要这个接口中具有唯一的一个抽象方法,那么这个接口就符合函数式接口的规范,它就是一个函数式接口。
那么,和普通接口相比,函数式接口只能有一个抽象方法,它到底能干什么呢?
这就要提起java8开发者们一直提倡的一种思想:函数式编程思想。
其实熟悉前端js语法的朋友们可能比较清楚“函数”这个概念。确切来讲,函数和java中的方法很类似,但是在使用上,函数却要比java的方法灵活的多。
java中要使用一个对象的方法,那么首先得创建这个对象,才能通过这个对象去调用这个方法。
而“函数”应该被描述为一段逻辑,一个函数的本质就是一段可执行逻辑。
在js中,函数之前可以直接传递一个参数,也可以直接传递一个函数,而传递的这个函数,其本质就是一段可执行逻辑。
因此,java中提倡的“函数式编程”,它实际上就是在模仿js中的函数传递的概念。也就是说,在java的方法调用间,也可以允许直接传递一个“函数”—-一段可执行的逻辑,来替代过去只能传递java对象的局限。
在java8中,这种在方法间可传递的逻辑代码就类似于一个“函数定义”,java8中将这种可传递的逻辑代码称作一个表达式—-Lambda表达式。

其实,Lambda表达式实际上是对匿名内部类的一个变相升级。
在java8之前,我们也能方便的通过匿名内部类的方式来创建一个接口的对象,其实际上是定义了一个该接口的实现类,然后实现了接口的抽象方法,最后创建了该实现类的一个匿名对象而已。
java8中,如果一个接口只有一个抽象方法的话,我们就可以直接通过lambda表达式来直接为该接口创建一个匿名内部类对象。
之所以有如此神奇的功能,是因为java8升级了java编译器和解释器,编译器和解释器会检测到接口是否只有一个抽象方法,如果是,则将lambda表达式翻译成该接口的一个实现类并创建它的对象。

因此,我们知道了,之所以使用函数式接口,是为了使用Lambda表达式来简化代码的编写,达到函数式编程的目的。因为Lambda表达式的本质是为一个只有一个抽象方法的接口创建一个匿名内部类对象。
即:Lambda表达式是为函数式接口创建的一个具名的匿名内部类对象。

注意:
(1)函数式接口是使用Lambda表达式的前提,如果一个接口不是函数式接口,则无法对该接口使用Lambda表达式。
(2)我们在很多地方可以方便的使用Lambda表达式,是因为jdk8中将源码中的很多接口都升级为函数式接口了,因此我们才能方便的通过一个Lambda表达式为该接口创建一个对象。
(3)我们看到的Lambda表达式是一个逻辑代码片段,实际上Lambda表达式返回的是一个函数式接口的实现子类对象,因此Lambda表达式可以方便的在方法之间作为参数传递,从而实现函数式编程。

下面使用Lambda表达式来创建上一节中定义的函数式接口的对象:

package zeh.myjavase.code42java8.demo02;

import org.junit.Test;

public class LambdaRun {
    @Test
    public void testYuanShi() {
        //原始方式:通过匿名内部类方便的实现接口或者父类的实现子类,注意匿名内部类同时也是一个匿名对象,往往只会使用一次。
        //当然也可以通过Eat类型去接收匿名对象使之成为具名对象。
        new TestDemo02() {
            @Override
            public void eat(String name) {
                System.out.println(name + "在吃东西...");
            }

            @Override
            public void test() {
                System.out.println("吃饭");
            }
        }.eat("赵二虎");
    }

    @Test
    public void testLambda() {
        EatDemo02 ea1 = (name) -> System.out.println(name + "在吃东西");
        ea1.eat("Lambda表达式1");
        EatDemo02 ea2 = name -> System.out.println(name + "在吃东西");
        ea2.eat("Lambda表达式2");
        EatDemo02 ea3 = (String name) -> System.out.println(name + "在吃东西");
        ea3.eat("Lambda表达式3");

        //Lambda表达式如何确定这个表达式实现的就是某个对应的接口呢?
        //答案:Lambda表达式必须具名,即不能直接使用Lambda表达式直接调用目标方法,因为这样找不到。
        //只有显式的使用对应接口的类型去接收lambda表达式,这样才能确定Lambda表达式实现的对应的函数式接口。从而确定关系。
//        (name)-> System.out.println(name + "吃").eat();

        //实现的接口方法存在多个参数,则使用()括起来,放入形式参数名称。
        //当实现的接口方法体有return返回值时,Lambda表达式中不用return,当然也可以用return,会根据接口中定义的抽象方法的返回值类型自动确认返回类型。
        SumDemo02 sum = (a, b) -> a + b;
        System.out.println("result:" + sum.sum(12, 34));

        SumDemo02 sum1 = (c, d) -> {
            return c + d;
        };
        System.out.println("result2:" + sum1.sum(111, 22));

        //lambda表达式中可以应用类成员和局部变量(在lambda表达式内部会将这些变量隐式转换成final的)
        int t = 100;
        SumDemo02 sum2 = (c, d) -> {
            return c + d + t;
        };
        System.out.println("result3:" + sum2.sum(222, 444));
    }
}

1.3 @FunctionalInterface 注解

一个接口如果是函数式接口,我们就可以使用Lambda表达式来方便的为该接口创建一个实现类对象了,从而从形式上可以实现“函数式编程”了。
但函数式接口本身又很脆弱,因为只要我们在一个函数式接口中新增了一个抽象方法,那么这个接口就不是函数式接口而变成普通接口了。
这样一来,凡是为该接口编写的Lambda表达式将全部出现语法错误,导致编译失败。
这显然是很扯的事情,加入jdk中已经为某个接口编写了大量的Lambda表达式,而后面谁又将该接口变为一个普通接口了,那么所有的Lambda表达式将编译失败。
为了克服这种代码层面的脆弱性,java8提供了一个特殊的注解—-@FunctionalInterface。
这个注解的作用就是标注在一个函数式接口上,用来显式地说明该接口是一个函数式接口,并且会有编译级别的校验,一旦标注该注解,当该接口不符合函数式接口的规范时,将导致编译失败,提示相关的错误信息。
说直白点,这个注解就是编译器用来校验一个接口是否符合函数式接口的规范,如果不符合,则提示编译错误,防止我们错误的将一个函数式接口修改为一个普通接口而导致大量的Lambda失败。

我们使用 @FunctionalInterface 注解来显式的标注一个函数式接口:

package zeh.myjavase.code42java8.demo02;

// 使用 @FunctionalInterface 注解标注一个函数式接口,当该接口存在多个抽象方法定义时,将直接编译报错
@FunctionalInterface
interface SumDemo02 {
    //有返回值的接口方法
    public abstract int sum(int a, int b);

    //==========================下面三种类型的方法都不是抽象方法==========================================================
    //java8中,默认方法,必须实现。
    default void defaultMethod() {
        System.out.println("接口中默认方法");
    }

    //java8中,静态方法必须实现
    public static void staticMethod() {
        System.out.println("接口中静态方法");
    }

    //java8中,object中的public方法,虽然是抽象的,但实际继承的是object中该方法的实现。
    @Override
    public abstract boolean equals(Object obj);
}

(1)jdk中所有的函数式接口都已经显式的标注了@FunctionalInterface注解了。
(2)自己在编写函数式接口时,建议也显式的使用 @FunctionalInterface 注解进行标注,这样一旦有人误操作修改了你的接口,编译器将直接暴露编译错误。

2. java8内置的函数式接口

java8除了在语法层面支持“函数式编程”风格外,在java8的jdk源码包中也添加了一个包,叫做 java.util.function。
这个包中包含了很多类,用来直接java8的函数式编程。
jdk8中提供的这些函数式接口,可以方便的使用Lambda表达式来创建这些函数式接口的对象,用简洁的Lambda来实现更多的逻辑行为。
说白了就是java8内置了很多函数式接口,方便外部通过lambda表达式去实现对应接口的目标方法逻辑。当然这些函数式接口也可以通过普通匿名内部类的方式去实现接口中的抽象方法。

java8中内置的函数式接口分类:
(1)消费型接口 Consumer
(2)供给型接口 Supplier
(3)函数型接口 Function
(4)断言型接口 Predicate

文档信息

Search

    Table of Contents