lambda方法引用总结——烧脑吃透

lambda是java8的新特性,基本使用比较容易理解,但有一个环节遇到了坎儿,那就是方法引用,尤其是类的实例方法引用,烧脑之后总结一下。

在需要函数参数的方法中,我们可以把另一个同类型的方法直接传入,这称为方法引用的绑定。类似于C语言中的函数指针。

lambda表达式可以替代方法引用;或者说方法引用是lambda的一种特例,方法引用不可以控制传递参数。

4.1) 构造器引用

private Person construntorRef(Supplier<Person> sup){
    Person p=sup.get();
    return p;
}

@Test
public void testConstructorRef(){
    Person p=construntorRef(Person::new);
    System.out.println(p);
}

需要有无参的构造器。

4.2) 静态方法引用

    private static void print(String s){
        System.out.println(s);
    }

    @Test
    public void testStaticRef(){
        Arrays.asList("aa","bb","cc").forEach(TestMethodReference::print);
    }

so easy,只要静态方法的参数列表和FI需要的参数一致就可以。

4.3) 成员方法引用

@Test
    public void testMemberMethodRef(){
        Arrays.asList("aa","bb","cc").forEach(System.out::println);
    }

so easy,只要成员方法的参数列表和FI需要的参数一致就可以。

4.4) 类的任意对象的实例方法引用(很怪异)

@Test
public void testClassMemberMethodRef(){
    String[] strs={"zzaa","xxbb","yycc"};
    Arrays.sort(strs,String::compareToIgnoreCase);//OK
    System.out.println(Arrays.asList(strs));
    File[] files = new File("C:").listFiles(File::isHidden); // OK
}

��,前方高能,请关掉耳机的音乐,认真思考,小心行事。

传统的java开发中,是不允许使用类名去调用成员方法的,这是一个基本原则,那么这里的这种写法就有点不太容易理解了。还是用实例说明:

用到的内部类:

import lombok.Data;

@Data
public static class Person{
    private String name;
    private Integer age;

    public int mycompare(Person p1){
        return p1.getAge()-this.getAge();
    }
    public void print(){
        System.out.println(this);
    }

    public void println(Person p){
        System.out.println(p);
    }

    public int compareByAge(Person a,Person b){
        return a.getAge().compareTo(b.getAge());
    }
}

public static class APerson{
    public void print(){
        System.out.println(this);
    }

    public void println(Person p){
        System.out.println(p);
    }
}

测试代码:

@Test
    public void testClassMemberMethodRef2() {
        // R apply(T t);//要求一个参数
        Function<String, String> upperfier1 = String::toUpperCase;
        UnaryOperator<String> upperfier2 = (x) -> x.toUpperCase();//这里没有参数,即0个

        /*
         * 小结:如果方法引用表达式 "String::toUpperCase" 可以用lambda表达式中参数的指定成员方法(这个成员方法的参数比FI要求的参数少一个改类型的参数)改写,
         *  那么就可以使用 "类的实例方法"来表示方法引用。
         *
         *  或者说:如果lambda表达式的lambda体中使用的方法是参数匹配的方法,那么方法引用表达式就用"类引用对象的实例方法"。
         *
         *  lambda的参数是方法的主体。
         */

        class Print {
            public void println(String s) {
                System.out.println(s);
            }
        }
        // void accept(T t);

        Consumer<String> sysout1 = new Print()::println;
        Consumer<String> sysout2 = (x) -> new Print().println(x);

        /*
         * 小结:如果方法引用表达式 "new Print()::println" 可以用lambda表达式中参数的具体对象的参数匹配的成员方法改写,
         *  那么就用 "对象的实例方法"来表示方法引用。
         *
         *  或者说:如果lambda表达式的lambda体中使用的方法来操作lambda的参数,那么方法引用表达式就用"对象的实例方法"。
         *
         *  lambda的参数是方法的参数。
         */

        //有一个更让人易混淆的例子,可以用上面的规则来验证,Arrays.sort(T t,Comparator<? extends t> c)

        class Person {
            public int com1(Person p) {
                return 1;
            }

            public int com2(Person p1, Person p2) {
                return 1;
            }
        }

        // int compare(T o1, T o2);//需要两个参数

        Person【】 ps = { new Person(), new Person() };
        Arrays.sort(ps, Person::com1);
        Arrays.sort(ps, (x,y)->x.com1(y));

        Arrays.sort(ps, new Person()::com2);
        Arrays.sort(ps, (x,y)->new Person().com2(x, y));

        //按照以上规则验证应该能说明清楚。

        /*
         * 但是一个接口为什么有两种写法?缺省的lambda会匹配FI方法,即"int compare(T o1, T o2);"
         * 从上面的lambda表达式来分析,默认的使用lambda应该是:
         */

        Comparator<Person> comparator1 = new Person()::com2;

        /*
         * 下面的方式又是怎么回事呢?
         */
        Comparator<Person> comparator2 = Person::com1;
        System.out.println(comparator2);

        /*
        *   任一个两个参数的FI接口方法(int compare(T o1, T o2)),都可以用引用减少一个参数的方法(int o1<T>.compare(T o2))来代替,而引用对象本身作为另一个隐含参数,那么方法引用的对象用类名,表示类的任意对象。

        还是有点乱?我们来换一个角度来看一下:
        首先,我们需要的是int compare(T o1, T o2)是两个参数;
        其次,先不考虑::前缀是类还是对象,你给了我一个compare(T o2),少一个参数?怎么办?
        lambda机制为了解决这个问题,它使用::前面的类名new一个对象,当做需要的缺少的那个参数,这就是类的实例方法。
        */
    }

