A Java method reference is a short form of a lambda expression that only calls one existing method, so instead of s -> s.toUpperCase() we write String::toUpperCase. We write the class or the object before the :: operator and the method name after it, without parentheses and without arguments. Java 8 added method references.
We use method references in stream pipelines and comparators, wherever a lambda only passes its arguments on. For example, a to-do app that prints every task title can write forEach(System.out::println) instead of forEach(t -> System.out.println(t)).
The following example shows the four kinds of method references, each next to the lambda it replaces.
BinaryOperator<Integer> maxLambda = (a, b) -> Math.max(a, b);
BinaryOperator<Integer> maxRef = Math::max; // 1. static method
int bigger = maxRef.apply(3, 7); // bigger = 7
String str = "hello";
Supplier<Integer> lengthLambda = () -> str.length();
Supplier<Integer> lengthRef = str::length; // 2. instance method of one object (bound)
int len = lengthRef.get(); // len = 5
Function<String, String> upperLambda = s -> s.toUpperCase();
Function<String, String> upperRef = String::toUpperCase; // 3. instance method of any object (unbound)
String upper = upperRef.apply("hello"); // upper = "HELLO"
Supplier<List<String>> listLambda = () -> new ArrayList<>();
Supplier<List<String>> listRef = ArrayList::new; // 4. constructor
List<String> empty = listRef.get(); // empty = []
Notice that each lambda and method reference pair does the same work, and only the syntax differs.
Next, we see when to prefer a method reference over a lambda expression and use each kind in stream code, including int[]::new, this::method and super::method. The FAQs explain the “reference to toString is ambiguous” compile error and why title::length throws a NullPointerException before the method ever runs.
1. What Is a Method Reference in Java?
Many lambdas do nothing except pass their arguments to one method, as in name -> System.out.println(name). A method reference names that method, so the parameter list and the arrow disappear and the code reads as “print each name”.
List<String> names = List.of("lokesh", "alex");
names.forEach(name -> System.out.println(name)); // lambda
names.forEach(System.out::println); // method reference, same output
A method reference does not call the method when Java evaluates the expression. The reference System.out::println prints nothing by itself, because the method runs later, when the code calls the single method of the functional interface, such as accept() or apply().
1.1. The Functional Interface Is the Target Type
Like a lambda, a method reference needs a target type, and the target type is always a functional interface (an interface with one abstract method). The common ones in java.util.function are Function, Supplier, Consumer, Predicate and BinaryOperator, and Comparator also qualifies.
The compiler matches the referenced method against that one abstract method. For Function<String, String>, the method must accept a String and return a String, which toUpperCase() does when it runs on the input string. When there is no target type, as with var, the code does not compile.
Function<String, String> toUpper = String::toUpperCase; // OK, apply() calls toUpperCase()
String shout = toUpper.apply("hello"); // "HELLO"
// does not compile: var gives the method reference no target type
var upper = String::toUpperCase;
Var.java:3: error: cannot infer type for local variable upper
var upper = String::toUpperCase;
^
(method reference needs an explicit target-type)
1 error
1.2. Four Kinds of Method References
The four kinds differ in what we write before ::, and that part decides where the lambda arguments go. Each kind replaces one lambda shape and fits a matching functional interface.
| Kind | Syntax | Example | Same lambda | Functional interface |
|---|---|---|---|---|
| Static method | ClassName::staticMethod | Math::max | (a, b) -> Math.max(a, b) | BinaryOperator<Integer> |
| Instance method of a particular object (bound) | object::method | str::length | () -> str.length() | Supplier<Integer> |
| Instance method of an arbitrary object (unbound) | ClassName::instanceMethod | String::toUpperCase | s -> s.toUpperCase() | Function<String, String> |
| Constructor | ClassName::new | ArrayList::new | () -> new ArrayList<>() | Supplier<List<String>> |
The unbound kind is the one that confuses most developers. The reference String::toUpperCase names a class and no object, so Java takes the first argument of the lambda and calls toUpperCase() on it.
Each kind of method reference replaces one lambda shape. For an unbound reference such as String::toUpperCase, the first argument becomes the object.1.3. When to Use a Method Reference Instead of a Lambda
We use a method reference when the lambda only passes its arguments to one method, in the same order, and we keep the lambda when it does anything more. A method reference cannot add a fixed argument, call a second method or negate a result.

