Back to Blog
Java

Java Method Reference: Syntax and Practical Usage

Learn the four forms of java method references, how each maps to a lambda expression, and when each form fits cleanly in production code.

JavaLambda ExpressionsFunctional InterfacesStream APIComparator
Editorial illustration of a Java method reference connecting a lambda expression to a named method through the double-colon operator.

A java method reference is a compact way to express a lambda that delegates to an existing method. The syntax ClassName::methodName or instance::methodName replaces a lambda body that would otherwise call that method. For example, list.forEach(System.out::println) is equivalent to list.forEach(x -> System.out.println(x)).

Method references do not introduce a new language capability. They desugar to the same functional interface implementation as a lambda. The compiler still generates a synthetic method that implements the target functional interface, so a method reference is not a function pointer in the C sense and cannot be stored independently of a functional interface type.

The Four Forms of Method References

The Java language defines four forms of method references, each covering a different calling convention. The form you choose depends on where the method is declared and how its receiver is supplied.

FormSyntaxExampleEquivalent lambda
Static methodClassName::staticMethodInteger::parseInts -> Integer.parseInt(s)
Instance method on a specific objectinstance::instanceMethodlogger::logmsg -> logger.log(msg)
Instance method on an arbitrary objectClassName::instanceMethodString::toLowerCases -> s.toLowerCase()
ConstructorClassName::newArrayList::new() -> new ArrayList<>()

The third form is the one developers most often confuse with the first. String::toLowerCase does not call a static method; it means "call toLowerCase() on whatever String instance is passed as the argument." The compiler resolves the receiver from the first functional interface parameter.

Static Method References

Static method references work when the functional interface's single abstract method accepts arguments that match the static method's parameters. A common example appears with comparators:

List<String> names = Arrays.asList("ada", "grace", "linus"); names.sort(Comparator.comparing(String::length));

Here String::length is not a static reference; it is the arbitrary-object instance form because length() is an instance method. A true static reference looks like this:

List<String> numbers = Arrays.asList("10", "2", "33"); numbers.sort(Comparator.comparing(Integer::parseInt));

Integer::parseInt maps to s -> Integer.parseInt(s). The comparator receives two strings, and comparing applies parseInt to each before comparing. The static method reference is valid because parseInt(String) matches the Function<String, Integer> signature that comparing expects.

Instance Method References on a Specific Object

When a method reference captures a particular instance, the syntax is instance::method. The captured object becomes the receiver for every invocation:

public class AuditLogger { public void record(String message) { System.out.println("AUDIT: " + message); } } AuditLogger logger = new AuditLogger(); Consumer<String> logAction = logger::record; logAction.accept("payment processed");

The bound receiver form captures the instance at the point where the reference is created. The captured object stays reachable until the functional interface is garbage collected, so a long-lived method reference can keep a short-lived object alive. This matters when you pass a method reference into an asynchronous task or store it in a cache that outlives the intended scope.

Instance Method References on an Arbitrary Object

The arbitrary-object form applies an instance method to whichever object arrives as the first argument. This is the form used most often with sorting and streams:

List<String> words = Arrays.asList("lambda", "method", "reference"); words.sort(String::compareToIgnoreCase);

String::compareToIgnoreCase means the comparator calls a.compareToIgnoreCase(b) for each pair. The first argument becomes the receiver, and the remaining arguments become method parameters. This works only when the functional interface's first parameter type matches the class that declares the method.

The same form appears with stream mapping:

List<String> upper = words.stream().map(String::toUpperCase).toList();

String::toUpperCase matches Function<String, String> because the input string is the receiver and the method takes no arguments.

Constructor References

Constructor references use ClassName::new and work with any functional interface whose parameters match the constructor's parameters:

Supplier<List<String>> listFactory = ArrayList::new; List<String> fresh = listFactory.get();

A BiFunction can reference a two-argument constructor:

record Item(String name, int quantity) {} BiFunction<String, Integer, Item> itemFactory = Item::new; Item item = itemFactory.apply("bolt", 12);

The compiler selects the constructor whose parameter list matches the functional interface's abstract method signature. When multiple constructors fit, the most specific one is chosen according to overload resolution rules.

Runtime Behavior and Performance Considerations

A method reference compiles to the same invokedynamic call site as a lambda. The JVM uses LambdaMetafactory to generate the functional interface implementation at runtime. There is no reflection call and no per-invocation overhead beyond the first linkage. In practice, method references and equivalent lambdas have the same performance profile.

One real difference is capture behavior. A method reference to an instance method on a specific object captures that object, exactly like a lambda that captures a local variable. If the captured object is large and the functional interface outlives its intended scope, the object remains reachable. This is a genuine memory consideration when the reference is stored in a static field or a long-lived collection.

Method references are also not always clearer. When the lambda body does more than forward arguments, a lambda is more readable:

list.forEach(item -> { if (item.isValid()) { repository.save(item); } });

A method reference cannot express conditional logic. Use one only when the body is a single method call that forwards all arguments unchanged.

Common Pitfalls and Limitations

Method references fail at compile time when the functional interface's parameter types do not align with the referenced method. Overloaded methods can make the reference ambiguous. For instance, Math::max is valid for BinaryOperator<Integer> but also matches BinaryOperator<Long> and BinaryOperator<Double>. The target type disambiguates it:

BinaryOperator<Integer> maxInt = Math::max;

Without a target type, the compiler reports that the method reference is ambiguous. Assigning the reference to a typed variable resolves the overload.

Checked exceptions behave the same as in lambdas. A method reference to a method that throws a checked exception cannot be assigned to a functional interface whose abstract method does not declare that exception. The compiler enforces this at the assignment site.

Method references also require a functional interface target. You cannot write var f = String::toUpperCase; without a target type, because the compiler cannot infer which functional interface to generate. The reference must appear in a context that supplies a target type, such as an assignment, a method argument, or a cast.

java method reference: Practical Usage and Code Examples | RYUSLOG DEV