Java – Method Reference

1. What is a Method Reference?

Imagine you are writing directions for someone. You could say: “Go to the kitchen, open the drawer, pick up the knife, and cut the bread.” Or you could simply say: “Cut the bread” — because the person already knows how to do it. A method reference works the same way. Instead of spelling out the steps inside a lambda expression, you point directly to an existing method that already does the work.

A method reference is a shorthand notation for a lambda expression that calls a single existing method. Introduced in Java 8 alongside lambdas and the Stream API, method references use the :: operator (double colon) to refer to a method without invoking it.

Here is the core idea: if your lambda expression does nothing more than call an existing method, you can replace the lambda with a method reference.

import java.util.Arrays;
import java.util.List;

public class MethodReferenceIntro {
    public static void main(String[] args) {
        List names = Arrays.asList("Alice", "Bob", "Charlie");

        // Lambda expression -- spells out the call
        names.forEach(name -> System.out.println(name));

        // Method reference -- points directly to the method
        names.forEach(System.out::println);

        // Both produce the same output:
        // Alice
        // Bob
        // Charlie
    }
}

In the example above, the lambda name -> System.out.println(name) receives a parameter and immediately passes it to println. Since the lambda is just a pass-through — it adds no extra logic — the method reference System.out::println is a cleaner, more readable alternative.

Key points about method references:

  • Syntactic sugar — A method reference is not a new feature in the JVM. It compiles to the same bytecode as the equivalent lambda expression. It is purely a readability improvement.
  • The :: operator — Separates the class or instance from the method name. It tells the compiler “look up this method and use it as the implementation of the functional interface.”
  • No parentheses — You write String::toUpperCase, not String::toUpperCase(). Adding parentheses would invoke the method immediately rather than referencing it.
  • Must match a functional interface — The referenced method’s signature (parameter types and return type) must be compatible with the single abstract method of the target functional interface.
  • Four types — Java defines exactly four kinds of method references, each covering a different scenario.

2. The Four Types of Method References

Every method reference falls into one of four categories. The table below summarizes each type, its syntax, and what the equivalent lambda expression looks like.

Type Syntax Lambda Equivalent Example
Static method ClassName::staticMethod (args) -> ClassName.staticMethod(args) Integer::parseInt
Instance method of a particular object instance::method (args) -> instance.method(args) System.out::println
Instance method of an arbitrary object ClassName::instanceMethod (obj, args) -> obj.instanceMethod(args) String::compareToIgnoreCase
Constructor ClassName::new (args) -> new ClassName(args) ArrayList::new

The first two are straightforward. The third — instance method of an arbitrary object — is the one that confuses most developers. We will dedicate extra attention to it in section 5. Let us now explore each type in detail.

3. Static Method Reference (ClassName::staticMethodName)

A static method reference points to a static method on a class. The compiler maps the functional interface’s parameters directly to the static method’s parameters.

Pattern:

Lambda Method Reference
(args) -> ClassName.staticMethod(args) ClassName::staticMethod

The arguments from the functional interface are passed directly to the static method in the same order.

3.1 Built-in Static Methods

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StaticMethodReference {
    public static void main(String[] args) {

        // --- Integer::parseInt ---
        // Lambda:   s -> Integer.parseInt(s)
        // Reference: Integer::parseInt
        List 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]
    }
}

3.2 Custom Static Methods

Method references are not limited to the standard library. You can reference any static method you write, as long as its signature matches the functional interface.

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class CustomStaticMethodReference {

    // Custom static method: checks if a number is even
    public static boolean isEven(int number) {
        return number % 2 == 0;
    }

    // Custom static method: formats a name
    public static String formatName(String name) {
        return name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();
    }

    // Custom static method: calculates the square
    public static int square(int n) {
        return n * n;
    }

    public static void main(String[] args) {

        // Using custom static method reference as a Predicate
        List 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
    }
}

4. Instance Method Reference of a Particular Object (instance::methodName)

This type references an instance method on a specific, already-existing object. The object is captured at the time the method reference is created, and the functional interface’s parameters are passed as arguments to that method.

