Java 8 双冒号的使用

声明:本篇文章均为原创内容,如有雷同纯属巧合,转载请附上原文出处链接与声明
本文链接:https://www.jianshu.com/p/d5d9c1ced33c
注:阅读本篇文章需掌握函数式接口、lambda表达式、泛型知识;若未掌握请先于网上了解再阅读本篇文章。若有错误,欢迎友好纠正

Java 8引入了lambda表达式,使代码看上去更加简单,清晰。特别是在进行Stream操作时,便更加能体现出lambda表达式的优点。双冒号表达式是对lambda表达式的进一步精简表达方式,可以使代码更加简单明了。但仅仅在某些特殊条件下lambda表达式才可以变换为双冒号表达式。笔者在此总结了在哪些情形下lambda表达式可以转换为双冒号表达式。

一、调用方法的入参恰好为lambda表达式提供的所有参数

变种一:作为类的静态方法或对象方法的全部入参
// 作为类的静态方法全部入参
public class Example {
    
    public Consumer<String> test() {
        return Example::getStringParam; // 等价于  (s) -> Example.getStringParam(s);
    }

    public static void getStringParam(String s) {
        System.out.println("这是一个静态方法,获取一个String类型的参数:" + s);
    }
}

在这种情形下,对于test方法而言,该方法需要返回一个Consumer<String> ,采用lambda表达式的写法为(s) -> Example.getStringParam(s) ,在该lambda表达式中,参数s作为了getStringParam方法的入参,所以可以简写为Example::getStringParam,可以这样理解:Consumer<String>一定会传递一个String类型的的参数,而getStringParam方法刚好又接收一个参数,所以s-> 便可以省略,表明Consumer的实现策略是采用getStringParam对传入的参数进行处理。

// 作为对象方法的全部入参
public class Example {
        List<String> container = Arrays.asList("1","2","3"); // 容器
        List<String> waitToFilter = Arrays.asList("1","2","3","4","5","6"); // 需要过滤的数据
        List<String> res = waitToFilter.stream()
                .filter(container::contains) // 等价于: wtf -> container.contains(wtf)
                .collect(Collectors.toList());
}
变种二:作为实例方法的入参
    public Consumer<String> test() {
        return this::getStringParam; // 等价于  (s) -> Example.getStringParam(s);
    }

    public void getStringParam(String s) {
        System.out.println("这是一个静态方法,获取一个String类型的参数:" + s);
    }

可以看到传入的参数只要刚好作为调用方法的入参,那么就可以写为,调用方::调用方法 的形式。

二、调用方法不需要接收入参

    public Supplier<String> test2() {
        return Example::getString; // 等价于  () -> Example.getString();
    }
    
    public static String getString() {
        return "返回一个String对象";
    }

在这种情形下,对于test2方法而言,需要返回一个Supplier<String>,采用lambda表达式的写法为() -> Example.getString()。对于这种情况,其实可以理解为是情形一的入参为空,而方法接收也为空的情形下。

三、参数之间可以完整组成为一个方法调用

变种一
    public BiConsumer<List<Integer>, Integer> test3() {
        return List::add; // 等价于  (list,num) -> list.add(num);
    }

相较于前两种这种情形更较难理解,对于test3方法,我们要实现的效果是将第二个入参加入至第一个入参中采用lambda表达式的写法为(list,num) -> list.add(num);表明传入的两个参数中第一个List类型的参数会采用add操作将第二个参数添加进List集合中。在编译时编译器将会自动识别到List入参以及num入参,匹配到List::add表达式并进行对应。
这里笔者在针对这种情况举一个参数复杂一点的例子。

变种二
    public defineFunctionInterface<Sum,Integer,Integer,Integer> test4() {
        return Sum::sumAll; // 等价于 ->  (sum,one,two,three) -> sum.sumAll(one,two,three);
    }

    // 自定义一个接收四个入参的函数式接口
    public interface defineFunctionInterface<A,B,C,D>  {
        void apply(A a,B b,C c,D d);
    }

    // 自定义类
    public class Sum{
        public Integer all = 0;

        public Integer sumAll(Integer one,Integer two,Integer three) {
            all = all + one + two + three;
            return all;
        }
    }

首先自定义一个函数式接口defineFunctionInterface<A,B,C,D> 该接口的apply方法需要消费四个参数,而test4方法需要返回一个defineFunctionInterface<Sum,Integer,Integer,Integer>类型,即传入的参数分别是Sum、Integer、Integer、Integer类型,若实现的方法是需要调用Sum参数的sumAll方法,并且将后三个Integer传入sumAll方法中。采用lambda表达式的写法则为:(sum,one,two,three) -> sum.sumAll(one,two,three); 该表达式可以转换为Sum::sumAll。编译器会自动识别4个入参中哪一个是Sum类型,然后识别到Sum类型参数后,再调用其sumAll方法将剩下的全部参数(注意,我说的是剩下的全部参数而不是剩下的3个参数,只是在这个例子中只有出去Sum类型的入参只剩下了3个参数)传入。
到这里可能有人会说,如果lambda表达式传入的参数和相关调用方法的实现是如下所示:

