☕ Java

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

A static method reference ClassName::staticMethod refers to a static method of a class. The equivalent lambda takes the same parameters as the static method and passes them directly: Integer::parseInt is equivalent to (s) -> Integer.parseInt(s), Function<String, Integer>. When matched to a functional interface, the method reference's parameter list and return type must match the functional interface's abstract method signature. Multiple parameters are supported: Math::max as BinaryOperator<Integer> or IntBinaryOperator is equivalent to (a, b) -> Math.max(a, b). An unbound instance method reference ClassName::instanceMethod refers to an instance method where the instance will be provided as the first argument. The lambda equivalent has one extra parameter compared to the method's declared parameter list — the first parameter is the receiver instance: String::toUpperCase as Function<String, String> is equivalent to (s) -> s.toUpperCase(). String::startsWith as BiPredicate<String, String> is equivalent to (s, prefix) -> s.startsWith(prefix). The method is called on the first argument. A bound instance method reference instance::instanceMethod refers to an instance method where a specific instance is captured at reference creation time. Every call uses the same captured instance: System.out::println as Consumer<String> is equivalent to (s) -> System.out.println(s) where System.out is captured. myList::add as Consumer<String> is equivalent to (s) -> myList.add(s). Bound references capture the instance reference — if the instance is mutable, subsequent changes to it affect all calls through the bound reference. A constructor reference ClassName::new refers to a constructor. The lambda equivalent calls the constructor with the provided arguments and returns the new instance: ArrayList::new as Supplier<ArrayList<String>> is equivalent to () -> new ArrayList<>(); ArrayList::new as IntFunction<ArrayList<String>> is equivalent to (n) -> new ArrayList<>(n) (which constructor is invoked depends on the target functional interface's method signature). Constructor references for generic types require the target type to supply the type argument.
Java
// ── 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

The compiler matches a method reference to a functional interface by examining the target type — the functional interface type expected in the context where the method reference appears. The matching rules differ by kind. For static method references: the method's parameter list and return type must match the functional interface's abstract method exactly. For unbound instance references: the first lambda parameter provides the receiver, so the functional interface must have one more parameter than the method — the first parameter's type must be the class containing the method (or a supertype), and the remaining parameters match the method's parameters. For bound instance references: the functional interface's parameter list must match the method's parameter list exactly (no extra first parameter for the receiver — it is captured). For constructor references: the functional interface's return type must be the constructed type, and its parameters must match the constructor's parameters. Overloaded methods require careful attention. When a class has multiple overloads of a method with different signatures, the compiler uses the target functional interface type to select the correct overload. PrintStream::println is ambiguous without a target type — there are overloads for String, int, long, double, Object, etc. Assigning PrintStream::println to Consumer<String> selects the println(String) overload; assigning to Consumer<Integer> selects println(int) (which gets autoboxed) or println(Object). If the overload resolution is ambiguous, a compilation error occurs and an explicit lambda with a cast must be used. Generic method references require the compiler to infer the type arguments. Collections::sort as Consumer<List<Comparable>> selects void sort(List<T> list) with T inferred from the Consumer's element type. When the inference is ambiguous or unavailable, an explicit lambda with a cast or explicit type witness is needed. Method references to methods that throw checked exceptions cannot be assigned to functional interfaces whose abstract method does not declare the same checked exceptions. A reference to Files::readString (throws IOException) cannot be assigned to Function<Path, String> directly — it can only be assigned to a functional interface that declares throws IOException. The common workaround: catch the IOException inside a wrapping lambda, or use a custom functional interface that declares the exception.
Java
// ── 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]

Related Topics in Java 8 Features

Lambda Expressions
Lambda expressions, introduced in Java 8, are anonymous functions — blocks of code that can be stored in variables, passed as arguments, and returned from methods, treating behavior as data. A lambda has three parts: a parameter list, an arrow token (->), and a body. The body is either a single expression (whose value is the implicit return value) or a block of statements wrapped in braces. Lambdas implement functional interfaces — interfaces with exactly one abstract method — allowing any lambda whose signature matches the abstract method's signature to be used wherever that interface is expected. The lambda syntax is syntactic sugar: every lambda is compiled to an invocation of the functional interface's abstract method, with the compiler generating a class (via invokedynamic) that implements the interface and delegates to the lambda body. This entry covers the complete lambda syntax including all shorthand forms, variable capture and the effectively-final constraint, method references as a specialized lambda syntax, the relationship between lambdas and the type system, how lambdas interact with exception handling, the invokedynamic compilation strategy and its performance characteristics, and the complete set of rules governing lambda type inference.
Functional Interfaces
A functional interface is any Java interface that has exactly one abstract method. This single-abstract-method (SAM) contract makes the interface a valid target type for a lambda expression or method reference — the lambda provides the implementation of that one abstract method. The @FunctionalInterface annotation is optional but strongly recommended: it causes the compiler to verify that the interface satisfies the SAM constraint, rejecting it at compile time if there is more than one abstract method. The java.util.function package, introduced in Java 8, provides 43 standard functional interfaces organized around four root types — Function, Consumer, Supplier, Predicate — and their variations for primitives (IntFunction, LongSupplier, DoubleConsumer, etc.), binary operations (BiFunction, BiConsumer, BiPredicate), and unary operators (UnaryOperator, IntUnaryOperator, etc.). This entry covers the design principles behind functional interfaces, the complete @FunctionalInterface contract including default and static methods, the full java.util.function hierarchy and the pattern that governs naming, creating custom functional interfaces with checked exceptions, composing functional interfaces via default methods, and the relationship between functional interfaces and the type system including the rules for lambda assignment and widening.
Predicate
Predicate<T> is a functional interface in java.util.function representing a boolean-valued function of one argument, with the single abstract method boolean test(T t). It is one of the four foundational functional interfaces in the Java standard library and is used throughout the Collections framework, Streams API, and Optional for filtering, condition testing, and validation. Predicate is designed for composition: its default methods and(Predicate), or(Predicate), and negate() allow building complex boolean expressions from simple predicates without boilerplate. The static methods isEqual(Object) and not(Predicate) provide factory methods for common cases. The primitive specializations IntPredicate, LongPredicate, and DoublePredicate avoid boxing overhead for numeric values. BiPredicate<T,U> extends the concept to two-argument boolean functions. This entry covers the complete Predicate API, all composition methods and their short-circuit semantics, the static factory methods, primitive specializations, BiPredicate, using Predicate in stream pipelines and Collections methods, building validation frameworks with Predicate composition, and the performance and readability trade-offs of different composition styles.
Function
Function<T,R> is a functional interface in java.util.function representing a function that accepts one argument of type T and produces a result of type R, with the single abstract method R apply(T t). It is the most general transformation interface in the standard library, used throughout the Streams API for mapping (Stream.map()), in Optional for value transformation (Optional.map(), Optional.flatMap()), and as a building block for more specialized functional interfaces. Function provides two default composition methods — andThen() and compose() — that create new functions by chaining two functions together, enabling functional pipeline construction without intermediate variables. The specializations cover all combinations of generic and primitive inputs and outputs: ToIntFunction, IntFunction, IntToLongFunction, and so on. UnaryOperator<T> extends Function<T,T> for operations that transform a value within the same type. BiFunction<T,U,R> generalizes to two input arguments. This entry covers the complete Function API, the semantics of andThen versus compose, all specializations and when each is appropriate, the functional relationship between Function and other java.util.function types, partial application patterns, and Function as the basis for building data pipelines.