Method References

1. What Are Method References?

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:

  • The lambda body is a single method call with no added logic
  • The lambda parameters are passed directly to the method in the same order
  • The method reference improves readability (not all do — use judgment)

When NOT to use method references:

  • The lambda adds extra logic (conditions, transformations, multiple statements)
  • The method reference would be less clear than the lambda
  • You need to pass additional arguments that are not from the lambda parameters

2. Reference to a Static Method

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 mixed = Arrays.asList(42, 3.14, true, 'A');

        List strings = mixed.stream()
                .map(String::valueOf)         // same as: obj -> String.valueOf(obj)
                .collect(Collectors.toList());

        System.out.println("As strings: " + strings);
        // Output: As strings: [42, 3.14, true, A]

        // Example 4: Math::max with reduce
        List scores = Arrays.asList(85, 92, 78, 96, 88);

        int highest = scores.stream()
                .reduce(Integer.MIN_VALUE, Math::max);  // same as: (a, b) -> Math.max(a, b)

        System.out.println("Highest score: " + highest);
        // Output: Highest score: 96
    }
}

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]
    }
}

3. Reference to an Instance Method of a Particular Object

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]
    }
}

4. Reference to an Instance Method of an Arbitrary Object of a Particular Type

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:

  • For a single-parameter lambda: s -> s.toLowerCase() becomes String::toLowerCase. The stream element itself is the object.
  • For a two-parameter lambda: (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]
    }
}

5. Reference to a Constructor

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)
    }
}

6. Method References vs Lambda Expressions

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

6.1 Conversion Examples

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.
    }
}

6.2 When Lambda Is Better

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]]
    }
}

7. Method References with Streams

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
    }
}

8. Method References with Comparator

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]
    }
}

9. Custom Method References

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.

9.1 Static Utility Methods

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]
    }
}

9.2 Instance Methods Designed as Method References

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)
    }
}

10. Method References with Overloaded Methods

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]
    }
}

10.1 Resolving Ambiguity

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 c2 = AmbiguityResolution::process;
        c2.accept("hello");
        // Output: Object: hello

        // If ambiguity occurs, use a lambda to be explicit
        Consumer c3 = s -> AmbiguityResolution.process((Object) s);
        c3.accept("hello");
        // Output: Object: hello

        // Or cast the method reference to the desired functional interface
        Consumer c4 = (Consumer) AmbiguityResolution::process;
        c4.accept("hello");
        // Output: String: hello

        System.out.println("Ambiguity resolved successfully.");
        // Output: Ambiguity resolved successfully.
    }
}

11. Common Mistakes

Method references look simple, but there are several pitfalls that trip up developers. Here are the most common mistakes and how to avoid them:

Mistake 1: Wrong Method Signature

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
    }
}

Mistake 2: Static vs Instance Confusion

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
    }
}

Mistake 3: Void Return vs Value Return

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
    }
}

Mistake 4: Trying to Add Logic to a Method Reference

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() + " ***";
    }
}

Mistake 5: Null Object References

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]
    }
}

12. Best Practices

Method references are a powerful tool for writing clean, readable Java code. Follow these guidelines to use them effectively:

12.1 Guidelines for When to Use Method References

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

12.2 Readability Rules of Thumb

  1. One-to-one delegation is a good candidate. If the lambda just passes parameters to a method, use a method reference.
  2. Short class names are better. String::toLowerCase reads well. AbstractDatabaseConnectionFactoryImpl::createConnection does not.
  3. Well-known methods are best. Developers instantly recognize System.out::println or Integer::parseInt. Custom methods need good naming.
  4. If you have to think about it, use a lambda. Method references should make code easier to read, not harder.
  5. Be consistent within a team. Choose a style and stick with it across the codebase.

12.3 Performance Considerations

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]]
    }
}

13. Complete Practical Example

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

14. Quick Reference

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::parseInt
Math::abs
String::valueOf
Objects::nonNull
Collections::unmodifiableList
Instance method (particular object) instance::method (args) -> instance.method(args) System.out::println
myList::add
myString::concat
"literal"::equals
Instance method (arbitrary object) ClassName::instanceMethod (obj, args) -> obj.method(args)
or obj -> obj.method()
String::toLowerCase
String::length
String::compareTo
Object::toString
Constructor ClassName::new (args) -> new ClassName(args) ArrayList::new
StringBuilder::new
String[]::new
File::new

Decision flowchart for choosing between lambda and method reference:

  1. Does the lambda body call a single existing method? If no, use a lambda.
  2. Are the lambda parameters passed directly and in order to that method? If no, use a lambda.
  3. Does the method reference improve readability? If no, use a lambda.
  4. Otherwise, use a method reference.

Key takeaways:

  • Method references are syntactic sugar for lambdas — they compile to identical bytecode
  • The :: operator creates a reference to a method without calling it
  • The compiler infers the functional interface from context (target typing)
  • Use method references when they make code more readable, not less
  • All four types serve different purposes — learn to recognize which one applies
  • IDEs will suggest method references when appropriate — pay attention to those hints
  • When in doubt, write the lambda first, then let the IDE suggest the method reference



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *