1. Introduction to Method References
Method references are a shorthand notation for lambda expressions to call methods. They provide a way to refer to methods without executing them, making code more readable and concise.
What are Method References?
- Short form of lambda expressions
- Used when lambda just calls an existing method
- Improve code readability
- Compile-time checked
Why Use Method References?
- More readable than lambda expressions
- Reduce code verbosity
- Self-documenting code
- Better performance in some cases
2. Syntax of Method References
Basic Syntax
ClassName::methodName
or
object::methodName
Double Colon Operator (::)
The :: operator is used to separate the class/object from the method name.
3. Types of Method References
There are four types of method references in Java:
Type 1: Reference to Static Method
ClassName::staticMethodName
Type 2: Reference to Instance Method of Particular Object
object::instanceMethodName
Type 3: Reference to Instance Method of Arbitrary Object
ClassName::instanceMethodName
Type 4: Reference to Constructor
ClassName::new
4. Complete Code Examples
Example 1: Reference to Static Method
import java.util.*;
import java.util.function.*;
public class StaticMethodReference {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Using lambda expression
numbers.stream()
.map(n -> Math.sqrt(n))
.forEach(n -> System.out.println(n));
System.out.println("--- Using Method Reference ---");
// Using method reference - much cleaner!
numbers.stream()
.map(Math::sqrt)
.forEach(System.out::println);
// More examples with static methods
Function<String, Integer> parser1 = s -> Integer.parseInt(s);
Function<String, Integer> parser2 = Integer::parseInt; // Method reference
System.out.println("Parsed number: " + parser2.apply("123"));
// Static method reference in custom class
numbers.stream()
.map(StaticMethodReference::square)
.forEach(StaticMethodReference::printNumber);
}
// Custom static method
public static int square(int n) {
return n * n;
}
public static void printNumber(Number n) {
System.out.println("Number: " + n);
}
}
Example 2: Reference to Instance Method of Particular Object
import java.util.*;
import java.util.function.*;
public class InstanceMethodReference {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Alice", "Bob", "Diana");
// Create an instance
StringProcessor processor = new StringProcessor();
// Using lambda expression
names.stream()
.map(s -> processor.processString(s))
.forEach(s -> System.out.println(s));
System.out.println("--- Using Method Reference ---");
// Using method reference - specific object
names.stream()
.map(processor::processString)
.forEach(System.out::println);
// Another example with Consumer
Consumer<String> printer1 = s -> System.out.println(s);
Consumer<String> printer2 = System.out::println; // Method reference
names.forEach(printer2);
// Example with custom object
Calculator calc = new Calculator();
Function<Integer, Integer> squarer1 = n -> calc.square(n);
Function<Integer, Integer> squarer2 = calc::square; // Method reference
System.out.println("Square of 5: " + squarer2.apply(5));
}
}
class StringProcessor {
public String processString(String str) {
return "Processed: " + str.toUpperCase();
}
}
class Calculator {
public int square(int n) {
return n * n;
}
public int cube(int n) {
return n * n * n;
}
}
Example 3: Reference to Instance Method of Arbitrary Object
import java.util.*;
import java.util.function.*;
public class ArbitraryInstanceMethodReference {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Alice", "Bob", "Diana");
// Using lambda expression
names.sort((s1, s2) -> s1.compareToIgnoreCase(s2));
// Using method reference - arbitrary object
names.sort(String::compareToIgnoreCase);
System.out.println("Sorted names: " + names);
// More examples
List<String> upperCaseNames = Arrays.asList("JOHN", "ALICE", "BOB");
// Using lambda
upperCaseNames.stream()
.map(s -> s.toLowerCase())
.forEach(s -> System.out.println(s));
System.out.println("--- Using Method Reference ---");
// Using method reference
upperCaseNames.stream()
.map(String::toLowerCase)
.forEach(System.out::println);
// Example with custom objects
List<Person> people = Arrays.asList(
new Person("John", 25),
new Person("Alice", 30),
new Person("Bob", 22)
);
// Sort by age using method reference
people.sort(Comparator.comparing(Person::getAge));
// Extract names using method reference
List<String> personNames = people.stream()
.map(Person::getName)
.toList();
System.out.println("People by age: " + people);
System.out.println("Names: " + personNames);
}
}
class Person {
private String name;
private 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 + ")";
}
}
Example 4: Reference to Constructor
import java.util.*;
import java.util.function.*;
public class ConstructorReference {
public static void main(String[] args) {
// Using lambda expression
Supplier<List<String>> supplier1 = () -> new ArrayList<>();
// Using constructor reference
Supplier<List<String>> supplier2 = ArrayList::new;
List<String> list = supplier2.get();
list.add("Hello");
list.add("World");
System.out.println("List: " + list);
// Constructor with parameters
Function<String, Person> personCreator1 = name -> new Person(name);
Function<String, Person> personCreator2 = Person::new; // Constructor reference
Person john = personCreator2.apply("John");
System.out.println("Created: " + john);
// Multiple parameters using BiFunction
BiFunction<String, Integer, Person> detailedPersonCreator = Person::new;
Person alice = detailedPersonCreator.apply("Alice", 30);
System.out.println("Created: " + alice);
// Array constructor reference
Function<Integer, String[]> arrayCreator = String[]::new;
String[] stringArray = arrayCreator.apply(5);
System.out.println("Array length: " + stringArray.length);
}
}
class Person {
private String name;
private int age;
// Constructor for name only
public Person(String name) {
this.name = name;
this.age = 0;
}
// Constructor for name and age
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
5. Real-World Complete Example
import java.util.*;
import java.util.stream.*;
class Product {
private String name;
private double price;
private String category;
public Product(String name, double price, String category) {
this.name = name;
this.price = price;
this.category = category;
}
// Getters
public String getName() { return name; }
public double getPrice() { return price; }
public String getCategory() { return category; }
// Business methods
public boolean isExpensive() {
return price > 1000;
}
public void applyDiscount(double percentage) {
this.price = this.price * (1 - percentage/100);
}
@Override
public String toString() {
return String.format("%s: $%.2f (%s)", name, price, category);
}
}
public class ECommerceExample {
public static void main(String[] args) {
List<Product> products = Arrays.asList(
new Product("Laptop", 1200.0, "Electronics"),
new Product("Phone", 800.0, "Electronics"),
new Product("Book", 20.0, "Education"),
new Product("Chair", 150.0, "Furniture"),
new Product("Table", 300.0, "Furniture"),
new Product("Monitor", 400.0, "Electronics")
);
// 1. Static method reference - Utility methods
System.out.println("=== Expensive Products ===");
products.stream()
.filter(Product::isExpensive) // Instance method reference
.forEach(System.out::println); // Specific object method reference
// 2. Instance method reference - Price formatting
System.out.println("\n=== Formatted Prices ===");
products.stream()
.map(Product::getPrice) // Instance method reference
.map(ECommerceExample::formatPrice) // Static method reference
.forEach(System.out::println);
// 3. Constructor reference - Creating product summaries
System.out.println("\n=== Product Summaries ===");
List<String> summaries = products.stream()
.map(Product::getName)
.map(String::toUpperCase) // Arbitrary instance method
.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
summaries.forEach(System.out::println);
// 4. Method reference in sorting
System.out.println("\n=== Products Sorted by Price ===");
products.stream()
.sorted(Comparator.comparing(Product::getPrice))
.forEach(System.out::println);
// 5. Grouping by category using method reference
System.out.println("\n=== Products by Category ===");
Map<String, List<Product>> byCategory = products.stream()
.collect(Collectors.groupingBy(Product::getCategory));
byCategory.forEach((category, productList) -> {
System.out.println(category + ":");
productList.forEach(product ->
System.out.println(" - " + product.getName()));
});
// 6. Method reference with optional
System.out.println("\n=== Most Expensive Product ===");
Optional<Product> mostExpensive = products.stream()
.max(Comparator.comparing(Product::getPrice));
mostExpensive.ifPresent(System.out::println);
}
// Static utility method
public static String formatPrice(Double price) {
return String.format("$%.2f", price);
}
}
6. Method References vs Lambda Expressions
import java.util.*;
import java.util.function.*;
public class MethodRefVsLambda {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Alice", "Bob", "Diana");
System.out.println("=== WHEN TO USE METHOD REFERENCES ===");
// Case 1: Simple method call - USE METHOD REFERENCE
System.out.println("\n1. Simple method call:");
// Lambda (verbose)
names.forEach(s -> System.out.println(s));
// Method reference (better)
names.forEach(System.out::println);
// Case 2: Static method call - USE METHOD REFERENCE
System.out.println("\n2. Static method call:");
// Lambda
names.stream()
.map(s -> s.toUpperCase())
.forEach(s -> System.out.println(s));
// Method reference (better)
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
// Case 3: When lambda just passes parameters - USE METHOD REFERENCE
System.out.println("\n3. Parameter forwarding:");
// Lambda (just forwards parameters)
Function<String, Integer> lambdaParser = s -> Integer.parseInt(s);
// Method reference (better)
Function<String, Integer> methodRefParser = Integer::parseInt;
// Case 4: WHEN TO USE LAMBDA INSTEAD
System.out.println("\n4. Complex operations - USE LAMBDA:");
// Method reference not possible for complex logic
names.stream()
.map(s -> {
String result = s.toUpperCase();
return result + " - Length: " + result.length();
})
.forEach(System.out::println);
// Case 5: When method name doesn't explain intent
System.out.println("\n5. Need explicit intent:");
// Lambda (clearer intent)
names.removeIf(s -> s.isEmpty());
// Method reference (less clear what it does)
names.removeIf(String::isEmpty);
}
}
7. Best Practices and Common Patterns
import java.util.*;
import java.util.function.*;
public class BestPractices {
public static void main(String[] args) {
System.out.println("=== BEST PRACTICES ===");
// 1. Use method references for better readability
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// Good - method references
numbers.stream()
.map(Math::sqrt)
.forEach(System.out::println);
// 2. Use meaningful class names for constructor references
Supplier<List<String>> goodSupplier = ArrayList::new;
Supplier<Map<String, Integer>> mapSupplier = HashMap::new;
// 3. Combine method references with other operations
List<String> names = Arrays.asList("john", "alice", "bob");
names.stream()
.filter(Objects::nonNull) // Static method reference
.map(String::toUpperCase) // Instance method reference
.sorted(String::compareTo) // Arbitrary instance method
.forEach(System.out::println); // Specific instance method
// 4. Avoid overusing method references when lambda is clearer
List<String> mixedCase = Arrays.asList("John", "ALICE", "bob");
// Sometimes lambda is more explicit
mixedCase.stream()
.map(s -> s.charAt(0)) // Clearer than method reference
.forEach(System.out::println);
// 5. Use method references with custom functional interfaces
StringProcessor stringProcessor = new StringProcessor();
Function<String, String> processor1 = s -> stringProcessor.process(s);
Function<String, String> processor2 = stringProcessor::process; // Better
System.out.println(processor2.apply("hello"));
}
}
class StringProcessor {
public String process(String input) {
return input.toUpperCase() + "!";
}
}
// Custom functional interface
@FunctionalInterface
interface StringTransformer {
String transform(String input);
// Static method in functional interface
static StringTransformer createDefault() {
return String::toUpperCase; // Method reference
}
}
8. Advanced Examples
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
public class AdvancedExamples {
public static void main(String[] args) throws Exception {
// 1. Method references in multithreading
System.out.println("=== MULTITHREADING ===");
ExecutorService executor = Executors.newFixedThreadPool(2);
// Lambda
executor.submit(() -> System.out.println("Hello from lambda"));
// Method reference
executor.submit(System.out::println);
// 2. Method references with composition
System.out.println("\n=== METHOD COMPOSITION ===");
Function<String, String> toUpper = String::toUpperCase;
Function<String, String> addExclamation = s -> s + "!";
// Compose functions
Function<String, String> processor = toUpper.andThen(addExclamation);
System.out.println(processor.apply("hello"));
// 3. Method references with optional
System.out.println("\n=== OPTIONAL ===");
Optional<String> optionalName = Optional.of("John");
// Lambda
optionalName.ifPresent(name -> System.out.println(name));
// Method reference
optionalName.ifPresent(System.out::println);
// 4. Method references with predicates
System.out.println("\n=== PREDICATES ===");
List<String> names = Arrays.asList("John", "Alice", "Bob", "");
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> longEnough = s -> s.length() > 3;
// Combine predicates
names.stream()
.filter(notEmpty.and(longEnough))
.forEach(System.out::println);
// 5. Method references in factory pattern
System.out.println("\n=== FACTORY PATTERN ===");
Map<String, Supplier<Shape>> shapeFactory = new HashMap<>();
shapeFactory.put("circle", Circle::new);
shapeFactory.put("rectangle", Rectangle::new);
Shape circle = shapeFactory.get("circle").get();
circle.draw();
executor.shutdown();
}
}
interface Shape {
void draw();
}
class Circle implements Shape {
public void draw() {
System.out.println("Drawing Circle");
}
}
class Rectangle implements Shape {
public void draw() {
System.out.println("Drawing Rectangle");
}
}
9. Common Pitfalls and Solutions
import java.util.*;
import java.util.function.*;
public class CommonPitfalls {
public static void main(String[] args) {
System.out.println("=== COMMON PITFALLS ===");
// 1. Null pointer with method references
List<String> names = Arrays.asList("John", null, "Alice");
try {
// This will throw NPE
// names.stream().map(String::toUpperCase).forEach(System.out::println);
} catch (Exception e) {
System.out.println("NPE caught: " + e.getMessage());
}
// Solution: Add null check
names.stream()
.filter(Objects::nonNull)
.map(String::toUpperCase)
.forEach(System.out::println);
// 2. Ambiguous method references
List<Integer> numbers = Arrays.asList(1, 2, 3);
// This works fine
numbers.stream()
.map(Object::toString)
.forEach(System.out::println);
// 3. Method reference vs lambda performance
// In most cases, they have similar performance
// Choose based on readability
// 4. Cannot use method references for methods that throw checked exceptions
List<String> fileNames = Arrays.asList("file1.txt", "file2.txt");
// This won't compile with method reference if readFile throws IOException
// fileNames.stream().map(this::readFile).forEach(System.out::println);
// Use lambda instead
fileNames.stream()
.map(name -> {
try {
return readFile(name);
} catch (Exception e) {
return "Error: " + e.getMessage();
}
})
.forEach(System.out::println);
}
private static String readFile(String fileName) throws Exception {
// Simulate file reading
return "Content of " + fileName;
}
}
10. Conclusion
Key Takeaways:
- Readability: Method references make code more readable and expressive
- Conciseness: Reduce boilerplate code compared to lambda expressions
- Four Types: Static, Instance (specific object), Instance (arbitrary object), Constructor
- Compile-time Safety: All method references are checked at compile time
When to Use Method References:
- When lambda just calls an existing method
- For better code readability
- When working with static methods
- When creating new objects
- When the method name clearly expresses the operation
When to Prefer Lambda Expressions:
- Complex logic that doesn't fit a single method call
- When you need to handle exceptions
- When the operation involves multiple steps
- When method name doesn't clearly express intent
Final Thoughts:
Method references are a powerful feature that, when used appropriately, can significantly improve code quality and developer productivity. They represent the zenith of Java's functional programming capabilities, working seamlessly with streams, optional, and other functional interfaces.
Master method references to write cleaner, more professional Java code that's easier to read and maintain.