小结一下:

首先明确此处需要的方法参数列表,此处标记参数个数为N,那么:

1. 如果传入的方法是一个类型的静态方法,而且参数匹配,使用“类的静态方法引用”;这应该不难理解。

2. 如果传入的方法是一个实例的成员方法,而且参数匹配,使用“实例的成员方法”;这也应该不难理解。

3. 如果传入的方法是一个类型T的实例的成员方法,而且参数为N-1个,缺少了一个T类型的参数,那么就使用“T类型的实例方法”。

烧脑分析类的实例方法省略了哪个参数

前面的例子,FI的两个参数是同一个类型,如果类型不同呢?省略了哪个参数呢?

是按照位置省略了第一个,亦或者是省略了最后一个?

还是按照类型自动去对应,而不关心第几个呢?

这个时候,我们能想到的办法可能是去看源码,但是一般看代码没有个把礼拜甚至更长,毛都看不出来。我们还是用一个例子来分析一下吧。

定义一个FI接口:

package com.pollyduan.fi;

public interface TestInterface {
    //随便什么名字,lambda并不关心,因为FI只有一个接口,并且根据参数来匹配
    public void anyStringAsName(TestBean1 bean1,TestBean2 bean2);
}

编写两个用于参数的类:

TestBean1.java

package com.pollyduan.fi;

public class TestBean1 {
    public void expect1(TestBean1 bean1){

    }
    public void expect2(TestBean2 bean2){

    }
    public void test1(TestInterface i){

    }
}

TestBean2.java

package com.pollyduan.fi;

public class TestBean2 {
    public void expect1(TestBean1 bean1){

    }
    public void expect2(TestBean2 bean2){

    }
    public void test1(TestInterface i){

    }
}

二者区别不大。

编写测试类:

package com.pollyduan.fi;

public class TestFIMain {
    public static void main(String[] args) {
        TestBean1 bean1=new TestBean1();
        bean1.test1(TestBean1::expect1);//①
        bean1.test1(TestBean1::expect2);//② ok
        bean1.test1(TestBean2::expect1);//③
        bean1.test1(TestBean2::expect2);//④

        TestBean2 bean2=new TestBean2();
        bean2.test1(TestBean1::expect1);//⑤
        bean2.test1(TestBean1::expect2);//⑥ ok
        bean2.test1(TestBean2::expect1);//⑦
        bean2.test1(TestBean2::expect2);//⑧
    }
}

测试方法中,除了标记OK的行正确,其他都报错。

分析:

首先我们要明确FI需要的参数列表是:(TestBean1,TestBean2)

  1. 我们先看①行,我们传入的”::”前导的类是TestBean1,而expect1方法匹配的是TestBean1类型的入参bean1,也就是说省略了TestBean2类型的参数bean2,FI中的最后一个参数。即便我们使用类TestBean1去new一个对象,也找不到TestBean2,因此这个错误。
  2. 我们先看②行,我们传入的”::”前导的类是TestBean1,而expect2方法匹配的是TestBean2类型的入参bean2,也就是说省略了TestBean1类型的参数bean1,那么lambda就可以使用”::”前导的TestBean1构建一个对象,作为第一个参数,从而匹配FI的接口方法。ok。
  3. 我们先看③行,我们传入的”::”前导的类是TestBean2,而expect1方法匹配的是TestBean1类型的入参bean1,也就是说省略了TestBean2类型的参数bean2,FI的最后一个参数。按照第二步的分析,我们用”::”前导的类TestBean2去new一个对象,应该可以凑足两个参数。实际测试会发现这不灵。这就证明了只能省略第一个参数,而且,用”::”前导的类也必须是第一个参数的类型。
  4. 同第一步类似,第④行代码,找不到TestBean1的参数,有错误可以理解。
  5. 至于⑤~⑧,只是替换了外层的test1的主体,没有任何区别。这证明了,lambda的匹配与外层是什么鬼没有任何关系,它只关心外层需要的FI的参数列表。
  6. 请不要看下一步,在这里停下来冷静的思考一下,如果我们把TestInterface中FI方法的参数位置换一下,即public void anyStringAsName(TestBean2 cat,TestBean1 dog);,结果应该是哪两行正确呢?认真思考一下,实在想不明白跑一下测试用例,也许对理解更有帮助。
  7. 如果想明白了用这个思路验证一下:参照参数列表(TestBean2,TestBean1),可以确定只可以省略第一个参数即TestBean2,那么”::”签到必须是TestBean2,用于自动创建对象;而未省略的参数是TestBean1,那么方法名为expect1,结果为xxx(TestBean2::expect1),即③和⑦,你答对了吗?
