Definition
- **Type 1 (static)**: `ClassName::staticMethod`. Lambda: `s -> Integer.parseInt(s)` becomes `Integer::parseInt`. - **Type 2 (bound instance)**: `object::instanceMethod`. Lambda: `x -> System.out.println(x)` becomes `System.out::println`. Bound to a specific object. - **Type 3 (unbound instance)**: `ClassName::instanceMethod`. Lambda: `s -> s.toUpperCase()` becomes `String::toUpperCase`. The stream element is the receiver. - **Type 4 (constructor)**: `ClassName::new`. Lambda: `s -> new BigDecimal(s)` becomes `BigDecimal::new`.
Type 1 (static): 'Use the RBI standard interest calculator' (RBI::calculateInterest). Type 2 (bound): 'Ask Rajesh specifically to verify' (rajesh::verify). Type 3 (unbound): 'Each account verifies itself' (Account::verify). Type 4 (constructor): 'Create a new receipt for each transaction' (Receipt::new).
Understanding the Concepts
What are the four types of method references?
package com.bank.service;
import java.math.BigDecimal;
import java.util.List;
import java.util.function.Supplier;
import java.util.ArrayList;
public class MethodRefDemo {
record Account(String id, BigDecimal balance, boolean active) {}
static boolean isValidId(String id) { return id.startsWith("SAV"); }
public static void main(String[] args) {
List<Account> accounts = List.of(
new Account("SAV2001", new BigDecimal("125000"), true),
new Account("CUR3001", new BigDecimal("250000"), true)
);
// Type 1: static method reference
List<String> ids = accounts.stream()
.map(Account::id)
.filter(MethodRefDemo::isValidId) // static: isValidId(String)
.toList();
// Type 2: bound instance method reference
accounts.forEach(System.out::println); // bound to System.out
// Type 3: unbound instance method reference
List<BigDecimal> balances = accounts.stream()
.map(Account::balance) // each Account calls its own balance()
.toList();
// Type 4: constructor reference
List<BigDecimal> parsed = List.of("100", "200", "300").stream()
.map(BigDecimal::new) // new BigDecimal(String)
.toList();
Supplier<List<String>> listFactory = ArrayList::new; // new ArrayList<>()
System.out.println("SAV IDs: " + ids);
System.out.println("Balances: " + balances);
System.out.println("Parsed: " + parsed);
}
}
Account[id=SAV2001, balance=125000, active=true]
Account[id=CUR3001, balance=250000, active=true]
SAV IDs: [SAV2001]
Balances: [125000, 250000]
Parsed: [100, 200, 300]
Comparison table
| Type | Syntax | Lambda Equivalent | Receiver | |------|--------|-------------------|----------| | 1. Static | Integer::parseInt | s -> Integer.parseInt(s) | None (class) | | 2. Bound | System.out::println | x -> System.out.println(x) | Specific object | | 3. Unbound | String::toUpperCase | s -> s.toUpperCase() | Stream element | | 4. Constructor | BigDecimal::new | s -> new BigDecimal(s) | New instance |
What is the confusion between Type 2 and Type 3?
Type 2 binds to a specific instance at pipeline creation time. Type 3 uses the stream element as the instance.
// Type 2: bound to a specific object
AuditLogger logger = new AuditLogger();
trades.forEach(logger::log); // calls logger.log(trade) on THIS specific logger
// Type 3: the stream element IS the receiver
trades.stream().map(Trade::getSymbol); // calls trade.getSymbol() on EACH trade
When should you use method references vs lambdas?
| Situation | Use | Why |
|-----------|-----|-----|
| Single method call | Method reference | Cleaner, reads like English |
| Multi-line logic | Lambda | References cannot have multiple statements |
| Additional arguments | Lambda | References cannot pass extra args |
| Clarity needed | Whichever is clearer | Readability over brevity |
// Method reference: cleaner
trades.stream().map(Trade::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
// Lambda needed: extra logic
trades.stream().map(t -> t.getAmount().multiply(taxRate)).toList();
How do constructor references work with collections?
import java.util.function.Supplier;
import java.util.function.Function;
// No-arg constructor: Supplier
Supplier<ArrayList<String>> listFactory = ArrayList::new;
ArrayList<String> list = listFactory.get();
// Single-arg constructor: Function
Function<String, BigDecimal> parser = BigDecimal::new;
BigDecimal amount = parser.apply("50000");
// Collect to specific collection type
List<Trade> sorted = trades.stream()
.sorted(Comparator.comparing(Trade::getAmount))
.collect(Collectors.toCollection(ArrayList::new));
What are common mistakes?
// WRONG: trying to use method reference with additional arguments
// trades.stream().filter(Trade::isAbove(limit)); // COMPILE ERROR
// CORRECT: use lambda for additional arguments
trades.stream().filter(t -> t.isAbove(limit));
// WRONG: ambiguous method reference (overloaded methods)
// Function<String, String> fn = String::valueOf; // which valueOf?
// CORRECT: use explicit lambda to resolve ambiguity
Function<Integer, String> fn = String::valueOf;
What about method references and generics?
// Method references work seamlessly with generic types
List<String> names = accounts.stream()
.map(Account::id)
.filter(Objects::nonNull)
.sorted(String::compareTo) // Type 3: element.compareTo(other)
.toList();
What should a senior engineer know?
- Use method references when the lambda body is a single method call. They make code read like English:
filter(Account::active).map(Account::balance).reduce(ZERO, BigDecimal::add). - For complex lambdas (multi-line or with additional logic), use explicit lambdas or extract into named methods.
- Method references are NOT always clearer.
str -> str.trim()may be clearer thanString::trimin some contexts. - The compiler resolves which overload to use based on the functional interface's type signature.
Visual Overview
Four Types:
──────────────────────────────────────────────────
Type 1: ClassName::staticMethod
Integer::parseInt s -> Integer.parseInt(s)
Type 2: object::instanceMethod
System.out::println x -> System.out.println(x)
Type 3: ClassName::instanceMethod
String::toUpperCase s -> s.toUpperCase()
Type 4: ClassName::new
BigDecimal::new s -> new BigDecimal(s)
Key Takeaway
There are four types of method references. Type 1 is static (Integer::parseInt). Type 2 is bound to a specific object (System.out::println). Type 3 uses the stream element as the receiver (String::toUpperCase). Type 4 is a constructor (BigDecimal::new).
Method references are syntactic sugar for lambdas that call one existing method. Use them when the lambda body is a single method call.
Remember one sentence: four types (static, bound instance, unbound instance, constructor) all replace lambdas that simply call one existing method.
---