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是一个CollectionCollection 有一个 stream() 方法,所以 Set 也有相同的方法,所有 Set 实现也是如此(例如 HashSetTreeSet 等).

将方法标识为属于任何特定超类型没有区别,因为它将始终解析为对象在运行时的实现声明的实际方法。


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