时间: 2024-09-09 06:18:27

lambda方法引用总结——烧脑吃透的相关文章

一起爪哇Java 8(二)——Lambda表达式和方法引用

定义 Java 表达式有很多种,声明一个class是一个表达式,定义一个变量是一个表达式,写一个=赋值逻辑是一个表达式-- Lambda表达式是这样一个表达式: lambdaParameters -> lambdaBody 在lambdaParameters传递参数,在lambdaBody中编写逻辑.lambda表达式生成的结果就是一个函数式接口(上文提到过的).lambdaBody中的逻辑内容(各种表达式)不会在定义时执行,在实际函数式接口调用时才会执行. 举几个官方的例子看看: () ->

Java 8方法引用使用指南

[编者按]本文作者为拥有15年 Java 开发经验的资深程序员 Per-Åke Minborg,主要介绍如何灵活地解析 Java 中的方法引用.文章系国内 ITOM 管理平台 OneAPM 编译呈现. 方法引用 众所周知,在Java 8中我们可以使用方法引用.譬如,在我们需要遍历流元素时,可以使用 String::isEmpty 来引用isEmpty方法.试看下面这段代码: Stream.of("A", "", "B").filter(Strea

Java函数式编程(八):字符串及方法引用_java

第三章 字符串,比较器和过滤器 JDK引入的一些方法对写出函数式风格的代码很有帮助.JDK库里的一些的类和接口我们已经用得非常熟悉了,比如说String,为了摆脱以前习惯的那种老的风格,我们得主动寻找机会来使用这些新的方法.同样,当我们需要用到只有一个方法的匿名内部类时,我们现在可以用lambda表达式来替换它了,不用再像原来那样写的那么繁琐了. 本章我们会使用lambda表达式和方法引用来遍历字符串,实现Comparator接口,查看目录中的文件,监视文件及目录的变更.上一章中介绍的一些方法还

JavaScript中的方法、方法引用和参数学习

首先,我们来看一段代码,如何觉得不甚明白的,则本文会对你有益: var player = function (e) {            return (function f(m) {    return m ? (function (n) {        return $('#Player', n).get(0) || f($('iframe', n).get(0));    })(m.contentWindow.document) : null;  })($(e).get(0)); }

JavaScript中的方法、方法引用和参数

  首先,我们来看一段代码,如果觉得不甚明白的,则本文会对你有益: var player = function (e) { return (function f(m) { return m ? (function (n) { return $('#Player', n).get(0) || f($('iframe', n).get(0)); })(m.contentWindow.document) : null; })($(e).get(0)); }; 该段代码涉及到的知识点包括:方法.匿名方法

html css-嵌入式方法引用样式单

问题描述 嵌入式方法引用样式单 使用嵌入式方法引用样式单应该使用的运用标记是 还是<link>呢 谢谢谢谢</p> 解决方案 http://www.jb51.net/web/172967.html

新加方法-在WallpaperManager添加setlockscreenstream方法引用不到

问题描述 在WallpaperManager添加setlockscreenstream方法引用不到 在android.app.WallpaperManager参照桌面壁纸的设置,添加setlockscreenstream方法设置锁屏壁纸问题.但是在Launcher中引用这个方法,编译的时候报找不到这个方法的错误. 这边已经把framework.jar.service.jar都编译了.并且原生的setstream能否引用到 求问问题原因~ 解决方案 有谁知道这个问题的具体原因么? 解决方案二: 添

Java 8新特性方法引用详细介绍_java

Java 8新特性方法引用 对于引用来说我们一般都是用在对象,而对象引用的特点是:不同的引用对象可以操作同一块内容! Java 8的方法引用定义了四种格式: 引用静态方法     ClassName :: staticMethodName 引用对象方法:  Object:: methodName 引用特定类型方法: ClassName :: methodName 引用构造方法: ClassName  :: new  静态方法引用示例 /** * 静态方法引用 * @param <P> 引用方法

java引用jpython的方法示例_java

本文实例讲述了java引用jpython的方法.分享给大家供大家参考,具体如下: import java.util.ArrayList; import java.util.List; import org.python.core.PyException; import org.python.core.PyInteger; import org.python.core.PyObject; import org.python.util.PythonInterpreter; public class