12.7 Method References
Method references are a Java 8 feature that lets you pass an existing method as if it were a lambda expression, using the :: operator. They produce more concise and readable code than lambdas.
1. Why Method References?
List<String> names = List.of("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name)); // Lambda
names.forEach(System.out::println); // Method reference (cleaner!)
When a lambda simply calls an existing method, replace it with a method reference.
2. Four Types of Method References
Type 1: Static Method Reference
ClassName::staticMethodName
Function<String, Integer> parser = Integer::parseInt; // Integer.parseInt(s)
System.out.println(parser.apply("42")); // 42
List<String> numberStrings = List.of("1", "2", "3", "4", "5");
List<Integer> numbers = numberStrings.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
Type 2: Bound Instance Method Reference
instance::instanceMethodName
String prefix = "Hello, ";
Function<String, String> greeter = prefix::concat; // prefix.concat(s)
System.out.println(greeter.apply("Java")); // Hello, Java
Type 3: Unbound Instance Method Reference
ClassName::instanceMethodName
The first parameter becomes the receiver object.
Function<String, String> upper = String::toUpperCase; // s -> s.toUpperCase()
Function<String, Integer> length = String::length; // s -> s.length()
Predicate<String> blank = String::isEmpty; // s -> s.isEmpty()
BiPredicate<String, String> starts = String::startsWith; // (s1,s2) -> s1.startsWith(s2)
System.out.println(upper.apply("hello")); // HELLO
System.out.println(starts.test("hello", "he")); // true
List<String> words = List.of("Hello", "World", "Java", "");
words.stream()
.filter(Predicate.not(String::isEmpty))
.map(String::toLowerCase)
.sorted(String::compareTo)
.forEach(System.out::println); // hello, java, world
Type 4: Constructor Reference
ClassName::new
Supplier<ArrayList<String>> listMaker = ArrayList::new;
Function<String, StringBuilder> sbMaker = StringBuilder::new;
List<String> newList = listMaker.get();
StringBuilder sb = sbMaker.apply("Hello");
// Useful with stream toArray
String[] words = List.of("apple", "banana", "cherry").stream()
.filter(s -> s.length() > 5)
.toArray(String[]::new); // new String[n] handled automatically
3. Comprehensive Example
record Person(String name, int age, String city) {
boolean isAdult() { return age >= 18; }
String greeting() { return "Hello, I'm " + name + "!"; }
}
List<Person> people = List.of(
new Person("Alice", 25, "Seoul"),
new Person("Bob", 16, "Busan"),
new Person("Charlie", 30, "Seoul")
);
// Filter adults and print greetings
people.stream()
.filter(Person::isAdult) // p -> p.isAdult()
.map(Person::greeting) // p -> p.greeting()
.forEach(System.out::println);
// Sort by name
people.stream()
.map(Person::name)
.sorted(String::compareTo)
.forEach(System.out::println);
// Find youngest
people.stream()
.min(Comparator.comparingInt(Person::age))
.ifPresent(p -> System.out.println("Youngest: " + p.name()));
Pro Tip
Method reference vs lambda: when to use which?
- Method reference: Lambda body only calls one existing method → more readable
- Lambda: Needs argument transformation, multiple operations, or complex conditions → lambda is clearer
// Method references are clearer here
.map(String::toUpperCase) // ✅ vs s -> s.toUpperCase()
.filter(Objects::nonNull) // ✅ vs s -> s != null
// Lambdas are clearer here
.filter(s -> s.length() > 3 && s.startsWith("A")) // complex condition
.map(s -> "[" + s + "]") // transformation logic