Q26 of 40 · Core Java

What is a method reference and how does it relate to a lambda?

Core JavaMidmethod-referenceslambdafunctional-programmingjava-streams

Short answer

Short answer: A method reference is shorthand for a lambda that does nothing but call an existing method. `String::toUpperCase` is equivalent to `s -> s.toUpperCase()`. There are four kinds: static method, instance method of an arbitrary instance, instance method of a particular instance, and constructor.

Detail

When a lambda does nothing but delegate to an existing method — s -> s.toUpperCase(), obj -> obj.toString(), n -> Integer.parseInt(n) — a method reference expresses the same thing more concisely. The compiler resolves the method reference to the same functional interface as the lambda would target.

Four kinds:

  1. Static method: Integer::parseIntn -> Integer.parseInt(n). The method is called on the class, not an instance.

  2. Instance method of an arbitrary instance (bound to type): String::toUpperCases -> s.toUpperCase(). The first parameter of the lambda becomes the receiver.

  3. Instance method of a particular instance (bound to object): System.out::printlnx -> System.out.println(x). The method is always called on that specific System.out instance.

  4. Constructor: ArrayList::new() -> new ArrayList<>(). Used with Supplier<T> or Function<Integer, ArrayList>.

Method references are not always clearer — s -> s.toUpperCase() is actually very readable. Prefer method references when:

  • The lambda body is a single call with no modification of arguments
  • The method name adds semantic meaning (User::getEmail tells you more than u -> u.getEmail())
  • You're using a well-known static method like Objects::nonNull, String::isEmpty

In test automation: method references appear in stream pipelines (cases.stream().map(TestCase::id).toList()), assertion helpers, and as Comparator arguments (Comparator.comparing(TestCase::severity)).

// EXAMPLE

MethodReferences.java

import java.util.List;
import java.util.Objects;

List<String> tags = List.of("smoke", null, "regression", null, "e2e");
List<TestCase> cases = getTestCases();

// 1. Static method reference
//    n -> Integer.parseInt(n)
List<Integer> ids = List.of("1", "2", "3").stream()
    .map(Integer::parseInt)        // static method ref
    .toList();

// 2. Instance method of arbitrary instance (bound to type)
//    s -> s.toUpperCase()
List<String> upper = tags.stream()
    .filter(Objects::nonNull)      // static: t -> Objects.nonNull(t)
    .map(String::toUpperCase)      // instance on receiver
    .toList();

// 3. Instance method of particular instance
//    x -> System.out.println(x)
tags.stream()
    .filter(Objects::nonNull)
    .forEach(System.out::println); // bound to System.out instance

// 4. Constructor reference
//    () -> new ArrayList<>()
import java.util.ArrayList;
import java.util.function.Supplier;
Supplier<List<String>> listFactory = ArrayList::new;
List<String> fresh = listFactory.get();

// Comparator composed with method refs
cases.sort(
    Comparator.comparing(TestCase::severity)  // instance method ref
              .thenComparing(TestCase::id)
);

// WHAT INTERVIEWERS LOOK FOR

All four kinds by name and example, the equivalence to lambdas, and a practical guideline for when to use each. Strong answers use method references in stream operations and note that readability (not dogma) should drive the choice.

// COMMON PITFALL

Using method references when they obscure intent: `list.stream().filter(x -> doComplexCheck(x, ctx))` is clearer than a partial application hack. The goal is readability — method references are a tool, not a rule.