我是靠谱客的博主 危机咖啡豆,这篇文章主要介绍Java函数式接口前世今生全面解析包教包会函数式接口,现在分享给大家,希望可以做个参考。

函数式接口

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

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


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

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

    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 市民类 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>(); } }
    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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()); } }

    执行结果:

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

    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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>(); } }
    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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()); } }

    执行结果:

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

    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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); }
  • 这也太蠢了,可以像下面这样合并为一个吗:

    复制代码
    1
    2
    3
    4
    5
    6
    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>增加一项内容)的函数!如果以后要添加的东西很多,就会像下面这样:

    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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等)写成函数,把重复的变量当作函数的参数。可是现在重复的部分就是函数,如果能把函数也像变量一样传入到函数里执行就好了,你可能会想到使用反射:

    复制代码
    1
    2
    3
    4
    5
    // 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里,函数的定义必须依托类或者接口,自然而然的,我们需要这样定义这样一个接口:

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

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

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

    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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()); } }
  • 执行结果:

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

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

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

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

    复制代码
    1
    2
    3
    4
    5
    6
    // 新增一个货物类,getter和构造器什么的省略了! public class Cargo { private String category; private List<String> routes; }
    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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()); } }
  • 执行结果:

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

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

    复制代码
    1
    2
    3
    4
    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一样:

    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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()的命名仅受接口的约束,与外面的函数名无关,就像我们正常函数中的形参一样,与实参无关:

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

    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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:

    复制代码
    1
    2
    3
    4
    5
    6
    // 泛型指定为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表达式传参:

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

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

    复制代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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,"南虹市"); } }
  • 至此,利用函数式接口,函数已经不仅是函数了,它甚至可以被赋值给变量,然后像其他变量一样传来传去,下面的代码都是合法的:

    复制代码
    1
    2
    3
    4
    5
    Consumer<String> fun1 = str->citizen1.getRoutes().add(str); Consumer<String> fun2 = citizen1.getRoutes()::add; guard.superAdd(fun1,"西红市"); guard.superAdd(fun2,"西黄市");

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

最后

以上就是危机咖啡豆最近收集整理的关于Java函数式接口前世今生全面解析包教包会函数式接口的全部内容,更多相关Java函数式接口前世今生全面解析包教包会函数式接口内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(62)

评论列表共有 0 条评论

立即
投稿
返回
顶部