Simplify lambdas further with method references — four kinds, when to use each.
Published September 21, 2026
A method reference is a compact way to write a lambda whose only job is to call one existing method. Instead of describing how to call it (s -> Integer.parseInt(s)), you just name the method (Integer::parseInt), and the compiler fills in the parameter passing.
List<Integer> numbers = Stream.of("1", "2", "3")
.map(s -> Integer.parseInt(s)) // lambda
.toList();
List<Integer> numbers = Stream.of("1", "2", "3")
.map(Integer::parseInt) // method reference: same meaning, less noise
.toList();
A method reference is not a different feature from lambdas. It compiles to the same thing: an instance of a functional interface (an interface with exactly one abstract method, like Function, Predicate, Supplier or Comparator). Which method is chosen, and how its parameters line up, is decided by the functional interface the reference is assigned to. This is called its target type.
| Kind | Syntax | Equivalent lambda |
|---|---|---|
| 1. Static method | ClassName::staticMethod | (args) -> ClassName.staticMethod(args) |
| 2. Instance method of a particular object | object::method | (args) -> object.method(args) |
| 3. Instance method of an arbitrary object of a type | ClassName::instanceMethod | (obj, args) -> obj.instanceMethod(args) |
| 4. Constructor | ClassName::new | (args) -> new ClassName(args) |
Function<String, Integer> parse = Integer::parseInt; // s -> Integer.parseInt(s)
BinaryOperator<Integer> max = Math::max; // (a, b) -> Math.max(a, b)
The object is fixed when the reference is created. Every call goes to that same object.
Consumer<String> print = System.out::println; // s -> System.out.println(s)
String prefix = "Hello, ";
Function<String, String> greet = prefix::concat; // s -> prefix.concat(s)
// Common inside classes: this:: and super::
button.addActionListener(this::handleClick); // e -> this.handleClick(e)
No object is fixed. The first parameter of the functional interface becomes the object the method is called on, and any remaining parameters become the method's arguments.
Function<String, String> trim = String::trim; // s -> s.trim()
Predicate<String> isEmpty = String::isEmpty; // s -> s.isEmpty()
Function<Person, String> name = Person::getName; // p -> p.getName()
BiFunction<String, String, Boolean> startsWith = String::startsWith; // (s, p) -> s.startsWith(p)
Comparator<String> byNatural = String::compareTo; // (a, b) -> a.compareTo(b)
This is the kind that confuses people most. It looks like a static call (String::trim), but trim isn't static. It works because the functional interface supplies the object as the first argument.
Supplier<List<String>> newList = ArrayList::new; // () -> new ArrayList<>()
Function<String, StringBuilder> sb = StringBuilder::new; // s -> new StringBuilder(s)
// Array constructors — the standard way to turn a stream into a typed array
String[] names = people.stream().map(Person::getName).toArray(String[]::new); // n -> new String[n]
// Choosing the collection type in a collector
TreeSet<String> sorted = names.stream().collect(Collectors.toCollection(TreeSet::new));
Which constructor is picked depends on the target type: ArrayList::new as a Supplier calls the no-arg constructor, and as a Function<Integer, ArrayList<T>> it calls ArrayList(int initialCapacity).
// Comparators read almost like English
people.sort(Comparator.comparing(Person::getLastName)
.thenComparing(Person::getFirstName)
.thenComparingInt(Person::getAge));
// Grouping and mapping
Map<String, List<Person>> byCity = people.stream().collect(Collectors.groupingBy(Person::getCity));
// Filtering with negation (Java 11+)
List<String> nonBlank = lines.stream().filter(Predicate.not(String::isBlank)).toList();
// Unwrapping optionals in a stream
List<User> found = ids.stream().map(repo::findById).flatMap(Optional::stream).toList();
Use a method reference only when the lambda does nothing but call one method with its arguments passed straight through. As soon as you need anything else, a lambda is clearer:
// Extra argument that isn't one of the lambda's parameters → lambda
Predicate<String> isEmail = s -> s.contains("@"); // String::contains can't supply "@"
// Arguments rearranged or transformed → lambda
Function<Order, BigDecimal> totalWithTax = o -> o.total().multiply(TAX_RATE);
// Several steps → lambda (or better, a named private method you then reference)
Function<User, String> label = u -> u.getLastName().toUpperCase() + ", " + u.getFirstName();
That last case suggests a good habit: if a lambda grows, move it into a well-named private method and reference that (this::formatLabel). The pipeline stays readable and the logic becomes testable.
Bound references evaluate their receiver immediately. With object::method, the object expression is evaluated when the reference is created. With a lambda, it's evaluated each time the lambda runs:
User user = null;
Supplier<String> ref = user::getName; // NullPointerException right here, at creation
Supplier<String> lambda = () -> user.getName(); // no error yet; NPE only when get() is called
It also means a bound reference keeps pointing at the object it captured, even if the variable is later reassigned. (Local variables must be effectively final either way.)
Overloaded methods can make a reference ambiguous. Integer::toString matches both the static Integer.toString(int) and the instance intValue.toString() for a Function<Integer, String>, and the compiler rejects it as ambiguous. Write the lambda (i -> Integer.toString(i)) or use String::valueOf.
Q: What's the difference between String::length and str::length?
A: String::length is unbound: the string to measure is supplied when the function is called, so it fits Function<String, Integer>. str::length is bound to one specific string captured at creation, so it fits Supplier<Integer> (no input) and always measures that same string.
Q: Are method references faster than lambdas?
A: No meaningful difference. Both compile to an invokedynamic call site, and the JVM creates similar functional-interface objects. Choose based on readability. The one behavioural difference is that a bound reference evaluates its receiver at creation time (see above).
Q: How does toArray(String[]::new) work?
A: toArray expects an IntFunction<A[]>, a function from a size to a new array. String[]::new is an array constructor reference meaning n -> new String[n]. The stream calls it with the number of elements to allocate a correctly typed array, which the untyped toArray() (returning Object[]) can't do.
Q: Can you reference a method that throws a checked exception?
A: Only if the target functional interface declares that exception. Function, Predicate and the other standard interfaces don't, so Files::readString won't compile as a Function<Path, String>. You either wrap the call in a lambda that catches and rethrows unchecked, or define your own functional interface that declares the exception.
Q: How do you identify which of the four kinds a given reference is?
A: Look at what's left of ::. A class name with a static method is kind 1. An expression or variable (System.out, this, prefix) is kind 2. A class name with an instance method is kind 3, where the first argument becomes the receiver. ClassName::new or Type[]::new is kind 4.