Lambdas & Functional Interfaces

Method References

15 min Lesson 7 of 13

Method References

A lambda expression is often nothing more than a wrapper that calls an existing method. Method references let you drop that wrapper entirely and point directly at the method you want to use. The result is more concise code that reads almost like English.

Java defines four kinds of method reference. Understanding when each applies — and why the compiler accepts it — is what this lesson is about.

1. Static Method Reference

Syntax: ClassName::staticMethod

Use this when the lambda just calls a static method, forwarding all of its arguments.

import java.util.List; import java.util.function.Function; public class StaticRefDemo { public static int doubleIt(int n) { return n * 2; } public static void main(String[] args) { // lambda version Function<Integer, Integer> lambda = n -> doubleIt(n); // method reference — same thing, less noise Function<Integer, Integer> ref = StaticRefDemo::doubleIt; List<Integer> numbers = List.of(1, 2, 3, 4); numbers.stream() .map(StaticRefDemo::doubleIt) .forEach(System.out::println); // 2 4 6 8 } }

The compiler sees that doubleIt takes one int and returns one int, matching the signature of Function<Integer, Integer>. That match is all it needs.

2. Instance Method Reference on a Particular Object

Syntax: instance::instanceMethod

Use this when you have a specific object in scope and the lambda forwards its arguments to a method on that object.

import java.util.List; import java.util.function.Predicate; public class InstanceRefDemo { public static void main(String[] args) { String prefix = "java"; // lambda version Predicate<String> lambda = s -> prefix.startsWith(s); // method reference on the captured object 'prefix' Predicate<String> ref = prefix::startsWith; List<String> tokens = List.of("ja", "py", "jav", "c"); tokens.stream() .filter(prefix::startsWith) .forEach(System.out::println); // ja jav } }
Key distinction: here the object (prefix) is fixed at the point the reference is created. The lambda receives one argument and passes it to prefix.startsWith(arg). The object is the receiver, not an argument.

3. Instance Method Reference on an Arbitrary Instance of a Type

Syntax: ClassName::instanceMethod

This is the trickiest kind. Use it when the lambda receives an instance of a class and calls an instance method on that instance — the first parameter becomes the receiver.

import java.util.List; import java.util.function.Function; public class ArbitraryInstanceDemo { public static void main(String[] args) { // lambda: receives a String, calls toUpperCase() ON IT Function<String, String> lambda = s -> s.toUpperCase(); // method reference — same idea: the argument IS the receiver Function<String, String> ref = String::toUpperCase; List<String> words = List.of("hello", "world"); words.stream() .map(String::toUpperCase) .forEach(System.out::println); // HELLO WORLD } }

When you write String::toUpperCase, the compiler checks: "Does toUpperCase() take zero extra arguments and return a String?" Yes. The stream element is plugged in as the receiver automatically.

This also works with extra parameters. String::contains matches BiPredicate<String, CharSequence> because the first argument is the receiver and the second is passed to contains:

import java.util.function.BiPredicate; BiPredicate<String, CharSequence> bp = String::contains; System.out.println(bp.test("functional", "tional")); // true

4. Constructor Reference

Syntax: ClassName::new

Use this when the lambda creates and returns a new object. It is common with factory functions and stream collectors.

import java.util.ArrayList; import java.util.List; import java.util.function.Supplier; import java.util.function.Function; import java.util.stream.Collectors; public class ConstructorRefDemo { record Point(double x, double y) {} public static void main(String[] args) { // Supplier — no-arg constructor Supplier<ArrayList<String>> listFactory = ArrayList::new; ArrayList<String> fresh = listFactory.get(); // Function — one-arg constructor Function<String, StringBuilder> sbFactory = StringBuilder::new; StringBuilder sb = sbFactory.apply("hello"); // In a stream: collect into a specific list type List<String> names = List.of("alice", "bob"); ArrayList<String> result = names.stream() .collect(Collectors.toCollection(ArrayList::new)); System.out.println(result); // [alice, bob] } }
Constructor references and generic types: ArrayList::new works whether you need a Supplier<ArrayList<String>> or a Supplier<ArrayList<Integer>> — the compiler infers the type from the context.

Choosing the Right Kind

A quick decision table when you have a lambda and wonder whether to replace it with a method reference:

  • Calls a static method with all arguments forwarded?Class::staticMethod
  • Calls a method on a captured, specific object?capturedVar::method
  • Calls a method on the lambda's own first argument?ParameterType::method
  • Creates a new object?ClassName::new
Static vs arbitrary-instance look-alike: MyClass::doSomething can refer to a static method or an instance method on an arbitrary instance — they use identical syntax. The compiler resolves ambiguity by matching the functional interface's parameter list. If both exist and both match, you get a compile error; rename one or use an explicit lambda.

Summary

Method references are shorthand for lambdas that do nothing except delegate to an existing method or constructor. They do not add new capability — every method reference can be written as a lambda — but they remove boilerplate and reveal intent. With practice you will recognise the pattern instantly: if a lambda body is a single method call that uses only its parameters (and possibly a captured object), swap it for a reference.