Pattern:

Lambda Method Reference
(args) -> existingObject.method(args) existingObject::method

The key distinction from the arbitrary-object type (section 5) is that here the object is known and fixed before the method reference is used. The most iconic example is System.out::printlnSystem.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]
    }
}

4.1 Using with Custom Objects

Method references on particular objects are especially useful when you have a service or helper object with instance methods you want to use in a stream pipeline.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class CustomObjectMethodReference {

    // A validator object with instance methods
    static class EmailValidator {
        private final String requiredDomain;

        public EmailValidator(String requiredDomain) {
            this.requiredDomain = requiredDomain;
        }

        public boolean isValid(String email) {
            return email != null
                    && email.contains("@")
                    && email.endsWith(requiredDomain);
        }
    }

    // A formatter object
    static class NameFormatter {
        private final String title;

        public NameFormatter(String title) {
            this.title = title;
        }

        public String format(String name) {
            return title + " " + name;
        }
    }

    public static void main(String[] args) {
        // Create specific objects
        EmailValidator companyValidator = new EmailValidator("@company.com");
        NameFormatter doctorFormatter = new NameFormatter("Dr.");

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

4.2 The “this” and “super” References

Inside an instance method, you can use this::methodName or super::methodName as method references. this refers to the current object, and super refers to the parent class implementation.

import java.util.Arrays;
import java.util.List;

public class ThisSuperReference {

    static class Animal {
        public void speak(String sound) {
            System.out.println("Animal says: " + sound);
        }
    }

    static class Dog extends Animal {
        @Override
        public void speak(String sound) {
            System.out.println("Dog barks: " + sound);
        }

        public void makeNoise(String sound) {
            System.out.println("Dog noise: " + sound.toUpperCase());
        }

        public void demonstrate() {
            List sounds = Arrays.asList("woof", "bark", "growl");

            System.out.println("--- Using this::makeNoise ---");
            sounds.forEach(this::makeNoise);
            // Dog noise: WOOF
            // Dog noise: BARK
            // Dog noise: GROWL

            System.out.println("--- Using this::speak (Dog's override) ---");
            sounds.forEach(this::speak);
            // Dog barks: woof
            // Dog barks: bark
            // Dog barks: growl

            System.out.println("--- Using super::speak (Animal's version) ---");
            sounds.forEach(super::speak);
            // Animal says: woof
            // Animal says: bark
            // Animal says: growl
        }
    }

    public static void main(String[] args) {
        new Dog().demonstrate();
    }
}

5. Instance Method Reference of an Arbitrary Object (ClassName::instanceMethodName)

This is the type that trips up most developers. The syntax looks identical to a static method reference — you write ClassName::methodName — but the method is not static. It is an instance method. The difference is in which object the method is called on.

Pattern:

Lambda Method Reference
(obj) -> obj.instanceMethod() ClassName::instanceMethod
(obj, arg) -> obj.instanceMethod(arg) ClassName::instanceMethod

How it works: The first parameter of the functional interface becomes the object on which the method is invoked. Any remaining parameters become the method’s arguments. The “arbitrary” object is whichever object happens to flow through the stream or be supplied by the functional interface at runtime.

Let us break this down step by step. Consider a list of strings and the task of converting each to uppercase:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ArbitraryObjectBasic {
    public static void main(String[] args) {
        List 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.

5.1 Understanding the Transformation

This is the critical mental model: the first parameter of the functional interface becomes the target object. The table below shows how this transformation works for common examples.

Method Reference Functional Interface Lambda Equivalent Who calls the method?
String::toUpperCase Function<String, String> s -> s.toUpperCase() The stream element (s)
String::length Function<String, Integer> s -> s.length() The stream element (s)
String::isEmpty Predicate<String> s -> s.isEmpty() The stream element (s)
String::trim Function<String, String> s -> s.trim() The stream element (s)
String::compareTo Comparator<String> (a, b) -> a.compareTo(b) The first parameter (a)
String::compareToIgnoreCase Comparator<String> (a, b) -> a.compareToIgnoreCase(b) The first parameter (a)

Notice the last two rows: when the functional interface has two parameters (like Comparator which takes two arguments), the first becomes the object, and the second becomes the method argument.

5.2 With One Parameter (No-arg instance method)

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ArbitraryObjectNoArg {
    public static void main(String[] args) {

        // --- String::length ---
        // Function: 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]
    }
}

5.3 With Two Parameters (Instance method takes an argument)

When the functional interface accepts two parameters, the first parameter becomes the object and the second becomes the method argument. This is common with Comparator and BiFunction.

import java.util.Arrays;
import java.util.List;

public class ArbitraryObjectWithArg {
    public static void main(String[] args) {

        // --- String::compareTo used as Comparator ---
        // 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!
    }
}

5.4 With Custom Classes

The arbitrary-object pattern works with your own classes too. If a stream contains objects of type Person, you can write Person::getName to call getName() on each Person object flowing through the stream.

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class ArbitraryObjectCustomClass {

    static class Person {
        private final String name;
        private final int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() { return name; }
        public int getAge()     { return age; }

        @Override
        public String toString() {
            return name + " (" + age + ")";
        }
    }

    public static void main(String[] args) {
        List 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)]
    }
}

5.5 How Does the Compiler Know Which Type?

You might wonder: when you write String::toUpperCase, how does the compiler know this is an arbitrary-object instance method reference and not a static method reference? The answer is simple: the compiler checks whether toUpperCase is a static method or an instance method on the String class. Since String.toUpperCase() is an instance method, the compiler resolves it as the arbitrary-object type.

If String had a static toUpperCase(String s) method, there would be an ambiguity, and the compiler would raise an error. Fortunately, this situation is rare in practice.

6. Constructor Reference (ClassName::new)

A constructor reference points to a constructor of a class. The compiler determines which constructor to call based on the functional interface’s parameter types.

Pattern:

Lambda Method Reference
() -> new ClassName() ClassName::new
(arg) -> new ClassName(arg) ClassName::new
(arg1, arg2) -> new ClassName(arg1, arg2) ClassName::new

The same ClassName::new syntax works for all constructors. The compiler infers which constructor to use from the context (the functional interface it needs to match).

6.1 No-Argument Constructor (Supplier)

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.function.Supplier;

public class ConstructorReferenceNoArg {
    public static void main(String[] args) {

        // --- ArrayList::new as Supplier> ---
        // 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
    }
}

6.2 One-Argument Constructor (Function)

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

public class ConstructorReferenceOneArg {

    static class User {
        private final String name;

        public User(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "User{name='" + name + "'}";
        }
    }

    public static void main(String[] args) {

        // --- User::new as Function ---
        // 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]
    }
}

6.3 Two-Argument Constructor (BiFunction)

import java.util.function.BiFunction;

public class ConstructorReferenceTwoArg {

    static class Product {
        private final String name;
        private final double price;

        public Product(String name, double price) {
            this.name = name;
            this.price = price;
        }

        @Override
        public String toString() {
            return name + " ($" + String.format("%.2f", price) + ")";
        }
    }

    public static void main(String[] args) {

        // --- Product::new as BiFunction ---
        // 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)
    }
}

6.4 Array Constructor Reference (Type[]::new)

A special form of constructor reference creates arrays. This is particularly useful with the toArray() method on streams, which needs an IntFunction<T[]> that takes a size and returns an array of that size.

import java.util.Arrays;
import java.util.List;
import java.util.function.IntFunction;
import java.util.stream.Stream;

public class ArrayConstructorReference {
    public static void main(String[] args) {

        // --- String[]::new with Stream.toArray() ---
        List 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]
    }
}

7. Method Reference vs Lambda Expression

Method references and lambdas are interchangeable when the lambda simply delegates to a single method call. But they are not always interchangeable, and one is not always better than the other. Here is a practical guide to deciding which to use.

7.1 When to Use a Method Reference

  • The lambda does nothing except call a single existing method
  • The method name clearly describes the operation (e.g., String::toUpperCase is self-documenting)
  • You want to reduce visual noise in a stream pipeline

7.2 When to Use a Lambda

  • You need to add extra logic (conditions, transformations, multiple statements)
  • You need to call a method with additional arguments that are not provided by the functional interface
  • The method reference would be ambiguous or confusing
  • You need to negate or combine the result

7.3 Conversion Examples

Lambda Method Reference Recommendation
s -> s.toUpperCase() String::toUpperCase Use method reference — cleaner
s -> System.out.println(s) System.out::println Use method reference — iconic pattern
() -> new ArrayList<>() ArrayList::new Use method reference — concise
s -> Integer.parseInt(s) Integer::parseInt Use method reference — clear intent
s -> s.length() > 3 No direct reference Use lambda — contains extra logic (comparison)
s -> "prefix_" + s No direct reference Use lambda — contains concatenation logic
(a, b) -> a + b Integer::sum Use method reference — Integer.sum() exists for this
x -> !x.isEmpty() Predicate.not(String::isEmpty) Use Predicate.not() (Java 11+) or lambda

7.4 Readability Comparison

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class MethodRefVsLambda {
    public static void main(String[] args) {
        List 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]
    }
}

