7

As I know lambda expression can be replaced by method reference without any issues. My IDEs say the same, but the following example shows the opposite. The method reference clearly returns the same object, where as lambda expression returns new objects each time.

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Instance {

    int member;

    Instance set(int value){
        this.member = value;
        return this;
    }

    @Override
    public String toString() {
        return member + "";
    }

    public static void main(String[] args) {

        Stream<Integer> stream1 = Stream.of(1, 2, 3, 4);
        Stream<Integer> stream2 = Stream.of(1, 2, 3, 4);

        List<Instance> collect1 = stream1.map(i -> new Instance().set(i)).collect(Collectors.toList());
        List<Instance> collect2 = stream2.map(new Instance()::set).collect(Collectors.toList());

        System.out.println(collect1);
        System.out.println(collect2);
    }
}

Here is my output:

[1, 2, 3, 4]
[4, 4, 4, 4]
Holger
  • 285,553
  • 42
  • 434
  • 765
st.ebberr
  • 153
  • 2
  • 6
  • 4
    See [What is the equivalent lambda expression for System.out::println?](https://stackoverflow.com/a/28025717/2711488) and [java.lang.NullPointerException is thrown using a method-reference but not a lambda expression](https://stackoverflow.com/q/37413106/2711488) – Holger Aug 30 '18 at 17:08

4 Answers4

6

Your lambda expression is calling new Instance() each time it's executed. This explains why the result of its toString() is different for each element.

The method reference retains the instance off which it is referenced, such that it's similar to:

Instance instance = new Instance();
List<Instance> collect2 = stream2.map(instance::set).collect(Collectors.toList());

The result of using the method reference in this case is that the same instance is used to call set, collected at the end. The member value displayed is the one last set.


As an experiment, make these changes and observe that the instance is changing in the case of the lambda expression:

/* a random string assigned per instance */
private String uid = UUID.randomUUID().toString();

Instance set(int value) {
    this.member = value;
    System.out.println("uid: " + uid); //print the ID
    return this;
}
ernest_k
  • 44,416
  • 5
  • 53
  • 99
6

The timing of method reference expression evaluation differs from which of lambda expressions.
With a method reference that has an expression (rather than a type) preceding the :: the subexpression is evaluated immediately and the result of evaluation is stored and reused then.
So here :

new Instance()::set

new Instance() is evaluated a single time.

From 15.12.4. Run-Time Evaluation of Method Invocation (emphasis is mine) :

The timing of method reference expression evaluation is more complex than that of lambda expressions (§15.27.4). When a method reference expression has an expression (rather than a type) preceding the :: separator, that subexpression is evaluated immediately. The result of evaluation is stored until the method of the corresponding functional interface type is invoked; at that point, the result is used as the target reference for the invocation. This means the expression preceding the :: separator is evaluated only when the program encounters the method reference expression, and is not re-evaluated on subsequent invocations on the functional interface type.

davidxxx
  • 125,838
  • 23
  • 214
  • 215
2

The difference in the second option is, you create one single (1) instance the moment you create the Stream pipeline. When you eventually iterate over the stream elements after calling the terminal method (toList), you invoke the set method on that same instance four times, where the last value is the final one. The resulting List (collect2) contains four times the same instance.

Gerald Mücke
  • 10,724
  • 2
  • 50
  • 67
2

In the first one, for each item in your stream, the lambda expression in your map() is creating a new Instance object.

In the second one, new Instance() is being called once, before the map() starts to pass the values.

If you want to use a method reference, add a constructor to Instance like this. (I actually recommend also making the Instance immutable by making member final so that you avoid confusion like this from other places, like this).

private final int member;

public Instance(int member) {
  this.member = member;
}

//remove the setter

Then change your stream processing to look like this:

List<Instance> collect2 = stream2.map(Instance::new).collect(Collectors.toList());

This way you are sure that the member is not changed once it is initialised, and you make use of method references concisely (in this case the constructor is the method reference with new).

jbx
  • 21,365
  • 18
  • 90
  • 144