13

I've been using lambdas and method references in Java 8 for a while and there is this one thing I do not understand. Here is the example code:

    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);

The two stream pipelines do the same thing, they print out the three numbers, one per line. The difference is in their second line, it seems you can simply replace the class name in the inheritance hierarchy as long as it has the method (the Collection interface has the default method "stream", which is not redefined in the Set interface). I tried out what happens if the method is redefined again and again, using these 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);
    }
}

After changing the first, second and third assignments to use these classes I could replace the method references (CustomCustomHashSet::stream) and not surprisingly they did print out the debugging messages in all cases, even when I used Collection::stream. It seems you cannot call the super, overriden method with method references.

Is there any runtime difference? What is the better practice, refer to the top level interface/class or use the concrete, known type (Set)? Thanks!

Edit: Just to be clear, I know about inheritance and LSP, my confusion is related to the design of the method references in Java 8. My first thought was that changing the class in a method reference would change the behavior, that it would invoke the super method from the chosen class, but as the tests showed, it makes no difference. Changing the created instance types does change the behavior.

Elopteryx
  • 285
  • 1
  • 5
  • 10
  • 4
    I always imagined these kinds of method references (i.e. static reference of instance method) work the same as in C++. The instance is added as a parameter, doing something like `A::someMethod` = `(A a) -> a.someMethod()`. You could also pass a subclass of A (LSP), at which point dynamic dispatch would kick in and call the overriding implementation. – Jorn Vernee Sep 17 '16 at 11:35
  • 1
    Can you post the test you made with `CustomHashSet` and `CustomCustomHashSet`? Because even using the method reference `Collection::stream`, if the instance of the object is `CustomHashSet`, it is `CustomHashSet.stream()` that will be invoked. Are you sure you constructed new `CustomHashSet` objects? – Tunaki Sep 17 '16 at 13:34
  • @Tunaki sorry if I wasn't clear enough. What you described is exactly what happened, if I changed the instances to CustomHashSet then the CustomHashSet.stream() was invoked, printing "Changed method!" just before printing the current number. My confusion is about the language design, my first thought was that changing the class in a method reference would change the behavior, just like how you can call the base method with "super". But the tests showed that nothing changes, it all depends on the actual instance type. – Elopteryx Sep 17 '16 at 20:56

2 Answers2

6

Even method references have to respect to OOP principle of method overriding. Otherwise, code like

public static List<String> stringify(List<?> o) {
    return o.stream().map(Object::toString).collect(Collectors.toList());
}

would not work as expected.

As to which class name to use for the method reference: I prefer to use the most general class or interface that declares the method.

The reason is this: you write your method to process a collection of Set. Later on you see that your method might also be useful for a collection of Collection, so you change your method signature accordingly. Now if your code within the method always references Set method, you will have to adjust these method references too:

From

public static <T> void test(Collection<Set<T>> data) {
    data.stream().flatMap(Set::stream).forEach(e -> System.out.println(e));
}

to

public static <T> void test(Collection<Collection<T>> data) {
    data.stream().flatMap(Collection::stream).forEach(e -> System.out.println(e));
}

you need to change the method body too, whereas if you had written your method as

public static <T> void test(Collection<Set<T>> data) {
    data.stream().flatMap(Collection::stream).forEach(e -> System.out.println(e));
}

you will not have to change the method body.

Thomas Kläger
  • 17,754
  • 3
  • 23
  • 34
  • I guess there is no reason for method references to work differently than the classic method invocations when it comes to inheritance. You answered my question with detailed examples, I'm marking this as the accepted answer. Thanks! – Elopteryx Sep 17 '16 at 21:13
4

A Set is a Collection. Collection has a stream() method, so Set has that same method too, as do all Set implementations (eg HashSet, TreeSet, etc).

Identifying the method as belonging to any particular supertype makes no difference, as it will always resolve to the actual method declared by the implementation of the object at runtime.


See the 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

Bohemian
  • 412,405
  • 93
  • 575
  • 722