Java method references are often used as a more concise alternative to lambda expressions. In many cases, the two forms are interchangeable and choosing between them is simply a matter of readability.
However, there are situations where replacing a lambda expression with a method reference changes the behaviour of the program. One such case involves Java Streams and the Iterable interface.
Making a Stream Iterable
Java Streams provide an iterator() method but do not implement the Iterable interface. This means they cannot be used directly in an enhanced for loop.
Since Iterable is a functional interface, we can adapt a Stream using a lambda expression:
Iterable<String> values = () -> createStream().iterator();
Alternatively, we can use a method reference:
Iterable<String> values = createStream()::iterator;
Both expressions compile and produce an Iterable<String> that can be used in an enhanced for loop:
for (String value : values) { System.out.println(value);}
Despite their similarity, the two implementations behave differently when iterated more than once.
Comparing the Two Approaches
To demonstrate the difference, we use a method that creates a Stream and prints a message whenever it is called:
static Stream<String> createStream() { System.out.println(" Creating a new stream."); return Stream.of("Example value");}
We also need a method that iterates over an Iterable twice:
static void iterateTwice(final Iterable<String> values) { System.out.println(" First iteration:"); for (final var value : values) { System.out.println(" " + value); } System.out.println(" Second iteration:"); for (final var value : values) { System.out.println(" " + value); }}
Now we can compare the lambda expression and the method reference:
public static void main() { System.out.println("Lambda:"); final Iterable<String> withLambda = () -> createStream().iterator(); iterateTwice(withLambda); System.out.println(); System.out.println("Method reference:"); final Iterable<String> withMethodReference = createStream()::iterator; try { iterateTwice(withMethodReference); } catch (final IllegalStateException e) { System.out.println(" Expected exception: " + e); }}
Running this code produces the following output:
Lambda: First iteration: Creating a new stream. Example value Second iteration: Creating a new stream. Example valueMethod reference: Creating a new stream. First iteration: Example value Second iteration: Expected exception: java.lang.IllegalStateException: stream has already been operated upon or closed
The lambda expression allows both iterations to complete successfully. The method reference throws an IllegalStateException during the second iteration.
There is also a difference in when the Stream is created. With the lambda expression, a new Stream is created at the beginning of each iteration. With the method reference, the Stream is created immediately when the Iterable is assigned.
Understanding the Difference
The difference comes from when Java evaluates the expressions involved.
Consider the lambda expression:
() -> createStream().iterator()
The body of the lambda is executed whenever the iterator() method of the Iterable is called. Consequently, every call creates a new Stream and returns its iterator. Its behaviour is equivalent to the following anonymous class:
Iterable<String> values = new Iterable<>() { @Override public Iterator<String> iterator() { return createStream().iterator(); }};
Each iteration uses an independent Stream instance.
The method reference behaves differently:
createStream()::iterator
This is a method reference to an instance method of a particular object. Java evaluates the expression to the left of :: when the method reference is created. In this case, createStream() is called immediately, and the resulting Stream instance is retained as the receiver of the method reference. The behaviour is therefore equivalent to:
final Stream<String> stream = createStream();Iterable<String> values = new Iterable<>() { @Override public Iterator<String> iterator() { return stream.iterator(); }};
Every call to iterator() operates on the same Stream instance.
Streams Can Only Be Consumed Once
Java Streams are designed for a single traversal. Once a Stream has been consumed, it cannot be reused for another traversal. This restriction also applies when consuming a Stream through its iterator.
In the method-reference example, the first iteration consumes the Stream. The second iteration attempts to obtain another iterator from the same Stream instance, which results in an IllegalStateException.
The lambda expression avoids this problem because it creates a new Stream for each call to iterator().
Using a Method Reference Without Reusing the Stream
It is possible to preserve the behaviour of the lambda expression while still using a method reference. Instead of referencing the iterator() method of an existing Stream, we can introduce a static method that creates a new iterator:
private static Iterator<String> createIterator() { return createStream().iterator();}
The Iterable can then be defined as:
Iterable<String> values = Main::createIterator;
In this case, the method reference points to a static method. Each invocation of iterator() calls createIterator(), which creates a new Stream. Both iterations now work as expected.
For this particular example, the original lambda expression is arguably simpler:
Iterable<String> values = () -> createStream().iterator();
It also makes the creation of a new Stream for each iteration more explicit.
A General Difference Between Lambdas and Method References
The behaviour demonstrated here is not specific to Streams. An expression of the form
someExpression()::someMethod
evaluates someExpression() immediately when the method reference is created. The resulting object is then used as the receiver whenever the referenced method is invoked.
In contrast, a lambda expression such as
() -> someExpression().someMethod()
evaluates someExpression() each time the lambda is invoked.
This distinction matters when the expression creates a new object, produces side effects, accesses mutable state, or returns a resource that cannot be reused.
It does not apply in the same way to every kind of method reference. For example, a reference to a static method such as SomeClass::someStaticMethod does not capture an instance produced by a receiver expression.
Conclusion
Lambda expressions and method references are not always semantically equivalent, even when they implement the same functional interface.
In this example, replacing
() -> createStream().iterator()
with
createStream()::iterator
changes when the Stream is created and whether a new Stream is available for each iteration.
Both expressions compile successfully, but only the lambda expression produces an Iterable that can be traversed repeatedly.
When refactoring lambda expressions into method references, it is therefore important to consider when the receiver expression is evaluated and whether the resulting object can safely be reused.