Java 8 个方法引用和覆盖的方法
Java 8 method references and overridden methods
我在 Java 8 中使用 lambda 和方法引用已有一段时间了,有一件事我不明白。这是示例代码:
Set<Integer> first = Collections.singleton(1);
Set<Integer> second = Collections.singleton(2);
Set<Integer> third = Collections.singleton(3);
Stream.of(first, second, third)
.flatMap(Collection::stream)
.map(String::valueOf)
.forEach(System.out::println);
Stream.of(first, second, third)
.flatMap(Set::stream)
.map(String::valueOf)
.forEach(System.out::println);
两个流管道做同样的事情,它们打印出三个数字,每行一个。不同的是他们的第二行,好像只要有方法就可以直接替换继承层次中的class名称(Collection接口有默认方法"stream",没有重新定义在设置界面)。
我尝试了如果使用这些 classes:
一次又一次地重新定义该方法会发生什么
private static class CustomHashSet<E> extends HashSet<E> {
@Override
public Stream<E> stream() {
System.out.println("Changed method!");
return StreamSupport.stream(spliterator(), false);
}
}
private static class CustomCustomHashSet<E> extends CustomHashSet<E> {
@Override
public Stream<E> stream() {
System.out.println("Changed method again!");
return StreamSupport.stream(spliterator(), false);
}
}
在将第一个、第二个和第三个赋值更改为使用这些 classes 之后,我可以替换方法引用 (CustomCustomHashSet::stream),毫不奇怪,它们确实在所有情况下都打印出了调试消息,甚至当我使用 Collection::stream 时。看来你不能用方法引用来调用超级的、重写的方法。
有运行时差异吗?什么是更好的做法,参考顶级 interface/class 或使用具体的已知类型(Set)?
谢谢!
编辑:
澄清一下,我知道继承和 LSP,我的困惑与 Java 8 中方法引用的设计有关。我的第一个想法是改变方法引用中的 class 会改变行为,它会从所选 class 调用 super 方法,但正如测试所示,它没有区别。更改创建的实例类型确实会改变行为。
即使是方法引用也必须遵守方法覆盖的 OOP 原则。否则,代码如
public static List<String> stringify(List<?> o) {
return o.stream().map(Object::toString).collect(Collectors.toList());
}
无法按预期工作。
至于使用哪个 class 名称作为方法引用:我更喜欢使用最通用的 class 或声明该方法的接口。
原因是这样的:你写你的方法来处理Set
的集合。稍后您会发现您的方法也可能对 Collection
的集合有用,因此您相应地更改了您的方法签名。现在,如果方法中的代码总是引用 Set 方法,则您也必须调整这些方法引用:
来自
public static <T> void test(Collection<Set<T>> data) {
data.stream().flatMap(Set::stream).forEach(e -> System.out.println(e));
}
到
public static <T> void test(Collection<Collection<T>> data) {
data.stream().flatMap(Collection::stream).forEach(e -> System.out.println(e));
}
您也需要更改方法主体,而如果您将方法编写为
public static <T> void test(Collection<Set<T>> data) {
data.stream().flatMap(Collection::stream).forEach(e -> System.out.println(e));
}
您不必更改方法体。
一个Set
是一个Collection
。 Collection
有一个 stream()
方法,所以 Set
也有相同的方法,所有 Set
实现也是如此(例如 HashSet
、TreeSet
等).
将方法标识为属于任何特定超类型没有区别,因为它将始终解析为对象在运行时的实现声明的实际方法。
见Liskov Substitution Principle:
if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of that program
我在 Java 8 中使用 lambda 和方法引用已有一段时间了,有一件事我不明白。这是示例代码:
Set<Integer> first = Collections.singleton(1);
Set<Integer> second = Collections.singleton(2);
Set<Integer> third = Collections.singleton(3);
Stream.of(first, second, third)
.flatMap(Collection::stream)
.map(String::valueOf)
.forEach(System.out::println);
Stream.of(first, second, third)
.flatMap(Set::stream)
.map(String::valueOf)
.forEach(System.out::println);
两个流管道做同样的事情,它们打印出三个数字,每行一个。不同的是他们的第二行,好像只要有方法就可以直接替换继承层次中的class名称(Collection接口有默认方法"stream",没有重新定义在设置界面)。 我尝试了如果使用这些 classes:
一次又一次地重新定义该方法会发生什么private static class CustomHashSet<E> extends HashSet<E> {
@Override
public Stream<E> stream() {
System.out.println("Changed method!");
return StreamSupport.stream(spliterator(), false);
}
}
private static class CustomCustomHashSet<E> extends CustomHashSet<E> {
@Override
public Stream<E> stream() {
System.out.println("Changed method again!");
return StreamSupport.stream(spliterator(), false);
}
}
在将第一个、第二个和第三个赋值更改为使用这些 classes 之后,我可以替换方法引用 (CustomCustomHashSet::stream),毫不奇怪,它们确实在所有情况下都打印出了调试消息,甚至当我使用 Collection::stream 时。看来你不能用方法引用来调用超级的、重写的方法。
有运行时差异吗?什么是更好的做法,参考顶级 interface/class 或使用具体的已知类型(Set)? 谢谢!
编辑: 澄清一下,我知道继承和 LSP,我的困惑与 Java 8 中方法引用的设计有关。我的第一个想法是改变方法引用中的 class 会改变行为,它会从所选 class 调用 super 方法,但正如测试所示,它没有区别。更改创建的实例类型确实会改变行为。
即使是方法引用也必须遵守方法覆盖的 OOP 原则。否则,代码如
public static List<String> stringify(List<?> o) {
return o.stream().map(Object::toString).collect(Collectors.toList());
}
无法按预期工作。
至于使用哪个 class 名称作为方法引用:我更喜欢使用最通用的 class 或声明该方法的接口。
原因是这样的:你写你的方法来处理Set
的集合。稍后您会发现您的方法也可能对 Collection
的集合有用,因此您相应地更改了您的方法签名。现在,如果方法中的代码总是引用 Set 方法,则您也必须调整这些方法引用:
来自
public static <T> void test(Collection<Set<T>> data) {
data.stream().flatMap(Set::stream).forEach(e -> System.out.println(e));
}
到
public static <T> void test(Collection<Collection<T>> data) {
data.stream().flatMap(Collection::stream).forEach(e -> System.out.println(e));
}
您也需要更改方法主体,而如果您将方法编写为
public static <T> void test(Collection<Set<T>> data) {
data.stream().flatMap(Collection::stream).forEach(e -> System.out.println(e));
}
您不必更改方法体。
一个Set
是一个Collection
。 Collection
有一个 stream()
方法,所以 Set
也有相同的方法,所有 Set
实现也是如此(例如 HashSet
、TreeSet
等).
将方法标识为属于任何特定超类型没有区别,因为它将始终解析为对象在运行时的实现声明的实际方法。
见Liskov Substitution Principle:
if S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of that program