JAVA / STREAMS, LAMBDAS AND OPTIONAL
Method references as shorthand for lambdas
Convert single-call lambdas into the four method reference forms, and know where the receiver comes from and when it is evaluated.
What you will learn
- Pick among Type::staticM, obj::m, Type::m and Type::new by asking where the receiver is
- Read Type::method as: the first parameter becomes the receiver
- obj::method evaluates the receiver once, when the reference is created, not on each call
- Keep a lambda when the body reorders arguments, negates, or chains a second call
Understanding Method references as shorthand for lambdas
The :: operator names a method without calling it. The compiler looks at the functional interface the expression is being assigned to, takes that interface's single abstract method, and generates an implementation whose entire body is a call to the method you named. Because the meaning depends completely on that target type, a method reference has no type of its own: var f = String::toUpperCase; does not compile, while Function<String, String> f = String::toUpperCase; does.
To read any method reference, ask where the receiver, the object the method runs on, comes from. Integer::parseInt has none, so every interface parameter becomes an argument; System.out::println fixes the receiver on the left and still passes the parameters as arguments. String::toUpperCase takes the receiver from the first parameter and shifts the rest down, which is why String::startsWith can implement a two-parameter BiPredicate even though startsWith declares one parameter. Tag::new is the same alignment with a constructor, and Tag[]::new takes an int length and hands back a new array.
That alignment is resolved at compile time from the target type, which is why identical text can mean different things in different places, and why Integer::toString is rejected as ambiguous for Function<Integer, String>: the static toString(int) and the instance toString() both fit. The bound form has one more consequence: the expression left of :: is evaluated once, exactly where the reference is created, so list::size snapshots which object you will ask, not what its size is. The shorthand reaches only as far as one call with arguments in their declared order, no reordering, no negation, no second step, so s -> s.trim().length() stays a lambda.
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
public class MethodRefs {
public static void main(String[] args) {
// static method: no receiver, every parameter is an argument
Function<String, Integer> parse = Integer::parseInt;
// bound instance method: the receiver is fixed by the left side
String greeting = "Method references";
Supplier<Integer> length = greeting::length;
// unbound instance method: the first parameter becomes the receiver
Function<String, String> upper = String::toUpperCase;
BiFunction<String, String, Boolean> startsWith = String::startsWith;
// constructor reference
Function<String, StringBuilder> build = StringBuilder::new;
System.out.println(parse.apply("42") + 1);
System.out.println(length.get());
System.out.println(upper.apply("shorthand"));
System.out.println(startsWith.apply("shorthand", "short"));
System.out.println(build.apply("abc").reverse());
List.of("io", "nio").forEach(System.out::println);
}
}A method reference is a lambda whose body is exactly one call, with the functional interface's parameters lined up against that method's receiver and arguments.
Worked examples
The bound receiver is evaluated once
Shows that the expression left of :: runs immediately and only once, unlike the same call written inside a lambda body.
import java.util.function.Supplier;
public class BoundReceiver {
static int calls = 0;
static StringBuilder makeBuffer() {
calls++;
return new StringBuilder("buf" + calls);
}
public static void main(String[] args) {
Supplier<String> viaRef = makeBuffer()::toString;
Supplier<String> viaLambda = () -> makeBuffer().toString();
System.out.println("calls after creating both: " + calls);
System.out.println(viaRef.get() + " " + viaRef.get());
System.out.println(viaLambda.get() + " " + viaLambda.get());
System.out.println("calls at the end: " + calls);
}
}Example explained
Line 1makeBuffer()::toString calls makeBuffer() on that line, which is why the counter is already 1 before any get().
Line 2The lambda stores the call itself, so creating viaLambda calls nothing.
Line 3Both viaRef.get() calls print buf1: the reference holds one fixed StringBuilder forever.
Line 4The two viaLambda.get() calls print buf2 and buf3 because the body re-runs makeBuffer() each time.
Which slot holds the receiver
Contrasts the unbound form, where the first parameter is the receiver, with the bound form, where the fixed object is the receiver rather than the argument.
import java.util.function.BiPredicate;
import java.util.function.Predicate;
public class ReceiverPosition {
public static void main(String[] args) {
BiPredicate<String, String> starts = String::startsWith;
System.out.println(starts.test("codec", "co"));
String fixed = "codec";
Predicate<String> codecStartsWith = fixed::startsWith;
System.out.println(codecStartsWith.test("co"));
System.out.println(codecStartsWith.test("codec is longer"));
Predicate<String> startsWithCo = s -> s.startsWith("co");
System.out.println(startsWithCo.test("codec is longer"));
}
}Example explained
Line 1String::startsWith fills a two-parameter interface: the first parameter is the receiver, the second is the prefix.
Line 2fixed::startsWith means "codec".startsWith(x), so its parameter is the prefix, not the string being tested.
Line 3That is why test("codec is longer") is false: the long string is being used as a prefix of "codec".
Line 4No method reference form can fix the argument instead of the receiver, so the last predicate must stay a lambda.
Constructor and array constructor references
Uses Tag::new as a one-argument factory and Tag[]::new as the int-to-array allocator that toArray needs (Java 16+ for the record).
import java.util.Arrays;
import java.util.stream.Stream;
public class ConstructorRefs {
record Tag(String name) {}
public static void main(String[] args) {
Tag[] tags = Stream.of("java", "streams")
.map(Tag::new)
.toArray(Tag[]::new);
System.out.println(Arrays.toString(tags));
System.out.println(tags.length + " " + tags.getClass().getSimpleName());
}
}Example explained
Line 1map needs a function from String to something, so Tag::new resolves to the constructor that takes one String.
Line 2Tag[]::new is an IntFunction<Tag[]>: the int it receives is the length, and it returns a fresh array.
Line 3Because that allocator names the component type, toArray hands back Tag[] rather than Object[].
Line 4Both forms are chosen purely from the parameter types of map and toArray, not from anything extra you write.
Important notes
A method reference has no standalone type, so it cannot be assigned to var and may need a cast such as (Runnable) this::tick when the method you pass it to is overloaded on several functional interfaces.
Two references to the same method are not guaranteed to be the same object or to be equal: non-capturing ones are usually a single cached instance while bound ones allocate per evaluation, so unregistering a listener you added as this::onEvent can fail.
Common mistakes
Reading obj::method as "the fixed object is the argument": prefix::startsWith compiles happily but means prefix.startsWith(x), so a filter written that way silently keeps the wrong strings instead of failing.
Trying to pack extra work into the reference, such as Person::getName().trim() or a negated String::isBlank; the first is a syntax error and the second needs Predicate.not(String::isBlank) or a plain lambda.
Building a bound reference from a null variable: s::isEmpty throws NullPointerException on the line that creates the reference, before the predicate is ever tested, so the stack trace points at setup code rather than at the stream.
Try it yourself
Change, predict, then run
In a browser editor, start from List.of(" 7 ", "42 ", " 13") and produce the sum 62 with a pipeline in which every function is a method reference. Then try to collapse s -> s.trim().length() into a single method reference and record the compiler error you get.
Open the Java workspaceCheck your understanding
A local variable sb refers to a StringBuilder. You create Supplier<String> a = sb::toString; and Supplier<String> b = () -> sb.toString();, then assign a different StringBuilder to sb on the next line. What happens?
- b no longer compiles because sb is not effectively final, while a compiles and still calls toString() on the original builder
- Both compile and both now call toString() on the new builder, since :: and the lambda body both re-read sb
- Both compile and both still call toString() on the original builder
- Neither compiles, because a bound method reference captures sb exactly the way the lambda does
Show answer
Only a lambda body captures the local variable, so reassigning sb breaks the effectively-final requirement and b fails to compile. The receiver expression of a bound method reference is evaluated where the reference is created and only its value is kept, so a never captures the variable and keeps calling toString() on the first builder. Option 2 is tempting because a really does stay on the original object, but 'both compile' is wrong, and option 1 wrongly treats :: as text that is re-read on each call.