Java 8 双冒号的使用

声明:本篇文章除部分引用外,均为原创内容,如有雷同纯属巧合,引用转载请附上原文链接与声明。
阅读条件:阅读本篇文章需掌握函数式接口、lambda表达式、泛型知识。
注:本文若包含部分下载内容,本着一站式阅读的想法,本站提供其对应软件的直接下载方式,但是由于带宽原因下载缓慢是必然的,建议读者去相关官网进行下载,若某些软件禁止三方传播,请在主页上通过联系作者的方式将相关项目进行取消。

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

文章大纲

  • 调用方法的入参恰好为lambda表达式提供的所有参数
  • 调用方法不需要接收入参
  • 参数之间可以完整组成为一个方法调用
  • 表达式所提供的参数不再作为任何其他方法的入参
  • 总结
一.调用方法的入参恰好为lambda表达式提供的所有参数

1.1 变种一:作为类的静态方法或对象方法的全部入参

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

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

在这种情形下,对于test方法而言,该方法需要返回一个Consumer ,采用lambda表达式的写法为(s) -> Example.getStringParam(s) ,在该lambda表达式中,参数s作为了getStringParam方法的入参,所以可以简写为Example::getStringParam,可以这样理解:Consumer一定会传递一个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()
                // 等价于: wtf -> container.contains(wtf)
                .filter(container::contains) 
                .collect(Collectors.toList());
}

1.2 变种二:作为实例方法的入参

    public Consumer<String> test() {
        // 等价于  (s) -> Example.getStringParam(s);
        return this::getStringParam; 
    }

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

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

二. 调用方法不需要接收入参
    public Supplier<String> test2() {
        // 等价于  () -> Example.getString();
        return Example::getString;
    }
    
    public static String getString() {
        return "返回一个String对象";
    }

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

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

3.1 变种一

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

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

    public defineFunctionInterface<Sum,Integer,Integer,Integer> test4() {
        // 等价于 ->  (sum,one,two,three) -> sum.sumAll(one,two,three);
        return 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;
        }
    }

首先自定义一个函数式接口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表达式传入的参数和相关调用方法的实现是如下所示:
3.3 变种三

    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表达式无法转换为双冒号表达式。
3.4 变种四

    public defineFunctionInterface<Integer, Integer, Sum, Integer> test6() {
        // 不可以改写为Sum::sumAll;
        return (one, two, sum, 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;
        }
    }

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

四. 表达式所提供的参数不再作为任何其他方法的入参
    // 实现为得到传入string类型参数的大写形式
    public Consumer<String> test7() {
        // 等价于 (str) -> str.toLowerCase();
        return String::toLowerCase; 
    }

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

五.总结

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