8. Method References with Streams

Method references truly shine in stream pipelines, where they make each step in the pipeline read like a sentence. This section shows practical patterns you will use daily.

8.1 map() — Transforming Elements

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamMapExamples {

    static class Employee {
        private final String name;
        private final String department;
        private final double salary;

        public Employee(String name, String department, double salary) {
            this.name = name;
            this.department = department;
            this.salary = salary;
        }

        public String getName()       { return name; }
        public String getDepartment() { return department; }
        public double getSalary()     { return salary; }

        @Override
        public String toString() {
            return name + " [" + department + "] $" + String.format("%.0f", salary);
        }
    }

    public static void main(String[] args) {

        // --- String transformations ---
        List 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]
    }
}

8.2 filter() — Selecting Elements

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public class StreamFilterExamples {

    static class Task {
        private final String title;
        private final boolean completed;

        public Task(String title, boolean completed) {
            this.title = title;
            this.completed = completed;
        }

        public String getTitle()    { return title; }
        public boolean isCompleted() { return completed; }

        @Override
        public String toString() { return title + (completed ? " [DONE]" : " [TODO]"); }
    }

    public static void main(String[] args) {

        // --- Objects::nonNull for null filtering ---
        List 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]]
    }
}

8.3 sorted() — Ordering Elements

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class StreamSortedExamples {

    static class Student {
        private final String name;
        private final double gpa;
        private final int age;

        public Student(String name, double gpa, int age) {
            this.name = name;
            this.gpa = gpa;
            this.age = age;
        }

        public String getName() { return name; }
        public double getGpa()  { return gpa; }
        public int getAge()     { return age; }

        @Override
        public String toString() {
            return name + " (GPA: " + gpa + ", age: " + age + ")";
        }
    }

    public static void main(String[] args) {

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

8.4 forEach() — Consuming Elements

import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class StreamForEachExamples {
    public static void main(String[] args) {

        // --- System.out::println -- the classic method reference ---
        List 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
    }
}

8.5 collect() and reduce() Patterns

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class StreamCollectReduceExamples {

    static class Order {
        private final String product;
        private final String category;
        private final double amount;

        public Order(String product, String category, double amount) {
            this.product = product;
            this.category = category;
            this.amount = amount;
        }

        public String getProduct()  { return product; }
        public String getCategory() { return category; }
        public double getAmount()   { return amount; }

        @Override
        public String toString() { return product + " ($" + amount + ")"; }
    }

    public static void main(String[] args) {

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

9. Common Patterns and Techniques

9.1 Chaining Method References in Comparators

The Comparator class was designed with method references in mind. Its static and default methods (comparing(), thenComparing(), reversed(), nullsFirst(), nullsLast()) all accept method references as key extractors.

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

public class ComparatorChaining {

    static class Employee {
        private final String name;
        private final String department;
        private final double salary;

        public Employee(String name, String department, double salary) {
            this.name = name;
            this.department = department;
            this.salary = salary;
        }

        public String getName()       { return name; }
        public String getDepartment() { return department; }
        public double getSalary()     { return salary; }

        @Override
        public String toString() {
            return String.format("%-10s %-12s $%,.0f", name, department, salary);
        }
    }

    public static void main(String[] args) {

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

9.2 Using with Optional

Method references work beautifully with Optional to handle values that may or may not be present.

import java.util.Optional;

public class OptionalMethodReferences {

    static class Config {
        private final String databaseUrl;

        public Config(String databaseUrl) {
            this.databaseUrl = databaseUrl;
        }

        public String getDatabaseUrl() { return databaseUrl; }
    }

    public static void main(String[] args) {

        // --- map() with method reference ---
        Optional 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
    }
}

9.3 Factory Methods and the Strategy Pattern

Method references can replace verbose strategy pattern implementations. Instead of creating multiple anonymous classes, you can pass method references that match the required functional interface.

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

public class StrategyWithMethodReferences {

    // Different text processing strategies -- each is a simple static method
    static class TextProcessor {
        public static String toUpperCase(String s) { return s.toUpperCase(); }
        public static String toLowerCase(String s) { return s.toLowerCase(); }
        public static String trim(String s)        { return s.trim(); }
        public static String reverse(String s)     { return new StringBuilder(s).reverse().toString(); }
    }

    // A pipeline that accepts a list of strategies
    static List 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]
    }
}

10. Common Mistakes

Method references are concise, but they introduce subtle pitfalls. Here are the mistakes developers encounter most often.

10.1 Trying to Add Logic to a Method Reference

A method reference is a direct pointer to a method. You cannot embed extra logic, conditions, or transformations inside it. If you need any additional behavior, use a lambda.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MistakeExtraLogic {
    public static void main(String[] args) {
        List 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]
    }
}

10.2 Ambiguous Method References with Overloaded Methods

When a class has overloaded methods (same name, different parameters), the compiler may not be able to determine which one you mean. This results in a compile-time error.

import java.util.function.Function;
import java.util.function.BiFunction;

public class MistakeAmbiguousOverload {

    // Two overloaded static methods
    public static String format(int number) {
        return "Number: " + number;
    }

    public static String format(String text) {
        return "Text: " + text;
    }

    public static void main(String[] args) {

        // This works -- compiler can infer which 'format' based on Function
        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);
    }
}

10.3 Forgetting That Method References Must Match the Functional Interface

The referenced method must have a compatible signature with the functional interface’s abstract method. Parameter types, count, and return type must all be compatible.

import java.util.function.Function;
import java.util.function.BiFunction;
import java.util.function.Supplier;

public class MistakeSignatureMismatch {

    public static int add(int a, int b) {
        return a + b;
    }

    public static String greet(String name) {
        return "Hello, " + name;
    }

    public static void main(String[] args) {

        // CORRECT: add(int, int) -> int matches BiFunction
        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
    }
}

10.4 Confusing Static and Arbitrary-Object References

Because both static method references and arbitrary-object references use the ClassName::methodName syntax, developers sometimes confuse them. Remember: the compiler distinguishes them based on whether the method is static or not.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MistakeStaticVsArbitrary {
    public static void main(String[] args) {
        List 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.
    }
}

10.5 Using Method References on a Null Object

If you create a method reference on a particular object and that object is null, you will get a NullPointerException at the point where the method reference is created (not when it is called).

import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;

public class MistakeNullObject {
    public static void main(String[] args) {

        String validString = "Hello";
        Consumer 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
    }
}

11. Best Practices

Follow these guidelines to use method references effectively in production code.

# Practice Do Avoid
1 Prefer method references when they improve readability map(String::trim) map(s -> s.trim()) when no extra logic
2 Use lambdas for multi-step operations map(s -> s.trim().toUpperCase()) Trying to chain method references inside map()
3 Let the method name tell the story filter(Objects::nonNull) filter(o -> o != null)
4 Extract complex lambdas into named methods filter(this::isEligible) A 5-line lambda inline
5 Use Comparator.comparing() with method references sorted(comparing(Person::getAge)) sorted((a, b) -> a.getAge() - b.getAge())
6 Use Predicate.not() in Java 11+ filter(Predicate.not(String::isEmpty)) filter(s -> !s.isEmpty())
7 Use toArray(Type[]::new) for stream-to-array toArray(String[]::new) toArray(size -> new String[size])
8 Be consistent within a pipeline All method refs or all lambdas for similar steps Mixing for no reason

11.1 Extract Complex Lambdas Into Named Methods

If a lambda is too complex to express as a single method reference but too verbose to inline, the best approach is to extract the logic into a well-named private method and then reference it.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ExtractToNamedMethod {

    // Before: inline lambda with complex logic
    static List 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]
    }
}