变种三
    public defineFunctionInterface<Sum2, Integer, Sum2, Integer> test5() {
        // 等价于 ->  (sum21,sum22,one,two) -> sum21.sumAll(sum22,one,two);  
        // 但是不等价于 (sum21,sum22,one,two) -> sum22.sumAll(sum21,one,two);
        return (sum21,one,sum22,two) -> sum21.sumAll(sum22,one,two);
    }

    // 自定义一个接收四个入参的函数式接口
    public interface defineFunctionInterface<A, B, C, D> {
        void apply(A a, B b, C c, D d);
    }

    // 自定义添加类2
    public class Sum2 {
        public Integer all = 0;

        public Integer sumAll(Sum2 sum2, Integer one, Integer two) {
            all = all + sum2.all + one + two;
            return all;
        }
    }

在这种情况下传入的参数存在两个Sum2类型的,若采用lambda表达式的写法则可以写成以下两种:(sum21,sum22,one,two) -> sum21.sumAll(sum22,one,two);(sum21,sum22,one,two) -> sum22.sumAll(sum21,one,two) 即sum21以及sum22均能够成为方法的调用者,且剩下的三个参数也都可以组成sumAll剩下的全部参数,如果写成Sum2::sumAll;编译器在识别时会识别为哪一种形式,答案是:(sum21,sum22,one,two) -> sum21.sumAll(sum22,one,two);编译器默认会采用第一个参数作为方法调用者,且参数间可以形成完整的方法调用的情况下也只会选择第一个参数作为方法的调用者。如果方法的调用者并不在传递参数的第一个位置,那么lambda表达式无法转换为双冒号表达式。

变种四
    public defineFunctionInterface<Integer, Integer, Sum, Integer> test6() {
        return (one, two, sum, three) -> sum.sumAll(one, two, three);// 不可以改写为Sum::sumAll;
    }

    // 自定义一个接收四个入参的函数式接口
    public interface defineFunctionInterface<A, B, C, D> {
        void apply(A a, B b, C c, D d);
    }
    
    // 自定义添加类
    public class Sum {
        public Integer all = 0;

        public Integer sumAll(Integer one, Integer two, Integer three) {
            all = all + one + two + three;
            return all;
        }
    }

可以看出来变种四与变种二相比只是在得到的defineFuncitonInterface的实现不同。变种二为:defineFunctionInterface<Sum, Integer, Integer, Integer>,变种四为defineFunctionInterface<Integer, Integer, Sum, Integer>,只是交换了一下Sum参数类型的位置,但是在参数能能够形成一次完整的方法调用且调用者必须为lambda表达式提供的第一个参数且剩下的全部参数作为调用方法的入参,否则不能改写为双冒号表达式。

四、lambda表达式所提供的参数不再作为任何其他方法的入参,仅仅进行一次不需要参数的调用

    // 实现为得到传入string类型参数的大写形式
    public Consumer<String> test7() {
        return String::toLowerCase; // 等价于 (str) -> str.toLowerCase();
    }

在该情形下lambda表达式提供一个String类型的参数,不同于前几个情形,前几个情形中lambda表达式提供的参数需要作为方法调用的入参,但是这里提供的参数并没有作为方法调用的入参。而且作为方法调用的执行者,且只执行了一个简单的方法。

划重点:一次完整的方法调用

笔者更推荐采用抽象的思维去理解上述情形下lambda表达式与双冒号表达式的转换,其实以上所有情形都可以理解为:只要利用所有的入参能够完成一次完整的方法调用,那么lambda表达式即可转换为双冒号表达式,调用的方法可以是实例的方法(情形一的变种二),静态方法(情形一变种一),也可以是第一个参数作为方法的调用者(情形二、三、四)。因为如果能够利用所有入参完成一次完整的方法调用,那么编译器将会尝试查找并将参数进行补齐到相应的方法中。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容

  • 第一章 为什么要关心Java 8 使用Stream库来选择最佳低级执行机制可以避免使用Synchronized(同...
    谢随安阅读 1,493评论 0 4
  • http://python.jobbole.com/85231/ 关于专业技能写完项目接着写写一名3年工作经验的J...
    燕京博士阅读 7,579评论 1 118
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,237评论 0 4
  • 本文是在学习和使用kotlin时的一些总结与体会,一些代码示例来自于网络或Kotlin官方文档,持续更新... 对...
    竹尘居士阅读 3,294评论 0 8
  • 注:之前关于Java8的认知一直停留在知道有哪些修改和新的API上,对Lambda的认识也是仅仅限于对匿名内部类的...
    mualex阅读 2,826评论 1 4