Part 5 of 16

Method References: Four Kinds and When to Use Each

What Are Method References?

A method reference is a compact syntax for a lambda expression that does nothing except call an existing method. If a lambda’s entire body is a method call, a method reference is almost always clearer.

// Lambda
names.forEach(s -> System.out.println(s));

// Method reference — identical behaviour, fewer characters, clearer intent
names.forEach(System.out::println);

The :: operator is the method reference operator. It does not call the method — it creates a reference to it, which the compiler wraps into a lambda that calls the method.


The Four Kinds

Kind 1: Static Method Reference

Syntax: ClassName::staticMethodName

// Lambda equivalent
Function<String, Integer> parse = s -> Integer.parseInt(s);

// Method reference
Function<String, Integer> parse = Integer::parseInt;

// Usage
List<Integer> numbers = Arrays.asList("1", "2", "3").stream()
    .map(Integer::parseInt)
    .collect(Collectors.toList());

More examples:

// Math.abs
Function<Double, Double> abs = Math::abs;

// String.valueOf
Function<Integer, String> str = String::valueOf;

// Custom static methods
Function<String, String> clean = StringUtils::trim;

When to use: Any time a lambda is just calling a static utility method. Very common with parsing, conversion, and utility methods.


Kind 2: Instance Method Reference on a Specific Object (Bound)

Syntax: instance::methodName

The instance is captured at reference-creation time (bound to the reference).

String prefix = "Hello, ";

// Lambda
Function<String, String> greet = name -> prefix.concat(name);

// Bound instance method reference
Function<String, String> greet = prefix::concat;

// System.out is a specific PrintStream instance
Consumer<String> print = System.out::println;
// equivalent to: s -> System.out.println(s)

More examples:

// A specific list
List<String> buffer = new ArrayList<>();
Consumer<String> collect = buffer::add;

// A specific logger
Logger log = LoggerFactory.getLogger(MyClass.class);
Consumer<String> logInfo = log::info;

When to use: When you have a specific object instance and want to reference one of its methods as a function. Very common with System.out::println and logger references.


Kind 3: Instance Method Reference on an Arbitrary Instance of a Type (Unbound)

Syntax: ClassName::instanceMethodName

The instance is supplied by the lambda’s first argument at call time (unbound — the instance is not captured).

// Lambda
Function<String, String> upper = s -> s.toUpperCase();

// Unbound instance method reference
Function<String, String> upper = String::toUpperCase;

// The first parameter of the lambda becomes the receiver
Function<String, Integer> length = String::length;

With two parameters:

// Lambda: (str, prefix) -> str.startsWith(prefix)
BiPredicate<String, String> startsWith = (str, prefix) -> str.startsWith(prefix);

// Unbound method reference: first param is the receiver, rest are arguments
BiPredicate<String, String> startsWith = String::startsWith;

The rule: For an unbound reference Type::method, the lambda’s first parameter is the object the method is called on, and the remaining parameters are passed as arguments.

More examples:

// Comparator
Comparator<String> cmp = String::compareTo;
// equivalent to: (a, b) -> a.compareTo(b)

// Used in sort
names.sort(String::compareTo);

// Custom class
Function<Person, String> getName = Person::getName;
ToIntFunction<String> hash = String::hashCode;

When to use: Extremely common in stream pipelines. Any time a lambda is s -> s.someMethod(), consider replacing with Type::someMethod.


Kind 4: Constructor Reference

Syntax: ClassName::new

Creates a new instance. The parameters of the lambda are passed to the constructor.

// No-arg constructor
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();

// One-arg constructor
Function<String, StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.apply("Hello");

// Two-arg constructor
BiFunction<String, Integer, Person> personFactory = Person::new;
Person p = personFactory.apply("Alice", 30);

With streams:

// Collect into a specific collection type
List<String> names = stream.collect(Collectors.toCollection(LinkedList::new));

// Create objects from stream elements
List<User> users = ids.stream()
    .map(User::new)  // assumes User(Long id) constructor
    .collect(Collectors.toList());

Array constructors:

IntFunction<String[]> arrayFactory = String[]::new;
String[] arr = arrayFactory.apply(10);  // new String[10]

// Used with Stream.toArray
String[] names = stream.toArray(String[]::new);

Reference Table

KindSyntaxLambda equivalentExample
StaticClass::staticMethodx -> Class.staticMethod(x)Integer::parseInt
Bound instanceobj::methodx -> obj.method(x)System.out::println
Unbound instanceClass::method(obj, x) -> obj.method(x)String::toUpperCase
ConstructorClass::new(x) -> new Class(x)ArrayList::new

When to Prefer Method References Over Lambdas

Use method references when:

  • The lambda does exactly one thing: call an existing named method
  • The method name is descriptive and communicates intent
  • The reference is shorter and easier to read at a glance
// Method reference — clearly "convert each element to its string form"
.map(Object::toString)

// Lambda — same thing, no benefit to the extra syntax
.map(s -> s.toString())

Keep lambdas when:

  • The method reference requires a confusing cast or disambiguation
  • The method name is cryptic and a lambda with a better variable name is clearer
  • The method has side effects that should be explicit
  • The reference is an unbound method reference and the receiver-vs-argument relationship isn’t obvious
// Method reference — who is the receiver? ambiguous at a glance
.sorted(String::compareToIgnoreCase)

// Lambda — explicit
.sorted((a, b) -> a.compareToIgnoreCase(b))

Practical Examples in Stream Pipelines

List<String> names = Arrays.asList("  Alice ", "BOB", "charlie", "  Dave  ");

List<String> cleaned = names.stream()
    .map(String::trim)           // unbound instance: trim whitespace
    .map(String::toLowerCase)    // unbound instance: lowercase
    .filter(s -> !s.isEmpty())   // lambda: predicate with logic
    .sorted(String::compareTo)   // unbound instance: natural order
    .collect(Collectors.toList());

System.out.println(cleaned); // [alice, bob, charlie, dave]
// Constructor reference to collect into specific type
Set<String> nameSet = names.stream()
    .map(String::trim)
    .collect(Collectors.toCollection(TreeSet::new));

// Static method reference for parsing
List<Integer> ids = Arrays.asList("1", "2", "3", "4").stream()
    .map(Integer::parseInt)
    .collect(Collectors.toList());

Overloaded Methods: Disambiguation

When a method is overloaded, the compiler picks the right overload by matching the functional interface signature:

// println is overloaded (String, int, Object, etc.)
// Compiler infers Consumer<String> → println(String)
Consumer<String> print = System.out::println;

// Compiler infers Consumer<Integer> → println(int)
Consumer<Integer> printInt = System.out::println;

// If ambiguous, you must cast

With valueOf:

// valueOf(int) vs valueOf(Object) — both could match
Function<Integer, String> f = String::valueOf;  // picks valueOf(Object)
// If you need valueOf(int) specifically, use a lambda: n -> String.valueOf(n)

Summary

KindWhen you haveUse
StaticA static utility methodClassName::staticMethod
BoundA captured instanceinstance::method
UnboundA method on the stream’s element typeElementType::method
ConstructorNeed to create objectsClassName::new

Next Step

Streams API: Lazy Pipelines and the Functional Data Model →

Part of the DevOps Monk Java tutorial series: Java 8Java 11Java 17Java 21