12. Complete Practical Example

Let us tie everything together with a realistic data processing pipeline that uses all four types of method references. We will build an employee report system that reads raw CSV-style input, parses it into objects, transforms and sorts the data, and produces a summary report.

This example demonstrates:

  • Static method reference — parsing strings into objects (Employee::fromCsv)
  • Instance method of a particular object — using a formatter object (formatter::format)
  • Instance method of an arbitrary object — extracting fields (Employee::getSalary, Employee::getName)
  • Constructor reference — creating department summary objects (DepartmentSummary::new) and output arrays (String[]::new)
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class EmployeeReportSystem {

    // ========== Domain Classes ==========

    static class Employee {
        private final String name;
        private final String department;
        private final double salary;

        public Employee(String name, String department, double salary) {
            this.name = name;
            this.department = department;
            this.salary = salary;
        }

        // STATIC METHOD -- used as a static method reference
        public static Employee fromCsv(String csv) {
            String[] parts = csv.split(",");
            return new Employee(
                    parts[0].trim(),
                    parts[1].trim(),
                    Double.parseDouble(parts[2].trim())
            );
        }

        // INSTANCE METHODS -- used as arbitrary-object method references
        public String getName()       { return name; }
        public String getDepartment() { return department; }
        public double getSalary()     { return salary; }

        public boolean isHighEarner() {
            return salary >= 90000;
        }

        @Override
        public String toString() {
            return String.format("%-12s %-15s $%,.2f", name, department, salary);
        }
    }


    static class DepartmentSummary {
        private final String department;
        private final long count;
        private final double totalSalary;

        // CONSTRUCTOR -- used as a constructor reference
        public DepartmentSummary(String department, List 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)

Method Reference Types Used in This Example

Type Usage in Example Line
Static method Employee::fromCsv — parses CSV string into Employee map(Employee::fromCsv)
Static method Double::sum — adds two doubles in reduce reduce(0.0, Double::sum)
Particular object System.out::println — prints to console forEach(System.out::println)
Particular object formatter::format — formats using company name map(formatter::format)
Arbitrary object Employee::getName — extracts name from each employee map(Employee::getName)
Arbitrary object Employee::getSalary — extracts salary for sorting Comparator.comparing(Employee::getSalary)
Arbitrary object Employee::isHighEarner — filters high earners filter(Employee::isHighEarner)
Arbitrary object Employee::getDepartment — groups by department Collectors.groupingBy(Employee::getDepartment)
Arbitrary object String::compareTo — sorts strings naturally sorted(String::compareTo)
Array constructor String[]::new — converts stream to String array toArray(String[]::new)

Quick Reference

Concept Summary
What it is Shorthand for a lambda that calls a single existing method
Operator :: (double colon)
Static reference ClassName::staticMethod — args passed to static method
Particular object instance::method — args passed to method on that specific instance
Arbitrary object ClassName::instanceMethod — first arg becomes the object, rest are method args
Constructor ClassName::new — args passed to matching constructor
Array constructor Type[]::new — takes size, returns new array
No parentheses Write String::length, NOT String::length()
Must match signature Referenced method must be compatible with the functional interface
When to prefer When the lambda does nothing except call a single method
When to use lambda When you need extra logic, multiple statements, or additional arguments



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 *