Method References: Four Kinds and When to Use Each
What Are Method References?
A method reference is a compact syntax for a lambda expression that does nothing except call an existing method. If a lambda’s entire body is a method call, a method reference is almost always clearer.
// Lambda
names.forEach(s -> System.out.println(s));
// Method reference — identical behaviour, fewer characters, clearer intent
names.forEach(System.out::println);
The :: operator is the method reference operator. It does not call the method — it creates a reference to it, which the compiler wraps into a lambda that calls the method.
The Four Kinds
Kind 1: Static Method Reference
Syntax: ClassName::staticMethodName
// Lambda equivalent
Function<String, Integer> parse = s -> Integer.parseInt(s);
// Method reference
Function<String, Integer> parse = Integer::parseInt;
// Usage
List<Integer> numbers = Arrays.asList("1", "2", "3").stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
More examples:
// Math.abs
Function<Double, Double> abs = Math::abs;
// String.valueOf
Function<Integer, String> str = String::valueOf;
// Custom static methods
Function<String, String> clean = StringUtils::trim;
When to use: Any time a lambda is just calling a static utility method. Very common with parsing, conversion, and utility methods.
Kind 2: Instance Method Reference on a Specific Object (Bound)
Syntax: instance::methodName
The instance is captured at reference-creation time (bound to the reference).
String prefix = "Hello, ";
// Lambda
Function<String, String> greet = name -> prefix.concat(name);
// Bound instance method reference
Function<String, String> greet = prefix::concat;
// System.out is a specific PrintStream instance
Consumer<String> print = System.out::println;
// equivalent to: s -> System.out.println(s)
More examples:
// A specific list
List<String> buffer = new ArrayList<>();
Consumer<String> collect = buffer::add;
// A specific logger
Logger log = LoggerFactory.getLogger(MyClass.class);
Consumer<String> logInfo = log::info;
When to use: When you have a specific object instance and want to reference one of its methods as a function. Very common with System.out::println and logger references.
Kind 3: Instance Method Reference on an Arbitrary Instance of a Type (Unbound)
Syntax: ClassName::instanceMethodName
The instance is supplied by the lambda’s first argument at call time (unbound — the instance is not captured).
// Lambda
Function<String, String> upper = s -> s.toUpperCase();
// Unbound instance method reference
Function<String, String> upper = String::toUpperCase;
// The first parameter of the lambda becomes the receiver
Function<String, Integer> length = String::length;
With two parameters:
// Lambda: (str, prefix) -> str.startsWith(prefix)
BiPredicate<String, String> startsWith = (str, prefix) -> str.startsWith(prefix);
// Unbound method reference: first param is the receiver, rest are arguments
BiPredicate<String, String> startsWith = String::startsWith;
The rule: For an unbound reference Type::method, the lambda’s first parameter is the object the method is called on, and the remaining parameters are passed as arguments.
More examples:
// Comparator
Comparator<String> cmp = String::compareTo;
// equivalent to: (a, b) -> a.compareTo(b)
// Used in sort
names.sort(String::compareTo);
// Custom class
Function<Person, String> getName = Person::getName;
ToIntFunction<String> hash = String::hashCode;
When to use: Extremely common in stream pipelines. Any time a lambda is s -> s.someMethod(), consider replacing with Type::someMethod.
Kind 4: Constructor Reference
Syntax: ClassName::new
Creates a new instance. The parameters of the lambda are passed to the constructor.
// No-arg constructor
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();
// One-arg constructor
Function<String, StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.apply("Hello");
// Two-arg constructor
BiFunction<String, Integer, Person> personFactory = Person::new;
Person p = personFactory.apply("Alice", 30);
With streams:
// Collect into a specific collection type
List<String> names = stream.collect(Collectors.toCollection(LinkedList::new));
// Create objects from stream elements
List<User> users = ids.stream()
.map(User::new) // assumes User(Long id) constructor
.collect(Collectors.toList());
Array constructors:
IntFunction<String[]> arrayFactory = String[]::new;
String[] arr = arrayFactory.apply(10); // new String[10]
// Used with Stream.toArray
String[] names = stream.toArray(String[]::new);
Reference Table
| Kind | Syntax | Lambda equivalent | Example |
|---|---|---|---|
| Static | Class::staticMethod | x -> Class.staticMethod(x) | Integer::parseInt |
| Bound instance | obj::method | x -> obj.method(x) | System.out::println |
| Unbound instance | Class::method | (obj, x) -> obj.method(x) | String::toUpperCase |
| Constructor | Class::new | (x) -> new Class(x) | ArrayList::new |
When to Prefer Method References Over Lambdas
Use method references when:
- The lambda does exactly one thing: call an existing named method
- The method name is descriptive and communicates intent
- The reference is shorter and easier to read at a glance
// Method reference — clearly "convert each element to its string form"
.map(Object::toString)
// Lambda — same thing, no benefit to the extra syntax
.map(s -> s.toString())
Keep lambdas when:
- The method reference requires a confusing cast or disambiguation
- The method name is cryptic and a lambda with a better variable name is clearer
- The method has side effects that should be explicit
- The reference is an unbound method reference and the receiver-vs-argument relationship isn’t obvious
// Method reference — who is the receiver? ambiguous at a glance
.sorted(String::compareToIgnoreCase)
// Lambda — explicit
.sorted((a, b) -> a.compareToIgnoreCase(b))
Practical Examples in Stream Pipelines
List<String> names = Arrays.asList(" Alice ", "BOB", "charlie", " Dave ");
List<String> cleaned = names.stream()
.map(String::trim) // unbound instance: trim whitespace
.map(String::toLowerCase) // unbound instance: lowercase
.filter(s -> !s.isEmpty()) // lambda: predicate with logic
.sorted(String::compareTo) // unbound instance: natural order
.collect(Collectors.toList());
System.out.println(cleaned); // [alice, bob, charlie, dave]
// Constructor reference to collect into specific type
Set<String> nameSet = names.stream()
.map(String::trim)
.collect(Collectors.toCollection(TreeSet::new));
// Static method reference for parsing
List<Integer> ids = Arrays.asList("1", "2", "3", "4").stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
Overloaded Methods: Disambiguation
When a method is overloaded, the compiler picks the right overload by matching the functional interface signature:
// println is overloaded (String, int, Object, etc.)
// Compiler infers Consumer<String> → println(String)
Consumer<String> print = System.out::println;
// Compiler infers Consumer<Integer> → println(int)
Consumer<Integer> printInt = System.out::println;
// If ambiguous, you must cast
With valueOf:
// valueOf(int) vs valueOf(Object) — both could match
Function<Integer, String> f = String::valueOf; // picks valueOf(Object)
// If you need valueOf(int) specifically, use a lambda: n -> String.valueOf(n)
Summary
| Kind | When you have | Use |
|---|---|---|
| Static | A static utility method | ClassName::staticMethod |
| Bound | A captured instance | instance::method |
| Unbound | A method on the stream’s element type | ElementType::method |
| Constructor | Need to create objects | ClassName::new |
Next Step
Streams API: Lazy Pipelines and the Functional Data Model →
Part of the DevOps Monk Java tutorial series: Java 8 → Java 11 → Java 17 → Java 21