Method references
On This Lesson 11 topics
Lesson Overview
Look back at the lambdas from the last lesson: s -> s.length(), s -> System.out.println(s), () -> new ArrayList<String>(). Each one does nothing except call one existing method. The lambda is only a courier: it takes the parameter and hands it straight to a method that already has a name. Java lets you drop the courier and point at the method directly. This is a method reference.
In the recipe-card picture: instead of writing a new card that says "do what page 12 of the cookbook says", you simply write "see page 12". The card stays valid, and there is less to read.
Why this matters: method references are everywhere in stream and collection code (map(String::trim), forEach(System.out::println)), so you must read all four forms fluently. They also teach a useful habit: when a lambda only forwards to a method, the method's name often describes the intent better than the lambda's body.
This lesson builds on the standard functional interfaces. A method reference, just like a lambda, needs a functional interface as its target type.
Learning Objectives
By the end of this lesson you will be able to:
- Rewrite a lambda that only calls one method as a method reference.
- Recognize and write the four forms: static method, method of a particular object, method of an arbitrary object of a type, and constructor.
- Say which parameter becomes the receiver in
Class::instanceMethod. - Explain why
ArrayList::newcan mean different constructors in different contexts. - Decide when to keep the lambda instead.
- Read the compiler error for a reference that does not match.
Written Explanation
The idea
A method reference is written target::methodName, with two colons and no parentheses. It is shorthand for a lambda that forwards its parameters to that method. The Oracle Java Tutorials describe method references as compact, easy-to-read lambda expressions for methods that already have a name.
names.forEach(s -> System.out.println(s)); // lambda
names.forEach(System.out::println); // method referenceBoth create the same behavior. The target type decides how the parameters are passed on: forEach wants a Consumer<String>, so the method reference becomes "accept a string, call println with it".
The four forms
| Form | Syntax | Equivalent lambda |
|---|---|---|
| Static method | Class::staticMethod | x -> Class.staticMethod(x) |
| Method of a particular object | object::method | x -> object.method(x) |
| Method of an arbitrary object of a type | Class::instanceMethod | (a, rest) -> a.instanceMethod(rest) |
| Constructor | Class::new | x -> new Class(x) |
The Oracle tutorial lists the same four kinds. Let us look at each with one example.
1. Static method. Integer::parseInt is x -> Integer.parseInt(x). The parameters of the functional method are passed as arguments to the static method.
Function<String, Integer> parse = Integer::parseInt;2. Method of a particular object. The object is named on the left, and it already exists, so every call uses that same object. System.out::println is the best-known case: System.out is one particular PrintStream.
Greeter g = new Greeter("Welcome");
Function<String, String> greet = g::greet; // x -> g.greet(x)3. Method of an arbitrary object of a type. The left side is a class name, but the method is an instance method. Then the first parameter becomes the object on which the method is called, and the remaining parameters become the arguments:
Function<String, Integer> len = String::length; // s -> s.length()
Comparator<String> ci = String::compareToIgnoreCase; // (a, b) -> a.compareToIgnoreCase(b)This is the one that confuses beginners, because String::length looks like a static call. It is not: there is no static String.length(...). The compiler reads it as "take a String and call its length". In compareToIgnoreCase the first string is the receiver and the second is the argument.
4. Constructor. Class::new creates a new object. Which constructor? The one whose parameters match the functional method:
Supplier<List<String>> make = ArrayList::new; // () -> new ArrayList<>()
Function<String, Person> create = Person::new; // name -> new Person(name)ArrayList::new is a Supplier when there are no parameters, and could be a Function<Integer, ArrayList<...>> when there is one, because ArrayList has a constructor taking an initial capacity. The target type picks. (The tutorial also shows array constructor references, such as int[]::new; we will not need them.)
Static versus instance: the two-sentence test
When you see Name::method, ask: is method static?
- If yes, it is form 1: the parameters are the arguments.
- If it is an instance method and
Nameis a class, it is form 3: the first parameter is the receiver.
If Name is a variable or expression (System.out, g, this), it is form 2.
When to keep the lambda
Use a method reference when:
- The lambda does nothing but call one method with its parameters in the same order.
- The name reads better than the lambda body.
Keep the lambda when:
- It does more than forward:
s -> s.length() > 4has a comparison, so there is no method to point to. (If you want a reference, give the logic a name:static boolean isLong(String s)and thenMain::isLong.) - The parameters are reordered, or used twice, or combined with other values.
- The reference would be ambiguous or surprising to a reader. Clarity wins; the tutorial wording is "compact, easy-to-read", and a reference that makes people stop to puzzle it out has failed.
When the compiler complains
If the method does not exist or does not fit the functional interface, you get an error such as this, for a mistaken String::size:
Bad.java:4: error: invalid method reference
Function<String, Integer> f = String::size;
^
cannot find symbol
symbol: method size(String)
location: class StringCheck, in order: the name, whether it is static, and whether the number of parameters matches the interface method.
Examples
Example 1: printing and sorting (simple)
List<String> names = new ArrayList<>(List.of("omar", "Sara", "layla", "Adam"));
names.forEach(System.out::println);
names.sort(String::compareToIgnoreCase);
System.out.println(names);Output:
omar
Sara
layla
Adam
[Adam, layla, omar, Sara]System.out::println is a particular-object reference; String::compareToIgnoreCase is an arbitrary-object reference with two parameters. Note the plain String.compareTo would put the capital letters first; ignoring case gives the order a person expects.
Example 2: converting and creating (practical)
Function<String, Integer> len = String::length;
Function<String, Integer> parse = Integer::parseInt;
System.out.println(len.apply("abcd") + " " + (parse.apply("42") + 1));
Supplier<List<String>> make = ArrayList::new;
List<String> fresh = make.get();
fresh.add("x");
System.out.println(fresh);
Function<String, Person> create = Person::new;
System.out.println(create.apply("Noor"));Output:
4 43
[x]
Person(Noor)This example uses the static form, the arbitrary-object form, and the constructor form, each matching a different functional interface.
Example 3: your own methods (realistic)
Any method can be referenced, including your own:
class Greeter {
private final String greeting;
Greeter(String g) { greeting = g; }
String greet(String n) { return greeting + ", " + n; }
}
static boolean isLong(String s) { return s.length() > 4; }
static int compareByName(Person a, Person b) { ... } // inside PersonGreeter g = new Greeter("Welcome");
Function<String, String> greet = g::greet;
System.out.println(greet.apply("Omar"));
Predicate<String> p = Main::isLong;
System.out.println(p.test("banana") + " " + p.test("fig"));
people.sort(Person::compareByName);
people.sort(Comparator.comparing(Person::getName).reversed());Output (the two sorts are shown with two people, Zed and Amal):
Welcome, Omar
true false
[Person(Amal), Person(Zed)]
[Person(Zed), Person(Amal)]The last line is the pattern you saw in BEG-032: Person::getName is a "key extractor", an arbitrary-object reference that means "take a person, get the name". Now the notation is no longer a preview. Moving isLong out of the lambda gave the logic a name that tells the reader what is being tested.
Backend Insight
In backend code, method references describe intent at the call site. Compare:
orders.stream().map(order -> order.getCustomer()).filter(c -> c.isActive())...
orders.stream().map(Order::getCustomer).filter(Customer::isActive)...(The stream methods are the subject of BEG-036; read them as "for each order, get its customer; keep the active ones".) The second form reads like a sentence, and it breaks no rule: each reference is just a named method.
Method references also encourage good design. If you cannot write Customer::isActive because the logic lives in an expression scattered across a lambda, that is a hint that the class lacks a method with a good name. Pulling the logic into a method gives you something you can reference, test, and reuse. Remember also that a constructor reference such as Person::new is how frameworks and factories are often configured: "here is how to create one".
Common Mistakes
Mistake 1: Adding parentheses
- Incorrect:
names.forEach(System.out::println()); - Correct:
names.forEach(System.out::println);
With parentheses you would call the method. A reference names it without calling.
Mistake 2: Thinking String::length is static
It is an instance method, referenced through the class name. The first parameter of the interface method becomes the receiver. If you call String.length() anywhere, it will not compile.
Mistake 3: Forcing a reference where the lambda does more
s -> s.length() > 4 cannot be written as a reference. Do not contort the code; keep the lambda, or extract a method with a clear name.
Mistake 4: Confusing this::method with Class::method
this::process calls process on the current object (a particular object). Order::process calls it on whatever order the interface passes in. Choose on purpose.
Mistake 5: Ignoring ambiguity and overloads
If a class has several overloads of a method (for instance, println has many), the functional interface's parameter types pick one. If the compiler says the reference is ambiguous or invalid, write the lambda and be explicit.
Exercises
Try each exercise yourself before opening the solutions.
Level A: Foundation
A1. Name the form (static, particular object, arbitrary object, constructor) of: (a) Integer::valueOf (b) String::trim (c) HashSet::new (d) System.out::print.
Show solution A1
A1. (a) static method. (b) arbitrary object of a type (s -> s.trim()). (c) constructor. (d) method of a particular object (System.out).
A2. Rewrite as method references: s -> s.isEmpty(), x -> Integer.valueOf(x), () -> new HashSet<Integer>().
Show solution A2
A2. String::isEmpty, Integer::valueOf, HashSet::new (target type Supplier<Set<Integer>>).
A3. In String::compareToIgnoreCase used as a Comparator<String>, which argument is the receiver of the call?
Show solution A3
A3. The first one. String::compareToIgnoreCase means (a, b) -> a.compareToIgnoreCase(b).
Level B: Application
B1. Sort List<String> w = new ArrayList<>(List.of("b", "A", "c")) ignoring case using a method reference. What is the result?
Show solution B1
B1. w.sort(String::compareToIgnoreCase); gives [A, b, c].
B2. Write a BiFunction<Integer, Integer, Integer> that returns the larger of two numbers, using a method reference to a method of the Math class.
Show solution B2
B2. BiFunction<Integer, Integer, Integer> m = Math::max; For m.apply(2, 9) the result is 9. The int method is chosen and the Integer values are unboxed and boxed automatically.
B3. The method static boolean isLong(String s) { return s.length() > 4; } exists. Use it as a Predicate<String> with a method reference, and call it with "abcde".
Show solution B3
B3. Predicate<String> p = Main::isLong; (replace Main with the class that holds the method). p.test("abcde") is true.
Level C: Challenge
C1. Given Greeter g = new Greeter("Hi") and String greet(String n) in Greeter, write both g::greet and Greeter::greet as references. What functional interface does each need?
Show solution C1
C1. Function<String, String> bound = g::greet; is a function of one string: the receiver g is fixed. BiFunction<Greeter, String, String> unbound = Greeter::greet; takes the greeter as its first parameter, then the string. For example unbound.apply(new Greeter("Yo"), "B") gives Yo, B.
C2. Explain why s -> s.length() > 4 cannot be turned into a method reference directly, and give two ways to handle it.
Show solution C2
C2. The body is not a plain call that forwards its parameter, because it also compares the result with 4. Option 1: keep the lambda. Option 2: extract a method such as isLong(String s) and reference that. The second is better when the rule is used in several places or deserves a name.
Mini Challenge
Create a small Product class with a constructor taking a name, getName(), and a static compareByName(Product a, Product b). Then, in main:
- Build a
Supplier<List<Product>>fromArrayList::newand add three products. - Create a product from a string with a
Function<String, Product>that usesProduct::new. - Sort the products using
Product::compareByName, then again withComparator.comparing(Product::getName). - Print each product name with a method reference to
System.out::println(after mapping names to strings in a loop or withgetName).
Check your work: every method reference corresponds to exactly one lambda you could write by hand; try writing it, and make sure the two forms behave the same.
Summary & Next Lesson
Key takeaways
- A method reference replaces a lambda that only forwards to an existing method. Write
target::method, with no parentheses. - Four forms:
Class::staticMethod,object::method,Class::instanceMethod(first parameter becomes the receiver), andClass::new. - The functional interface decides how parameters are passed on, and which constructor or overload is chosen.
- Keep the lambda when the body does more than forward, or when the reference would be less clear.
- Give repeated logic a named method, and then reference it.
Next lesson
The next lesson is a demo, "Replacing anonymous classes with lambdas". You will take noisy code, shrink it step by step with lambdas and method references, and meet the "effectively final" error on purpose, which prepares the lab "Filters".
Self-check
- How is
String::lengthdifferent fromString.length()? - What does
System.out::printlnrefer to? - In
Class::instanceMethod, what is the role of the first parameter? - Which form is
ArrayList::new? - When should you keep a lambda?
Show answers
String::lengthis a reference to an instance method meaning "take a string and call itslength";String.length()is not valid Java at all, sincelengthis not static.- The
printlnmethod of the one objectSystem.out. - It is the object on which the method is called; the other parameters are the arguments.
- A constructor reference.
- When it does more than forward its parameters, or when the lambda is clearer than the reference.
References
- Method References, The Java Tutorials (Oracle). Supports: the four kinds of method reference, the rule that a method reference is for a lambda that calls an existing method, and the
String::compareToIgnoreCaseandHashSet::newexamples. - Lambda Expressions, The Java Tutorials (Oracle). Supports: lambdas as the starting point and the target-typing idea that picks the interface.
- All code was compiled and run with JDK 21; outputs and the compiler message shown are real. The statement that the first parameter becomes the receiver in the arbitrary-object form is from the Oracle method references page; the rule of thumb on when to keep a lambda is the course's own advice.
Practice
Apply this lesson with the module's lab, exercises and quiz.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.