Skip to main content

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 equivalent lambdas when the lambda does nothing but call an existing method.

1. Why Method References?

// Lambda — calling println for each element
List<String> names = List.of("Alice", "Bob", "Charlie");
names.forEach(name -> System.out.println(name));

// Method reference — exactly the same, much cleaner
names.forEach(System.out::println);

When a lambda simply delegates to an existing method, a method reference can replace it.


2. The Four Types of Method References

Type 1: Static Method Reference

ClassName::staticMethodName

// Lambda
Function<String, Integer> parser1 = s -> Integer.parseInt(s);
// Method reference
Function<String, Integer> parser2 = Integer::parseInt;

System.out.println(parser2.apply("42")); // 42

// In streams
List<String> numberStrings = List.of("1", "2", "3", "4", "5");
List<Integer> numbers = numberStrings.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
System.out.println(numbers); // [1, 2, 3, 4, 5]

Type 2: Bound Instance Method Reference

instanceVariable::instanceMethodName

String prefix = "Hello, ";

// Lambda
Function<String, String> greeter1 = name -> prefix.concat(name);
// Method reference (the specific instance 'prefix' is the receiver)
Function<String, String> greeter2 = prefix::concat;

System.out.println(greeter2.apply("Java")); // Hello, Java

// Practical example
List<String> fruits = List.of("apple", "banana", "apricot", "cherry");
long countStartWithA = fruits.stream()
.filter(f -> f.startsWith("a")) // lambda is clearer here
.count();
System.out.println(countStartWithA); // 2 (apple, apricot)

Type 3: Unbound Instance Method Reference

ClassName::instanceMethodName

The first parameter of the lambda becomes the receiver of the method call.

// Lambda
Function<String, String> upper1 = s -> s.toUpperCase();
Function<String, Integer> len1 = s -> s.length();
Predicate<String> empty1 = s -> s.isEmpty();

// Method reference (s is the receiver)
Function<String, String> upper2 = String::toUpperCase;
Function<String, Integer> len2 = String::length;
Predicate<String> empty2 = String::isEmpty;
BiPredicate<String, String> starts = String::startsWith; // (s1, s2) -> s1.startsWith(s2)

System.out.println(upper2.apply("hello")); // HELLO
System.out.println(starts.test("hello", "he")); // true

// Most common form in stream pipelines
List<String> words = List.of("Hello", "World", "Java", "");

words.stream()
.filter(Predicate.not(String::isEmpty)) // where isEmpty() is false
.map(String::toLowerCase)
.sorted(String::compareTo)
.forEach(System.out::println);
// hello, java, world

// Sorting with Comparator
List<String> names = new ArrayList<>(List.of("Charlie", "Alice", "Bob"));
names.sort(String::compareToIgnoreCase); // (a, b) -> a.compareToIgnoreCase(b)
System.out.println(names); // [Alice, Bob, Charlie]

Type 4: Constructor Reference

ClassName::new

// Lambda
Supplier<ArrayList<String>> listMaker1 = () -> new ArrayList<>();
Function<String, StringBuilder> sbMaker1 = s -> new StringBuilder(s);

// Constructor reference
Supplier<ArrayList<String>> listMaker2 = ArrayList::new;
Function<String, StringBuilder> sbMaker2 = StringBuilder::new;

List<String> newList = listMaker2.get();
StringBuilder sb = sbMaker2.apply("Hello");

// Collecting into a specific implementation
List<String> words = List.of("apple", "banana", "cherry");
ArrayList<String> arrayList = words.stream()
.collect(Collectors.toCollection(ArrayList::new));
LinkedList<String> linkedList = words.stream()
.collect(Collectors.toCollection(LinkedList::new));

// Array constructor reference
Function<Integer, String[]> arrayMaker = String[]::new;
String[] arr = arrayMaker.apply(5); // new String[5]
System.out.println(arr.length); // 5

// In Stream.toArray()
String[] result = words.stream()
.filter(s -> s.length() > 5)
.toArray(String[]::new); // handles new String[n] automatically
System.out.println(Arrays.toString(result)); // [banana, cherry]

3. Comprehensive Example: Method Reference Refactoring

import java.util.*;
import java.util.stream.*;
import java.util.function.*;

record Person(String name, int age, String city) {
static Person of(String name, int age, String city) {
return new Person(name, age, city);
}
boolean isAdult() { return age >= 18; }
String greeting() { return "Hello, I'm " + name + "!"; }
}

public class MethodRefDemo {
public static void main(String[] args) {
List<Person> people = List.of(
Person.of("Alice", 25, "Seoul"),
Person.of("Bob", 16, "Busan"),
Person.of("Charlie", 30, "Seoul"),
Person.of("Dave", 14, "Daegu"),
Person.of("Eve", 22, "Seoul")
);

// 1. Filter adults (unbound instance method reference)
people.stream()
.filter(Person::isAdult) // p -> p.isAdult()
.map(Person::greeting) // p -> p.greeting()
.forEach(System.out::println);

System.out.println("---");

// 2. Extract names, sort alphabetically
people.stream()
.map(Person::name) // p -> p.name()
.sorted(String::compareTo) // (a,b) -> a.compareTo(b)
.forEach(System.out::println);

System.out.println("---");

// 3. Find youngest person
people.stream()
.min(Comparator.comparingInt(Person::age)) // p -> p.age()
.ifPresent(p -> System.out.println("Youngest: " + p.name()));
}
}

Pro Tips

Method reference vs lambda — when to use each?

  • Method reference: when the lambda body is only a call to an existing method— more readable
  • Lambda: when arguments need transformation, multiple operations, or complex conditions — lambda is clearer
// Method reference is better
.map(String::toUpperCase) // vs: s -> s.toUpperCase()
.filter(Objects::nonNull) // vs: s -> s != null
.sorted(Comparator.naturalOrder())

// Lambda is better
.filter(s -> s.length() > 3 && s.startsWith("A")) // complex condition
.map(s -> "[" + s + "]") // transformation logic