Java函数式接口前世今生全面解析包教包会

函数式接口

一句话总结:函数式接口的作用是让函数成为函数的参数。

​ 如果你直接去搜“函数式接口”,可能会得到一句“有用的废话”:只有一个抽象方法的接口就叫函数式接口。这个解释并没有体现它设计的本意,仅从实现角度解释了什么样的接口叫函数式接口。如果从设计角度看,所谓函数式接口其实是非常简单而基础的概念,只不过Java的特性导致它看起来有些复杂,我们可以抛开以往对函数式接口的认知,抛开函数式接口自身的概念,从它的形成过程解析为什么是“函数式接口”。


​ 我们知道函数是代码复用性最基本的体现,拥有相同&相似功能的代码块应该被抽象成函数,不仅方便多处调用也方便维护和修改。先看一个十分简单(可能不恰当)的例子:

  • 响应防疫号召,假设要写一个Guard类,帮助每个市民记录最近的行程路线:

    // 市民类
    public class Citizen {
        private String name;
        private List<String> routes;
        
        public String getName() {
            return name;
        }
        
        public List<String> getRoutes() {
            return routes;
        }
        
        public Citizen(String name) {
            this.name = name;
            this.routes = new ArrayList<String>();
        }
    }
    
    
    public class Guard {
    	// 仅负责把路线记录到市民上
        public void addRoute(Citizen citizen,String address){
            citizen.getRoutes().add(address);
        }
    
        public static void main(String[] args) {
            Guard guard = new Guard();
            Citizen citizen1 = new Citizen("热心市民刘先生");
            guard.addRoute(citizen1,"钱逛光商场");
            guard.addRoute(citizen1,"买买买超市");
            System.out.println(citizen1.getName()+"的路线:"+citizen1.getRoutes());
        }
    }
    

    执行结果:

    热心市民刘先生的路线:[钱逛光商场, 买买买超市]
    
    Process finished with exit code 0
    
  • 现在有了新的改动,Guard不仅要负责市民的路线,还要记录每个地点的体温,有的地方还要报备危险等级。emm再加两个List吧。

    public class Citizen {
        private String name;
        private List<String> routes;
        private List<String> dangerLevel;
        private List<String> temperature;
    
        //... getter省略了!
    
        public Citizen(String name) {
            this.name = name;
            this.routes = new ArrayList<String>();
            this.dangerLevel = new ArrayList<String>();
            this.temperature = new ArrayList<String>();
        }
    }
    
    public class Guard {
    
        public void addRoute(Citizen citizen, String address) {
            citizen.getRoutes().add(address);
        }
    
        public void addDangerLevel(Citizen citizen, String dangerLevel) {
            citizen.getDangerLevel().add(dangerLevel);
        }
    
        public void addTemperature(Citizen citizen, String temperature) {
            citizen.getTemperature().add(temperature);
        }
    
        public static void main(String[] args) {
            Guard guard = new Guard();
            Citizen citizen1 = new Citizen("热心市民刘先生");
    
            guard.addRoute(citizen1, "钱逛光商场");
            guard.addTemperature(citizen1, "36");
    
            guard.addRoute(citizen1, "买买买超市");
            guard.addTemperature(citizen1, "37");
    
            guard.addRoute(citizen1, "西虹市");
            guard.addTemperature(citizen1, "37");
            guard.addDangerLevel(citizen1, "AA");
    
            System.out.println(citizen1.getName() + "的路线:" + citizen1.getRoutes());
            System.out.println(citizen1.getName() + "的体温:" + citizen1.getTemperature());
            System.out.println(citizen1.getName() + "的危险等级:" + citizen1.getDangerLevel());
        }
    }
    

    执行结果:

    热心市民刘先生的路线:[钱逛光商场, 买买买超市, 西虹市]
    热心市民刘先生的体温:[36, 37, 37]
    热心市民刘先生的危险等级:[AA]
    
    Process finished with exit code 0
    
  • 等等,快看看这三个函数:

    public void addRoute(Citizen citizen, String address) {
        citizen.getRoutes().add(address);
    }
    public void addDangerLevel(Citizen citizen, String dangerLevel) {
        citizen.getDangerLevel().add(dangerLevel);
    }
    public void addTemperature(Citizen citizen, String temperature) {
        citizen.getTemperature().add(temperature);
    }
    
  • 这也太蠢了,可以像下面这样合并为一个吗:

    public void add(Citizen citizen,String address,String dangerLevel,String temperature){
        citizen.getRoutes().add(address);
        citizen.getDangerLevel().add(dangerLevel);
        citizen.getTemperature().add(temperature);
    }
    
  • 似乎好了一点,但是并非每个地方都要报备危险等级啊,搞不好后期还要再加东西,参数列表会越来越长,而且这样看起来仍然是在做重复的工作,执行了三个功能几乎相同(都是给某个List<String>增加一项内容)的函数!如果以后要添加的东西很多,就会像下面这样:

    public void add(Citizen citizen,String address,String dangerLevel,String temperature,.....){
        citizen.getRoutes().add(address);
        citizen.getDangerLevel().add(dangerLevel);
        citizen.getTemperature().add(temperature);
        citizen.getAAA().add(aaa);
        citizen.getBBB().add(bbb);
        citizen.getCCC.add(ccc);
        ...
      }
    
  • 遇到“重复代码”,我们一般都会把重复的部分(这里是getRoutes、getDangerLevel等)写成函数,把重复的变量当作函数的参数。可是现在重复的部分就是函数,如果能把函数也像变量一样传入到函数里执行就好了,你可能会想到使用反射:

    // method就是获取list的函数
    public void add(Method method,String str){ //要抛异常
        method.invoke().add(str);
    }
    
  • 但是这不对啊,反射是获得一个”具体函数”,我们这里想定义的是“一类函数”,就好像我们看到123、456会想到他们同属于Integer,可以这样写Integer a = 123;,看到"abc"、"def"会想到他们同属于String,可以这样写String a = "abc";。我们现在就需要一个可以描述“返回值类型是List<String>的函数”的东西,然后直接调用这个东西的add方法就行了。

  • 那Java里有没有东西能描述“一类函数”呢,当然有!抽象函数:List<String> getList();这个抽象函数不就完美描述了函数的行为吗,但是在Java里,函数的定义必须依托类或者接口,自然而然的,我们需要这样定义这样一个接口:

    public interface ListGetter {
        List<String> getList();
    }
    
  • 然后在之前的Guard类里把参数设置为接口,再传入要add的字符串不就行了:

    // 新的Guard类的定义方式,三个函数合并为一个,真的好简洁
    public class Guard {
        public void add(ListGetter getter,String str){
            getter.getList().add(str);
        }
        psvm...
    }
    
  • 但是使用的时候又又又出问题了,在Java里接口的函数是虚的,要想使用这个函数必须要有一个实在的类,然后重写这个函数(Java,你真的好严格),所以,不可避免地,调用方法的时候使用匿名内部类,然后重写接口的函数:

    // 调用时依然很麻烦的匿名类
    guard.add(new ListGetter() {
        @Override
        public List<String> getList() {
            return citizen1.getRoutes();
        }
    },"西虹市");
    
  • 所以就变成了这样(省略一小部分重复内容):

    public class Guard {
    
        public void add(ListGetter getter,String str){
            getter.getList().add(str);
        }
    
        public static void main(String[] args){
    
            Guard guard = new Guard();
    
            Citizen citizen1 = new Citizen("热心市民刘先生");
    
            guard.add(new ListGetter() {
                @Override
                public List<String> getList() {
                    return citizen1.getRoutes();
                }
            },"西虹市");
    
            guard.add(new ListGetter() {
                @Override
                public List<String> getList() {
                    return citizen1.getTemperature();
                }
            },"36");
    
            guard.add(new ListGetter() {
                @Override
                public List<String> getList() {
                    return citizen1.getDangerLevel();
                }
            },"AA");
    
            System.out.println(citizen1.getName() + "的路线:" + citizen1.getRoutes());
            System.out.println(citizen1.getName() + "的体温:" + citizen1.getTemperature());
            System.out.println(citizen1.getName() + "的危险等级:" + citizen1.getDangerLevel());
        }
    }
    
    
  • 执行结果:

    热心市民刘先生的路线:[西虹市]
    热心市民刘先生的体温:[36]
    热心市民刘先生的危险等级:[AA]
    
    Process finished with exit code 0
    
  • 天哪!虽然Guard类的add方法变得万能了,但是这调用的时候真不比刚才简单,我还是用之前的那个吧。

  • 别急,Java为了拯救你,在1.8版本隆重推出Lambda表达式,如果接口下只有一个抽象函数(ps:为什么只有一个接口才能用lambda,因为如果接口下有多个抽象函数,使用()->{}的格式就不知道你重写哪个函数了),你可以使用下面的办法简化匿名类:

    // 使用lambda简化三个匿名类
    guard.add(() -> citizen1.getRoutes(),"西虹市");
    guard.add(() -> citizen1.getTemperature(),"36");
    guard.add(() -> citizen1.getDangerLevel(),"AA");
    
  • 简洁吗?还有更好的,如果匿名类的里的函数是已经存在的函数,还可以再简化成方法引用,即双冒号::这个双冒号几乎可以当作中文里的**”的“**字来看待,代码可读性大大增加:

    // 一看就知道要调用 citizen1 的 某某方法
    guard.add(citizen1::getRoutes,"西虹市");
    guard.add(citizen1::getTemperature,"36");
    guard.add(citizen1::getDangerLevel,"AA");
    
  • 到现在,代码十分简洁,让Guard记录货物的路线也没问题:

    // 新增一个货物类,getter和构造器什么的省略了!
    public class Cargo {
        private String category;
        private List<String> routes;
    }
    
    public class Guard {
    
        public void add(ListGetter getter, String str) {
            getter.getList().add(str);
        }
    
        public static void main(String[] args) {
    
            Guard guard = new Guard();
            Citizen citizen1 = new Citizen("热心市民刘先生");
            Cargo cargo1 = new Cargo("救援物资");
    
            guard.add(citizen1::getRoutes, "西虹市");
            guard.add(citizen1::getTemperature, "36");
            guard.add(citizen1::getDangerLevel, "AA");
    
            System.out.println(citizen1.getName() + "的路线:" + citizen1.getRoutes());
            System.out.println(citizen1.getName() + "的体温:" + citizen1.getTemperature());
            System.out.println(citizen1.getName() + "的危险等级:" + citizen1.getDangerLevel());
    
            guard.add(cargo1::getRoutes,"五湖四海");
            guard.add(cargo1::getRoutes,"武汉");
    
            System.out.println(cargo1.getCategory() + "的路线:" + cargo1.getRoutes());
        }
    }
    
  • 执行结果:

    热心市民刘先生的路线:[西虹市]
    热心市民刘先生的体温:[36]
    热心市民刘先生的危险等级:[AA]
    救援物资的路线:[五湖四海, 武汉]
    
    Process finished with exit code 0
    
  • 这个的确很过瘾,但是我们要讲什么来着,函数式接口!这些东西有什么关系吗?

  • 好兄弟,快去看看我们刚才为了描述函数而定义的接口:

    public interface ListGetter {
        List<String> getList();
    }
    
  • 发现什么没有,这不就是"只有一个抽象方法的接口"吗?刚才整个的分析过程就已经完成了函数式接口的开发过程!

  • 当然,开发Java的大佬们编写的函数式接口复用性要更强,我们这里定义的ListGetter接口(现在可以称它为函数式接口了!)仅仅只描述了“返回值是List<String>类型”的这类函数。

  • 就好比数字要分为:整型,浮点型,长整型等等,他们虽然都是数字但是有不同的格式,所以是不同的类型,大佬们也将函数分为四个大类型:消费型(Consumer)、供给型(Supplier)、函数型(Function)、断言型(Predicate)。同时使用泛型来定义某类型函数中的参数类型。

  • 这四种类型的接口(他们当然得是接口,虽然接口下的函数才是设计本体)分别描述了以下四种函数的类型:

    函数式接口接口下的函数描述的函数类型
    Consumer<T> 消费型接口void accept(T t);有参数,无返回值的函数
    Supplier<T> 供给型接口T get();无参数,有返回值的函数
    Function<T, R> 函数型接口R apply(T t);有参数,有返回值的函数
    Predicate<T> 断言型接口boolean test(T t);有参数,返回值是布尔值的函数
  • 像官方一样,自己定义函数式接口也应该加@FunctionalInterface注解,以保证接口下仅有一个抽象函数(但可以有多个默认函数和静态函数)

  • 再来看看我们之前定义的接口,它描述了返回值类型是List<String>的函数,显然是属于无参数,有返回值的Supplier类型的函数,这个函数的返回值的类型就是List<String>,用官方的函数式接口实现一下试试:

  • 使用函数式接口定义函数就像使用String、Integer一样:

    public class Guard {
        // 这里在泛型的位置放上List<String>它就知道这个函数的参数必须是一个能返回List<String>的函数式接口
        // 第二个参数str是为里面的函数准备的
        private void newAdd(Supplier< List<String> > supplier, String str) {
            // 这里的get()才是函数本体,它就像正常函数里的形参,supplier是为它服务的接口
            supplier.get().add(str);
            // 因为我们已经在泛型中说明了传进来的函数返回值必须是List<String>,所以后面的add就是调用List<String>的add方法。
        }
    }
    
  • 这里函数get()的命名仅受接口的约束,与外面的函数名无关,就像我们正常函数中的形参一样,与实参无关:

    int a = 0;
    
    int setNumber(int asdagasdasfagsdadf){
        // 形参的命名外面的实参(整型a)无关
        asdagasdasfagsdadf = 10;
    }
    
    a = setNumber(a);
    
  • 使用官方的函数式接口,我们可以删除原来的接口,代码变成这样:

    public class Guard {
    
        private void newAdd(Supplier<List<String>> supplier, String str) {
            supplier.get().add(str);
        }
    
        public static void main(String[] args) {
    
            Guard guard = new Guard();
    
            Citizen citizen1 = new Citizen("热心市民刘先生");
    
            // 使用lambda和方法引用都可以,显然方法引用可读性更强
            guard.newAdd(citizen1::getRoutes,"西虹市");
            
            ...
        }
    }
    
    
  • 结束了吗?还没有,你可能注意很久了,这个函数里这个add()可太扎眼了,早就忍不了了,既然这么方便,传函数的时候不能直接传这个add函数吗?

  • 当然可以!不过我们要分析一下add(String str)这个函数属于哪类函数,它有参数,无返回值,显然属于Consumer接口,add(String str)需要的参数是String类型,所以我们在使用Consumer时的泛型要指定为String:

    // 泛型指定为String,第二个参数str是为add准备的
    private void superAdd(Consumer<String> consumer, String str) {
        // 和get一样,accept的命名来自Consumer接口
        consumer.accept(str);
    }
    
  • 接下来就要分析add(String str)这个函数是谁来调用了,显然是Citizen里面的List<String>,可以直接使用对应对象里的getter获得List<String>

  • 使用时可以使用lambda表达式传参:

    guard.superAdd((String str)->{
        citizen1.getRoutes().add(str);
    },"西虹市");
    
    // 当然lambda也有简化版本
    guard.superAdd(str->citizen1.getRoutes().add(str),"西虹市");
    
  • 或者使用可读性更强的方法引用来传参:

    // 这里表示citizen1.getRoutes()的add方法,双冒号 ==> “的”
    guard.superAdd(citizen1.getRoutes()::add,"西红市");
    
  • 整理下来,新代码就是这样的了

    public class Guard {
    
        private void newAdd(Supplier<List<String>> supplier, String str) {
            supplier.get().add(str);
        }
    
        private void superAdd(Consumer<String> consumer, String str) {
            consumer.accept(str);
        }
    
        public static void main(String[] args) {
    
            Guard guard = new Guard();
    
            Citizen citizen1 = new Citizen("热心市民刘先生");
      
            guard.newAdd(citizen1::getRoutes,"西虹市");
            
            guard.superAdd(str->citizen1.getRoutes().add(str),"北虹市");
    
            guard.superAdd(citizen1.getRoutes()::add,"南虹市");
        }
    }
    
    
  • 至此,利用函数式接口,函数已经不仅是函数了,它甚至可以被赋值给变量,然后像其他变量一样传来传去,下面的代码都是合法的:

    Consumer<String> fun1 = str->citizen1.getRoutes().add(str);
    
    Consumer<String> fun2 = citizen1.getRoutes()::add;
    
    guard.superAdd(fun1,"西红市");
    
    guard.superAdd(fun2,"西黄市");
    

​ 总结下来,函数式接口的重点不是接口,而是它里面的函数,使用接口就是为了在调用时让抽象函数有依托,真正的核心是对所有函数的分类和抽象,如同对所有数字类型分类为整型、浮点型、双精度一样。正如我开始所述,函数式接口其实是偏基础的概念,就是为了定义函数的类型而创建的接口,让函数可以成为函数的参数。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值