Method References in Java
1. What is a method reference
This code throws a NullPointerException on line 2 — before anyone ever calls supplier.get():
String s = null;
Supplier<Integer> supplier = s::length; // NullPointerException right here
System.out.println(supplier.get()); // never reached Swap the method reference for the equivalent lambda () -> s.length() and the exception moves to line 3. The difference is when the object on the left of :: is evaluated — and that single detail is where most interview questions on this topic start.
A method reference is a shorthand for a lambda expression that does nothing except call one already existing method. The syntax uses the :: operator (double colon): the class or object goes on the left, the method name goes on the right — with no parentheses and no arguments. Method references were introduced in Java 8 together with lambda expressions and, just like lambdas, they can only be assigned to a functional interface.
The rule of thumb is simple: if a lambda calls exactly one existing method and just forwards its own parameters to it, refer to that method by name instead. For example:
Consumer<String> consumer = str -> System.out.println(str); can be rewritten with a method reference, which reads better:
Consumer<String> consumer = System.out::println; There are four kinds of method references:
| Type | Syntax | Example | Equivalent lambda |
|---|---|---|---|
| Reference to a static method | ContainingClass::staticMethodName | Integer::parseInt | s -> Integer.parseInt(s) |
| Reference to an instance method of a particular object (bound receiver) | containingObject::instanceMethodName | System.out::println | x -> System.out.println(x) |
| Reference to an instance method of an arbitrary object of a particular type (unbound receiver) | ContainingType::methodName | String::toLowerCase | s -> s.toLowerCase() |
| Reference to a constructor (constructor reference) | ClassName::new | ArrayList::new | () -> new ArrayList<>() |
The highlighted row is the one that confuses people the most: the method is not static, yet a class name sits on the left of ::. It is explained in section 4.
2. Reference to a static method (ContainingClass::staticMethodName)
This is the most straightforward kind: a class name on the left of ::, a static method name on the right. Every parameter of the lambda becomes an argument of the method, one for one.
Syntax:
ContainingClass::staticMethodName Take a lambda that calls the static Boolean.valueOf method:
Function<String, Boolean> function = e -> Boolean.valueOf(e);
System.out.println(function.apply("TRUE")); // true With a method reference the code becomes:
Function<String, Boolean> function = Boolean::valueOf;
System.out.println(function.apply("TRUE")); // true The number of parameters does not matter, as long as the method signature fits the functional interface:
Function<String, Integer> parse = Integer::parseInt; // s -> Integer.parseInt(s)
UnaryOperator<Integer> abs = Math::abs; // x -> Math.abs(x)
BiFunction<Integer, Integer, Integer> max = Math::max; // (a, b) -> Math.max(a, b)
System.out.println(parse.apply("42")); // 42
System.out.println(abs.apply(-7)); // 7
System.out.println(max.apply(3, 9)); // 9 The typical place you meet them is the Stream API:
List<String> numbers = List.of("10", "20", "30");
int sum = numbers.stream()
.mapToInt(Integer::parseInt)
.sum();
System.out.println(sum); // 60 3. Reference to an instance method of a particular object
Syntax:
containingObject::instanceMethodName This kind — often called a bound receiver reference — is used when the lambda calls a method on an external object that already exists. The receiver is baked into the reference itself, and the parameters of the functional interface become the arguments of the method.
For example:
Consumer<String> consumer = e -> System.out.println(e);
consumer.accept("Method references"); Since System.out is an instance of PrintStream, this can be rewritten as:
Consumer<String> consumer = System.out::println;
consumer.accept("Method references"); Another example:
Integer number = 5;
Supplier<String> supplier = () -> number.toString();
System.out.println(supplier.get()); // 5 After refactoring:
Integer number = 5;
Supplier<String> supplier = number::toString;
System.out.println(supplier.get()); // 5 The receiver can also be the current object, which is what this:: and super:: are for:
class Printer {
void print(String text) {
System.out.println("> " + text);
}
Consumer<String> asConsumer() {
return this::print; // reference to a method of the current object
}
} Important
The expression on the left of :: is evaluated once, at the moment the reference is created — not on every call. That is why s::length throws a NullPointerException immediately when s is null, and why System.out::println captures whatever output stream was installed at creation time: a later System.setOut(...) will not affect it. The lambda x -> System.out.println(x) behaves differently — it reads the field on every invocation.
4. Reference to an instance method of an arbitrary object of a particular type
Syntax:
ContainingType::methodName Here a class name appears on the left even though the method is not static (this is the unbound receiver form). It works like this: the first parameter of the functional interface becomes the object the method is invoked on, and the remaining parameters become its arguments.
For example:
Function<String, String> function = s -> s.toLowerCase();
System.out.println(function.apply("Method References")); // method references It can be refactored as:
Function<String, String> function = String::toLowerCase;
System.out.println(function.apply("Method References")); // method references If the method takes its own arguments, they shift to the following interface parameters:
BiFunction<String, String, Boolean> equals = String::equalsIgnoreCase;
// same as: (a, b) -> a.equalsIgnoreCase(b)
System.out.println(equals.apply("Java", "JAVA")); // true
Comparator<String> comparator = String::compareToIgnoreCase;
// same as: (a, b) -> a.compareToIgnoreCase(b) In practice this is the most common form — it is what makes stream pipelines readable:
List<String> names = List.of("bob", "alice", "eve");
names.stream()
.map(String::toUpperCase) // s -> s.toUpperCase()
.sorted()
.forEach(System.out::println);
List<Person> people = getPeople();
people.sort(Comparator.comparing(Person::getName)); // p -> p.getName() How to tell type 3 from type 4
Look at what stands on the left of ::. A variable or expression (system.out, number, this) means the receiver is fixed and all interface parameters go to the method arguments. A class name (String, Person) means the first interface parameter becomes the receiver. That is also why the parameter counts differ: Function<String, String> fits String::toLowerCase, but not str::toLowerCase.
5. Reference to a constructor
Syntax:
ClassName::new ClassName must not be an abstract class or an interface — you cannot instantiate either. The exact constructor is never spelled out in the reference: the compiler matches the parameter count and types of the functional interface against all declared constructors and picks the best fit.
For example:
Supplier<List<String>> listSupplier = () -> new ArrayList<>();
List<String> list = listSupplier.get(); becomes:
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get(); With a constructor that takes arguments:
class Person {
private final String name;
Person(String name) {
this.name = name;
}
String getName() {
return name;
}
}
Function<String, Person> creator = Person::new; // name -> new Person(name)
Person person = creator.apply("Alice");
System.out.println(person.getName()); // Alice A separate form of the constructor reference creates arrays: Type[]::new. It is what Stream.toArray() expects:
IntFunction<String[]> arrayCreator = String[]::new; // size -> new String[size]
String[] empty = arrayCreator.apply(3);
String[] result = Stream.of("a", "b", "c")
.toArray(String[]::new);
System.out.println(result.length); // 3 Constructor references are also used as factories in collectors:
Set<String> set = Stream.of("a", "b", "a")
.collect(Collectors.toCollection(TreeSet::new));
System.out.println(set); // [a, b] Do not use Integer::new and friends
Older tutorials are full of Function<String, Integer> f = Integer::new;. Wrapper class constructors have been deprecated since Java 9 and marked deprecated for removal since Java 16 — they always allocate a new object and bypass the value cache. Use the factory methods instead: Integer::valueOf, or Integer::parseInt when you want the primitive int.
6. Method reference vs lambda expression: which to use
A method reference is not faster or more powerful than a lambda — it is syntactic sugar. Both compile down to an invokedynamic instruction linked through LambdaMetafactory. What differs is readability, plus a couple of behavioural details.
| Criterion | Lambda expression | Method reference |
|---|---|---|
| Body | Any code: several calls, conditions, arithmetic | Exactly one call to an existing method or constructor |
| Parameters | Written out explicitly | Not written at all — inferred from the interface |
| Reordering arguments | Possible: (a, b) -> f(b, a) | Not possible |
| Supplying a constant argument | Possible: s -> s.substring(1) | Not possible |
| When the receiver is evaluated | On every call | Once, when the reference is created |
| Readability | Noisier for plain parameter forwarding | Cleaner: String::trim instead of s -> s.trim() |
Practical rule: use a method reference whenever the lambda looks like x -> something(x) or x -> x.something(). The moment anything else appears inside the body, stay with the lambda.
7. Where method references trip people up
The receiver is evaluated eagerly
Already shown above, but it is mistake number one. obj::method evaluates obj immediately and blows up with a NullPointerException on the declaration line if the object is null.
reference to ... is ambiguous
If a class declares both a static and an instance method with the same name and both fit the signature, the code will not compile:
class Foo {
static String bar(Foo f) { return "static"; }
String bar() { return "instance"; }
}
Function<Foo, String> f = Foo::bar; // compile error: reference to bar is ambiguous The compiler cannot choose between "static method taking a Foo" and "instance method invoked on a Foo". Switching to a lambda with an explicit call resolves it.
You cannot inject your own argument
A method reference passes the interface parameters through as they are. Anything that needs a constant or a different order cannot be written as a reference:
Function<String, String> ok = String::trim; // fine
Function<String, String> broken = String::substring; // will not compile: missing argument
Function<String, String> useLambda = s -> s.substring(1); // keep the lambda here Overloads are resolved by the target type
String::valueOf is ambiguous on its own — there are more than ten overloads. The compiler picks one based on the type of the variable the reference is assigned to:
Function<Integer, String> fromInt = String::valueOf; // valueOf(int)
Function<char[], String> fromChars = String::valueOf; // valueOf(char[]) If the target type does not narrow it down to a single candidate, you get a compile error; an explicit cast or a lambda fixes it.
A method reference does not freeze a variable by itself
Like a lambda, a method reference can only capture effectively final local variables. Reassigning a local variable after you created a reference to one of its methods will not compile.
Frequently asked questions
Why does String::toLowerCase work like s -> s.toLowerCase() if the method is not static?
Because it is a reference to an instance method of an arbitrary object of that type (an unbound receiver). The compiler takes the first parameter of the functional interface and uses it as the object the method is called on, then passes the remaining parameters as arguments. That is why Function<String, String> with a single parameter fits a no-argument method, while BiFunction<String, String, Boolean> fits String::equalsIgnoreCase, which takes one argument.
Can a method reference point to an overloaded method?
Yes, as long as the target functional interface identifies exactly one overload. Function<Integer, String> f = String::valueOf; selects valueOf(int), while Function<char[], String> selects valueOf(char[]). If several candidates still match, compilation fails with "reference is ambiguous"; in that case fall back to a lambda with an explicit call.
Can I write this::method or super::method?
Yes. Both are valid inside a non-static context and count as references to an instance method of a particular object. this::print refers to a method of the current object, and super::print refers to the superclass implementation, which is handy when you override a method and still need the parent version. Inside a static method this:: is not allowed.
Are method references faster than lambda expressions?
No. Both are compiled into an invokedynamic instruction and linked through LambdaMetafactory, so performance is effectively the same. A method reference can sometimes avoid generating an extra synthetic method in the class, but that makes no measurable difference in real applications. Choose based on readability, not speed.
Comments