Method References
Method references are a clean shorthand for lambda expressions that simply call an already-existing method. Instead of writing x -> SomeClass.someMethod(x), you write SomeClass::someMethod. Same behavior, less noise.
Why Method References?
When a lambda does nothing but forward its arguments to another method, the lambda itself becomes visual clutter. Method references strip that boilerplate away.
// Lambda version
List<String> names = List.of("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));
// Method reference version — exactly the same behavior
names.forEach(System.out::println);
Both compile to the same bytecode effect. The method reference is just easier to read at a glance.
The Four Kinds of Method References
Java has four flavors of method references. The :: operator is used in all four.
| Kind | Syntax | Lambda equivalent |
|---|---|---|
| Static method | ClassName::staticMethod | (args) -> ClassName.staticMethod(args) |
| Instance method on a specific object | instance::instanceMethod | (args) -> instance.instanceMethod(args) |
| Instance method on an arbitrary object of a type | ClassName::instanceMethod | (obj, args) -> obj.instanceMethod(args) |
| Constructor | ClassName::new | (args) -> new ClassName(args) |
Let’s walk through each one with real examples.
1. Static Method Reference
Use this when the lambda calls a static method, passing its parameter(s) directly as arguments.
import java.util.Arrays;
import java.util.List;
public class StaticRefDemo {
public static void printUpper(String s) {
System.out.println(s.toUpperCase());
}
public static void main(String[] args) {
List<String> words = List.of("java", "method", "reference");
// Lambda
words.forEach(s -> StaticRefDemo.printUpper(s));
// Static method reference — cleaner
words.forEach(StaticRefDemo::printUpper);
}
}
Output:
JAVA
METHOD
REFERENCE
JAVA
METHOD
REFERENCE
Another common case is Integer::parseInt for conversions:
List<String> numbers = List.of("1", "2", "3");
numbers.stream()
.map(Integer::parseInt) // same as s -> Integer.parseInt(s)
.forEach(System.out::println);
2. Instance Method Reference on a Specific Object
Use this when you have a particular object and the lambda always calls a method on that same object.
public class InstanceRefDemo {
public static void main(String[] args) {
String prefix = "Hello, ";
// Lambda
List<String> names = List.of("Alice", "Bob");
names.forEach(name -> System.out.println(prefix.concat(name)));
// Instance method reference on the specific object 'prefix'
// prefix::concat takes one argument and returns prefix + that argument
names.stream()
.map(prefix::concat)
.forEach(System.out::println);
}
}
Output:
Hello, Alice
Hello, Bob
Hello, Alice
Hello, Bob
System.out::println is the most common example of this kind — System.out is the specific PrintStream instance.
3. Instance Method Reference on an Arbitrary Object of a Type
This is the trickiest kind. The first parameter of the lambda becomes the target object, and any remaining parameters become arguments to the method.
import java.util.Arrays;
import java.util.List;
public class ArbitraryRefDemo {
public static void main(String[] args) {
List<String> words = List.of("banana", "Apple", "cherry");
// Lambda: s is the object, compareToIgnoreCase is called on it
words.sort((a, b) -> a.compareToIgnoreCase(b));
// Method reference: same thing, String is the type, first arg becomes the receiver
words.sort(String::compareToIgnoreCase);
System.out.println(words);
}
}
Output:
[Apple, banana, cherry]
Another clear example: converting strings to uppercase in a stream.
List<String> upper = List.of("hello", "world")
.stream()
.map(String::toUpperCase) // same as s -> s.toUpperCase()
.toList(); // toList() added in Java 16
System.out.println(upper); // [HELLO, WORLD]
Note:
String::toUpperCaseworks here becausetoUpperCase()takes no arguments — the string itself is the receiver (the “arbitrary instance of type String”).
4. Constructor Reference
Use ClassName::new when a lambda creates a new object. The functional interface’s abstract method signature must match one of the class’s constructors.
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class ConstructorRefDemo {
public static void main(String[] args) {
List<String> names = List.of("Alice", "Bob", "Charlie");
// Lambda
List<StringBuilder> builders1 = names.stream()
.map(s -> new StringBuilder(s))
.collect(Collectors.toList());
// Constructor reference — cleaner
List<StringBuilder> builders2 = names.stream()
.map(StringBuilder::new)
.collect(Collectors.toList());
builders2.forEach(System.out::println);
}
}
Output:
Alice
Bob
Charlie
Constructor references are especially handy when working with factories. For instance, ArrayList::new matches Supplier<ArrayList>.
import java.util.ArrayList;
import java.util.function.Supplier;
Supplier<ArrayList<String>> listFactory = ArrayList::new;
ArrayList<String> freshList = listFactory.get();
Practical Examples with Streams
Method references really shine inside stream pipelines.
import java.util.List;
import java.util.stream.Collectors;
public class StreamMethodRefDemo {
record Person(String name, int age) {}
public static void main(String[] args) {
List<Person> people = List.of(
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Charlie", 35)
);
// Extracting names
List<String> names = people.stream()
.map(Person::name) // instance method on arbitrary Person
.sorted(String::compareTo) // static-style comparison
.collect(Collectors.toList());
System.out.println(names); // [Alice, Bob, Charlie]
// Printing each name
names.forEach(System.out::println);
}
}
Tip: Combine method references with functional interfaces like
Predicate,Function,Consumer, andSupplierfor maximum expressiveness.
Method References with Comparator
Sorting with method references is particularly readable.
import java.util.Arrays;
import java.util.Comparator;
public class SortDemo {
public static void main(String[] args) {
String[] fruits = {"Mango", "apple", "Banana"};
// Sort alphabetically ignoring case
Arrays.sort(fruits, String::compareToIgnoreCase);
System.out.println(Arrays.toString(fruits));
// Reverse order using Comparator.reversed()
Arrays.sort(fruits, Comparator.comparing(String::toLowerCase).reversed());
System.out.println(Arrays.toString(fruits));
}
}
Output:
[apple, Banana, Mango]
[Mango, Banana, apple]
Under the Hood
At the bytecode level, a method reference compiles to an invokedynamic instruction — the same mechanism used for lambdas. The JVM’s LambdaMetafactory generates a lightweight implementation of the functional interface at runtime, linking it to the target method.
Key points for experienced readers:
- No extra class file. Unlike anonymous inner classes, neither lambdas nor method references generate a
$1.classfile at compile time. The actual class is synthesized byLambdaMetafactoryat runtime and cached. - No capture overhead for unbound references. A reference like
String::toUpperCasecaptures nothing — the JVM creates a stateless singleton-like adapter. A bound reference likeSystem.out::printlncaptures thePrintStreaminstance, so it has a small allocation, but the JDK JIT typically eliminates this via escape analysis. - Identity semantics. Two calls to
String::toUpperCasemay or may not return the same object (==), so never rely on reference equality of method references. - Virtual dispatch is preserved. When you use an instance method reference on an interface or superclass type, the JVM still performs the usual virtual dispatch — it doesn’t devirtualize unless the JIT can prove the type is final.
Note: If you’re coming from the lambda expressions page, think of method references as syntactic sugar layered on top of the same
invokedynamicmachinery.
Quick Reference Cheat Sheet
// 1. Static
Function<String, Integer> parse = Integer::parseInt;
// 2. Bound instance (specific object)
Consumer<String> print = System.out::println;
// 3. Unbound instance (arbitrary object of a type)
Function<String, String> upper = String::toUpperCase;
Comparator<String> cmp = String::compareToIgnoreCase;
// 4. Constructor
Supplier<StringBuilder> sbFactory = StringBuilder::new;
Function<String, StringBuilder> sbFromStr = StringBuilder::new;
Warning: A method reference only works when the method’s signature (parameter types and return type) matches the abstract method of the target functional interface. If they don’t align, you’ll get a compile-time error — which is actually a feature, not a bug.
Related Topics
- Lambda Expressions — the foundation that method references build upon
- Functional Interfaces — the interfaces (
Function,Consumer,Predicate, etc.) that method references implement - Stream API — where method references are used most heavily in practice
- Stream Operations (filter/map/reduce) — hands-on stream pipeline patterns using method references
- Comparator — powerful sorting with method references like
Comparator.comparing(Person::name) - Collectors — collecting stream results, often combined with constructor references