Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 method references and overridden methods

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.

like image 231
Elopteryx Avatar asked Sep 17 '16 10:09

Elopteryx


People also ask

WHAT ARE method references in Java 8?

Java provides a new feature called method reference in Java 8. Method reference is used to refer method of functional interface. It is compact and easy form of lambda expression. Each time when you are using lambda expression to just referring a method, you can replace your lambda expression with method reference.

What is the advantage of using method reference in Java 8?

That's all about what is method reference is in Java 8 and how you can use it to write clean code in Java 8. The biggest benefit of the method reference or constructor reference is that they make the code even shorter by eliminating lambda expression, which makes the code more readable.

What is method reference and constructor references in Java 8?

A method reference requires a target type similar to lambda expressions. But instead of providing an implementation of a method, they refer to a method of an existing class or object while a constructor reference provides a different name to different constructors within a class.

What is the difference between a method reference and a lambda expression?

Lambda expression is an anonymous method (method without a name) that has used to provide the inline implementation of a method defined by the functional interface while a method reference is similar to a lambda expression that refers a method without executing it.


2 Answers

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.

like image 187
Thomas Kläger Avatar answered Sep 23 '22 16:09

Thomas Kläger


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

like image 26
Bohemian Avatar answered Sep 20 '22 16:09

Bohemian