Method reference in Java 8
-
Last Updated: August 30, 2023
-
By: javahandson
-
Series

Method reference in Java lets you reuse an existing method wherever a lambda would go. Learn the :: operator, all four reference types, and when to pick each one.
You write a lambda. Then you look at it and realise it does nothing new. It just calls a method that already exists somewhere in your code.
That happens more often than you would think. A lambda that only forwards its arguments to another method is pure ceremony. It adds a line, adds a pair of brackets, and adds nothing else.
Java 8 gave us a shortcut for exactly that case. The double colon operator, written as ::, points straight at an existing method. No wrapper, no forwarding, no duplicated logic.
Think of it like giving someone a phone number instead of relaying the call yourself. You are not doing the work. You hand over a way to reach whoever already does it.
This shortcut shows up everywhere in modern Java. Stream pipelines lean on it heavily, and so do sorting, factories, and collectors. Once you can read it, a lot of real-world Java suddenly gets shorter and clearer.
We start from a plain lambda and shrink it step by step. From there we open up every form the :: operator can take. Here is the plan:
A method reference is a compact way to pass an existing method as behaviour. Instead of describing the work again inside a lambda, you name the method that already does it.
Say we have a small class with an addition method that adds two integers.
package com.javahandson.method.reference;
public class Test {
public static void addition(int a, int b) {
System.out.println(a + b);
}
}Now we need a functional interface with a matching shape. Its single method also takes two integers.
@FunctionalInterface
interface Operation {
void add(int a, int b);
}The obvious move is a lambda. So we write one that adds the two numbers and prints the total.
public class Demo {
public static void main(String[] args) {
Operation operation = (int a, int b) -> System.out.println(a + b);
operation.add(10, 15); // Output: 25
}
}Look closely and something feels off. Our lambda body repeats what Test.addition already does, character for character. We copied logic that had a perfectly good home.
So why write the logic twice? Point at the existing method instead.
public class Demo {
public static void main(String[] args) {
Operation operation = Test::addition;
operation.add(10, 15); // Output: 25
}
}Same result, one clean line. The add method of Operation now routes straight to the addition method of Test. That routing is what the name “method reference” describes.
Notice the missing brackets after addition. We are not calling the method here. We hand over a way to call it later, and the interface decides when that happens.
Every method reference has the same shape. A target sits on the left of the ::, and a method name sits on the right.
Target :: methodName ClassName :: staticMethod // static instanceName :: instanceMethod // bound to one object ClassName :: instanceMethod // unbound, any object ClassName :: new // constructor
The target tells Java where to look. A class name sends it to the class, while a variable sends it to that specific object.
Three rules cover the whole syntax:
A method reference is not a new kind of value. It produces an instance of a functional interface, exactly like a lambda does.
That means every rule you already know still applies. The target type must have exactly one abstract method, and that method decides which references fit.
You can spot this in the code above. Operation holds just one abstract method, so the line compiles. Add a second one and the same line breaks.
Shaky on that idea? Start with Custom Functional Interface in Java. The :: form makes far more sense once that piece clicks.
Many tutorials stop at two types. Java actually defines four, and the two instance forms behave very differently.
Here are the four, in the order we will cover them:
Here is the syntax for each:
ClassName :: staticMethod // 1. static instanceName :: instanceMethod // 2. bound ClassName :: instanceMethod // 3. unbound ClassName :: new // 4. constructor
Types 2 and 3 look almost alike on the page. One names an object, the other names a class. That single difference changes how arguments flow.
This one is the easiest. You point at a static method through its class name.
import java.util.function.Function;
public class StaticRefDemo {
public static void main(String[] args) {
Function<String, Integer> parser = Integer::parseInt;
int value = parser.apply("42");
System.out.println(value + 8); // Output: 50
}
}The parseInt method takes a String and gives back an int. Function wants exactly that shape, so the reference fits without any glue.
Our earlier addition example belongs here too, since it carries the static keyword. A class name on the left is your clue.
Sometimes you already hold the object you want to call. Put that variable on the left of the :: and the reference locks onto it.
import java.util.function.Supplier;
public class BoundRefDemo {
public static void main(String[] args) {
String greeting = "Hello Java";
Supplier<String> shout = greeting::toUpperCase;
System.out.println(shout.get()); // Output: HELLO JAVA
}
}The word “bound” says it plainly. This reference ties itself to the greeting object and never touches another one.
Count the inputs and the pattern jumps out. Supplier takes none at all, since the object is already fixed. Nothing is left for the caller to pass.
Your old Test example works the same way once addition drops the static keyword.
package com.javahandson.method.reference;
public class Test {
void addition(int a, int b) {
System.out.println(a + b);
}
}
class Demo {
public static void main(String[] args) {
Test test = new Test();
Operation operation = test::addition;
operation.add(10, 15); // Output: 25
}
}Because addition now belongs to an object, we create that object first. Then the variable name test goes on the left instead of the class name.
Here is the form that confuses people. You write a class name on the left, yet the method on the right is an instance method.
import java.util.function.Function;
public class UnboundRefDemo {
public static void main(String[] args) {
Function<String, Integer> lengthOf = String::length;
System.out.println(lengthOf.apply("javahandson")); // Output: 11
}
}The length method takes no inputs, so why does Function pass one? Because that first input becomes the object. Java quietly moves it to the left of the dot:
lengthOf.apply("javahandson") // what you write
"javahandson".length() // what Java callsSo the rule is simple. Input one supplies the object, and the rest go to the method itself.
import java.util.function.BiPredicate;
public class UnboundTwoArgs {
public static void main(String[] args) {
// "javahandson".startsWith("java")
BiPredicate<String, String> startsWith = String::startsWith;
System.out.println(startsWith.test("javahandson", "java")); // Output: true
}
}Read that comment carefully. The first input slides left of the dot, and the second stays inside the brackets. Every unbound reference follows this shift.
Constructors get a reference form too. Write new on the right of the ::, and you have a factory.
import java.util.ArrayList;
import java.util.function.Supplier;
public class ConstructorRefDemo {
public static void main(String[] args) {
Supplier<ArrayList<String>> maker = ArrayList::new;
ArrayList<String> names = maker.get();
names.add("Ravi");
System.out.println(names); // Output: [Ravi]
}
}Constructors with parameters work just as smoothly. Pick a functional interface whose arguments match the constructor you want.
import java.util.function.Function;
class Employee {
private final String name;
Employee(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public class EmployeeFactory {
public static void main(String[] args) {
Function<String, Employee> factory = Employee::new;
Employee ravi = factory.apply("Ravi");
System.out.println(ravi.getName()); // Output: Ravi
}
}Which constructor runs? Java picks the one matching the interface arguments. A single String argument selects the single String constructor.
| Type | Written as | Example | Equivalent lambda |
|---|---|---|---|
| Static | Class::staticMethod | Integer::parseInt | s -> Integer.parseInt(s) |
| Bound instance | object::instanceMethod | greeting::toUpperCase | () -> greeting.toUpperCase() |
| Unbound instance | Class::instanceMethod | String::length | s -> s.length() |
| Constructor | Class::new | ArrayList::new | () -> new ArrayList() |
Keep the last column handy. Whenever a reference puzzles you, expand it back into its lambda and the argument flow becomes visible again.
The compiler checks a short list before it accepts your reference. Some parts must match exactly, and others enjoy a little slack.
This rule has no flexibility. The referenced method must accept the arguments the interface hands over.
public class Test {
public static int addition(int a, int b) {
return a + b;
}
}
@FunctionalInterface
interface Operation {
int add(int a, int b, int c); // three arguments
}
class Demo {
public static void main(String[] args) {
Operation operation = Test::addition; // compile error
System.out.println(operation.add(10, 15, 20));
}
}
/*
java: incompatible types: invalid method reference
method addition in class Test cannot be applied to given types
required: int,int
found: int,int,int
reason: actual and formal argument lists differ in length
*/Operation promises three integers while addition accepts two. Java refuses at compile time, which beats discovering the mismatch in production.
Return types get gentler treatment. A narrower primitive can flow into a wider one through normal widening conversion.
public class Test {
public static int addition(int a, int b) {
return a + b; // returns int
}
}
@FunctionalInterface
interface Operation {
float add(int a, int b); // wants float
}
class Demo {
public static void main(String[] args) {
Operation operation = Test::addition;
System.out.println(operation.add(10, 15)); // Output: 25.0
}
}An int widens to a float without complaint, so the reference compiles. Note the println wrapper. Without it nothing reaches the console, because addition returns a value rather than printing one.
Turn the rule around and it fails. A method returning float cannot satisfy an interface that promises int, since that direction loses data.
Here is a handy asymmetry. A method that returns something can satisfy a void abstract method, and Java simply discards the result.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
public class VoidSlotDemo {
public static void main(String[] args) {
List<String> names = new ArrayList<>();
// List.add returns boolean, Consumer wants void
Consumer<String> adder = names::add;
adder.accept("Ravi");
adder.accept("Anjali");
System.out.println(names); // Output: [Ravi, Anjali]
}
}List.add hands back a boolean, yet Consumer declares void accept. The boolean quietly disappears and the code still compiles.
Try the reverse and you hit a wall. A void method can never fill a slot that demands a value, because there is nothing to hand back.
The modifier on the referenced method does not have to match the interface. A private method can back a public abstract method without any trouble.
@FunctionalInterface
interface Operation {
float add(int a, int b); // implicitly public
}
public class Demo {
private static int addition(int a, int b) { // private
return a + b;
}
public static void main(String[] args) {
Operation operation = Demo::addition;
System.out.println(operation.add(10, 15)); // Output: 25.0
}
}One catch deserves attention. Ordinary visibility rules still bind you, so this compiles only because main sits inside Demo. Move that reference to another class and the private method disappears from view.
Textbook examples are fine, but streams are where you will meet the :: operator daily. A few patterns cover most real code.
This is the reference every Java developer types first.
import java.util.Arrays;
import java.util.List;
public class PrintDemo {
public static void main(String[] args) {
List<String> cities = Arrays.asList("Pune", "Delhi", "Kochi");
cities.forEach(System.out::println);
}
}
/*
Pune
Delhi
Kochi
*/Which of the four types is this? Here System.out names an object, not a class. So we have a bound reference, and every item goes to that one stream.
Mapping is the natural home for unbound references. Each item arrives and takes its turn as the object.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MapDemo {
public static void main(String[] args) {
List<String> cities = Arrays.asList("Pune", "Delhi", "Kochi");
List<String> loud = cities.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(loud); // Output: [PUNE, DELHI, KOCHI]
}
}Compare the two styles side by side. The lambda form says the same thing with more noise, while the reference states the intent and stops.
Sorting needs a key, and a reference names that key in one short phrase.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class SortDemo {
public static void main(String[] args) {
List<String> cities = Arrays.asList("Pune", "Delhi", "Kochi");
cities.sort(Comparator.comparing(String::length));
System.out.println(cities); // Output: [Pune, Delhi, Kochi]
cities.sort(String::compareToIgnoreCase);
System.out.println(cities); // Output: [Delhi, Kochi, Pune]
}
}The second sort hides a neat trick. Comparator needs two inputs, yet the compare method takes just one. Unbound rules save the day here, since input one becomes the object and input two goes inside.
The :: form is not always the better one. Reach for it when the code truly reads more clearly.
| Aspect | Lambda expression | Method reference |
|---|---|---|
| Shape | Parameters, arrow, body | Target, ::, method name |
| Logic | You write it inline | It already lives elsewhere |
| Extra logic | Negation, maths, branching | Not allowed |
| Argument order | You choose freely | Must pass straight through |
| Reuse | Copy the body around | One method, many call sites |
| Best for | Anything with real logic | Plain forwarding calls |
Notice that both columns end up at the same place. Each one builds an instance of the interface, so you choose on style, not on power.
Why bother learning a second syntax? Four payoffs make it worthwhile:
That last point carries the most weight. Copied lambda bodies drift apart over time, and a shared method cannot.
These six trip up almost everyone at least once. Learn them here rather than during a code review.
Your fingers add brackets out of habit. Resist them.
Function<String, Integer> wrong = Integer::parseInt(); // compile error Function<String, Integer> right = Integer::parseInt; // correct
Brackets mean “run this now”. A method reference means “here is how to run it later”, so the two ideas cannot mix.
A bound reference evaluates its target immediately, not at call time. That timing surprises people.
String text = null; Supplier<String> shout = text::toUpperCase; // NullPointerException right here System.out.println(shout.get()); // never reaches this line
Java grabs the object the moment you build the reference. So a null target blows up on that very line, long before anyone calls it.
The two instance forms look alike but act very differently. Count the inputs to tell them apart.
String greeting = "Hello";
Supplier<String> bound = greeting::toUpperCase; // zero arguments
Function<String, String> unbound = String::toUpperCase; // one argument
System.out.println(bound.get()); // Output: HELLO
System.out.println(unbound.apply("world")); // Output: WORLDA small letter on the left usually means a variable, so the reference is bound. A capital means a class name, so the caller must pass the object.
We saw this in section 4.1. It earns a second mention, because the error text throws beginners off. Look for the words “invalid method reference”, then the required and found lines below it.
Those two lines tell you everything. Compare the counts, fix whichever side is wrong, and move on.
Give one class both a static and an instance method that share a name, and Java can no longer tell which one you mean.
class Util {
static String clean(String s) { return s.trim(); }
String clean() { return "instance"; }
}
// Util::clean -> compile error: reference to clean is ambiguousBoth readings fit, so the compiler stops and asks. Rename one method and the problem goes away.
Some lambdas should stay as they are. They carry real logic, and forcing them into :: form only hurts.
// Fine as a lambda, because of the negation .filter(name -> !name.isEmpty()) // Fine as a lambda, because of the extra maths .map(price -> price * 1.18)
Neither line has an equivalent reference. Keep the lambda and let the code stay honest.
Let us pull the ideas together in one small program. We will clean a messy list of usernames.
Real data is rarely tidy. Ours has stray spaces, mixed case, and a couple of blank rows.
The job breaks into four steps:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class UserReport {
public static void main(String[] args) {
List<String> raw = Arrays.asList(" ravi ", "ANJALI", "", " meera", "kabir ", " ");
List<String> clean = raw.stream()
.map(String::trim) // unbound instance
.filter(name -> !name.isEmpty()) // lambda: negation
.map(String::toUpperCase) // unbound instance
.sorted(String::compareTo) // unbound instance
.collect(Collectors.toList());
clean.forEach(System.out::println); // bound instance
}
}
/*
ANJALI
KABIR
MEERA
RAVI
*/Four of the five steps use a method reference. Walk through what each one does:
Notice how the chain reads almost like the four-step plan we wrote above. That closeness between plan and code is the real payoff.
One honest note sits in the middle. Mixing lambdas and short forms in one chain is fine, so do not fight for a pure pipeline.
[IMAGE PLACEHOLDER: diagram showing the four method reference types, with arrows from each :: form to its equivalent lambda]
A: A method reference is a shorthand for a lambda that does nothing but call one existing method. You write a target, then ::, then the method name. It creates a functional interface instance exactly as a lambda would, so Integer::parseInt and s -> Integer.parseInt(s) mean the same thing.
A: Four. The static form points at a class, the bound form points at one object, the unbound form names a class but calls an instance method, and the constructor form ends in new. Many tutorials list only two, because they merge the bound and unbound forms. Those two behave very differently, so it pays to keep them apart.
A: A bound reference names a specific object, so that object always receives the call and the interface passes no extra argument for it. An unbound reference names a class, so the first argument at call time becomes the receiver. Compare greeting::toUpperCase, which needs zero arguments, with String::toUpperCase, which needs one.
A: Yes, as long as the conversion widens. A method returning int satisfies an interface method returning float, because int widens to float. A method returning a value can also satisfy a void abstract method, and Java discards the result. The reverse never works, so a void method cannot fill a slot that expects a value.
A: No. The referenced method must accept what the interface hands over, in the same order. A mismatch produces a compile error reading “invalid method reference”, with required and found lines showing both signatures. Remember that an unbound reference spends its first argument on the receiver.
A: Brackets would invoke the method immediately. A method reference only points at the method so the functional interface can invoke it later, which is why Integer::parseInt compiles and Integer::parseInt() does not.
A: A constructor reference uses the keyword new on the right of the ::, as in ArrayList::new. It turns a constructor into a factory you can pass around. Java picks whichever constructor matches the argument list of the functional interface, so Function<String, Employee> selects the single String constructor.
A: Keep the lambda whenever the body does more than forward a call. Negation such as name -> !name.isEmpty(), extra arithmetic, reordered arguments, and multi-statement bodies all rule out a reference. Readability decides the rest, so pick whichever version a teammate grasps faster.
A: Java throws a NullPointerException at the line that creates the reference, not at the line that calls it. The receiver expression evaluates eagerly when you build the reference, so text::toUpperCase fails on the assignment itself when text holds null.
A: Treat them as equal in speed. Both compile down to invokedynamic and both create a functional interface instance at runtime. A method reference occasionally skips one synthetic wrapper method, but that difference never shows up in real benchmarks, so choose on clarity alone.
Let us wrap up what we covered. A method reference replaces a lambda whose whole job is calling one method that already exists.
The syntax puts a target on the left of the :: and a method name on the right. Brackets never follow that name, because you are pointing rather than calling.
Four forms cover every case. Static references name a class, bound references name an object, unbound references name a class but call an instance method, and constructor references end in new.
Arguments must line up exactly, while return types enjoy some slack. An int widens into a float, and a returned value can fill a void slot.
One last tip to carry with you. Expand any confusing reference back into its lambda. The moment you see where each argument lands, the :: form stops looking cryptic and starts looking obvious.