jdk8 一个颠覆了面向对象认知的例子

1 起源

如果你看了上一篇文章关于 Spliterator 分割迭代器,最后一部分说到了图中的两个强制转换逻辑的不合理之处。

  • 第一处,我传入的是一个 Consumer 接口,但是判断的情况却是判断
1
action instanceof IntConsumer

而 IntConsumer 和 Consumer 是没有继承关系的平行函数式接口。这样的转换为何会成功?

  • 第二处,当第一个判断条件不满足的是时候,使用(IntConsumer)强转 lambda 表达式,这样为何能成功被需要传入 IntConsumer 函数所接受呢?

如下图:

image_1bf48rn8nbi1lrs1hgq1f72ass1t.png-236.6kB

2 尝试解释看看喽

先看第二处:lambda表达式也可以强转?

我们先写一个类似的例子,有一个需要传入 Consumer 的方法,我们尝试传入一个 IntConsumer 会发生什么情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ConsumerTest {
public void test(Consumer<? super Integer> consumer) {
System.out.println(consumer instanceof IntConsumer);
consumer.accept(100);
}
public static void main(String[] args) {
ConsumerTest consumerTest = new ConsumerTest();
Consumer<Integer> consumer = i -> System.out.println(i);
IntConsumer intConsumer = i -> System.out.println(i);
consumerTest.test(consumer); // 面向对象的方式
consumerTest.test(consumer::accept); // 函数式的方式
consumerTest.test(intConsumer::accept); // 函数式的方式
}
}

这个例子的输出结果是:

image_1bf77758l13l11agu1ekeshv1134l.png-28kB

我们发现这三种传入方式都是可以的,第一第二种没什么好说的,因为 Test 方法就是需要传入一个 Consumer, 无论你使用原来传递对象的方式,还是通过方法引用的方法,都是没有问题,可以被执行。

问题在于,第三种方式

1
consumerTest.test(intConsumer::accept); // 函数式的方式

当你按住 command 把鼠标放在双冒号的时候 出现的如下:
image_1bf7diai619ibfi64uk13hp1viv1s.png-121.5kB

当你按住 command 把鼠标放在的 accept 时候 出现的如下:
image_1bf7dhepa1cpp7gi28b1r7v1m3r1f.png-155.1kB

双冒号代表是的当前 lambda 表达式的类型。因为你看我初始化两个consumer的代码

1
2
Consumer<Integer> consumer = i -> System.out.println(i);
IntConsumer intConsumer = i -> System.out.println(i);

我后面的表达式是一模一样的。但是却可以赋予两个不同的类型。
所以,重点来了

重点是 lambda 表达式的类型,是要靠上下文进行推断的

这个是和传统面向的编程不一样的地方。需要注意。
刚刚上面的第三种调用的方式,就是如此,编译器推断出,你这个 lambda 表达式 intConsumer::accept 肯定是 Consumer<T super Integer> 类型的。所以不报错而这个时候如果你前面加一个强制转换,就像文章一开始的那张图的第二个强转逻辑一样,也是可以的。不过,略显多余就是了。

image_1bf7e1upp1aa69mamubh2krap29.png-57.5kB

在看第一处

那么什么情况下,才会出现第一种情况,传入的是 Consumer ,但却同时是 instanceof IntConsumer 呢?

对了!(对什么对,你又没想到)就是这样,你同时继承者两个接口就可以了呀!

上代码!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* Created by charleszhu on 2017/5/2.
*/
public class ConsumerTest {
// 要求传入 Consumer
public void testInt(Consumer<Integer> consumer) {
// 判断是否为 IntConsumer
System.out.println(consumer instanceof IntConsumer);
consumer.accept(100);
}
public static void main(String[] args) {
ConsumerTest consumerTest = new ConsumerTest();
consumerTest.testInt(new MyConsumer2<>());
}
}
/**
* 同时实现两个方法
* @param <Integer>
*/
class MyConsumer2<Integer> implements IntConsumer, Consumer<Integer> {
public void accept(int value) {
System.out.println(value);
}
public void accept(Integer t) {
System.out.println(t);
}
}

运行一下结果:

image_1bf7ehq7bgaj1qrg8k61eiq1e1b2m.png-28.5kB

就可以发现,这个时候就和开头的那个例子中的,第一处转换: 需要传入的是 Consumer, 但是也是 IntConsumer 的实例,就会进入第一个判断了

至此两个强转就解释完毕了!

总结

这个例子想给大家说的就是函数式接口的很传统的命令式编程还是有一定差别的。尤其是 lambda 表达式的类型是要靠上下文推断的这一点,需要好好的理解~

只有慢慢理解这些,才能真正理解函数式编程。

朱老师&敏哥 wechat
有惊喜,朋友🙄
我要拿铁不加糖.