1. Introduction
A method reference is special syntax in Java that lets you pass an existing method (or constructor) as an implementation of a functional interface. You can “pass” a method where a lambda expression is expected if the signatures match.
Syntax:
Class::method
object::method
Class::new
If a lambda is a tiny “on-the-fly” function, then a method reference is simply “pass an already existing method”. It’s like giving a link to a recipe page instead of rewriting the recipe.
An example in simple terms
Instead of this:
list.forEach(s -> System.out.println(s));
You can do this:
list.forEach(System.out::println);
Looks concise, doesn’t it?
2. Types of method references
There are four main forms of method references. Here they are—and that’s all you need.
Reference to a static method
Syntax: Class::staticMethod
Example:
Function<Integer, String> intToString = String::valueOf;
System.out.println(intToString.apply(123)); // "123"
The same using a lambda:
Function<Integer, String> intToString = i -> String.valueOf(i);
Reference to an instance method of an object
Syntax: object::method
Example:
PrintStream printer = System.out;
Consumer<String> consumer = printer::println;
consumer.accept("Hello, world!");
Equivalent to a lambda:
Consumer<String> consumer = s -> printer.println(s);
Reference to an instance method of a class
Syntax: Class::method
Here, the first parameter of the functional interface becomes the instance on which the method is invoked.
Example:
Function<String, Integer> stringLength = String::length;
System.out.println(stringLength.apply("Java")); // 4
Here, String::length becomes the function: (String s) -> s.length()
Reference to a constructor
Syntax: Class::new
Example:
Supplier<ArrayList<String>> listSupplier = ArrayList::new;
ArrayList<String> list = listSupplier.get();
Equivalent to a lambda:
Supplier<ArrayList<String>> listSupplier = () -> new ArrayList<>();
3. When to use method references?
A method reference is handy when a lambda simply calls an existing method with no additional logic—the code becomes shorter and more readable.
Example: sorting a list
Instead of this:
List<String> names = Arrays.asList("John", "Peter", "Anna");
names.sort((a, b) -> a.compareToIgnoreCase(b));
You can do this:
names.sort(String::compareToIgnoreCase);
Example: processing collections
Instead of:
list.forEach(s -> System.out.println(s));
Make it even shorter:
list.forEach(System.out::println);
Example: transforming elements
Instead of:
List<String> numbers = Arrays.asList("1", "2", "3");
List<Integer> ints = numbers.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
Use this:
List<Integer> ints = numbers.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
You’ll learn more about the Stream API and the utility class Collectors at level 30 :P
4. Comparison of method references and lambda expressions
Equivalence
Method references and lambda expressions are often interchangeable. Both implement a functional interface if their signatures match.
Example:
Consumer<String> c1 = s -> System.out.println(s);
Consumer<String> c2 = System.out::println;
When is it better to use a method reference?
- When a lambda simply calls an existing method with no extra logic.
- To improve readability, especially in long call chains.
- When you want to make it explicit: “this is just a method call”.
When is a method reference not a good fit?
- If you need additional logic (validation, conditions, error handling).
- If parameters need to be transformed before calling the method.
Example:
list.forEach(s -> {
if (s != null) System.out.println(s);
});
// A method reference won’t work here—only a lambda.
5. Practice: rewriting lambda expressions using method references
Example 1: printing animal names
List<String> animals = Arrays.asList("Cat", "Dog", "Parrot");
animals.forEach(animal -> System.out.println(animal));
Becomes:
animals.forEach(System.out::println);
Example 2: converting strings to numbers
List<String> numbers = Arrays.asList("10", "20", "30");
List<Integer> ints = numbers.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());
Becomes:
List<Integer> ints = numbers.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
Example 3: sorting objects by name
List<Animal> animalList = ...;
animalList.sort((a, b) -> a.getName().compareTo(b.getName()));
Becomes:
animalList.sort(Comparator.comparing(Animal::getName));
Here, Animal::getName is a reference to an instance method of a class.
Example 4: creating objects via a constructor
Supplier<Dog> dogFactory = () -> new Dog();
Dog dog = dogFactory.get();
Becomes:
Supplier<Dog> dogFactory = Dog::new;
Dog dog = dogFactory.get();
6. How signature matching works
You can use a method reference only when the method’s signature matches the abstract method of the functional interface.
@FunctionalInterface
interface IntToString {
String convert(int value);
}
public class Demo {
public static String intToHex(int value) {
return Integer.toHexString(value);
}
public static void main(String[] args) {
IntToString converter = Demo::intToHex;
System.out.println(converter.convert(255)); // ff
}
}
Here, Demo::intToHex fits because it takes an int and returns a String.
7. Method references and constructors with parameters
If a constructor takes parameters, you can still use a method reference—as long as the signatures match.
@FunctionalInterface
interface AnimalFactory {
Animal create(String name);
}
class Animal {
private String name;
public Animal(String name) { this.name = name; }
public String getName() { return name; }
}
AnimalFactory factory = Animal::new;
Animal cat = factory.create("Kitty");
System.out.println(cat.getName()); // Kitty
8. Common mistakes when using method references
Error #1: Signature mismatch.
If a method’s signature does not match the interface’s abstract method, the compiler will report an error. For example, the interface expects two parameters, but the reference points to a method with one parameter.
Error #2: Attempting to use a reference to an instance method of a class without an instance.
When you use the form Class::method, the first parameter of the interface becomes the receiver object. If you mix up the number or order of parameters, you’ll get a matching error.
Error #3: Using a method reference where additional logic is needed.
If you need a condition, logging, or exception handling—use a lambda, not a method reference.
Error #4: References to overloaded methods.
If a class has multiple methods with the same name, the compiler may not know which one to choose. Sometimes explicitly specifying the functional interface type helps.
Error #5: Using a reference to an instance method without an object.
For example, String::toUpperCase works correctly in map because the first parameter is the String instance itself. But outside an appropriate context that expects a static method, this will lead to an error.
GO TO FULL VERSION