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

2018-10-24 11:21:13 +08:00
 logtheone
代码如下
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;
这又是为什么?
2841 次点击
所在节点    Java
17 条回复
xbigfat
2018-10-24 11:33:59 +08:00
new ArrayList ()实现了 add 方法
logtheone
2018-10-24 11:35:24 +08:00
@xbigfat 能解释再清楚一点么?
ffeii
2018-10-24 11:37:16 +08:00
List::add 等同于 (list, str) -> list.add(str)
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
2018-10-24 11:41:10 +08:00
取决于 List<String>的实现,你这里就是 ArrayList 了呀
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
2018-10-24 11:48:29 +08:00
具体实现取决于 v.accept 第一个参数的类型
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
2018-10-24 11:48:55 +08:00
方法引用的几种写法,其中有
类名::实例方法名
若 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
2018-10-24 11:50:23 +08:00
这条语句是在声明一个方法引用,而并不是真的调用这个方法。这样理解就知道为什么符合语法了。
logtheone
2018-10-24 14:13:12 +08:00
@ffeii
你这个等同有依据么?? List 的 add 的方法签名都和 accept 不一样,怎么等同过来的?
logtheone
2018-10-24 14:24:40 +08:00
@kuko126
你的这个回答说到点子上了。

再问一句,这句话“若 Lambda 表达式的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数时,就可以使用这种方法”当 Lambda 表达式有 3 个以上的参数时,还适用么?另外这句话有权威来源么?
kuko126
2018-10-24 14:49:28 +08:00
如果有三个及以上参数 就要用 (a, b, c) -> a.func(b, c); 这种形式
要权威的话可以看下这个 https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html
logtheone
2018-10-24 15:43:14 +08:00
@kuko126
感谢!
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
2018-10-24 18:48:10 +08:00
忽略我上边的还原过程吧,错了。A::b 表示的就是方法签名,即 A 类 /接口的 b 方法,所以 List::add 就代表 List 接口的 add 方法,然后再结合 accpet ( list, "aaa"),要推断要执行的是:<List>list.add("aaa")。
passerbytiny
2018-10-24 18:58:19 +08:00
SomeClass::new,就是该类构造器的方法签名,invoke 的时候不需要第一个参数; someInstance::method,表示方法签名以及 invoke 时的第一个参数。因此,Lambda 表达式右边方法的参数个数,才跟函数式接口方法参数的个数相同。

跟反射机制已对比,容易理解多了

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/500576

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX