Imagine you are giving someone directions. You could say: “Walk to the kitchen, open the drawer, grab a knife, and cut the bread.” Or you could simply say: “Cut the bread” — because the person already knows the steps. A method reference is that shorter instruction. Instead of writing out the full steps in a lambda expression, you point directly to a method that already does the job.
A method reference is a shorthand notation introduced in Java 8 that lets you refer to an existing method by name using the :: operator (double colon). It produces the same result as a lambda expression that does nothing more than call that one method.
Key concept: If a lambda expression simply passes its arguments to an existing method without adding any logic, you can replace it with a method reference for cleaner, more readable code.
There are four types of method references in Java:
| # | Type | Syntax | Example |
|---|---|---|---|
| 1 | Reference to a static method | ClassName::staticMethod |
Integer::parseInt |
| 2 | Reference to an instance method of a particular object | instance::method |
System.out::println |
| 3 | Reference to an instance method of an arbitrary object of a particular type | ClassName::instanceMethod |
String::toLowerCase |
| 4 | Reference to a constructor | ClassName::new |
ArrayList::new |
Before diving into each type, here is a quick side-by-side to see why method references exist:
import java.util.Arrays;
import java.util.List;
public class MethodReferenceIntro {
public static void main(String[] args) {
List names = Arrays.asList("Alice", "Bob", "Charlie");
// Lambda expression -- spells out each step
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
}
}
The lambda name -> System.out.println(name) does nothing but pass its parameter to println. The method reference System.out::println says the same thing more directly. The compiler knows the parameter type from context, so no explicit argument is needed.
When to use method references:
When NOT to use method references:
The simplest form of method reference points to a static method on a class. The syntax is ClassName::staticMethodName.
How it works: The compiler sees the functional interface’s abstract method signature, finds the matching static method, and wires them together. The lambda’s parameters become the static method’s arguments.
| Lambda Expression | Method Reference | Explanation |
|---|---|---|
s -> Integer.parseInt(s) |
Integer::parseInt |
String argument passed to parseInt |
d -> Math.abs(d) |
Math::abs |
Double argument passed to abs |
s -> String.valueOf(s) |
String::valueOf |
Object argument passed to valueOf |
(a, b) -> Math.max(a, b) |
Math::max |
Two arguments passed to max |
(list) -> Collections.unmodifiableList(list) |
Collections::unmodifiableList |
List argument passed to unmodifiableList |
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class StaticMethodRef {
public static void main(String[] args) {
// Example 1: Integer::parseInt -- convert strings to integers
List numberStrings = Arrays.asList("1", "2", "3", "4", "5");
List numbers = numberStrings.stream()
.map(Integer::parseInt) // same as: s -> Integer.parseInt(s)
.collect(Collectors.toList());
System.out.println("Parsed integers: " + numbers);
// Output: Parsed integers: [1, 2, 3, 4, 5]
// Example 2: Math::abs -- absolute values
List values = Arrays.asList(-5, 3, -1, 7, -4);
List absolutes = values.stream()
.map(Math::abs) // same as: n -> Math.abs(n)
.collect(Collectors.toList());
System.out.println("Absolute values: " + absolutes);
// Output: Absolute values: [5, 3, 1, 7, 4]
// Example 3: String::valueOf -- convert objects to strings
List
Here is a practical example using Collections.sort with a static helper method:
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class StaticMethodRefSort {
// Custom static comparison method
public static int compareByLength(String a, String b) {
return Integer.compare(a.length(), b.length());
}
public static void main(String[] args) {
List words = Arrays.asList("elephant", "cat", "dog", "butterfly", "ant");
// Lambda version
// Collections.sort(words, (a, b) -> StaticMethodRefSort.compareByLength(a, b));
// Method reference version
Collections.sort(words, StaticMethodRefSort::compareByLength);
System.out.println("Sorted by length: " + words);
// Output: Sorted by length: [cat, dog, ant, elephant, butterfly]
}
}
This type of method reference calls an instance method on a specific, known object. The syntax is objectReference::instanceMethod.
How it works: You have a particular object already in scope. The lambda’s parameter(s) become the argument(s) to that object’s method. The most common example is System.out::println, where System.out is the specific PrintStream object.
Key distinction: The object is determined before the method reference is used. It is captured just like a variable in a lambda.
import java.util.Arrays;
import java.util.List;
public class ParticularObjectMethodRef {
public static void main(String[] args) {
// Example 1: System.out::println -- the most common method reference
List fruits = Arrays.asList("Apple", "Banana", "Cherry");
// Lambda version
fruits.forEach(fruit -> System.out.println(fruit));
// Method reference version
fruits.forEach(System.out::println);
// Output (each call):
// Apple
// Banana
// Cherry
// Example 2: Instance method on a custom object
String prefix = "Hello, ";
List names = Arrays.asList("Alice", "Bob", "Charlie");
// Lambda version
names.forEach(name -> System.out.println(prefix.concat(name)));
// NOTE: prefix::concat returns the concatenated string but does not print it.
// So we need to chain or use lambda when extra logic is needed.
names.stream()
.map(prefix::concat) // same as: name -> prefix.concat(name)
.forEach(System.out::println);
// Output:
// Hello, Alice
// Hello, Bob
// Hello, Charlie
}
}
A more practical example with a custom object:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ParticularObjectAdvanced {
static class TextFormatter {
private final String delimiter;
TextFormatter(String delimiter) {
this.delimiter = delimiter;
}
public String wrap(String text) {
return delimiter + text + delimiter;
}
public boolean isLongEnough(String text) {
return text.length() > 3;
}
}
public static void main(String[] args) {
TextFormatter quoter = new TextFormatter("\"");
TextFormatter stars = new TextFormatter("***");
List words = Arrays.asList("Hi", "Java", "is", "awesome", "OK");
// Use the specific quoter object's wrap method
List quoted = words.stream()
.map(quoter::wrap) // same as: w -> quoter.wrap(w)
.collect(Collectors.toList());
System.out.println("Quoted: " + quoted);
// Output: Quoted: ["Hi", "Java", "is", "awesome", "OK"]
// Use the specific stars object's wrap method
List starred = words.stream()
.map(stars::wrap) // same as: w -> stars.wrap(w)
.collect(Collectors.toList());
System.out.println("Starred: " + starred);
// Output: Starred: [***Hi***, ***Java***, ***is***, ***awesome***, ***OK***]
// Use the quoter object's filter method
List longWords = words.stream()
.filter(quoter::isLongEnough) // same as: w -> quoter.isLongEnough(w)
.collect(Collectors.toList());
System.out.println("Long words: " + longWords);
// Output: Long words: [Java, awesome]
}
}
This is the trickiest type to understand. The syntax looks like a static method reference — ClassName::instanceMethod — but it calls an instance method. The difference is that the first parameter of the lambda becomes the object on which the method is called.
How it works:
s -> s.toLowerCase() becomes String::toLowerCase. The stream element itself is the object.(a, b) -> a.compareTo(b) becomes String::compareTo. The first parameter is the object, the second is the argument.The rule: If the first parameter of the lambda is the receiver (the object on which the method is called), and the remaining parameters (if any) are the method arguments, you can use ClassName::instanceMethod.
| Lambda | Method Reference | First Param Becomes |
|---|---|---|
s -> s.toLowerCase() |
String::toLowerCase |
The receiver (s.toLowerCase()) |
s -> s.length() |
String::length |
The receiver (s.length()) |
(a, b) -> a.compareTo(b) |
String::compareTo |
The receiver (a), b becomes the argument |
s -> s.isEmpty() |
String::isEmpty |
The receiver (s.isEmpty()) |
s -> s.trim() |
String::trim |
The receiver (s.trim()) |
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class ArbitraryObjectMethodRef {
public static void main(String[] args) {
// Example 1: String::toLowerCase -- each string in the stream calls its own toLowerCase
List names = Arrays.asList("ALICE", "BOB", "CHARLIE");
List lower = names.stream()
.map(String::toLowerCase) // same as: s -> s.toLowerCase()
.collect(Collectors.toList());
System.out.println("Lowercase: " + lower);
// Output: Lowercase: [alice, bob, charlie]
// Example 2: String::length -- each string calls its own length()
List lengths = names.stream()
.map(String::length) // same as: s -> s.length()
.collect(Collectors.toList());
System.out.println("Lengths: " + lengths);
// Output: Lengths: [5, 3, 7]
// Example 3: String::trim -- each string calls its own trim()
List messy = Arrays.asList(" hello ", " world ", " java ");
List trimmed = messy.stream()
.map(String::trim) // same as: s -> s.trim()
.collect(Collectors.toList());
System.out.println("Trimmed: " + trimmed);
// Output: Trimmed: [hello, world, java]
// Example 4: String::compareTo -- used for sorting
List cities = Arrays.asList("Denver", "Austin", "Chicago", "Boston");
cities.sort(String::compareTo); // same as: (a, b) -> a.compareTo(b)
System.out.println("Sorted: " + cities);
// Output: Sorted: [Austin, Boston, Chicago, Denver]
// Example 5: String::isEmpty -- used for filtering
List mixed = Arrays.asList("Hello", "", "World", "", "Java");
List nonEmpty = mixed.stream()
.filter(s -> !s.isEmpty()) // Cannot use method ref for negation
.collect(Collectors.toList());
System.out.println("Non-empty: " + nonEmpty);
// Output: Non-empty: [Hello, World, Java]
}
}
Important: Notice Example 5 above. When you need to negate a method reference (!s.isEmpty()), you cannot use String::isEmpty directly. You need either the lambda or Predicate.not(String::isEmpty) (Java 11+):
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class PredicateNotExample {
public static void main(String[] args) {
List mixed = Arrays.asList("Hello", "", "World", "", "Java");
// Java 11+: Predicate.not() enables method reference for negation
List nonEmpty = mixed.stream()
.filter(Predicate.not(String::isEmpty))
.collect(Collectors.toList());
System.out.println("Non-empty: " + nonEmpty);
// Output: Non-empty: [Hello, World, Java]
}
}
A constructor reference uses the syntax ClassName::new. It creates new objects by pointing to a constructor, just like other method references point to methods.
How it works: The compiler matches the functional interface’s abstract method signature to the appropriate constructor. If the interface takes one String parameter, the compiler finds the constructor that takes one String parameter.
| Lambda | Constructor Reference | Which Constructor |
|---|---|---|
() -> new ArrayList<>() |
ArrayList::new |
No-arg constructor |
s -> new StringBuilder(s) |
StringBuilder::new |
String parameter constructor |
n -> new int[n] |
int[]::new |
Array constructor |
s -> new File(s) |
File::new |
String parameter constructor |
import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.function.Function;
import java.util.function.Supplier;
public class ConstructorRefExamples {
public static void main(String[] args) {
// Example 1: Supplier -- no-arg constructor
Supplier> listFactory = ArrayList::new; // same as: () -> new ArrayList<>()
List newList = listFactory.get();
newList.add("Created via constructor reference");
System.out.println(newList);
// Output: [Created via constructor reference]
// Example 2: Function -- single-arg constructor
Function sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.apply("Hello, Java!");
System.out.println(sb);
// Output: Hello, Java!
// Example 3: Stream.toArray with array constructor reference
List names = Arrays.asList("Alice", "Bob", "Charlie");
// Without constructor reference -- returns Object[]
Object[] objectArray = names.stream().toArray();
// With constructor reference -- returns String[]
String[] stringArray = names.stream().toArray(String[]::new);
System.out.println("Array type: " + stringArray.getClass().getSimpleName());
System.out.println("Array: " + Arrays.toString(stringArray));
// Output: Array type: String[]
// Output: Array: [Alice, Bob, Charlie]
// Example 4: Creating objects from a stream of data
List numberStrings = Arrays.asList("100", "200", "300");
List integers = numberStrings.stream()
.map(Integer::new) // same as: s -> new Integer(s)
.collect(Collectors.toList());
System.out.println("Integers: " + integers);
// Output: Integers: [100, 200, 300]
// Note: Integer::new is deprecated since Java 9, prefer Integer::valueOf
// Example 5: Better approach using valueOf
List integersModern = numberStrings.stream()
.map(Integer::valueOf) // preferred over Integer::new
.collect(Collectors.toList());
System.out.println("Integers (valueOf): " + integersModern);
// Output: Integers (valueOf): [100, 200, 300]
}
}
Constructor references with custom classes:
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
public class CustomConstructorRef {
static class Person {
private final String name;
private final int age;
// No-arg constructor
Person() {
this.name = "Unknown";
this.age = 0;
}
// Single-arg constructor
Person(String name) {
this.name = name;
this.age = 0;
}
// Two-arg constructor
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return name + " (age " + age + ")";
}
}
// A functional interface for three-arg constructor
@FunctionalInterface
interface TriFunction {
R apply(A a, B b, C c);
}
public static void main(String[] args) {
// Single-arg constructor reference
Function personByName = Person::new;
Person alice = personByName.apply("Alice");
System.out.println(alice);
// Output: Alice (age 0)
// Two-arg constructor reference
BiFunction personByNameAge = Person::new;
Person bob = personByNameAge.apply("Bob", 30);
System.out.println(bob);
// Output: Bob (age 30)
// Using constructor reference in streams
List names = Arrays.asList("Charlie", "Diana", "Eve");
List people = names.stream()
.map(Person::new) // calls Person(String name)
.collect(Collectors.toList());
people.forEach(System.out::println);
// Output:
// Charlie (age 0)
// Diana (age 0)
// Eve (age 0)
}
}
Method references and lambda expressions are interchangeable when the lambda simply delegates to an existing method. But they are not always interchangeable, and one is not always better than the other. Here is a detailed comparison:
| Aspect | Lambda Expression | Method Reference |
|---|---|---|
| Syntax | (params) -> expression |
ClassName::method |
| Readability | Shows the how | Shows the what |
| Flexibility | Can contain any logic | Can only point to one method |
| Multi-step logic | Supports multiple statements | Not supported |
| Extra arguments | Can pass extra arguments | Cannot pass extra arguments |
| Negation | s -> !s.isEmpty() |
Needs Predicate.not() (Java 11+) |
| Debugging | Stack trace shows lambda$main$0 | Stack trace shows actual method name |
| Performance | Identical at runtime | Identical at runtime |
| IDE Support | IDEs suggest converting to method ref | IDEs can expand to lambda |
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.function.BinaryOperator;
import java.util.ArrayList;
public class LambdaVsMethodRef {
public static void main(String[] args) {
// --- CAN be converted to method reference ---
// 1. Static method
Function f1_lambda = s -> Integer.parseInt(s);
Function f1_ref = Integer::parseInt;
// 2. Instance method on particular object
Consumer c1_lambda = s -> System.out.println(s);
Consumer c1_ref = System.out::println;
// 3. Instance method on arbitrary object
Function f2_lambda = s -> s.toUpperCase();
Function f2_ref = String::toUpperCase;
// 4. Constructor
Supplier> s1_lambda = () -> new ArrayList<>();
Supplier> s1_ref = ArrayList::new;
// 5. Two-arg instance method
BinaryOperator b1_lambda = (a, b) -> a.concat(b);
BinaryOperator b1_ref = String::concat;
// --- CANNOT be converted to method reference ---
// 1. Extra logic in the lambda
Function noRef1 = s -> s.toUpperCase().trim();
// Cannot be a single method reference -- two operations
// 2. Extra arguments not from lambda parameters
int multiplier = 3;
Function noRef2 = n -> n * multiplier;
// No method to reference -- it is custom arithmetic
// 3. Negation
Predicate noRef3 = s -> !s.isEmpty();
// Cannot negate with method reference (unless Java 11+ Predicate.not)
// 4. Conditional logic
Function noRef4 = n -> n > 0 ? "positive" : "non-positive";
// Cannot express ternary as a method reference
System.out.println("Conversions demonstrated successfully.");
// Output: Conversions demonstrated successfully.
}
}
Method references are not always the right choice. Here are situations where lambdas are clearer:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class WhenLambdaIsBetter {
public static void main(String[] args) {
List names = Arrays.asList("Alice", "Bob", "Charlie", "Diana");
// Lambda is clearer when you need to add context
// CONFUSING method reference with long class name:
// names.stream().map(WhenLambdaIsBetter::formatGreeting).collect(Collectors.toList());
// CLEAR lambda:
List greetings = names.stream()
.map(name -> "Hello, " + name + "!")
.collect(Collectors.toList());
System.out.println(greetings);
// Output: [Hello, Alice!, Hello, Bob!, Hello, Charlie!, Hello, Diana!]
// Lambda is better when extracting a field from a complex object
// This reads more naturally:
List lengths = names.stream()
.map(name -> name.length() + 10) // added logic -- can't be method ref
.collect(Collectors.toList());
System.out.println(lengths);
// Output: [15, 13, 17, 15]
// Lambda is better for multi-step operations
List processed = names.stream()
.map(name -> {
String upper = name.toUpperCase();
return "[" + upper + "]";
})
.collect(Collectors.toList());
System.out.println(processed);
// Output: [[ALICE], [BOB], [CHARLIE], [DIANA]]
}
}
Method references become especially powerful when used with the Stream API. Every stream operation that takes a functional interface — map, filter, forEach, sorted, flatMap, reduce — can often be written more concisely with method references.
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class MethodRefWithStreams {
public static void main(String[] args) {
List words = Arrays.asList("hello", "WORLD", "Java", "streams", "METHOD");
// --- map() with method reference ---
List upper = words.stream()
.map(String::toUpperCase) // each element calls its own toUpperCase()
.collect(Collectors.toList());
System.out.println("Uppercase: " + upper);
// Output: Uppercase: [HELLO, WORLD, JAVA, STREAMS, METHOD]
// --- filter() with method reference (Java 11+ for isBlank) ---
List withBlanks = Arrays.asList("Hello", "", " ", "World", "");
List nonBlank = withBlanks.stream()
.filter(s -> !s.isBlank()) // negation still needs lambda or Predicate.not
.collect(Collectors.toList());
System.out.println("Non-blank: " + nonBlank);
// Output: Non-blank: [Hello, World]
// --- forEach() with method reference ---
System.out.print("Names: ");
List names = Arrays.asList("Alice", "Bob", "Charlie");
names.forEach(System.out::println);
// Output:
// Alice
// Bob
// Charlie
// --- sorted() with method reference ---
List cities = Arrays.asList("Denver", "Austin", "Chicago");
List sorted = cities.stream()
.sorted(String::compareToIgnoreCase) // case-insensitive sort
.collect(Collectors.toList());
System.out.println("Sorted: " + sorted);
// Output: Sorted: [Austin, Chicago, Denver]
// --- flatMap() with method reference ---
List> nested = Arrays.asList(
Arrays.asList("A", "B"),
Arrays.asList("C", "D"),
Arrays.asList("E", "F")
);
List flat = nested.stream()
.flatMap(Collection::stream) // same as: list -> list.stream()
.collect(Collectors.toList());
System.out.println("Flat: " + flat);
// Output: Flat: [A, B, C, D, E, F]
// --- reduce() with method reference ---
List numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum); // same as: (a, b) -> Integer.sum(a, b)
System.out.println("Sum: " + sum);
// Output: Sum: 15
}
}
A more realistic stream pipeline combining multiple method references:
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class StreamPipelineMethodRef {
static class Product {
private final String name;
private final double price;
private final String category;
Product(String name, double price, String category) {
this.name = name;
this.price = price;
this.category = category;
}
public String getName() { return name; }
public double getPrice() { return price; }
public String getCategory() { return category; }
public static boolean isAffordable(Product p) {
return p.getPrice() < 50.0;
}
public String toDisplayString() {
return String.format("%s ($%.2f)", name, price);
}
@Override
public String toString() {
return name + " - $" + price;
}
}
public static void main(String[] args) {
List products = Arrays.asList(
new Product("Keyboard", 29.99, "Electronics"),
new Product("Mouse", 19.99, "Electronics"),
new Product("Monitor", 299.99, "Electronics"),
new Product("Notebook", 4.99, "Office"),
new Product("Pen", 1.99, "Office"),
new Product("Desk Lamp", 45.00, "Office")
);
// Pipeline using method references
List affordableProducts = products.stream()
.filter(Product::isAffordable) // static method reference
.map(Product::toDisplayString) // arbitrary object method reference
.collect(Collectors.toList());
System.out.println("Affordable products:");
affordableProducts.forEach(System.out::println); // particular object method reference
// Output:
// Affordable products:
// Keyboard ($29.99)
// Mouse ($19.99)
// Notebook ($4.99)
// Pen ($1.99)
// Desk Lamp ($45.00)
// Finding the cheapest product
Optional cheapest = products.stream()
.min((a, b) -> Double.compare(a.getPrice(), b.getPrice()));
// Note: This particular comparison cannot be a method reference
// because we need to extract the field first
cheapest.ifPresent(System.out::println);
// Output: Pen - $1.99
}
}
The Comparator interface works naturally with method references. Java 8 introduced Comparator.comparing(), thenComparing(), and reversed() methods that were designed specifically to work with method references.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class ComparatorMethodRef {
static class Employee {
private final String name;
private final String department;
private final double salary;
private final int yearsOfService;
Employee(String name, String department, double salary, int yearsOfService) {
this.name = name;
this.department = department;
this.salary = salary;
this.yearsOfService = yearsOfService;
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public int getYearsOfService() { return yearsOfService; }
@Override
public String toString() {
return String.format("%-10s %-12s $%,.0f %d yrs",
name, department, salary, yearsOfService);
}
}
public static void main(String[] args) {
List employees = Arrays.asList(
new Employee("Alice", "Engineering", 95000, 5),
new Employee("Bob", "Marketing", 75000, 8),
new Employee("Charlie", "Engineering", 110000, 3),
new Employee("Diana", "Marketing", 85000, 6),
new Employee("Eve", "Engineering", 95000, 7)
);
// Sort by name (natural order)
employees.sort(Comparator.comparing(Employee::getName));
System.out.println("=== By Name ===");
employees.forEach(System.out::println);
// Output:
// Alice Engineering $95,000 5 yrs
// Bob Marketing $75,000 8 yrs
// Charlie Engineering $110,000 3 yrs
// Diana Marketing $85,000 6 yrs
// Eve Engineering $95,000 7 yrs
// Sort by salary descending
employees.sort(Comparator.comparingDouble(Employee::getSalary).reversed());
System.out.println("\n=== By Salary (Highest First) ===");
employees.forEach(System.out::println);
// Output:
// Charlie Engineering $110,000 3 yrs
// Alice Engineering $95,000 5 yrs
// Eve Engineering $95,000 7 yrs
// Diana Marketing $85,000 6 yrs
// Bob Marketing $75,000 8 yrs
// Multi-level sort: department, then salary descending
employees.sort(Comparator.comparing(Employee::getDepartment)
.thenComparing(Comparator.comparingDouble(Employee::getSalary).reversed()));
System.out.println("\n=== By Department, then Salary Desc ===");
employees.forEach(System.out::println);
// Output:
// Charlie Engineering $110,000 3 yrs
// Alice Engineering $95,000 5 yrs
// Eve Engineering $95,000 7 yrs
// Diana Marketing $85,000 6 yrs
// Bob Marketing $75,000 8 yrs
// Sort by years of service (ascending), using comparingInt
employees.sort(Comparator.comparingInt(Employee::getYearsOfService));
System.out.println("\n=== By Years of Service ===");
employees.forEach(System.out::println);
// Output:
// Charlie Engineering $110,000 3 yrs
// Alice Engineering $95,000 5 yrs
// Diana Marketing $85,000 6 yrs
// Eve Engineering $95,000 7 yrs
// Bob Marketing $75,000 8 yrs
// Natural order for Comparable types
List names = Arrays.asList("Charlie", "Alice", "Bob");
names.sort(Comparator.naturalOrder());
System.out.println("\nNatural order: " + names);
// Output: Natural order: [Alice, Bob, Charlie]
// Reverse natural order
names.sort(Comparator.reverseOrder());
System.out.println("Reverse order: " + names);
// Output: Reverse order: [Charlie, Bob, Alice]
// Null-safe comparator
List withNulls = Arrays.asList("Charlie", null, "Alice", null, "Bob");
withNulls.sort(Comparator.nullsLast(Comparator.naturalOrder()));
System.out.println("Nulls last: " + withNulls);
// Output: Nulls last: [Alice, Bob, Charlie, null, null]
}
}
You can design your own methods specifically to be used as method references. This is a powerful technique for creating reusable, composable utilities. The key is to ensure your method signature matches the functional interface that will be used.
Create static methods in utility classes that match common functional interface signatures:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class CustomMethodRefUtils {
// --- Predicate-compatible static methods (T -> boolean) ---
public static boolean isEven(int number) {
return number % 2 == 0;
}
public static boolean isPositive(int number) {
return number > 0;
}
public static boolean isValidEmail(String email) {
return email != null && email.contains("@") && email.contains(".");
}
public static boolean isNotEmpty(String s) {
return s != null && !s.isEmpty();
}
// --- Function-compatible static methods (T -> R) ---
public static String toSlug(String text) {
return text.toLowerCase().replaceAll("\\s+", "-").replaceAll("[^a-z0-9-]", "");
}
public static String maskEmail(String email) {
int atIndex = email.indexOf('@');
if (atIndex <= 1) return email;
return email.charAt(0) + "***" + email.substring(atIndex);
}
public static String formatCurrency(double amount) {
return String.format("$%,.2f", amount);
}
// --- Consumer-compatible static methods (T -> void) ---
public static void logInfo(String message) {
System.out.println("[INFO] " + message);
}
public static void main(String[] args) {
// Using custom Predicate methods
List numbers = Arrays.asList(-3, -1, 0, 2, 4, 7, 10);
List evenNumbers = numbers.stream()
.filter(CustomMethodRefUtils::isEven)
.collect(Collectors.toList());
System.out.println("Even: " + evenNumbers);
// Output: Even: [0, 2, 4, 10]
List positiveNumbers = numbers.stream()
.filter(CustomMethodRefUtils::isPositive)
.collect(Collectors.toList());
System.out.println("Positive: " + positiveNumbers);
// Output: Positive: [2, 4, 7, 10]
// Using custom Function methods
List titles = Arrays.asList("Hello World!", "Java 8 Features", "Method Refs");
List slugs = titles.stream()
.map(CustomMethodRefUtils::toSlug)
.collect(Collectors.toList());
System.out.println("Slugs: " + slugs);
// Output: Slugs: [hello-world, java-8-features, method-refs]
// Using custom Consumer methods
List emails = Arrays.asList("alice@example.com", "bob@test.org", "charlie@mail.com");
emails.stream()
.map(CustomMethodRefUtils::maskEmail)
.forEach(CustomMethodRefUtils::logInfo);
// Output:
// [INFO] a***@example.com
// [INFO] b***@test.org
// [INFO] c***@mail.com
// Validate and transform pipeline
List inputEmails = Arrays.asList("alice@example.com", "", "invalid", "bob@test.org", null);
List validMasked = inputEmails.stream()
.filter(CustomMethodRefUtils::isNotEmpty)
.filter(CustomMethodRefUtils::isValidEmail)
.map(CustomMethodRefUtils::maskEmail)
.collect(Collectors.toList());
System.out.println("Valid masked: " + validMasked);
// Output: Valid masked: [a***@example.com, b***@test.org]
}
}
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class CustomInstanceMethodRef {
static class PriceCalculator {
private final double taxRate;
private final double discountPercent;
PriceCalculator(double taxRate, double discountPercent) {
this.taxRate = taxRate;
this.discountPercent = discountPercent;
}
// Designed to be used as Function
public double applyTax(double price) {
return price * (1 + taxRate);
}
// Designed to be used as Function
public double applyDiscount(double price) {
return price * (1 - discountPercent);
}
// Designed to be used as Predicate
public boolean isAboveMinimum(double price) {
return price > 10.0;
}
// Designed to be used as Function
public String formatWithTax(double price) {
double total = applyTax(price);
return String.format("$%.2f (incl. %.0f%% tax)", total, taxRate * 100);
}
}
public static void main(String[] args) {
PriceCalculator calc = new PriceCalculator(0.08, 0.15); // 8% tax, 15% discount
List prices = Arrays.asList(5.99, 12.50, 25.00, 8.75, 49.99);
// Use instance method references on a specific calculator
List formattedPrices = prices.stream()
.filter(calc::isAboveMinimum) // Predicate: only prices > $10
.map(calc::applyDiscount) // Function: apply 15% discount
.map(calc::formatWithTax) // Function: format with tax
.collect(Collectors.toList());
System.out.println("Final prices (after discount + tax):");
formattedPrices.forEach(System.out::println);
// Output:
// Final prices (after discount + tax):
// $11.48 (incl. 8% tax)
// $22.95 (incl. 8% tax)
// $45.89 (incl. 8% tax)
}
}
When a class has overloaded methods (same name, different parameters), the compiler resolves which overload to use based on the functional interface’s abstract method signature. In most cases this works seamlessly, but ambiguity can arise.
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
public class OverloadedMethodRef {
// Overloaded static methods
public static String format(String value) {
return "[" + value + "]";
}
public static String format(String value, int width) {
return String.format("%" + width + "s", value);
}
public static String format(int number) {
return String.format("%,d", number);
}
public static void main(String[] args) {
// The compiler selects the correct overload based on the functional interface
// Function --> matches format(String)
Function f1 = OverloadedMethodRef::format;
System.out.println(f1.apply("hello"));
// Output: [hello]
// BiFunction --> matches format(String, int)
BiFunction f2 = OverloadedMethodRef::format;
System.out.println(f2.apply("hello", 20));
// Output: hello
// Function --> matches format(int)
Function f3 = OverloadedMethodRef::format;
System.out.println(f3.apply(1000000));
// Output: 1,000,000
// Using in streams -- the target type determines which overload
List words = Arrays.asList("cat", "dog", "bird");
// Stream's map expects Function, so format(String) is chosen
List formatted = words.stream()
.map(OverloadedMethodRef::format)
.collect(Collectors.toList());
System.out.println("Formatted: " + formatted);
// Output: Formatted: [[cat], [dog], [bird]]
// Example of String::valueOf -- many overloads
// valueOf(int), valueOf(long), valueOf(double), valueOf(boolean), valueOf(Object), etc.
List numbers = Arrays.asList(1, 2, 3);
List strings = numbers.stream()
.map(String::valueOf) // compiler picks valueOf(Object) or valueOf(int)
.collect(Collectors.toList());
System.out.println("Strings: " + strings);
// Output: Strings: [1, 2, 3]
}
}
When the compiler cannot determine which overload to use, you get a compilation error. This typically happens when two overloads could both match the functional interface. The solution is to use a lambda expression instead, or add an explicit type cast:
import java.util.function.Function;
import java.util.function.Consumer;
public class AmbiguityResolution {
// These two overloads can cause ambiguity in certain contexts
public static void process(String value) {
System.out.println("String: " + value);
}
public static void process(Object value) {
System.out.println("Object: " + value);
}
public static void main(String[] args) {
// Direct method reference works when the target type is clear
Consumer c1 = AmbiguityResolution::process;
c1.accept("hello");
// Output: String: hello
Consumer
Method references look simple, but there are several pitfalls that trip up developers. Here are the most common mistakes and how to avoid them:
The method you reference must match the functional interface’s expected signature. The number and types of parameters must align.
import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.function.Predicate;
public class MistakeWrongSignature {
public static String greet(String name, String greeting) {
return greeting + ", " + name + "!";
}
public static boolean hasMinLength(String text, int minLength) {
return text.length() >= minLength;
}
public static void main(String[] args) {
// WRONG: Function expects 1 argument, greet takes 2
// Function f = MistakeWrongSignature::greet; // COMPILE ERROR
// CORRECT: Use BiFunction for 2-argument methods
BiFunction f = MistakeWrongSignature::greet;
System.out.println(f.apply("Alice", "Hello"));
// Output: Hello, Alice!
// WRONG: Predicate expects 1 argument, hasMinLength takes 2
// Predicate p = MistakeWrongSignature::hasMinLength; // COMPILE ERROR
// CORRECT: Use a lambda to supply the extra argument
Predicate p = text -> MistakeWrongSignature.hasMinLength(text, 5);
System.out.println(p.test("Hello"));
// Output: true
System.out.println(p.test("Hi"));
// Output: false
}
}
A common error is confusing when to use ClassName::method (static or arbitrary instance) versus instance::method (specific instance).
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
public class MistakeStaticVsInstance {
// Instance method
public String addPrefix(String text) {
return ">> " + text;
}
// Static method
public static String addSuffix(String text) {
return text + " <<";
}
public static void main(String[] args) {
List words = Arrays.asList("Hello", "World");
MistakeStaticVsInstance obj = new MistakeStaticVsInstance();
// CORRECT: Static method uses ClassName::method
List withSuffix = words.stream()
.map(MistakeStaticVsInstance::addSuffix) // static method
.collect(Collectors.toList());
System.out.println(withSuffix);
// Output: [Hello <<, World <<]
// CORRECT: Instance method on a specific object uses instance::method
List withPrefix = words.stream()
.map(obj::addPrefix) // instance method on obj
.collect(Collectors.toList());
System.out.println(withPrefix);
// Output: [>> Hello, >> World]
// WRONG: Using ClassName::instanceMethod treats it as "arbitrary object" type
// MistakeStaticVsInstance::addPrefix would expect the FIRST parameter
// to be a MistakeStaticVsInstance object, not a String.
// Function wrong = MistakeStaticVsInstance::addPrefix; // COMPILE ERROR
// CORRECT as arbitrary object: ClassName::instanceMethod
// where the stream elements ARE of that class type
Function correct =
o -> o.addPrefix("test");
System.out.println(correct.apply(obj));
// Output: >> test
}
}
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
public class MistakeVoidReturn {
public static void printUpper(String s) {
System.out.println(s.toUpperCase());
// returns void
}
public static String toUpper(String s) {
return s.toUpperCase();
// returns String
}
public static void main(String[] args) {
List names = Arrays.asList("Alice", "Bob");
// Consumer expects void return -- use void method
Consumer consumer = MistakeVoidReturn::printUpper; // OK
names.forEach(consumer);
// Output:
// ALICE
// BOB
// Function expects a return value -- use returning method
Function function = MistakeVoidReturn::toUpper; // OK
System.out.println(function.apply("charlie"));
// Output: CHARLIE
// WRONG: Using void method where Function is expected
// Function wrong = MistakeVoidReturn::printUpper; // COMPILE ERROR
// printUpper returns void, but Function expects String return
// NOTE: A Function CAN be used where Consumer is expected (return value is ignored)
// This is valid because Consumer.accept() discards the return value
Consumer consumerFromFunction = MistakeVoidReturn::toUpper; // OK, return ignored
consumerFromFunction.accept("test"); // toUpper runs but return value is discarded
}
}
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class MistakeAddingLogic {
public static void main(String[] args) {
List names = Arrays.asList("Alice", "Bob", "Charlie");
// WRONG: You cannot add logic to a method reference
// names.forEach(System.out::println + " is here"); // COMPILE ERROR
// names.forEach(String::toUpperCase::trim); // COMPILE ERROR
// CORRECT: Use a lambda when you need additional logic
names.forEach(name -> System.out.println(name + " is here"));
// Output:
// Alice is here
// Bob is here
// Charlie is here
// CORRECT: Use a lambda when chaining method calls
List processed = names.stream()
.map(name -> name.toUpperCase().trim())
.collect(Collectors.toList());
System.out.println(processed);
// Output: [ALICE, BOB, CHARLIE]
// ALTERNATIVE: Create a helper method, then use a method reference to it
List processed2 = names.stream()
.map(MistakeAddingLogic::processName)
.collect(Collectors.toList());
System.out.println(processed2);
// Output: [*** ALICE ***, *** BOB ***, *** CHARLIE ***]
}
private static String processName(String name) {
return "*** " + name.toUpperCase().trim() + " ***";
}
}
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class MistakeNullRef {
public static void main(String[] args) {
// PROBLEM: Method reference on a null object
String prefix = null;
// This compiles fine, but throws NullPointerException at runtime
// when the method reference is evaluated
try {
List names = Arrays.asList("Alice", "Bob");
List result = names.stream()
.map(prefix::concat) // NPE! prefix is null
.collect(Collectors.toList());
} catch (NullPointerException e) {
System.out.println("NullPointerException: Cannot call method on null object");
}
// Output: NullPointerException: Cannot call method on null object
// SOLUTION: Null-check before using the method reference
String safePrefix = "Hello, ";
if (safePrefix != null) {
List names = Arrays.asList("Alice", "Bob");
List result = names.stream()
.map(safePrefix::concat)
.collect(Collectors.toList());
System.out.println(result);
}
// Output: [Hello, Alice, Hello, Bob]
// SOLUTION for null elements in a stream: filter with Objects::nonNull
List mixed = Arrays.asList("Alice", null, "Bob", null, "Charlie");
List upper = mixed.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println(upper);
// Output: [ALICE, BOB, CHARLIE]
}
}
Method references are a powerful tool for writing clean, readable Java code. Follow these guidelines to use them effectively:
| Do | Don’t |
|---|---|
| Use method references when they are more readable than the lambda | Force a method reference when a lambda is clearer |
Prefer built-in method references like Integer::parseInt, String::toUpperCase |
Create trivial wrapper methods just to have a method reference |
Use System.out::println for debugging stream pipelines |
Leave debugging method references in production code |
| Create well-named utility methods that serve as method references | Create methods with side effects for use in map() |
Use Comparator.comparing(Entity::getField) for sorting |
Write manual comparator lambdas when Comparator.comparing works |
Use Objects::nonNull to filter null values |
Use method references on potentially null objects |
| Let the IDE suggest method references | Ignore IDE warnings about possible method references |
| Use constructor references with factory patterns | Use deprecated constructors like Integer::new |
String::toLowerCase reads well. AbstractDatabaseConnectionFactoryImpl::createConnection does not.System.out::println or Integer::parseInt. Custom methods need good naming.Method references and lambdas have identical runtime performance. The JVM compiles both into equivalent bytecode using the invokedynamic instruction. Choose based on readability, never performance.
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class BestPracticesDemo {
public static void main(String[] args) {
// GOOD: Clear, well-known method references
List names = Arrays.asList("Charlie", null, "Alice", null, "Bob");
List result = names.stream()
.filter(Objects::nonNull) // clear: filters nulls
.map(String::toUpperCase) // clear: converts to uppercase
.sorted(String::compareTo) // clear: sorts naturally
.collect(Collectors.toList());
result.forEach(System.out::println); // clear: prints each element
// Output:
// ALICE
// BOB
// CHARLIE
// GOOD: Comparator.comparing with method reference
List words = Arrays.asList("elephant", "cat", "butterfly", "ant");
words.sort(Comparator.comparing(String::length));
System.out.println("By length: " + words);
// Output: By length: [cat, ant, elephant, butterfly]
// BAD then GOOD: If the lambda has extra logic, don't force a method reference
// BAD: words.stream().map(SomeVeryLongClassName::someHelperMethod)...
// GOOD: words.stream().map(w -> "[" + w.toUpperCase() + "]")...
List formatted = words.stream()
.map(w -> "[" + w.toUpperCase() + "]")
.collect(Collectors.toList());
System.out.println("Formatted: " + formatted);
// Output: Formatted: [[CAT], [ANT], [ELEPHANT], [BUTTERFLY]]
}
}
This comprehensive example demonstrates an Employee Processing System that uses all four types of method references in a realistic business scenario. It processes employee data through multiple pipeline stages: validation, transformation, sorting, reporting, and output.
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class EmployeeProcessingSystem {
// ==============================
// Domain Model
// ==============================
static class Employee {
private final String name;
private final String department;
private final double salary;
private final int yearsOfExperience;
private final boolean active;
Employee(String name, String department, double salary,
int yearsOfExperience, boolean active) {
this.name = name;
this.department = department;
this.salary = salary;
this.yearsOfExperience = yearsOfExperience;
this.active = active;
}
// Constructor for stream-based creation (name only)
Employee(String name) {
this(name, "Unassigned", 0, 0, true);
}
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public int getYearsOfExperience() { return yearsOfExperience; }
public boolean isActive() { return active; }
// Instance method designed for method references
public String toReportLine() {
return String.format(" %-15s %-12s $%,10.2f %d yrs %s",
name, department, salary, yearsOfExperience,
active ? "Active" : "Inactive");
}
// Instance method for CSV export
public String toCsv() {
return String.join(",", name, department,
String.valueOf(salary), String.valueOf(yearsOfExperience),
String.valueOf(active));
}
@Override
public String toString() {
return name + " (" + department + ")";
}
}
// ==============================
// Utility class with static methods (for static method references)
// ==============================
static class EmployeeUtils {
public static boolean isSenior(Employee e) {
return e.getYearsOfExperience() >= 5;
}
public static boolean isHighEarner(Employee e) {
return e.getSalary() >= 90000;
}
public static boolean isValid(Employee e) {
return e != null && e.getName() != null && !e.getName().isEmpty();
}
public static int compareBySalary(Employee a, Employee b) {
return Double.compare(a.getSalary(), b.getSalary());
}
public static String formatForEmail(Employee e) {
return e.getName().toLowerCase().replace(" ", ".") + "@company.com";
}
}
// ==============================
// Report printer (for particular object method references)
// ==============================
static class ReportPrinter {
private final String reportTitle;
private int lineCount = 0;
ReportPrinter(String reportTitle) {
this.reportTitle = reportTitle;
}
public void printHeader() {
System.out.println("\n========================================");
System.out.println(" " + reportTitle);
System.out.println("========================================");
}
public void printLine(String line) {
lineCount++;
System.out.println(line);
}
public void printSummary() {
System.out.println("----------------------------------------");
System.out.println(" Total lines printed: " + lineCount);
System.out.println("========================================");
}
}
// ==============================
// Main processing
// ==============================
public static void main(String[] args) {
// --- Sample data ---
List employees = Arrays.asList(
new Employee("Alice Johnson", "Engineering", 105000, 7, true),
new Employee("Bob Smith", "Marketing", 72000, 3, true),
new Employee("Charlie Brown", "Engineering", 95000, 5, true),
new Employee("Diana Prince", "HR", 88000, 9, true),
new Employee("Eve Williams", "Marketing", 91000, 6, false),
new Employee("Frank Castle", "Engineering", 115000, 10, true),
new Employee("Grace Hopper", "Engineering", 98000, 4, true),
null, // intentional null for demonstration
new Employee("Henry Ford", "Operations", 78000, 2, true),
new Employee("Irene Adler", "HR", 83000, 8, true)
);
// ================================================
// TYPE 1: Static method reference (ClassName::staticMethod)
// ================================================
System.out.println("=== STEP 1: Filter valid, active, senior, high-earning employees ===");
List qualifiedEmployees = employees.stream()
.filter(Objects::nonNull) // static: remove nulls
.filter(EmployeeUtils::isValid) // static: validate
.filter(Employee::isActive) // arbitrary: check active
.filter(EmployeeUtils::isSenior) // static: >= 5 years
.filter(EmployeeUtils::isHighEarner) // static: >= $90K
.collect(Collectors.toList());
qualifiedEmployees.forEach(System.out::println);
// Output:
// Alice Johnson (Engineering)
// Charlie Brown (Engineering)
// Frank Castle (Engineering)
// ================================================
// TYPE 2: Particular object method reference (instance::method)
// ================================================
System.out.println("\n=== STEP 2: Generate formatted report ===");
ReportPrinter printer = new ReportPrinter("Qualified Employee Report");
printer.printHeader();
qualifiedEmployees.stream()
.map(Employee::toReportLine) // arbitrary: format each
.forEach(printer::printLine); // particular: print on this printer
printer.printSummary();
// Output:
// ========================================
// Qualified Employee Report
// ========================================
// Alice Johnson Engineering $105,000.00 7 yrs Active
// Charlie Brown Engineering $ 95,000.00 5 yrs Active
// Frank Castle Engineering $115,000.00 10 yrs Active
// ----------------------------------------
// Total lines printed: 3
// ========================================
// ================================================
// TYPE 3: Arbitrary object method reference (ClassName::instanceMethod)
// ================================================
System.out.println("\n=== STEP 3: Extract and process names ===");
List emailAddresses = employees.stream()
.filter(Objects::nonNull) // static
.filter(Employee::isActive) // arbitrary: each emp calls isActive()
.map(EmployeeUtils::formatForEmail) // static
.sorted(String::compareTo) // arbitrary: each string calls compareTo()
.collect(Collectors.toList());
emailAddresses.forEach(System.out::println);
// Output:
// alice.johnson@company.com
// bob.smith@company.com
// charlie.brown@company.com
// diana.prince@company.com
// frank.castle@company.com
// grace.hopper@company.com
// henry.ford@company.com
// irene.adler@company.com
// ================================================
// TYPE 4: Constructor reference (ClassName::new)
// ================================================
System.out.println("\n=== STEP 4: Create new employees from names ===");
List newHires = Arrays.asList("Jack Ryan", "Kate Bishop", "Leo Messi");
// Constructor reference -- Employee(String name)
List newEmployees = newHires.stream()
.map(Employee::new) // constructor reference
.collect(Collectors.toList());
newEmployees.forEach(System.out::println);
// Output:
// Jack Ryan (Unassigned)
// Kate Bishop (Unassigned)
// Leo Messi (Unassigned)
// Convert to array using array constructor reference
Employee[] employeeArray = newEmployees.stream()
.toArray(Employee[]::new); // array constructor reference
System.out.println("Array length: " + employeeArray.length);
// Output: Array length: 3
// ================================================
// Combining all types in a single pipeline
// ================================================
System.out.println("\n=== STEP 5: Department salary report ===");
// Supplier> for fresh data
Supplier> dataSource = ArrayList::new;
Map avgSalaryByDept = employees.stream()
.filter(Objects::nonNull) // static
.filter(Employee::isActive) // arbitrary
.collect(Collectors.groupingBy(
Employee::getDepartment, // arbitrary
Collectors.averagingDouble(
Employee::getSalary))); // arbitrary
avgSalaryByDept.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.forEach(entry -> System.out.printf(" %-15s $%,.2f%n",
entry.getKey(), entry.getValue()));
// Output:
// Engineering $103,250.00
// HR $85,500.00
// Marketing $72,000.00
// Operations $78,000.00
// ================================================
// CSV export using method reference
// ================================================
System.out.println("\n=== STEP 6: CSV export ===");
System.out.println("name,department,salary,experience,active");
employees.stream()
.filter(Objects::nonNull)
.filter(Employee::isActive)
.sorted(Comparator.comparing(Employee::getName))
.map(Employee::toCsv) // arbitrary: each calls toCsv()
.forEach(System.out::println); // particular: System.out
// Output:
// name,department,salary,experience,active
// Alice Johnson,Engineering,105000.0,7,true
// Bob Smith,Marketing,72000.0,3,true
// Charlie Brown,Engineering,95000.0,5,true
// Diana Prince,HR,88000.0,9,true
// Frank Castle,Engineering,115000.0,10,true
// Grace Hopper,Engineering,98000.0,4,true
// Henry Ford,Operations,78000.0,2,true
// Irene Adler,HR,83000.0,8,true
// ================================================
// Statistics using method references
// ================================================
System.out.println("\n=== STEP 7: Statistics ===");
List validEmployees = employees.stream()
.filter(Objects::nonNull)
.filter(Employee::isActive)
.collect(Collectors.toList());
Optional highestPaid = validEmployees.stream()
.max(Comparator.comparingDouble(Employee::getSalary));
highestPaid.map(Employee::toReportLine).ifPresent(System.out::println);
// Output: Frank Castle Engineering $115,000.00 10 yrs Active
Optional mostExperienced = validEmployees.stream()
.max(Comparator.comparingInt(Employee::getYearsOfExperience));
mostExperienced.map(Employee::toReportLine).ifPresent(System.out::println);
// Output: Frank Castle Engineering $115,000.00 10 yrs Active
double totalSalary = validEmployees.stream()
.mapToDouble(Employee::getSalary)
.sum();
System.out.printf(" Total salary budget: $%,.2f%n", totalSalary);
// Output: Total salary budget: $734,000.00
long engineeringCount = validEmployees.stream()
.map(Employee::getDepartment)
.filter("Engineering"::equals) // particular: "Engineering" string's equals()
.count();
System.out.println(" Engineering headcount: " + engineeringCount);
// Output: Engineering headcount: 4
}
}
This example demonstrates all four types of method references working together in a realistic data processing scenario:
| Type | Examples Used | Purpose |
|---|---|---|
| Static method | Objects::nonNull, EmployeeUtils::isSenior, EmployeeUtils::formatForEmail |
Validation, filtering, transformation |
| Particular object | System.out::println, printer::printLine, "Engineering"::equals |
Output, reporting, string matching |
| Arbitrary object | Employee::isActive, Employee::getDepartment, Employee::toReportLine, String::compareTo |
Field extraction, formatting, sorting |
| Constructor | Employee::new, Employee[]::new, ArrayList::new |
Object creation, array creation |
Use this summary table as a cheat sheet for method references:
| Type | Syntax | Lambda Equivalent | Common Examples |
|---|---|---|---|
| Static method | ClassName::staticMethod |
(args) -> ClassName.staticMethod(args) |
Integer::parseIntMath::absString::valueOfObjects::nonNullCollections::unmodifiableList |
| Instance method (particular object) | instance::method |
(args) -> instance.method(args) |
System.out::printlnmyList::addmyString::concat"literal"::equals |
| Instance method (arbitrary object) | ClassName::instanceMethod |
(obj, args) -> obj.method(args)or obj -> obj.method() |
String::toLowerCaseString::lengthString::compareToObject::toString |
| Constructor | ClassName::new |
(args) -> new ClassName(args) |
ArrayList::newStringBuilder::newString[]::newFile::new |
Decision flowchart for choosing between lambda and method reference:
Key takeaways:
:: operator creates a reference to a method without calling it