1. What is a Method Reference?
Imagine you are writing directions for someone. You could say: “Go to the kitchen, open the drawer, pick up the knife, and cut the bread.” Or you could simply say: “Cut the bread” — because the person already knows how to do it. A method reference works the same way. Instead of spelling out the steps inside a lambda expression, you point directly to an existing method that already does the work.
A method reference is a shorthand notation for a lambda expression that calls a single existing method. Introduced in Java 8 alongside lambdas and the Stream API, method references use the :: operator (double colon) to refer to a method without invoking it.
Here is the core idea: if your lambda expression does nothing more than call an existing method, you can replace the lambda with a method reference.
import java.util.Arrays;
import java.util.List;
public class MethodReferenceIntro {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// Lambda expression -- spells out the call
names.forEach(name -> System.out.println(name));
// Method reference -- points directly to the method
names.forEach(System.out::println);
// Both produce the same output:
// Alice
// Bob
// Charlie
}
}In the example above, the lambda name -> System.out.println(name) receives a parameter and immediately passes it to println. Since the lambda is just a pass-through — it adds no extra logic — the method reference System.out::println is a cleaner, more readable alternative.
Key points about method references:
- Syntactic sugar — A method reference is not a new feature in the JVM. It compiles to the same bytecode as the equivalent lambda expression. It is purely a readability improvement.
- The
::operator — Separates the class or instance from the method name. It tells the compiler “look up this method and use it as the implementation of the functional interface.” - No parentheses — You write
String::toUpperCase, notString::toUpperCase(). Adding parentheses would invoke the method immediately rather than referencing it. - Must match a functional interface — The referenced method’s signature (parameter types and return type) must be compatible with the single abstract method of the target functional interface.
- Four types — Java defines exactly four kinds of method references, each covering a different scenario.
2. The Four Types of Method References
Every method reference falls into one of four categories. The table below summarizes each type, its syntax, and what the equivalent lambda expression looks like.
| Type | Syntax | Lambda Equivalent | Example |
|---|---|---|---|
| Static method | ClassName::staticMethod |
(args) -> ClassName.staticMethod(args) |
Integer::parseInt |
| Instance method of a particular object | instance::method |
(args) -> instance.method(args) |
System.out::println |
| Instance method of an arbitrary object | ClassName::instanceMethod |
(obj, args) -> obj.instanceMethod(args) |
String::compareToIgnoreCase |
| Constructor | ClassName::new |
(args) -> new ClassName(args) |
ArrayList::new |
The first two are straightforward. The third — instance method of an arbitrary object — is the one that confuses most developers. We will dedicate extra attention to it in section 5. Let us now explore each type in detail.
3. Static Method Reference (ClassName::staticMethodName)
A static method reference points to a static method on a class. The compiler maps the functional interface’s parameters directly to the static method’s parameters.
Pattern:
| Lambda | Method Reference |
|---|---|
(args) -> ClassName.staticMethod(args) |
ClassName::staticMethod |
The arguments from the functional interface are passed directly to the static method in the same order.
3.1 Built-in Static Methods
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StaticMethodReference {
public static void main(String[] args) {
// --- Integer::parseInt ---
// Lambda: s -> Integer.parseInt(s)
// Reference: Integer::parseInt
List<String> numberStrings = Arrays.asList("1", "2", "3", "4", "5");
List<Integer> numbers = numberStrings.stream()
.map(Integer::parseInt) // static method reference
.collect(Collectors.toList());
System.out.println(numbers);
// Output: [1, 2, 3, 4, 5]
// --- Math::max ---
// Lambda: (a, b) -> Math.max(a, b)
// Reference: Math::max
int result = numbers.stream()
.reduce(Math::max) // static method reference
.orElse(0);
System.out.println("Max: " + result);
// Output: Max: 5
// --- String::valueOf ---
// Lambda: n -> String.valueOf(n)
// Reference: String::valueOf
List<String> backToStrings = numbers.stream()
.map(String::valueOf) // static method reference
.collect(Collectors.toList());
System.out.println(backToStrings);
// Output: [1, 2, 3, 4, 5]
// --- Collections::sort with a list ---
// Math::abs as a function
List<Integer> values = Arrays.asList(-5, 3, -1, 4, -2);
values.sort((a, b) -> Integer.compare(Math.abs(a), Math.abs(b)));
System.out.println("Sorted by absolute value: " + values);
// Output: Sorted by absolute value: [-1, -2, 3, 4, -5]
}
}3.2 Custom Static Methods
Method references are not limited to the standard library. You can reference any static method you write, as long as its signature matches the functional interface.
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class CustomStaticMethodReference {
// Custom static method: checks if a number is even
public static boolean isEven(int number) {
return number % 2 == 0;
}
// Custom static method: formats a name
public static String formatName(String name) {
return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
}
// Custom static method: calculates the square
public static int square(int n) {
return n * n;
}
public static void main(String[] args) {
// Using custom static method reference as a Predicate
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List<Integer> evens = numbers.stream()
.filter(CustomStaticMethodReference::isEven) // custom static ref
.collect(Collectors.toList());
System.out.println("Even numbers: " + evens);
// Output: Even numbers: [2, 4, 6, 8]
// Using custom static method reference as a Function
List<String> rawNames = Arrays.asList("aLICE", "bOB", "cHARLIE");
List<String> formatted = rawNames.stream()
.map(CustomStaticMethodReference::formatName) // custom static ref
.collect(Collectors.toList());
System.out.println("Formatted: " + formatted);
// Output: Formatted: [Alice, Bob, Charlie]
// Using custom static method reference as a UnaryOperator
List<Integer> squares = numbers.stream()
.map(CustomStaticMethodReference::square) // custom static ref
.collect(Collectors.toList());
System.out.println("Squares: " + squares);
// Output: Squares: [1, 4, 9, 16, 25, 36, 49, 64]
// Storing a method reference in a variable
Predicate<Integer> evenCheck = CustomStaticMethodReference::isEven;
Function<String, String> nameFormatter = CustomStaticMethodReference::formatName;
System.out.println("Is 7 even? " + evenCheck.test(7));
// Output: Is 7 even? false
System.out.println("Format 'dAVID': " + nameFormatter.apply("dAVID"));
// Output: Format 'dAVID': David
}
}4. Instance Method Reference of a Particular Object (instance::methodName)
This type references an instance method on a specific, already-existing object. The object is captured at the time the method reference is created, and the functional interface’s parameters are passed as arguments to that method.
Pattern:
| Lambda | Method Reference |
|---|---|
(args) -> existingObject.method(args) |
existingObject::method |
The key distinction from the arbitrary-object type (section 5) is that here the object is known and fixed before the method reference is used. The most iconic example is System.out::println — System.out is a specific PrintStream instance.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ParticularObjectMethodReference {
public static void main(String[] args) {
// --- System.out::println ---
// System.out is a specific PrintStream object
// Lambda: item -> System.out.println(item)
// Reference: System.out::println
List<String> languages = Arrays.asList("Java", "Python", "Go", "Rust");
languages.forEach(System.out::println);
// Output:
// Java
// Python
// Go
// Rust
// --- Custom object reference ---
StringBuilder sb = new StringBuilder();
// Lambda: s -> sb.append(s)
// Reference: sb::append
languages.forEach(sb::append);
System.out.println("Appended: " + sb);
// Output: Appended: JavaPythonGoRust
// --- Using a specific String instance ---
String prefix = "Hello, ";
// Lambda: name -> prefix.concat(name)
// Reference: prefix::concat
List<String> greetings = languages.stream()
.map(prefix::concat)
.collect(Collectors.toList());
System.out.println(greetings);
// Output: [Hello, Java, Hello, Python, Hello, Go, Hello, Rust]
}
}4.1 Using with Custom Objects
Method references on particular objects are especially useful when you have a service or helper object with instance methods you want to use in a stream pipeline.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class CustomObjectMethodReference {
// A validator object with instance methods
static class EmailValidator {
private final String requiredDomain;
public EmailValidator(String requiredDomain) {
this.requiredDomain = requiredDomain;
}
public boolean isValid(String email) {
return email != null
&& email.contains("@")
&& email.endsWith(requiredDomain);
}
}
// A formatter object
static class NameFormatter {
private final String title;
public NameFormatter(String title) {
this.title = title;
}
public String format(String name) {
return title + " " + name;
}
}
public static void main(String[] args) {
// Create specific objects
EmailValidator companyValidator = new EmailValidator("@company.com");
NameFormatter doctorFormatter = new NameFormatter("Dr.");
List<String> emails = Arrays.asList(
"alice@company.com", "bob@gmail.com",
"charlie@company.com", "dave@yahoo.com"
);
// Use instance method reference on a particular object
// Lambda: email -> companyValidator.isValid(email)
// Reference: companyValidator::isValid
List<String> validEmails = emails.stream()
.filter(companyValidator::isValid)
.collect(Collectors.toList());
System.out.println("Valid company emails: " + validEmails);
// Output: Valid company emails: [alice@company.com, charlie@company.com]
List<String> names = Arrays.asList("Smith", "Johnson", "Williams");
// Lambda: name -> doctorFormatter.format(name)
// Reference: doctorFormatter::format
List<String> formattedNames = names.stream()
.map(doctorFormatter::format)
.collect(Collectors.toList());
System.out.println("Formatted: " + formattedNames);
// Output: Formatted: [Dr. Smith, Dr. Johnson, Dr. Williams]
}
}4.2 The “this” and “super” References
Inside an instance method, you can use this::methodName or super::methodName as method references. this refers to the current object, and super refers to the parent class implementation.
import java.util.Arrays;
import java.util.List;
public class ThisSuperReference {
static class Animal {
public void speak(String sound) {
System.out.println("Animal says: " + sound);
}
}
static class Dog extends Animal {
@Override
public void speak(String sound) {
System.out.println("Dog barks: " + sound);
}
public void makeNoise(String sound) {
System.out.println("Dog noise: " + sound.toUpperCase());
}
public void demonstrate() {
List<String> sounds = Arrays.asList("woof", "bark", "growl");
System.out.println("--- Using this::makeNoise ---");
sounds.forEach(this::makeNoise);
// Dog noise: WOOF
// Dog noise: BARK
// Dog noise: GROWL
System.out.println("--- Using this::speak (Dog's override) ---");
sounds.forEach(this::speak);
// Dog barks: woof
// Dog barks: bark
// Dog barks: growl
System.out.println("--- Using super::speak (Animal's version) ---");
sounds.forEach(super::speak);
// Animal says: woof
// Animal says: bark
// Animal says: growl
}
}
public static void main(String[] args) {
new Dog().demonstrate();
}
}5. Instance Method Reference of an Arbitrary Object (ClassName::instanceMethodName)
This is the type that trips up most developers. The syntax looks identical to a static method reference — you write ClassName::methodName — but the method is not static. It is an instance method. The difference is in which object the method is called on.
Pattern:
| Lambda | Method Reference |
|---|---|
(obj) -> obj.instanceMethod() |
ClassName::instanceMethod |
(obj, arg) -> obj.instanceMethod(arg) |
ClassName::instanceMethod |
How it works: The first parameter of the functional interface becomes the object on which the method is invoked. Any remaining parameters become the method’s arguments. The “arbitrary” object is whichever object happens to flow through the stream or be supplied by the functional interface at runtime.
Let us break this down step by step. Consider a list of strings and the task of converting each to uppercase:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ArbitraryObjectBasic {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world", "java");
// Lambda version: each element 's' calls its own toUpperCase()
List<String> upper1 = words.stream()
.map(s -> s.toUpperCase())
.collect(Collectors.toList());
// Method reference version: String::toUpperCase
// The stream element (a String) becomes the object on which toUpperCase() is called
List<String> upper2 = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper1); // [HELLO, WORLD, JAVA]
System.out.println(upper2); // [HELLO, WORLD, JAVA]
}
}In String::toUpperCase, we are telling Java: “For each element in the stream, call its toUpperCase() method.” The word “arbitrary” means we do not know in advance which specific String object will be used — it is whichever object the stream provides at that moment.
5.1 Understanding the Transformation
This is the critical mental model: the first parameter of the functional interface becomes the target object. The table below shows how this transformation works for common examples.
| Method Reference | Functional Interface | Lambda Equivalent | Who calls the method? |
|---|---|---|---|
String::toUpperCase |
Function<String, String> |
s -> s.toUpperCase() |
The stream element (s) |
String::length |
Function<String, Integer> |
s -> s.length() |
The stream element (s) |
String::isEmpty |
Predicate<String> |
s -> s.isEmpty() |
The stream element (s) |
String::trim |
Function<String, String> |
s -> s.trim() |
The stream element (s) |
String::compareTo |
Comparator<String> |
(a, b) -> a.compareTo(b) |
The first parameter (a) |
String::compareToIgnoreCase |
Comparator<String> |
(a, b) -> a.compareToIgnoreCase(b) |
The first parameter (a) |
Notice the last two rows: when the functional interface has two parameters (like Comparator which takes two arguments), the first becomes the object, and the second becomes the method argument.
5.2 With One Parameter (No-arg instance method)
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ArbitraryObjectNoArg {
public static void main(String[] args) {
// --- String::length ---
// Function<String, Integer>: takes a String, returns its length
List<String> words = Arrays.asList("Java", "is", "powerful");
List<Integer> lengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println("Lengths: " + lengths);
// Output: Lengths: [4, 2, 8]
// --- String::trim ---
List<String> messy = Arrays.asList(" hello ", " world ", " java ");
List<String> trimmed = messy.stream()
.map(String::trim)
.collect(Collectors.toList());
System.out.println("Trimmed: " + trimmed);
// Output: Trimmed: [hello, world, java]
// --- String::isEmpty used as a Predicate ---
List<String> mixed = Arrays.asList("Java", "", "Python", "", "Go");
List<String> nonEmpty = mixed.stream()
.filter(s -> !s.isEmpty()) // cannot directly negate a method ref
.collect(Collectors.toList());
System.out.println("Non-empty: " + nonEmpty);
// Output: Non-empty: [Java, Python, Go]
}
}5.3 With Two Parameters (Instance method takes an argument)
When the functional interface accepts two parameters, the first parameter becomes the object and the second becomes the method argument. This is common with Comparator and BiFunction.
import java.util.Arrays;
import java.util.List;
public class ArbitraryObjectWithArg {
public static void main(String[] args) {
// --- String::compareTo used as Comparator<String> ---
// Comparator<String> has: int compare(String a, String b)
// String::compareTo maps to: a.compareTo(b)
List<String> names = Arrays.asList("Charlie", "Alice", "Bob");
names.sort(String::compareTo);
System.out.println("Sorted: " + names);
// Output: Sorted: [Alice, Bob, Charlie]
// --- String::compareToIgnoreCase ---
List<String> mixed = Arrays.asList("banana", "Apple", "cherry");
mixed.sort(String::compareToIgnoreCase);
System.out.println("Case-insensitive sort: " + mixed);
// Output: Case-insensitive sort: [Apple, banana, cherry]
// --- String::concat used as BiFunction<String, String, String> ---
// BiFunction<String, String, String>: (a, b) -> a.concat(b)
java.util.function.BiFunction<String, String, String> joiner = String::concat;
String result = joiner.apply("Hello, ", "World!");
System.out.println(result);
// Output: Hello, World!
}
}5.4 With Custom Classes
The arbitrary-object pattern works with your own classes too. If a stream contains objects of type Person, you can write Person::getName to call getName() on each Person object flowing through the stream.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class ArbitraryObjectCustomClass {
static class Person {
private final String name;
private final int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (" + age + ")";
}
}
public static void main(String[] args) {
List<Person> people = Arrays.asList(
new Person("Charlie", 30),
new Person("Alice", 25),
new Person("Bob", 35)
);
// --- Person::getName used as Function<Person, String> ---
// Lambda: person -> person.getName()
List<String> names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
System.out.println("Names: " + names);
// Output: Names: [Charlie, Alice, Bob]
// --- Person::getAge used as Function<Person, Integer> ---
List<Integer> ages = people.stream()
.map(Person::getAge)
.collect(Collectors.toList());
System.out.println("Ages: " + ages);
// Output: Ages: [30, 25, 35]
// --- Sorting with Comparator.comparing and method reference ---
people.sort(Comparator.comparing(Person::getName));
System.out.println("Sorted by name: " + people);
// Output: Sorted by name: [Alice (25), Bob (35), Charlie (30)]
people.sort(Comparator.comparing(Person::getAge));
System.out.println("Sorted by age: " + people);
// Output: Sorted by age: [Alice (25), Charlie (30), Bob (35)]
people.sort(Comparator.comparing(Person::getAge).reversed());
System.out.println("Sorted by age descending: " + people);
// Output: Sorted by age descending: [Bob (35), Charlie (30), Alice (25)]
}
}5.5 How Does the Compiler Know Which Type?
You might wonder: when you write String::toUpperCase, how does the compiler know this is an arbitrary-object instance method reference and not a static method reference? The answer is simple: the compiler checks whether toUpperCase is a static method or an instance method on the String class. Since String.toUpperCase() is an instance method, the compiler resolves it as the arbitrary-object type.
If String had a static toUpperCase(String s) method, there would be an ambiguity, and the compiler would raise an error. Fortunately, this situation is rare in practice.
6. Constructor Reference (ClassName::new)
A constructor reference points to a constructor of a class. The compiler determines which constructor to call based on the functional interface’s parameter types.
Pattern:
| Lambda | Method Reference |
|---|---|
() -> new ClassName() |
ClassName::new |
(arg) -> new ClassName(arg) |
ClassName::new |
(arg1, arg2) -> new ClassName(arg1, arg2) |
ClassName::new |
The same ClassName::new syntax works for all constructors. The compiler infers which constructor to use from the context (the functional interface it needs to match).
6.1 No-Argument Constructor (Supplier)
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.function.Supplier;
public class ConstructorReferenceNoArg {
public static void main(String[] args) {
// --- ArrayList::new as Supplier<List<String>> ---
// Lambda: () -> new ArrayList<>()
// Reference: ArrayList::new
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list1 = listFactory.get();
list1.add("Java");
list1.add("Python");
System.out.println("List 1: " + list1);
// Output: List 1: [Java, Python]
// Each call to get() creates a new instance
List<String> list2 = listFactory.get();
System.out.println("List 2 (empty): " + list2);
// Output: List 2 (empty): []
// --- HashMap::new ---
Supplier<HashMap<String, Integer>> mapFactory = HashMap::new;
HashMap<String, Integer> scores = mapFactory.get();
scores.put("Alice", 95);
System.out.println("Scores: " + scores);
// Output: Scores: {Alice=95}
// --- StringBuilder::new ---
Supplier<StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.get();
sb.append("Hello").append(" World");
System.out.println(sb);
// Output: Hello World
}
}6.2 One-Argument Constructor (Function)
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class ConstructorReferenceOneArg {
static class User {
private final String name;
public User(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{name='" + name + "'}";
}
}
public static void main(String[] args) {
// --- User::new as Function<String, User> ---
// Lambda: name -> new User(name)
// Reference: User::new
Function<String, User> userFactory = User::new;
User alice = userFactory.apply("Alice");
System.out.println(alice);
// Output: User{name='Alice'}
// --- Using constructor reference in a stream ---
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<User> users = names.stream()
.map(User::new) // creates a new User for each name
.collect(Collectors.toList());
System.out.println(users);
// Output: [User{name='Alice'}, User{name='Bob'}, User{name='Charlie'}]
// --- Integer::new (deprecated, prefer Integer::valueOf) ---
// Lambda: s -> new Integer(s) // deprecated since Java 9
// Better: Integer::valueOf
List<String> numberStrings = Arrays.asList("10", "20", "30");
List<Integer> numbers = numberStrings.stream()
.map(Integer::valueOf)
.collect(Collectors.toList());
System.out.println("Numbers: " + numbers);
// Output: Numbers: [10, 20, 30]
}
}6.3 Two-Argument Constructor (BiFunction)
import java.util.function.BiFunction;
public class ConstructorReferenceTwoArg {
static class Product {
private final String name;
private final double price;
public Product(String name, double price) {
this.name = name;
this.price = price;
}
@Override
public String toString() {
return name + " ($" + String.format("%.2f", price) + ")";
}
}
public static void main(String[] args) {
// --- Product::new as BiFunction<String, Double, Product> ---
// Lambda: (name, price) -> new Product(name, price)
// Reference: Product::new
BiFunction<String, Double, Product> productFactory = Product::new;
Product laptop = productFactory.apply("Laptop", 999.99);
Product phone = productFactory.apply("Phone", 699.99);
System.out.println(laptop);
// Output: Laptop ($999.99)
System.out.println(phone);
// Output: Phone ($699.99)
}
}6.4 Array Constructor Reference (Type[]::new)
A special form of constructor reference creates arrays. This is particularly useful with the toArray() method on streams, which needs an IntFunction<T[]> that takes a size and returns an array of that size.
import java.util.Arrays;
import java.util.List;
import java.util.function.IntFunction;
import java.util.stream.Stream;
public class ArrayConstructorReference {
public static void main(String[] args) {
// --- String[]::new with Stream.toArray() ---
List<String> list = Arrays.asList("Java", "Python", "Go");
// Without array constructor reference: must specify how to create the array
// Lambda: size -> new String[size]
String[] array1 = list.stream().toArray(size -> new String[size]);
// With array constructor reference
String[] array2 = list.stream().toArray(String[]::new);
System.out.println(Arrays.toString(array1));
// Output: [Java, Python, Go]
System.out.println(Arrays.toString(array2));
// Output: [Java, Python, Go]
// --- int[]::new ---
IntFunction<int[]> intArrayFactory = int[]::new;
int[] intArray = intArrayFactory.apply(5); // creates int[5]
System.out.println("int array length: " + intArray.length);
// Output: int array length: 5
// --- Common stream-to-array pattern ---
String[] upperNames = Stream.of("alice", "bob", "charlie")
.map(String::toUpperCase)
.toArray(String[]::new);
System.out.println(Arrays.toString(upperNames));
// Output: [ALICE, BOB, CHARLIE]
// --- Integer[]::new ---
Integer[] numbers = Stream.of(1, 2, 3, 4, 5)
.filter(n -> n % 2 != 0)
.toArray(Integer[]::new);
System.out.println(Arrays.toString(numbers));
// Output: [1, 3, 5]
}
}7. Method Reference vs Lambda Expression
Method references and lambdas are interchangeable when the lambda simply delegates to a single method call. But they are not always interchangeable, and one is not always better than the other. Here is a practical guide to deciding which to use.
7.1 When to Use a Method Reference
- The lambda does nothing except call a single existing method
- The method name clearly describes the operation (e.g.,
String::toUpperCaseis self-documenting) - You want to reduce visual noise in a stream pipeline
7.2 When to Use a Lambda
- You need to add extra logic (conditions, transformations, multiple statements)
- You need to call a method with additional arguments that are not provided by the functional interface
- The method reference would be ambiguous or confusing
- You need to negate or combine the result
7.3 Conversion Examples
| Lambda | Method Reference | Recommendation |
|---|---|---|
s -> s.toUpperCase() |
String::toUpperCase |
Use method reference — cleaner |
s -> System.out.println(s) |
System.out::println |
Use method reference — iconic pattern |
() -> new ArrayList<>() |
ArrayList::new |
Use method reference — concise |
s -> Integer.parseInt(s) |
Integer::parseInt |
Use method reference — clear intent |
s -> s.length() > 3 |
No direct reference | Use lambda — contains extra logic (comparison) |
s -> "prefix_" + s |
No direct reference | Use lambda — contains concatenation logic |
(a, b) -> a + b |
Integer::sum |
Use method reference — Integer.sum() exists for this |
x -> !x.isEmpty() |
Predicate.not(String::isEmpty) |
Use Predicate.not() (Java 11+) or lambda |
7.4 Readability Comparison
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class MethodRefVsLambda {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", null, "Bob", "", "Charlie", null, "Dave");
// ===== METHOD REFERENCES: cleaner for simple delegation =====
// Filter nulls: Objects::nonNull is instantly readable
List<String> nonNull = names.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
System.out.println("Non-null: " + nonNull);
// Output: Non-null: [Alice, Bob, , Charlie, Dave]
// Filter non-null AND non-empty: lambda required (two conditions)
List<String> nonEmpty = names.stream()
.filter(Objects::nonNull)
.filter(s -> !s.isEmpty()) // lambda needed for negation
.collect(Collectors.toList());
System.out.println("Non-empty: " + nonEmpty);
// Output: Non-empty: [Alice, Bob, Charlie, Dave]
// Java 11+: Predicate.not() lets you use method reference for negation
List<String> nonEmptyJava11 = names.stream()
.filter(Objects::nonNull)
.filter(Predicate.not(String::isEmpty)) // method reference with not()
.collect(Collectors.toList());
System.out.println("Non-empty (Java 11): " + nonEmptyJava11);
// Output: Non-empty (Java 11): [Alice, Bob, Charlie, Dave]
// ===== LAMBDA: necessary for complex logic =====
// Transformation with extra logic -- lambda is the right choice
List<String> processed = names.stream()
.filter(Objects::nonNull)
.filter(Predicate.not(String::isEmpty))
.map(name -> name.length() > 4 ? name.substring(0, 4) + "..." : name)
.collect(Collectors.toList());
System.out.println("Processed: " + processed);
// Output: Processed: [Alic..., Bob, Char..., Dave]
}
}8. Method References with Streams
Method references truly shine in stream pipelines, where they make each step in the pipeline read like a sentence. This section shows practical patterns you will use daily.
8.1 map() — Transforming Elements
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StreamMapExamples {
static class Employee {
private final String name;
private final String department;
private final double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
@Override
public String toString() {
return name + " [" + department + "] $" + String.format("%.0f", salary);
}
}
public static void main(String[] args) {
// --- String transformations ---
List<String> words = Arrays.asList(" hello ", " world ", " java ");
List<String> cleaned = words.stream()
.map(String::trim) // arbitrary object: s -> s.trim()
.map(String::toUpperCase) // arbitrary object: s -> s.toUpperCase()
.collect(Collectors.toList());
System.out.println(cleaned);
// Output: [HELLO, WORLD, JAVA]
// --- Extracting fields from objects ---
List<Employee> employees = Arrays.asList(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Marketing", 75000),
new Employee("Charlie", "Engineering", 105000),
new Employee("Diana", "Marketing", 80000)
);
List<String> names = employees.stream()
.map(Employee::getName) // arbitrary object: e -> e.getName()
.collect(Collectors.toList());
System.out.println("Names: " + names);
// Output: Names: [Alice, Bob, Charlie, Diana]
// --- Converting types ---
List<String> numberStrings = Arrays.asList("100", "200", "300");
List<Integer> numbers = numberStrings.stream()
.map(Integer::parseInt) // static: s -> Integer.parseInt(s)
.collect(Collectors.toList());
System.out.println("Numbers: " + numbers);
// Output: Numbers: [100, 200, 300]
}
}8.2 filter() — Selecting Elements
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class StreamFilterExamples {
static class Task {
private final String title;
private final boolean completed;
public Task(String title, boolean completed) {
this.title = title;
this.completed = completed;
}
public String getTitle() { return title; }
public boolean isCompleted() { return completed; }
@Override
public String toString() { return title + (completed ? " [DONE]" : " [TODO]"); }
}
public static void main(String[] args) {
// --- Objects::nonNull for null filtering ---
List<String> items = Arrays.asList("Java", null, "Python", null, "Go");
List<String> safe = items.stream()
.filter(Objects::nonNull) // static: obj -> Objects.nonNull(obj)
.collect(Collectors.toList());
System.out.println("Non-null: " + safe);
// Output: Non-null: [Java, Python, Go]
// --- Using a method reference on a boolean getter ---
List<Task> tasks = Arrays.asList(
new Task("Write code", true),
new Task("Review PR", false),
new Task("Deploy", true),
new Task("Write tests", false)
);
// Task::isCompleted as Predicate<Task>
List<Task> done = tasks.stream()
.filter(Task::isCompleted) // arbitrary object: t -> t.isCompleted()
.collect(Collectors.toList());
System.out.println("Done: " + done);
// Output: Done: [Write code [DONE], Deploy [DONE]]
// Negate with Predicate.not() (Java 11+)
List<Task> pending = tasks.stream()
.filter(Predicate.not(Task::isCompleted))
.collect(Collectors.toList());
System.out.println("Pending: " + pending);
// Output: Pending: [Review PR [TODO], Write tests [TODO]]
}
}8.3 sorted() — Ordering Elements
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class StreamSortedExamples {
static class Student {
private final String name;
private final double gpa;
private final int age;
public Student(String name, double gpa, int age) {
this.name = name;
this.gpa = gpa;
this.age = age;
}
public String getName() { return name; }
public double getGpa() { return gpa; }
public int getAge() { return age; }
@Override
public String toString() {
return name + " (GPA: " + gpa + ", age: " + age + ")";
}
}
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("Charlie", 3.5, 22),
new Student("Alice", 3.9, 20),
new Student("Bob", 3.5, 21),
new Student("Diana", 3.8, 23)
);
// --- Sort by single field ---
List<Student> byName = students.stream()
.sorted(Comparator.comparing(Student::getName))
.collect(Collectors.toList());
System.out.println("By name: " + byName);
// Output: By name: [Alice (GPA: 3.9, age: 20), Bob (GPA: 3.5, age: 21),
// Charlie (GPA: 3.5, age: 22), Diana (GPA: 3.8, age: 23)]
// --- Sort by field descending ---
List<Student> byGpaDesc = students.stream()
.sorted(Comparator.comparing(Student::getGpa).reversed())
.collect(Collectors.toList());
System.out.println("By GPA (desc): " + byGpaDesc);
// Output: By GPA (desc): [Alice (GPA: 3.9, age: 20), Diana (GPA: 3.8, age: 23),
// Charlie (GPA: 3.5, age: 22), Bob (GPA: 3.5, age: 21)]
// --- Multi-field sort: GPA descending, then name ascending ---
List<Student> multiSort = students.stream()
.sorted(Comparator.comparing(Student::getGpa).reversed()
.thenComparing(Student::getName))
.collect(Collectors.toList());
System.out.println("By GPA desc, then name: " + multiSort);
// Output: By GPA desc, then name: [Alice (GPA: 3.9, age: 20), Diana (GPA: 3.8, age: 23),
// Bob (GPA: 3.5, age: 21), Charlie (GPA: 3.5, age: 22)]
// --- Natural order for strings ---
List<String> words = Arrays.asList("banana", "apple", "cherry");
words.sort(String::compareToIgnoreCase);
System.out.println("Sorted words: " + words);
// Output: Sorted words: [apple, banana, cherry]
}
}8.4 forEach() — Consuming Elements
import java.util.Arrays;
import java.util.List;
import java.util.Map;
public class StreamForEachExamples {
public static void main(String[] args) {
// --- System.out::println -- the classic method reference ---
List<String> languages = Arrays.asList("Java", "Python", "Go", "Rust");
languages.forEach(System.out::println);
// Output:
// Java
// Python
// Go
// Rust
// --- With a Map ---
Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 87, "Charlie", 92);
// Print each entry -- lambda is needed here for formatting
scores.forEach((name, score) ->
System.out.println(name + ": " + score));
// Output (order may vary):
// Alice: 95
// Bob: 87
// Charlie: 92
// --- Collecting to a list then printing ---
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
System.out.print("Doubled: ");
numbers.stream()
.map(n -> n * 2) // lambda needed (extra logic)
.forEach(System.out::print); // method reference for output
System.out.println();
// Output: Doubled: 246810
}
}8.5 collect() and reduce() Patterns
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class StreamCollectReduceExamples {
static class Order {
private final String product;
private final String category;
private final double amount;
public Order(String product, String category, double amount) {
this.product = product;
this.category = category;
this.amount = amount;
}
public String getProduct() { return product; }
public String getCategory() { return category; }
public double getAmount() { return amount; }
@Override
public String toString() { return product + " ($" + amount + ")"; }
}
public static void main(String[] args) {
List<Order> orders = Arrays.asList(
new Order("Laptop", "Electronics", 999.99),
new Order("Phone", "Electronics", 699.99),
new Order("Shirt", "Clothing", 29.99),
new Order("Pants", "Clothing", 49.99),
new Order("Tablet", "Electronics", 399.99)
);
// --- Grouping by field using method reference ---
Map<String, List<Order>> byCategory = orders.stream()
.collect(Collectors.groupingBy(Order::getCategory));
byCategory.forEach((category, categoryOrders) ->
System.out.println(category + ": " + categoryOrders));
// Output:
// Electronics: [Laptop ($999.99), Phone ($699.99), Tablet ($399.99)]
// Clothing: [Shirt ($29.99), Pants ($49.99)]
// --- Mapping to field, then joining ---
String productList = orders.stream()
.map(Order::getProduct)
.collect(Collectors.joining(", "));
System.out.println("Products: " + productList);
// Output: Products: Laptop, Phone, Shirt, Pants, Tablet
// --- reduce() with method reference ---
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum); // static method reference
System.out.println("Sum: " + sum);
// Output: Sum: 15
int max = numbers.stream()
.reduce(Integer::max) // static method reference
.orElse(0);
System.out.println("Max: " + max);
// Output: Max: 5
}
}9. Common Patterns and Techniques
9.1 Chaining Method References in Comparators
The Comparator class was designed with method references in mind. Its static and default methods (comparing(), thenComparing(), reversed(), nullsFirst(), nullsLast()) all accept method references as key extractors.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
public class ComparatorChaining {
static class Employee {
private final String name;
private final String department;
private final double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
@Override
public String toString() {
return String.format("%-10s %-12s $%,.0f", name, department, salary);
}
}
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("Alice", "Engineering", 105000),
new Employee("Bob", "Marketing", 75000),
new Employee("Charlie", "Engineering", 95000),
new Employee("Diana", "Marketing", 80000),
new Employee("Eve", "Engineering", 105000)
);
// Sort by department, then by salary descending, then by name
List<Employee> sorted = employees.stream()
.sorted(Comparator.comparing(Employee::getDepartment)
.thenComparing(Comparator.comparing(Employee::getSalary).reversed())
.thenComparing(Employee::getName))
.collect(Collectors.toList());
System.out.println("Department | Salary | Name");
System.out.println("--------------------------------------");
sorted.forEach(System.out::println);
// Output:
// Alice Engineering $105,000
// Eve Engineering $105,000
// Charlie Engineering $95,000
// Diana Marketing $80,000
// Bob Marketing $75,000
}
}9.2 Using with Optional
Method references work beautifully with Optional to handle values that may or may not be present.
import java.util.Optional;
public class OptionalMethodReferences {
static class Config {
private final String databaseUrl;
public Config(String databaseUrl) {
this.databaseUrl = databaseUrl;
}
public String getDatabaseUrl() { return databaseUrl; }
}
public static void main(String[] args) {
// --- map() with method reference ---
Optional<String> name = Optional.of(" Alice ");
String trimmed = name
.map(String::trim) // arbitrary object ref
.map(String::toUpperCase) // arbitrary object ref
.orElse("UNKNOWN");
System.out.println("Name: " + trimmed);
// Output: Name: ALICE
// --- orElseGet() with constructor reference ---
Optional<Config> maybeConfig = Optional.empty();
// Instead of: orElseGet(() -> new Config("default"))
// We cannot use a constructor reference here because we need an argument.
// Constructor references work only when the supplier interface matches.
Config config = maybeConfig.orElseGet(() -> new Config("jdbc:h2:mem:default"));
System.out.println("DB URL: " + config.getDatabaseUrl());
// Output: DB URL: jdbc:h2:mem:default
// --- ifPresent() with method reference ---
Optional<String> greeting = Optional.of("Hello, World!");
greeting.ifPresent(System.out::println);
// Output: Hello, World!
// --- filter() with method reference ---
Optional<String> word = Optional.of("Java");
Optional<String> longWord = word.filter(s -> s.length() > 3);
longWord.ifPresent(System.out::println);
// Output: Java
Optional<String> shortWord = word.filter(String::isEmpty);
System.out.println("Short word present? " + shortWord.isPresent());
// Output: Short word present? false
}
}9.3 Factory Methods and the Strategy Pattern
Method references can replace verbose strategy pattern implementations. Instead of creating multiple anonymous classes, you can pass method references that match the required functional interface.
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
public class StrategyWithMethodReferences {
// Different text processing strategies -- each is a simple static method
static class TextProcessor {
public static String toUpperCase(String s) { return s.toUpperCase(); }
public static String toLowerCase(String s) { return s.toLowerCase(); }
public static String trim(String s) { return s.trim(); }
public static String reverse(String s) { return new StringBuilder(s).reverse().toString(); }
}
// A pipeline that accepts a list of strategies
static List<String> process(List<String> input, List<UnaryOperator<String>> strategies) {
List<String> result = input;
for (UnaryOperator<String> strategy : strategies) {
result = result.stream()
.map(strategy)
.collect(Collectors.toList());
}
return result;
}
public static void main(String[] args) {
List<String> data = Arrays.asList(" Hello ", " World ", " Java ");
// Build a pipeline of strategies using method references
List<UnaryOperator<String>> pipeline = Arrays.asList(
TextProcessor::trim, // step 1: trim whitespace
TextProcessor::toUpperCase, // step 2: convert to uppercase
TextProcessor::reverse // step 3: reverse
);
List<String> result = process(data, pipeline);
System.out.println(result);
// Output: [OLLEH, DLROW, AVAJ]
}
}10. Common Mistakes
Method references are concise, but they introduce subtle pitfalls. Here are the mistakes developers encounter most often.
10.1 Trying to Add Logic to a Method Reference
A method reference is a direct pointer to a method. You cannot embed extra logic, conditions, or transformations inside it. If you need any additional behavior, use a lambda.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MistakeExtraLogic {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// WRONG -- cannot add logic to a method reference
// names.stream().map(String::toUpperCase + "!").collect(...) // compile error
// CORRECT -- use a lambda when extra logic is needed
List<String> shouting = names.stream()
.map(name -> name.toUpperCase() + "!")
.collect(Collectors.toList());
System.out.println(shouting);
// Output: [ALICE!, BOB!, CHARLIE!]
// WRONG -- cannot negate inside a method reference
// names.stream().filter(!String::isEmpty) // compile error
// CORRECT -- use a lambda for negation (or Predicate.not() in Java 11+)
List<String> nonEmpty = names.stream()
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
System.out.println(nonEmpty);
// Output: [Alice, Bob, Charlie]
}
}10.2 Ambiguous Method References with Overloaded Methods
When a class has overloaded methods (same name, different parameters), the compiler may not be able to determine which one you mean. This results in a compile-time error.
import java.util.function.Function;
import java.util.function.BiFunction;
public class MistakeAmbiguousOverload {
// Two overloaded static methods
public static String format(int number) {
return "Number: " + number;
}
public static String format(String text) {
return "Text: " + text;
}
public static void main(String[] args) {
// This works -- compiler can infer which 'format' based on Function<Integer, String>
Function<Integer, String> intFormatter = MistakeAmbiguousOverload::format;
System.out.println(intFormatter.apply(42));
// Output: Number: 42
// This also works -- different functional interface type resolves the overload
Function<String, String> strFormatter = MistakeAmbiguousOverload::format;
System.out.println(strFormatter.apply("hello"));
// Output: Text: hello
// The compiler resolves overloads based on the target functional interface.
// Problems arise when the target type is ambiguous itself.
// Example of a potentially confusing situation:
// If a method accepts both Function<Integer, String> and Function<String, String>,
// you would need to cast:
// processData((Function<Integer, String>) MistakeAmbiguousOverload::format);
}
}10.3 Forgetting That Method References Must Match the Functional Interface
The referenced method must have a compatible signature with the functional interface’s abstract method. Parameter types, count, and return type must all be compatible.
import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.function.Supplier;
public class MistakeSignatureMismatch {
public static int add(int a, int b) {
return a + b;
}
public static String greet(String name) {
return "Hello, " + name;
}
public static void main(String[] args) {
// CORRECT: add(int, int) -> int matches BiFunction<Integer, Integer, Integer>
BiFunction<Integer, Integer, Integer> adder = MistakeSignatureMismatch::add;
System.out.println(adder.apply(3, 4));
// Output: 7
// WRONG: add(int, int) does NOT match Function<Integer, Integer>
// Function<Integer, Integer> wrongAdder = MistakeSignatureMismatch::add;
// Compile error: method add in class MistakeSignatureMismatch cannot be applied to given types
// CORRECT: greet(String) -> String matches Function<String, String>
Function<String, String> greeter = MistakeSignatureMismatch::greet;
System.out.println(greeter.apply("Alice"));
// Output: Hello, Alice
// WRONG: greet(String) does NOT match Supplier<String> (Supplier takes no args)
// Supplier<String> wrongGreeter = MistakeSignatureMismatch::greet;
// Compile error: method greet expects 1 argument but Supplier provides 0
}
}10.4 Confusing Static and Arbitrary-Object References
Because both static method references and arbitrary-object references use the ClassName::methodName syntax, developers sometimes confuse them. Remember: the compiler distinguishes them based on whether the method is static or not.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MistakeStaticVsArbitrary {
public static void main(String[] args) {
List<String> words = Arrays.asList("hello", "world");
// String::toUpperCase -- INSTANCE method on String
// This is an ARBITRARY OBJECT reference: s -> s.toUpperCase()
// The stream element becomes the object
List<String> upper = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper);
// Output: [HELLO, WORLD]
// Integer::parseInt -- STATIC method on Integer
// This is a STATIC method reference: s -> Integer.parseInt(s)
// The stream element is passed as an argument
List<String> nums = Arrays.asList("1", "2", "3");
List<Integer> parsed = nums.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());
System.out.println(parsed);
// Output: [1, 2, 3]
// Both use ClassName::methodName syntax, but:
// - String::toUpperCase => arbitrary object (toUpperCase is an instance method)
// - Integer::parseInt => static reference (parseInt is a static method)
// The compiler knows the difference. You should too.
}
}10.5 Using Method References on a Null Object
If you create a method reference on a particular object and that object is null, you will get a NullPointerException at the point where the method reference is created (not when it is called).
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class MistakeNullObject {
public static void main(String[] args) {
String validString = "Hello";
Consumer<String> validRef = System.out::println; // fine
validRef.accept(validString);
// Output: Hello
// Dangerous: if the object is null, creating the method reference throws NPE
String nullString = null;
try {
// This throws NullPointerException when the reference is CREATED
// because you are trying to reference a method on a null object
Consumer<Integer> nullRef = nullString::charAt;
// NullPointerException is thrown on the line above, not on the line below
nullRef.accept(0);
} catch (NullPointerException e) {
System.out.println("Caught NPE: " + e.getMessage());
// Output: Caught NPE: null
}
// Safe approach: validate before creating the reference
List<String> items = Arrays.asList("Java", null, "Python");
items.stream()
.filter(s -> s != null) // filter nulls first
.map(String::toUpperCase) // now safe to use method reference
.forEach(System.out::println);
// Output:
// JAVA
// PYTHON
}
}11. Best Practices
Follow these guidelines to use method references effectively in production code.
| # | Practice | Do | Avoid |
|---|---|---|---|
| 1 | Prefer method references when they improve readability | map(String::trim) |
map(s -> s.trim()) when no extra logic |
| 2 | Use lambdas for multi-step operations | map(s -> s.trim().toUpperCase()) |
Trying to chain method references inside map() |
| 3 | Let the method name tell the story | filter(Objects::nonNull) |
filter(o -> o != null) |
| 4 | Extract complex lambdas into named methods | filter(this::isEligible) |
A 5-line lambda inline |
| 5 | Use Comparator.comparing() with method references |
sorted(comparing(Person::getAge)) |
sorted((a, b) -> a.getAge() - b.getAge()) |
| 6 | Use Predicate.not() in Java 11+ |
filter(Predicate.not(String::isEmpty)) |
filter(s -> !s.isEmpty()) |
| 7 | Use toArray(Type[]::new) for stream-to-array |
toArray(String[]::new) |
toArray(size -> new String[size]) |
| 8 | Be consistent within a pipeline | All method refs or all lambdas for similar steps | Mixing for no reason |
11.1 Extract Complex Lambdas Into Named Methods
If a lambda is too complex to express as a single method reference but too verbose to inline, the best approach is to extract the logic into a well-named private method and then reference it.
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ExtractToNamedMethod {
// Before: inline lambda with complex logic
static List<String> processNamesInline(List<String> names) {
return names.stream()
.filter(name -> name != null && !name.isBlank()
&& name.length() >= 2 && name.length() <= 50
&& Character.isLetter(name.charAt(0)))
.map(name -> name.substring(0, 1).toUpperCase()
+ name.substring(1).toLowerCase().trim())
.collect(Collectors.toList());
}
// After: extract into named methods, then use method references
private static boolean isValidName(String name) {
return name != null && !name.isBlank()
&& name.length() >= 2 && name.length() <= 50
&& Character.isLetter(name.charAt(0));
}
private static String capitalizeName(String name) {
String trimmed = name.trim();
return trimmed.substring(0, 1).toUpperCase()
+ trimmed.substring(1).toLowerCase();
}
static List<String> processNamesClean(List<String> names) {
return names.stream()
.filter(ExtractToNamedMethod::isValidName) // reads like English
.map(ExtractToNamedMethod::capitalizeName) // clear intent
.collect(Collectors.toList());
}
public static void main(String[] args) {
List<String> names = Arrays.asList(
"aLICE", " ", null, "bOB", "x", "cHARLIE", "123bad"
);
System.out.println("Inline: " + processNamesInline(names));
// Output: Inline: [Alice, Bob, Charlie]
System.out.println("Clean: " + processNamesClean(names));
// Output: Clean: [Alice, Bob, Charlie]
}
}12. Complete Practical Example
Let us tie everything together with a realistic data processing pipeline that uses all four types of method references. We will build an employee report system that reads raw CSV-style input, parses it into objects, transforms and sorts the data, and produces a summary report.
This example demonstrates:
- Static method reference — parsing strings into objects (
Employee::fromCsv) - Instance method of a particular object — using a formatter object (
formatter::format) - Instance method of an arbitrary object — extracting fields (
Employee::getSalary,Employee::getName) - Constructor reference — creating department summary objects (
DepartmentSummary::new) and output arrays (String[]::new)
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
public class EmployeeReportSystem {
// ========== Domain Classes ==========
static class Employee {
private final String name;
private final String department;
private final double salary;
public Employee(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
// STATIC METHOD -- used as a static method reference
public static Employee fromCsv(String csv) {
String[] parts = csv.split(",");
return new Employee(
parts[0].trim(),
parts[1].trim(),
Double.parseDouble(parts[2].trim())
);
}
// INSTANCE METHODS -- used as arbitrary-object method references
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public boolean isHighEarner() {
return salary >= 90000;
}
@Override
public String toString() {
return String.format("%-12s %-15s $%,.2f", name, department, salary);
}
}
static class DepartmentSummary {
private final String department;
private final long count;
private final double totalSalary;
// CONSTRUCTOR -- used as a constructor reference
public DepartmentSummary(String department, List<Employee> employees) {
this.department = department;
this.count = employees.size();
this.totalSalary = employees.stream()
.mapToDouble(Employee::getSalary)
.sum();
}
public String getDepartment() { return department; }
public long getCount() { return count; }
public double getTotalSalary() { return totalSalary; }
public double getAvgSalary() { return totalSalary / count; }
@Override
public String toString() {
return String.format("%-15s %d employees, avg $%,.2f",
department, count, getAvgSalary());
}
}
// A formatter instance -- used as a particular-object method reference
static class ReportFormatter {
private final String companyName;
public ReportFormatter(String companyName) {
this.companyName = companyName;
}
public String format(Employee emp) {
return String.format("[%s] %s - %s ($%,.2f)",
companyName, emp.getName(), emp.getDepartment(), emp.getSalary());
}
}
// ========== Main Pipeline ==========
public static void main(String[] args) {
// Raw input data (simulating CSV lines)
List<String> rawData = Arrays.asList(
"Alice, Engineering, 105000",
"Bob, Marketing, 72000",
"Charlie, Engineering, 95000",
"Diana, Marketing, 88000",
"Eve, Engineering, 115000",
"Frank, Sales, 67000",
"Grace, Sales, 71000",
"Henry, Engineering, 98000"
);
// ---- STEP 1: Parse using STATIC method reference ----
// Employee::fromCsv is a static method: (String) -> Employee
List<Employee> employees = rawData.stream()
.map(Employee::fromCsv) // static method reference
.collect(Collectors.toList());
System.out.println("=== ALL EMPLOYEES ===");
employees.forEach(System.out::println); // particular object (System.out)
System.out.println();
// ---- STEP 2: Filter using ARBITRARY OBJECT method reference ----
// Employee::isHighEarner: the stream element calls its own isHighEarner()
List<Employee> highEarners = employees.stream()
.filter(Employee::isHighEarner) // arbitrary object method reference
.collect(Collectors.toList());
System.out.println("=== HIGH EARNERS (>= $90,000) ===");
highEarners.forEach(System.out::println);
System.out.println();
// ---- STEP 3: Extract names using ARBITRARY OBJECT method reference ----
String[] nameArray = employees.stream()
.map(Employee::getName) // arbitrary object method reference
.sorted(String::compareTo) // arbitrary object method reference
.toArray(String[]::new); // array constructor reference
System.out.println("=== SORTED NAMES (as array) ===");
System.out.println(Arrays.toString(nameArray));
System.out.println();
// ---- STEP 4: Sort using Comparator with method references ----
List<Employee> sortedBySalary = employees.stream()
.sorted(Comparator.comparing(Employee::getSalary).reversed()
.thenComparing(Employee::getName))
.collect(Collectors.toList());
System.out.println("=== SORTED BY SALARY (DESC) ===");
sortedBySalary.forEach(System.out::println);
System.out.println();
// ---- STEP 5: Format using PARTICULAR OBJECT method reference ----
ReportFormatter formatter = new ReportFormatter("Acme Corp");
List<String> formattedReport = employees.stream()
.filter(Employee::isHighEarner)
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.map(formatter::format) // particular object method reference
.collect(Collectors.toList());
System.out.println("=== FORMATTED HIGH-EARNER REPORT ===");
formattedReport.forEach(System.out::println);
System.out.println();
// ---- STEP 6: Group and summarize using CONSTRUCTOR reference ----
Map<String, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
List<DepartmentSummary> summaries = byDepartment.entrySet().stream()
.map(entry -> new DepartmentSummary(entry.getKey(), entry.getValue()))
.sorted(Comparator.comparing(DepartmentSummary::getAvgSalary).reversed())
.collect(Collectors.toList());
System.out.println("=== DEPARTMENT SUMMARIES ===");
summaries.forEach(System.out::println);
System.out.println();
// ---- STEP 7: Statistics using method references with reduce ----
double totalPayroll = employees.stream()
.map(Employee::getSalary)
.reduce(0.0, Double::sum); // static method reference
Optional<Employee> highestPaid = employees.stream()
.max(Comparator.comparing(Employee::getSalary));
Optional<Employee> lowestPaid = employees.stream()
.min(Comparator.comparing(Employee::getSalary));
System.out.println("=== PAYROLL STATISTICS ===");
System.out.printf("Total payroll: $%,.2f%n", totalPayroll);
System.out.printf("Average salary: $%,.2f%n", totalPayroll / employees.size());
highestPaid.ifPresent(e ->
System.out.printf("Highest paid: %s ($%,.2f)%n", e.getName(), e.getSalary()));
lowestPaid.ifPresent(e ->
System.out.printf("Lowest paid: %s ($%,.2f)%n", e.getName(), e.getSalary()));
}
}
// ========== OUTPUT ==========
// === ALL EMPLOYEES ===
// Alice Engineering $105,000.00
// Bob Marketing $72,000.00
// Charlie Engineering $95,000.00
// Diana Marketing $88,000.00
// Eve Engineering $115,000.00
// Frank Sales $67,000.00
// Grace Sales $71,000.00
// Henry Engineering $98,000.00
//
// === HIGH EARNERS (>= $90,000) ===
// Alice Engineering $105,000.00
// Charlie Engineering $95,000.00
// Eve Engineering $115,000.00
// Henry Engineering $98,000.00
//
// === SORTED NAMES (as array) ===
// [Alice, Bob, Charlie, Diana, Eve, Frank, Grace, Henry]
//
// === SORTED BY SALARY (DESC) ===
// Eve Engineering $115,000.00
// Alice Engineering $105,000.00
// Henry Engineering $98,000.00
// Charlie Engineering $95,000.00
// Diana Marketing $88,000.00
// Bob Marketing $72,000.00
// Grace Sales $71,000.00
// Frank Sales $67,000.00
//
// === FORMATTED HIGH-EARNER REPORT ===
// [Acme Corp] Eve - Engineering ($115,000.00)
// [Acme Corp] Alice - Engineering ($105,000.00)
// [Acme Corp] Henry - Engineering ($98,000.00)
// [Acme Corp] Charlie - Engineering ($95,000.00)
//
// === DEPARTMENT SUMMARIES ===
// Engineering 4 employees, avg $103,250.00
// Marketing 2 employees, avg $80,000.00
// Sales 2 employees, avg $69,000.00
//
// === PAYROLL STATISTICS ===
// Total payroll: $711,000.00
// Average salary: $88,875.00
// Highest paid: Eve ($115,000.00)
// Lowest paid: Frank ($67,000.00)Method Reference Types Used in This Example
| Type | Usage in Example | Line |
|---|---|---|
| Static method | Employee::fromCsv — parses CSV string into Employee |
map(Employee::fromCsv) |
| Static method | Double::sum — adds two doubles in reduce |
reduce(0.0, Double::sum) |
| Particular object | System.out::println — prints to console |
forEach(System.out::println) |
| Particular object | formatter::format — formats using company name |
map(formatter::format) |
| Arbitrary object | Employee::getName — extracts name from each employee |
map(Employee::getName) |
| Arbitrary object | Employee::getSalary — extracts salary for sorting |
Comparator.comparing(Employee::getSalary) |
| Arbitrary object | Employee::isHighEarner — filters high earners |
filter(Employee::isHighEarner) |
| Arbitrary object | Employee::getDepartment — groups by department |
Collectors.groupingBy(Employee::getDepartment) |
| Arbitrary object | String::compareTo — sorts strings naturally |
sorted(String::compareTo) |
| Array constructor | String[]::new — converts stream to String array |
toArray(String[]::new) |
Quick Reference
| Concept | Summary |
|---|---|
| What it is | Shorthand for a lambda that calls a single existing method |
| Operator | :: (double colon) |
| Static reference | ClassName::staticMethod — args passed to static method |
| Particular object | instance::method — args passed to method on that specific instance |
| Arbitrary object | ClassName::instanceMethod — first arg becomes the object, rest are method args |
| Constructor | ClassName::new — args passed to matching constructor |
| Array constructor | Type[]::new — takes size, returns new array |
| No parentheses | Write String::length, NOT String::length() |
| Must match signature | Referenced method must be compatible with the functional interface |
| When to prefer | When the lambda does nothing except call a single method |
| When to use lambda | When you need extra logic, multiple statements, or additional arguments |