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 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:
:: 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.”String::toUpperCase, not String::toUpperCase(). Adding parentheses would invoke the method immediately rather than referencing it.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.
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.
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 numberStrings = Arrays.asList("1", "2", "3", "4", "5");
List 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 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 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]
}
}
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 numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
List 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 rawNames = Arrays.asList("aLICE", "bOB", "cHARLIE");
List 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 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 evenCheck = CustomStaticMethodReference::isEven;
Function 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
}
}
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 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 greetings = languages.stream()
.map(prefix::concat)
.collect(Collectors.toList());
System.out.println(greetings);
// Output: [Hello, Java, Hello, Python, Hello, Go, Hello, Rust]
}
}
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 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 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 names = Arrays.asList("Smith", "Johnson", "Williams");
// Lambda: name -> doctorFormatter.format(name)
// Reference: doctorFormatter::format
List formattedNames = names.stream()
.map(doctorFormatter::format)
.collect(Collectors.toList());
System.out.println("Formatted: " + formattedNames);
// Output: Formatted: [Dr. Smith, Dr. Johnson, Dr. Williams]
}
}
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 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();
}
}
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 words = Arrays.asList("hello", "world", "java");
// Lambda version: each element 's' calls its own toUpperCase()
List 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 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.
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.
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: takes a String, returns its length
List words = Arrays.asList("Java", "is", "powerful");
List lengths = words.stream()
.map(String::length)
.collect(Collectors.toList());
System.out.println("Lengths: " + lengths);
// Output: Lengths: [4, 2, 8]
// --- String::trim ---
List messy = Arrays.asList(" hello ", " world ", " java ");
List 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 mixed = Arrays.asList("Java", "", "Python", "", "Go");
List 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]
}
}
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 ---
// Comparator has: int compare(String a, String b)
// String::compareTo maps to: a.compareTo(b)
List names = Arrays.asList("Charlie", "Alice", "Bob");
names.sort(String::compareTo);
System.out.println("Sorted: " + names);
// Output: Sorted: [Alice, Bob, Charlie]
// --- String::compareToIgnoreCase ---
List 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 ---
// BiFunction: (a, b) -> a.concat(b)
java.util.function.BiFunction joiner = String::concat;
String result = joiner.apply("Hello, ", "World!");
System.out.println(result);
// Output: Hello, World!
}
}
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 people = Arrays.asList(
new Person("Charlie", 30),
new Person("Alice", 25),
new Person("Bob", 35)
);
// --- Person::getName used as Function ---
// Lambda: person -> person.getName()
List names = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
System.out.println("Names: " + names);
// Output: Names: [Charlie, Alice, Bob]
// --- Person::getAge used as Function ---
List 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)]
}
}
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.
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).
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> ---
// Lambda: () -> new ArrayList<>()
// Reference: ArrayList::new
Supplier> listFactory = ArrayList::new;
List 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 list2 = listFactory.get();
System.out.println("List 2 (empty): " + list2);
// Output: List 2 (empty): []
// --- HashMap::new ---
Supplier> mapFactory = HashMap::new;
HashMap scores = mapFactory.get();
scores.put("Alice", 95);
System.out.println("Scores: " + scores);
// Output: Scores: {Alice=95}
// --- StringBuilder::new ---
Supplier sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.get();
sb.append("Hello").append(" World");
System.out.println(sb);
// Output: Hello World
}
}
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 ---
// Lambda: name -> new User(name)
// Reference: User::new
Function userFactory = User::new;
User alice = userFactory.apply("Alice");
System.out.println(alice);
// Output: User{name='Alice'}
// --- Using constructor reference in a stream ---
List names = Arrays.asList("Alice", "Bob", "Charlie");
List 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 numberStrings = Arrays.asList("10", "20", "30");
List numbers = numberStrings.stream()
.map(Integer::valueOf)
.collect(Collectors.toList());
System.out.println("Numbers: " + numbers);
// Output: Numbers: [10, 20, 30]
}
}
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 ---
// Lambda: (name, price) -> new Product(name, price)
// Reference: Product::new
BiFunction 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)
}
}
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 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 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]
}
}
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.
String::toUpperCase is self-documenting)| 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 |
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 names = Arrays.asList("Alice", null, "Bob", "", "Charlie", null, "Dave");
// ===== METHOD REFERENCES: cleaner for simple delegation =====
// Filter nulls: Objects::nonNull is instantly readable
List 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 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 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 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]
}
}
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.
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 words = Arrays.asList(" hello ", " world ", " java ");
List 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 employees = Arrays.asList(
new Employee("Alice", "Engineering", 95000),
new Employee("Bob", "Marketing", 75000),
new Employee("Charlie", "Engineering", 105000),
new Employee("Diana", "Marketing", 80000)
);
List 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 numberStrings = Arrays.asList("100", "200", "300");
List numbers = numberStrings.stream()
.map(Integer::parseInt) // static: s -> Integer.parseInt(s)
.collect(Collectors.toList());
System.out.println("Numbers: " + numbers);
// Output: Numbers: [100, 200, 300]
}
}
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 items = Arrays.asList("Java", null, "Python", null, "Go");
List 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 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
List 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 pending = tasks.stream()
.filter(Predicate.not(Task::isCompleted))
.collect(Collectors.toList());
System.out.println("Pending: " + pending);
// Output: Pending: [Review PR [TODO], Write tests [TODO]]
}
}
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 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 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 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 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 words = Arrays.asList("banana", "apple", "cherry");
words.sort(String::compareToIgnoreCase);
System.out.println("Sorted words: " + words);
// Output: Sorted words: [apple, banana, cherry]
}
}
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 languages = Arrays.asList("Java", "Python", "Go", "Rust");
languages.forEach(System.out::println);
// Output:
// Java
// Python
// Go
// Rust
// --- With a Map ---
Map 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 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
}
}
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 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> 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 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
}
}
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 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 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
}
}
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 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 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 greeting = Optional.of("Hello, World!");
greeting.ifPresent(System.out::println);
// Output: Hello, World!
// --- filter() with method reference ---
Optional word = Optional.of("Java");
Optional longWord = word.filter(s -> s.length() > 3);
longWord.ifPresent(System.out::println);
// Output: Java
Optional shortWord = word.filter(String::isEmpty);
System.out.println("Short word present? " + shortWord.isPresent());
// Output: Short word present? false
}
}
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 process(List input, List> strategies) {
List result = input;
for (UnaryOperator strategy : strategies) {
result = result.stream()
.map(strategy)
.collect(Collectors.toList());
}
return result;
}
public static void main(String[] args) {
List data = Arrays.asList(" Hello ", " World ", " Java ");
// Build a pipeline of strategies using method references
List> pipeline = Arrays.asList(
TextProcessor::trim, // step 1: trim whitespace
TextProcessor::toUpperCase, // step 2: convert to uppercase
TextProcessor::reverse // step 3: reverse
);
List result = process(data, pipeline);
System.out.println(result);
// Output: [OLLEH, DLROW, AVAJ]
}
}
Method references are concise, but they introduce subtle pitfalls. Here are the mistakes developers encounter most often.
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 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 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 nonEmpty = names.stream()
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
System.out.println(nonEmpty);
// Output: [Alice, Bob, Charlie]
}
}
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
Function intFormatter = MistakeAmbiguousOverload::format;
System.out.println(intFormatter.apply(42));
// Output: Number: 42
// This also works -- different functional interface type resolves the overload
Function 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 and Function,
// you would need to cast:
// processData((Function) MistakeAmbiguousOverload::format);
}
}
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
BiFunction adder = MistakeSignatureMismatch::add;
System.out.println(adder.apply(3, 4));
// Output: 7
// WRONG: add(int, int) does NOT match Function
// Function wrongAdder = MistakeSignatureMismatch::add;
// Compile error: method add in class MistakeSignatureMismatch cannot be applied to given types
// CORRECT: greet(String) -> String matches Function
Function greeter = MistakeSignatureMismatch::greet;
System.out.println(greeter.apply("Alice"));
// Output: Hello, Alice
// WRONG: greet(String) does NOT match Supplier (Supplier takes no args)
// Supplier wrongGreeter = MistakeSignatureMismatch::greet;
// Compile error: method greet expects 1 argument but Supplier provides 0
}
}
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 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 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 nums = Arrays.asList("1", "2", "3");
List 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.
}
}
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 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 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 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
}
}
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 |
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 processNamesInline(List 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 processNamesClean(List 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 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]
}
}
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:
Employee::fromCsv)formatter::format)Employee::getSalary, Employee::getName)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 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 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 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 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 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 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> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
List 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 highestPaid = employees.stream()
.max(Comparator.comparing(Employee::getSalary));
Optional 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)
| 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) |
| 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 |