Method References
Method references are a shorthand syntax for lambda expressions that simply call an existing method. Where a lambda expression is () -> SomeClass.someMethod() or x -> x.someMethod(), a method reference is SomeClass::someMethod or SomeClass::someMethod. The compiler translates a method reference into a functional interface implementation that calls the referenced method, producing code identical to the equivalent lambda expression — method references are purely syntactic sugar with no behavioral difference. Java defines four kinds: static method references (ClassName::staticMethod), unbound instance method references (ClassName::instanceMethod, where the first lambda argument provides the instance), bound instance method references (instance::instanceMethod, where a specific instance is captured), and constructor references (ClassName::new). Method references improve readability by eliminating lambda boilerplate when the lambda does nothing but forward to an existing method. This entry covers all four kinds with their exact lambda equivalents, the rules for matching method references to functional interface types, method references to overloaded methods, and how the compiler resolves ambiguity.
The Four Kinds of Method References
// ── Kind 1: Static method reference ──────────────────────────────────
// Lambda: (s) -> Integer.parseInt(s)
// Method reference: Integer::parseInt
Function<String, Integer> parseInt = Integer::parseInt;
System.out.println(parseInt.apply("42")); // 42
// Lambda: (a, b) -> Math.max(a, b)
// Method reference: Math::max
IntBinaryOperator max = Math::max;
System.out.println(max.applyAsInt(3, 7)); // 7
// Lambda: () -> System.currentTimeMillis()
// Method reference: System::currentTimeMillis
LongSupplier clock = System::currentTimeMillis;
// ── Kind 2: Unbound instance method reference ─────────────────────────
// Lambda: (s) -> s.toUpperCase()
// Method reference: String::toUpperCase
Function<String, String> upper = String::toUpperCase;
System.out.println(upper.apply("hello")); // HELLO
// Lambda: (s, prefix) -> s.startsWith(prefix)
// Method reference: String::startsWith
BiPredicate<String, String> startsWith = String::startsWith;
System.out.println(startsWith.test("Hello", "He")); // true
// Lambda: (list) -> list.size()
// Method reference: List::size
ToIntFunction<List<?>> size = List::size;
System.out.println(size.applyAsInt(List.of(1, 2, 3))); // 3
// ── Kind 3: Bound instance method reference ────────────────────────────
// Lambda: (s) -> System.out.println(s) (System.out captured)
// Method reference: System.out::println
Consumer<String> print = System.out::println;
print.accept("Bound reference"); // Bound reference
// Lambda: (s) -> "prefix_".concat(s) (literal captured)
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat;
System.out.println(greet.apply("World")); // Hello, World
// Lambda: (n) -> myList.add(n) (specific list captured)
List<String> myList = new ArrayList<>();
Consumer<String> addToList = myList::add;
addToList.accept("Alice");
addToList.accept("Bob");
System.out.println(myList); // [Alice, Bob]
// ── Kind 4: Constructor reference ─────────────────────────────────────
// Lambda: () -> new ArrayList<>()
// Method reference: ArrayList::new
Supplier<ArrayList<String>> listFactory = ArrayList::new;
ArrayList<String> newList = listFactory.get(); // new empty ArrayList
// Lambda: (n) -> new ArrayList<>(n)
// Method reference: ArrayList::new (different constructor selected by target type)
IntFunction<ArrayList<String>> sizedFactory = ArrayList::new;
ArrayList<String> sized = sizedFactory.apply(100); // pre-sized ArrayList
// Lambda: (s) -> new StringBuilder(s)
// Method reference: StringBuilder::new
Function<String, StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.apply("initial");Matching Method References to Functional Interfaces
// ── Unbound reference: first param is the receiver ───────────────────
// String::compareToIgnoreCase requires BiFunction or Comparator:
// Lambda: (s1, s2) -> s1.compareToIgnoreCase(s2)
Comparator<String> cmp = String::compareToIgnoreCase;
BiFunction<String, String, Integer> bf = String::compareToIgnoreCase;
System.out.println(cmp.compare("Apple", "apple")); // 0
// ── Overload resolution via target type ───────────────────────────────
// PrintStream has many println overloads:
Consumer<String> printStr = System.out::println; // println(String) selected
Consumer<Integer> printInt = System.out::println; // println(int) via unboxing? No:
// println(Object) selected (Integer is Object)
// Explicitly select println(int) via IntConsumer:
IntConsumer printPrimInt = System.out::println; // println(int) selected
// ── Ambiguous: explicit lambda required ───────────────────────────────
// If two overloads match equally, use a lambda with explicit cast:
// someMethod has overloads: void foo(String s) and void foo(Object o)
Consumer<String> c = SomeClass::foo; // may be ambiguous if both match Consumer<String>
// Resolution: (String s) -> SomeClass.foo(s) selects the String overload explicitly
// ── Constructor reference: type inference ────────────────────────────
// ArrayList has: ArrayList(), ArrayList(int), ArrayList(Collection<?>)
Supplier<ArrayList<String>> noArg = ArrayList::new; // ArrayList()
IntFunction<ArrayList<String>> intArg = ArrayList::new; // ArrayList(int)
Function<Collection<?>, ArrayList<?>> colArg = ArrayList::new; // ArrayList(Collection)
// Streams.toCollection uses Supplier<C>:
TreeSet<String> ts = Stream.of("b","a","c")
.collect(Collectors.toCollection(TreeSet::new)); // TreeSet() selected
System.out.println(ts); // [a, b, c]
// ── Checked exceptions: method refs to throwing methods ───────────────
// Files.readString(Path) throws IOException — cannot use as Function<Path, String>:
// Function<Path, String> reader = Files::readString; // COMPILE ERROR
// Workaround 1: wrap in lambda with try-catch
Function<Path, String> reader = path -> {
try { return Files.readString(path); }
catch (IOException e) { throw new UncheckedIOException(e); }
};
// Workaround 2: define a throwing functional interface:
@FunctionalInterface
interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}
ThrowingFunction<Path, String> throwingReader = Files::readString; // compiles fine
// ── Method references in sorted, map, filter ─────────────────────────
List<String> names = List.of("Charlie", "Alice", "Bob", "Dave");
// sorted with Comparator method reference:
List<String> sorted = names.stream()
.sorted(String::compareToIgnoreCase) // Comparator<String> from unbound ref
.collect(Collectors.toList());
System.out.println(sorted); // [Alice, Bob, Charlie, Dave]
// map with constructor reference:
List<StringBuilder> builders = names.stream()
.map(StringBuilder::new) // Function<String, StringBuilder> — constructor(String)
.collect(Collectors.toList());
// filter with method reference (predicate):
List<String> nonEmpty = List.of("Alice", "", "Bob", "", "Carol").stream()
.filter(Predicate.not(String::isEmpty)) // negate the isEmpty unbound ref
.collect(Collectors.toList());
System.out.println(nonEmpty); // [Alice, Bob, Carol]