A method reference is an expression such as String::length that names an existing method and lets the compiler turn it into an instance of a functional interface. Where a lambda says how to compute the result, a method reference says which method already does it. names.stream().map(String::length) and names.stream().map(s -> s.length()) behave the same, but the first is checked more strictly: the compiler must find exactly one method whose parameters match the interface's, so argument order and arity cannot silently drift.
To use them well you need more than the syntax table. This page explains how the compiler resolves a reference against its target type, what bytecode it emits and what the JVM does at the first call, when the receiver expression is evaluated, why method references help generic inference where lambdas fail, and the production bugs that come from treating this::onEvent as a stable identity. It assumes you know lambdas; if not, read lambda expressions in Java first.
The four kinds and their variants
| Kind | Example | Equivalent lambda | Receiver comes from |
|---|---|---|---|
| Static | Integer::parseInt | s -> Integer.parseInt(s) | No receiver |
| Bound instance | log::info | m -> log.info(m) | Evaluated when the reference is created |
| Unbound instance | String::toUpperCase | s -> s.toUpperCase() | First interface argument |
| Constructor | ArrayList::new | () -> new ArrayList<>() | A new object |
There are a few variants of these. this::validate and super::toString are bound references whose receiver is the current object, and the super form calls the superclass implementation. int[]::new is an array constructor reference, so IntFunction<int[]> receives the length; it is what stream.toArray(String[]::new) uses. Generic arguments can be written explicitly, as in ArrayList<String>::new or Collections::<String>emptyList, but inference almost always makes that unnecessary.
Bound versus unbound is the distinction that matters most. In log::info, log is fixed now and every call goes to the same logger. In String::toUpperCase, no string exists yet; the first argument of the functional method becomes the receiver. That is why the same text String::compareTo fits Comparator<String>: two parameters, the first becomes the receiver and the second the argument.
The four kinds and their variants
| Kind | Example | Equivalent lambda | Receiver comes from |
|---|---|---|---|
| Static | Integer::parseInt | s -> Integer.parseInt(s) | No receiver |
| Bound instance | log::info | m -> log.info(m) | Evaluated when the reference is created |
| Unbound instance | String::toUpperCase | s -> s.toUpperCase() | First interface argument |
| Constructor | ArrayList::new | () -> new ArrayList<>() | A new object |
There are a few variants of these. this::validate and super::toString are bound references whose receiver is the current object, and the super form calls the superclass implementation. int[]::new is an array constructor reference, so IntFunction<int[]> receives the length; it is what stream.toArray(String[]::new) uses. Generic arguments can be written explicitly, as in ArrayList<String>::new or Collections::<String>emptyList, but inference almost always makes that unnecessary.
Bound versus unbound is the distinction that matters most. In log::info, log is fixed now and every call goes to the same logger. In String::toUpperCase, no string exists yet; the first argument of the functional method becomes the receiver. That is why the same text String::compareTo fits Comparator<String>: two parameters, the first becomes the receiver and the second the argument.
How the compiler resolves a reference
A method reference has no type of its own. Like a lambda, it is a poly expression and needs a target type: an assignment, a method argument or a cast whose type is a functional interface. The compiler takes the interface's single abstract method, its function type, and searches for a method that could be invoked with those parameter types. For ClassName::method forms it runs two searches, as specified in JLS section 15.13.1: one treating all parameters as arguments to a static method, and one treating the first parameter as the receiver of an instance method. Exactly one must succeed.
Function<String, Integer> parse = Integer::parseInt; // static int parseInt(String)
BiFunction<String, String, Boolean> eq = String::equals; // unbound: s1.equals(s2)
Supplier<List<String>> make = ArrayList::new; // ArrayList()
Function<Integer, List<String>> sized = ArrayList::new; // ArrayList(int initialCapacity)
Function<Integer, String> bad = Integer::toString;
// error: reference to toString is ambiguous:
// static toString(int) matches via the first search, instance toString() via the secondThe last line is the classic ambiguity. Integer::toString could mean the static Integer.toString(int) applied to the argument or the instance toString() called on it. Both searches succeed, so the compiler rejects the reference; write the lambda i -> i.toString() or use String::valueOf. Note also that ArrayList::new resolves to a different constructor depending on the target type. Resolution is entirely compile-time; a reference that compiles will never fail to find its method at run time unless the classpath changes underneath it.
What javac emits and what the JVM does
javac compiles both lambdas and method references to an invokedynamic instruction whose bootstrap method is LambdaMetafactory.metafactory. The static arguments include the functional interface's method type and a MethodHandle for the implementation. For a lambda, that handle points to a synthetic private method such as lambda$main$0 that javac generates from the lambda body. For most method references there is no synthetic method; the handle points straight at the target, with a kind that matches the reference: REF_invokeStatic, REF_invokeVirtual, REF_invokeInterface or REF_newInvokeSpecial for constructors. A few shapes, such as array constructors, varargs adaptation and some super or protected-access cases, are still routed through a synthetic method.
$ javap -c -p Demo.class
invokedynamic #7, 0 // InvokeDynamic #0:apply:()Ljava/util/function/Function;
BootstrapMethods:
0: LambdaMetafactory.metafactory
Method arguments:
(Ljava/lang/Object;)Ljava/lang/Object;
REF_invokeVirtual java/lang/String.length:()I <- the target itself, no lambda$ method
(Ljava/lang/String;)Ljava/lang/Integer;The first time the instruction executes, the bootstrap spins a hidden class implementing the interface and links the call site. Non-capturing references, static and unbound ones, usually link to a call site that returns one shared instance. Bound references capture the receiver, so each evaluation allocates a new object holding it, unless escape analysis removes the allocation. After linking, the JIT inlines through the call site, so steady-state cost matches the lambda and usually a direct call. The relationship with method handles is direct: a method reference is a method handle wrapped in an interface.
Evaluation: the receiver is captured eagerly
The receiver expression of a bound reference is evaluated once, when the reference expression is evaluated, not when the function is called. Three consequences follow.
Runnable r = maybeNull::run; // NullPointerException HERE (javac inserts Objects.requireNonNull)
Runnable l = () -> maybeNull.run(); // no exception until l.run(), and only if still null
Supplier<Order> s = repo()::latest; // repo() runs once, now
Supplier<Order> t = () -> repo().latest(); // repo() runs on every get()
button.addListener(this::onClick);
button.removeListener(this::onClick); // removes nothing: a second evaluation, a different objectThe null check fires early, which is usually what you want but surprises code that builds handlers before dependencies are injected. Side effects in the receiver expression run once, which matters for factories and non-idempotent getters. And identity is not guaranteed: the JLS does not promise that two evaluations of the same reference produce the same or equal objects. For listeners, store the reference in a field and pass that same object to both add and remove.
Why references help generic inference
Method references sometimes compile where the equivalent lambda does not. An implicitly typed lambda such as p -> p.getAge() has unknown parameter types until inference finishes, and a chained call cannot supply them, so Comparator.comparing(p -> p.getAge()).reversed() fails: p is inferred as Object. Comparator.comparing(Person::getAge).reversed() compiles. Person::getAge is an exact method reference, one with a single non-overloaded, non-generic target, so the compiler can use its parameter and return types during inference. The fixes for the lambda version are an explicit parameter type (Person p) -> p.getAge() or a type witness, and the method reference is the cleanest of the three. Overloaded targets are not exact, which is why references to heavily overloaded methods such as println occasionally need help in generic contexts.
Designing APIs that accept references
References are only as useful as the APIs that accept them, so it pays to design for them. Accept the standard interfaces from java.util.function (Function, Predicate, Supplier, Consumer and their primitive specialisations such as ToIntFunction) rather than inventing near-duplicates, because a caller's existing method then fits without an adapter. Use wildcards in parameters, Function<? super T, ? extends R>, so that a caller's existing Function<Object, String> can be passed where orders are being formatted. Avoid overloading a method on two functional interfaces with the same arity, such as submit(Callable) beside submit(Runnable): inexact references and implicit lambdas can then become ambiguous at the call site, and every caller pays with a cast.
The JDK's own combinators show the payoff. Predicate.not(String::isBlank), added in Java 11, negates a reference without a lambda. Comparator.comparing(Order::customer).thenComparing(Order::placedAt) builds a multi-key sort from two getters. Collectors.groupingBy(Order::region, Collectors.counting()) reads as a sentence. Function.identity() and x -> x are interchangeable in behaviour; the former is simply easier to search for. In each case the reference names domain operations already on your types, which is the strongest argument for writing small, well-named methods in the first place.
Worked example: refactoring an order report
Here is an order report before refactoring, followed by a version that uses references where they fit and keeps lambdas where they do not.
// Before
List<String> lines = orders.stream()
.filter(o -> o.isPaid())
.filter(o -> o.total().compareTo(MIN) >= 0)
.sorted((a, b) -> b.placedAt().compareTo(a.placedAt()))
.map(o -> formatter.format(o))
.collect(Collectors.toList());
lines.forEach(l -> System.out.println(l));
// After
List<String> lines = orders.stream()
.filter(Order::isPaid) // unbound: o.isPaid()
.filter(o -> o.total().compareTo(MIN) >= 0) // stays a lambda: uses MIN
.sorted(Comparator.comparing(Order::placedAt).reversed())
.map(formatter::format) // bound: formatter fixed now
.toList();
lines.forEach(System.out::println); // bound: System.out evaluated nowEach change is a pure pass-through: the parameters go to one existing method, unchanged and in order. The threshold filter stays a lambda because it combines a call with a constant and an operator; extracting it to static boolean aboveMinimum(Order o) and referencing Order::aboveMinimum is worthwhile only if the predicate has a name worth reusing. The comparator gains readability and relies on the exact-reference inference above. One subtle change: formatter::format captures the formatter when the pipeline is built, so if formatter is a field reassigned while the pipeline is running, the reference keeps the old object while the lambda would read the field per element. For more on the stream operations used here, see Java stream operations.
Failure modes
- Listener removal fails silently. Two evaluations of
this::onClickare different objects. Keep one reference in a field. - Early NullPointerException. A bound receiver that is null fails at creation, often during wiring. Use a lambda if the receiver is legitimately set later.
- Stale receiver. The bound object is captured once; reassigning the field later has no effect.
- Overload ambiguity.
Integer::toStringand similar fail to compile; adding an overload to a library can break callers' method references, a source-compatibility hazard for API authors. - Checked exceptions.
Files::readStringthrowsIOException, so it cannot be aFunction. Wrap it in a lambda or define a throwing functional interface. - Debugging confusion. Breakpoints and profilers attribute time to the target method, not the call site, so a hot
Order::totalinside a stream may look like a slow getter rather than a pipeline invoked millions of times. Read the caller frames before optimising the method itself. - Serialization. A method reference is serializable only when its target type is, as in a cast to
Runnable & Serializable, and the serialized form depends on compiler details. Avoid serializing them across versions.
Performance, operations and trade-offs
Operationally, method references have three visible effects. Startup: each call site links on first use, which adds a little work for code with thousands of sites; class data sharing archives and AOT caches in recent JDKs reduce it. Stack traces: a method reference usually has no lambda$ frame, so traces show the target method directly, while a lambda adds a synthetic frame with a line number inside the lambda body; both show a frame from the generated hidden class. Allocation: bound references in hot loops allocate per evaluation, so hoist them out of the loop when a profiler shows pressure.
| Situation | Prefer | Why |
|---|---|---|
| Body is one existing method, arguments unchanged | Method reference | Exact mapping, compiler-checked |
| Needs a constant, reordering or two calls | Lambda | A reference cannot express it |
| Receiver may be null or set later | Lambda | Evaluated at call time |
| Chained comparator or generic inference | Method reference | Exact references feed inference |
| Logic deserves a name | Named method plus reference | Readable and testable |
Collection pipelines are where most method references live; the Streams API guide covers the collectors they feed.
What to do next
- Learn the four kinds and say which one each reference in your code is.
- Replace pass-through lambdas with references; leave everything else as lambdas.
- Keep a single stored reference for anything you must later remove or compare.
- Check bound receivers for null and for reassignment after capture.
- Use
Comparator.comparing(Type::getter)chains instead of hand-written comparators. - Run
javap -c -pon one class to see theinvokedynamicsites for yourself. - As an API author, think twice before adding overloads callers may reference.