Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Java Streams & Functional Programming

Lambdas & Functional Interfaces

  • Lambda Expressions
  • Method References

Streams API

  • Streams API
  • Collectors and groupingBy
  • Optional
Chaturmind
← Java Streams & Functional Programming

Lambdas & Functional Interfaces

  • Lambda Expressions
  • Method References

Streams API

  • Streams API
  • Collectors and groupingBy
  • Optional
HomeLearnJavaJava Streams & Functional ProgrammingLambdas & Functional Interfaces
✓ FreeIntermediate· 6 min read

Method References

Simplify lambdas further with method references — four kinds, when to use each.

Published September 21, 2026


Method References

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.

The four kinds

KindSyntaxEquivalent lambda
1. Static methodClassName::staticMethod(args) -> ClassName.staticMethod(args)
2. Instance method of a particular objectobject::method(args) -> object.method(args)
3. Instance method of an arbitrary object of a typeClassName::instanceMethod(obj, args) -> obj.instanceMethod(args)
4. ConstructorClassName::new(args) -> new ClassName(args)

1. Static method

Function<String, Integer> parse = Integer::parseInt;        // s -> Integer.parseInt(s)
BinaryOperator<Integer> max     = Math::max;                // (a, b) -> Math.max(a, b)

2. Instance method of a particular object (a "bound" reference)

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)

3. Instance method of an arbitrary object (an "unbound" reference)

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.

4. Constructor

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).

Where they shine

// 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();

When a lambda is the better choice

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.

Subtle behaviour worth knowing

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.

Follow-up questions this topic invites — and their answers

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.

Previous

Lambda Expressions

Next

Streams API

AI Tutor

Lesson: Method References

Quick actions

AI responses can be inaccurate. Verify critical information.