| Lambda | Method reference? | Reason |
|---|---|---|
| name -> System.out.println(name) | Yes, System.out::println | Passes the argument on |
| t -> t.title() | Yes, Task::title | Calls one getter |
| (a, b) -> a.compareToIgnoreCase(b) | Yes, String::compareToIgnoreCase | First argument is the object |
| n -> n.startsWith(“a”) | No | Adds the fixed argument “a” |
| n -> n.substring(0, 1).toUpperCase() | No | Calls two methods |
| s -> !s.isEmpty() | Yes, with Predicate.not(String::isEmpty) | Predicate.not() negates the result |
List<String> names = List.of("lokesh", "alex", "brian");
List<String> initials = names.stream().map(n -> n.substring(0, 1).toUpperCase()).toList(); // [L, A, B], keep the lambda
List<Boolean> startsWithA = names.stream().map(n -> n.startsWith("a")).toList(); // [false, true, false], keep the lambda
List<String> nonEmpty = Stream.of("a", "", "b").filter(Predicate.not(String::isEmpty)).toList(); // [a, b]
List<String> byLength = names.stream().sorted(Comparator.comparing(String::length)).toList(); // [alex, brian, lokesh]
When both forms are possible, we pick the one that reads better. The reference Task::title is clearer than t -> t.title(), whereas a long name such as ReportFormatter::formatWithDefaultLocale can be harder to scan than a short lambda. Speed is not a reason to pick either form, as FAQ 3.3 shows.
2. Method Reference Examples
A to-do app is a good fit for method references, because it sorts and filters tasks all the time. Our examples use a small Task record (a compact data class that gets its constructor and accessor methods from the compiler). A task has a title and an estimate in minutes, and the extra one-argument constructor gives every new task a default estimate of 15 minutes.
record Task(String title, int minutes) {
Task(String title) {
this(title, 15); // default estimate
}
boolean isLong() {
return minutes > 30;
}
static int byMinutes(Task a, Task b) {
return Integer.compare(a.minutes(), b.minutes());
}
}
static List<Task> tasks = List.of(new Task("email", 10), new Task("report", 45), new Task("review", 30));
2.1. Reference to a Static Method
A static method reference has the form ClassName::staticMethod, and Java passes all lambda arguments to the static method. The reference Task::byMinutes takes two tasks and returns an int, so the reference fits Comparator<Task> and we can pass it to sorted().
List<String> sorted = tasks.stream().sorted(Task::byMinutes).map(Task::title).toList(); // [email, review, report]
int total = tasks.stream().map(Task::minutes).reduce(0, Integer::sum); // 85
Optional<Integer> max = Stream.of(1, 12, 433, 5).reduce(Math::max); // Optional[433]
The references Integer::sum and Math::max both fit BinaryOperator<Integer>, which takes two values of one type and returns a value of the same type. That shape is what reduce() expects, so both methods plug in without a lambda.
2.2. Reference to an Instance Method of a Particular Object
A bound method reference has the form object::method. Java evaluates the object once, when it creates the reference, and every later call runs the method on that same object.
tasks.stream().map(Task::title).forEach(System.out::println);
// email
// report
// review
String str = "hello";
Supplier<Integer> length = str::length; // the object is str
int size = length.get(); // size = 5
The reference System.out::println is the bound reference we see most often, because System.out is a PrintStream object and println() is one of its instance methods. The forEach() method passes each title to println(), so the program prints one title per line.
2.3. Reference to an Instance Method of an Arbitrary Object
An unbound method reference has the form ClassName::instanceMethod. The first lambda argument becomes the object that the method runs on, and any remaining arguments go to the method as parameters.
List<String> titles = tasks.stream().map(Task::title).toList(); // [email, report, review]
List<Task> longTasks = tasks.stream().filter(Task::isLong).toList(); // [Task[title=report, minutes=45]]
BiFunction<String, String, Boolean> same = String::equalsIgnoreCase; // (a, b) -> a.equalsIgnoreCase(b)
boolean equal = same.apply("Java", "JAVA"); // equal = true
List<String> names = new ArrayList<>(List.of("brian", "Alex", "lokesh"));
names.sort(String::compareToIgnoreCase); // [Alex, brian, lokesh]
The method map() passes each Task as the only argument, so Task::title calls task.title(), and filter() does the same with Task::isLong. A Comparator passes two strings, so String::compareToIgnoreCase calls a.compareToIgnoreCase(b) and the list ends up sorted without regard to case.
2.4. Reference to a Constructor
A constructor reference has the form ClassName::new, and Java passes the lambda arguments to the constructor. When a class has several constructors, the compiler picks the one whose parameters match the functional interface method.
Function<String, Task> oneArg = Task::new; // new Task(title)
BiFunction<String, Integer, Task> twoArgs = Task::new; // new Task(title, minutes)
Task email = oneArg.apply("email"); // Task[title=email, minutes=15]
Task report = twoArgs.apply("report", 45); // Task[title=report, minutes=45]
List<Task> newTasks = List.of("email", "call").stream().map(Task::new).toList();
// [Task[title=email, minutes=15], Task[title=call, minutes=15]]
ArrayList<String> list = tasks.stream().map(Task::title)
.collect(Collectors.toCollection(ArrayList::new)); // [email, report, review]
The same text Task::new calls a different constructor in each assignment. A Function passes one argument, so Java uses Task(String), whereas a BiFunction passes two, so Java uses Task(String, int). The collector Collectors.toCollection(ArrayList::new) always returns an ArrayList we can modify, which is the main difference from toList().
2.5. Array Constructor References With int[]::new
An array constructor reference has the form Type[]::new, and its only argument is the array length. The matching interface is IntFunction<int[]>, because the function takes an int and returns a new array of that size.
IntFunction<int[]> intArrayLambda = size -> new int[size];
IntFunction<int[]> intArrayRef = int[]::new;
int arrayLength = intArrayRef.apply(3).length; // arrayLength = 3
String[] titles = Stream.of("email", "report").toArray(String[]::new); // [email, report]
String[] copy = List.of("a", "b").toArray(String[]::new); // [a, b]
The most common use is Stream.toArray(String[]::new), because toArray() without an argument returns Object[], and we cannot cast that to String[]. Java 11 added Collection.toArray(IntFunction), so lists and sets accept the same reference. The same reference works when converting a stream to an array of any type.
2.6. this::method and super::method
Inside an instance method, this::method is a bound reference to the current object. The form super::method calls the parent class version of the method, even when the current class overrides it. For example, a report class can print a detailed task list for managers and a short one for a daily email from the same data.
class BaseReport {
String format(Task task) {
return task.title();
}
}
class DetailedReport extends BaseReport {
@Override
String format(Task task) {
return task.title() + " (" + task.minutes() + " min)";
}
List<String> detailed(List<Task> tasks) {
return tasks.stream().map(this::format).toList();
}
List<String> shortForm(List<Task> tasks) {
return tasks.stream().map(super::format).toList();
}
}
DetailedReport report = new DetailedReport();
List<String> forManagers = report.detailed(tasks); // [email (10 min), report (45 min), review (30 min)]
List<String> forEmail = report.shortForm(tasks); // [email, report, review]
The reference this::format runs the overriding method in DetailedReport, while super::format runs the original method in BaseReport. The lambda form t -> super.format(t) does the same thing. Neither form compiles inside a static method, because a static method has no this and no super.
3. Method Reference FAQs
Most questions about method references start with a compiler error or a NullPointerException that the same lambda would not produce at that line.
3.1. Why Does Integer::toString Give “reference to toString is ambiguous”?
Because Integer has two toString methods that both fit Function<Integer, String>. The static toString(int) fits as a static reference, and the instance method toString() fits as an unbound reference, so javac cannot choose and reports an error.
// does not compile: reference to toString is ambiguous
Function<Integer, String> toText = Integer::toString;
Amb.java:5: error: incompatible types: invalid method reference
Function<Integer, String> toText = Integer::toString;
^
reference to toString is ambiguous
both method toString(int) in Integer and method toString() in Integer match
1 error
To fix the error, we write a lambda or reference a method that has only one matching overload. An overload is another method with the same name and different parameters, as explained in method overloading and overriding.
Function<Integer, String> lambda = i -> i.toString();
Function<Integer, String> valueOf = String::valueOf;
String fromLambda = lambda.apply(42); // "42"
String fromValueOf = valueOf.apply(42); // "42"
The same error appears for the other wrapper classes, which also have a static method and an instance method of the same shape, for example Double::toString and Long::hashCode.
3.2. When Is the Object of a Method Reference Evaluated?
When the reference is created. A bound method reference evaluates its object once, at creation, whereas a lambda reads its captured variables only when it runs. The difference becomes visible when the object is null.
String title = null;
Supplier<Integer> lambda = () -> title.length(); // OK, no error yet
int size = lambda.get(); // NullPointerException
Supplier<Integer> ref = title::length; // NullPointerException here, at creation
The two exceptions also carry different messages. The lambda gets a helpful NullPointerException message that names the variable, whereas the method reference fails inside Objects.requireNonNull(), which javac inserts for every bound reference, and its message is empty.
lambda.get(): java.lang.NullPointerException: Cannot invoke "String.length()" because "title" is null
title::length: java.lang.NullPointerException
java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:220)
at com.howtodoinjava.core.streams.methodreference.MethodReferences.eagerReceiver(MethodReferences.java:220)
at com.howtodoinjava.core.streams.methodreference.MethodReferences.main(MethodReferences.java:74)
The lambda message shows the name title only when the class is compiled with debug information (javac -g, which Maven uses by default). Without it, the message says “<parameter1>” is null, because the captured variable becomes a parameter of a generated method.
A method reference checks its object for null when the reference is created. A lambda fails later, when it runs.Early evaluation has a second effect. A variable used inside a lambda must be effectively final (never reassigned after its first assignment), whereas the variable before :: has no such rule, because the reference keeps the object it saw at creation.
StringBuilder builder = new StringBuilder("draft");
Supplier<String> ref = builder::toString;
builder = new StringBuilder("final"); // a lambda () -> builder.toString() would not compile here
String text = ref.get(); // text = "draft"
Java evaluates the object when it evaluates the method reference expression, so a null check belongs before the reference. When the object can be null, we check it with Objects.requireNonNull() and a clear message, or we keep the lambda.
3.3. Is a Method Reference Faster Than a Lambda?
No. Both compile to an invokedynamic instruction, and at runtime the JVM creates the functional interface object in the same way for both, so there is no speed difference worth measuring.
The difference is only in the class file. For a lambda, javac generates a private method such as lambda$eagerReceiver$0 that holds the lambda body, whereas most method references point to the existing method, for example String.toUpperCase. Array constructor references such as int[]::new are the exception, because javac compiles them into a generated method like a lambda.
3.4. Can We Pass Arguments to a Method Reference?
No. A method reference receives the same arguments as the functional interface method, so we cannot add a fixed value. For a fixed extra argument, we write a lambda such as n -> n.startsWith(“a”) or t -> format(t, Locale.US).
4. Conclusion
A method reference replaces a lambda that only calls one method with the same arguments, and we write it as ClassName::staticMethod, object::method, ClassName::instanceMethod, ClassName::new or Type[]::new. We keep a lambda when the code adds arguments, calls several methods or hits an ambiguous overload. A bound reference checks its object for null when Java creates the reference, so a null object throws NullPointerException earlier than the same lambda would.
5. References
- Method References (The Java Tutorials)
- Java Language Specification 15.13, Method Reference Expressions
- java.util.function package (Java 25)
Happy Learning !!
ArrayList::new equivalent to new ArrayList() this is not correct this applicable only if “The target type of this expression must be a functional interface”
I meant not always correct there is a condition.
Consumer con = System.out::println;
con.accept(“Hello”);
How do you come to know about the parameters that are taken by these static methods.
For Example:
If I want to write System.out.println(“Hello”);
How do i write it using method reference ?
Consumer con = System.out::println;
con.accept(“Hello”);
System.out::println
This use case is usually used when working with java streams or for-each. In this case, we can normally write System.out::println after that we don’t need to create a separate instance of Consumer object or call its method. This can normally be used to iterate contents of Stream or any java objects of type java.lang.Iterable interface like List, Set and other collections.
This use case is usually used when working with java streams or for-each. In this case, we can normally write System.out::println after that we don’t need to create a separate instance of Consumer object or call its method. This can normally be used to iterate contents of Stream or any java objects of type java.lang.Iterable interface like List, Set and other collections.