V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
logtheone
V2EX  ›  Java

Java8 方法引用的一个疑问:为什么能够引用接口的抽象方法?

  •  
  •   logtheone · 2018-10-24 11:21:13 +08:00 · 2994 次点击
    这是一个创建于 2225 天前的主题,其中的信息可能已经有所发展或是发生改变。
    代码如下
    public static void main(String[] args) {
    List<String> list = new ArrayList<>();
    list.add("hello");
    list.add("alex");
    list.add("front");
    BiConsumer<List<String>, String> v = List::add;
    System.out.println(v == null);
    v.accept(list, "ddd");
    System.out.println(list);
    }

    IDE 为 IDEA,不报错,正常运行。
    我的疑问在于为什么下面这句话没有报错:
    BiConsumer<List<String>, String> v = List::add;
    List 是个接口,add 方法只有声明没有具体的实现,而且其前面明显和 BiConsumer 接口的 accept 不匹配。
    另外如果我把泛型去掉,变成下面这样就报错了:
    BiConsumer v = List::add;
    这又是为什么?
    17 条回复    2018-10-24 18:58:19 +08:00
    xbigfat
        1
    xbigfat  
       2018-10-24 11:33:59 +08:00 via iPhone
    new ArrayList ()实现了 add 方法
    logtheone
        2
    logtheone  
    OP
       2018-10-24 11:35:24 +08:00
    @xbigfat 能解释再清楚一点么?
    ffeii
        3
    ffeii  
       2018-10-24 11:37:16 +08:00 via iPhone
    List::add 等同于 (list, str) -> list.add(str)
    Cbdy
        4
    Cbdy  
       2018-10-24 11:38:26 +08:00
    List::add 你可以理解成为这样一个函数
    BiConsumer<List<String>, String> v = (List<String> l, String e) -> l.add(e);
    solupro
        5
    solupro  
       2018-10-24 11:41:10 +08:00
    取决于 List<String>的实现,你这里就是 ArrayList 了呀
    xbigfat
        6
    xbigfat  
       2018-10-24 11:43:38 +08:00
    1.泛型去掉后,编译器不知道你输入输出的是什么类型,所以报错。BiConsumer 是输入 T 返回 R,不写出类型是编译器报错,运行时泛型是擦除掉的。
    2. new ArrayList( ) 里面,ArrayList 实现了 add ( ) 方法,所以可以运行。
    3.你困惑是因为 Lambda 的缩减.将
    ```BiConsumer<List<String>, String> v = List::add;```
    替换为:
    ```
    List<String> list = new ArrayList<>();
    list.add("hello");
    list.add("alex");
    list.add("front");
    BiConsumer<List<String>, String> v = new BiConsumer<List<String>, String>() {
    @Override
    public void accept(List<String> strings, String e) throws Exception {
    strings.add(e);
    }
    };
    ```
    这里还原成匿名内部类,能看懂了吗?

    strings 传递来的是 list 的引用,e 传来的是 “ ddd"
    serical
        7
    serical  
       2018-10-24 11:48:29 +08:00 via Android
    具体实现取决于 v.accept 第一个参数的类型
    xbigfat
        8
    xbigfat  
       2018-10-24 11:48:46 +08:00
    补充一下去除泛型报错的原因。
    public interface BiConsumer<T1, T2> {

    /**
    * Performs an operation on the given values.
    * @param t1 the first value
    * @param t2 the second value
    * @throws Exception on error
    */
    void accept(T1 t1, T2 t2) throws Exception;
    }

    accept( ) 方法依赖泛型指定对象的类型,要不然,谁都没法操作吧。。。
    kuko126
        9
    kuko126  
       2018-10-24 11:48:55 +08:00   ❤️ 2
    方法引用的几种写法,其中有
    类名::实例方法名
    若 Lambda 表达式的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数时,就可以使用这种方法
    https://blog.csdn.net/TimHeath/article/details/71194938
    所以可以从
    BiConsumer<List<String>, String> v = (list1, s) -> list1.add(s);
    转换成
    BiConsumer<List<String>, String> v = List::add;

    2 是因为泛型不写默认就是 Object,Object 里没有 add 方法所以编译会报错
    可以试一下下面的看一下区别
    BiConsumer<List<String>, String> v = (list1, s) -> list1.add(s);
    BiConsumer<List<String>, String> v = ArrayList::add;
    BiConsumer v = Object::equals;
    yidinghe
        10
    yidinghe  
       2018-10-24 11:50:23 +08:00
    这条语句是在声明一个方法引用,而并不是真的调用这个方法。这样理解就知道为什么符合语法了。
    logtheone
        11
    logtheone  
    OP
       2018-10-24 14:13:12 +08:00
    @ffeii
    你这个等同有依据么?? List 的 add 的方法签名都和 accept 不一样,怎么等同过来的?
    logtheone
        12
    logtheone  
    OP
       2018-10-24 14:24:40 +08:00
    @kuko126
    你的这个回答说到点子上了。

    再问一句,这句话“若 Lambda 表达式的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数时,就可以使用这种方法”当 Lambda 表达式有 3 个以上的参数时,还适用么?另外这句话有权威来源么?
    kuko126
        13
    kuko126  
       2018-10-24 14:49:28 +08:00   ❤️ 1
    如果有三个及以上参数 就要用 (a, b, c) -> a.func(b, c); 这种形式
    要权威的话可以看下这个 https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
    logtheone
        14
    logtheone  
    OP
       2018-10-24 15:43:14 +08:00
    @kuko126
    感谢!
    passerbytiny
        15
    passerbytiny  
       2018-10-24 18:26:14 +08:00
    Lambda 表达式为啥要跟函数式接口一起出现,把我给看晕了。

    List::add 的意思,不是调用 List 接口的 add 方法,你这点理解错了,所有后面的理解就走不通了。

    A::B 是一种 Lambda 缩写方式,并不是调用 A 的 B 方法,当 Lambda 表达式只有一个语句,并且可以通过函数式接口、A、B,来进行推断的时候,才能使用这种缩写形式。当 A 是类的实例,B 是方法名的时候,看起来可能像是表示调用 A 对象的 B 方法。但当 A 是类,或者 B 是 new 这种特殊字的时候,就不能那么看了。

    函数式接口是“ BiConsumer<T, U> void accept(T t, U u)”,上下文语句“ BiConsumer<List<String>, String> v ”决定了:T 是 List<String>、U 是 String。

    结合查看 void accept(T t, U u) 和 List::add:
    accept 提供了两个参数; List.add 方法只需要一个参数; List 是接口定义而不是具体实例。

    那么自然的,将第一个参数当成 List.add 方法的执行主体,将第二个参数当成方法的参数,于是还原成了:(arg_List,arg_String)->{arg_List.add(arg_String);}


    你也可以这样认为,List::add 定义了一个代表 List.add 方法的 Method,然后执行的时候就是 Method.invoke(obj,args...)。此时,“ v.accept(list, "ddd")”相当于:method.invoke(list,"aaa")。执行的是你通过“ List<String> list = new ArrayList<>();”创建的 list 对象,而不是 List 接口。
    passerbytiny
        16
    passerbytiny  
       2018-10-24 18:48:10 +08:00 via Android
    忽略我上边的还原过程吧,错了。A::b 表示的就是方法签名,即 A 类 /接口的 b 方法,所以 List::add 就代表 List 接口的 add 方法,然后再结合 accpet ( list, "aaa"),要推断要执行的是:<List>list.add("aaa")。
    passerbytiny
        17
    passerbytiny  
       2018-10-24 18:58:19 +08:00 via Android
    SomeClass::new,就是该类构造器的方法签名,invoke 的时候不需要第一个参数; someInstance::method,表示方法签名以及 invoke 时的第一个参数。因此,Lambda 表达式右边方法的参数个数,才跟函数式接口方法参数的个数相同。

    跟反射机制已对比,容易理解多了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3403 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 34ms · UTC 12:00 · PVG 20:00 · LAX 04:00 · JFK 07:00
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.