Lambda Expressions ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-09-08

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:

Types of method references in Java
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.

Lambda expression and method reference compared
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

Please log in or register to have a possibility to add comment.