Method references in Java are part of the broader set of features introduced and enhanced in Java 8 and beyond. They are a concise way to refer to methods of classes or objects. Method references are especially useful in the context of lambda expressions, where they are used to write cleaner, more readable code. This article is a deep dive into understanding and using method references in your Java programs.

What are method references?

Method references are a shorthand way to write lambda expressions that call a single method. Rather than implementing a method in a functional interface, a method reference simply points to an existing method. Method references are most useful for replacing lambda expressions that do nothing but call an existing method.

Method references are best used to simplify or enhance the clarity of your code. There are a few cases where they are particularly useful:

  • Simple method delegation: Use method references for straightforward delegations that don’t require modifying or processing arguments. For example, list.forEach(System.out::println) is clearer than a lambda expression like list.forEach(item -> System.out.println(item)).
  • Reusable logic: If the same logic or method is used repeatedly across your code, encapsulating it in a method and then referencing it can reduce duplication. An example is using String::trim on multiple streams or collections.
  • Enhancing readability: In cases where the method name clearly describes the action being performed, method references can make your code more readable. For example, the purpose of map(String::toLowerCase) is immediately clear.

The structure of a method reference

The following is a classic example of a method reference:


ClassName::staticMethod

The code instructs the program to “use the static method from this class” instead of writing a lambda.

Method references vs. lambda expressions

Now let’s compare two examples. The code below uses a lambda expression to convert Strings to Integers:


strings.stream().map(s -> Integer.parseInt(s));

Here’s the same operation using a method reference:

strings.stream().map(Integer::parseInt);

Both examples do the same thing: converting Strings to Integers. However, the method reference is shorter, which makes it easier to read and understand. You might also notice that the Integer.parseInt method contract fits with the map method, which is a Function that takes and returns a value:

 Stream map(Function super T, ? extends R> mapper);

The parseInt method receives a parameter and returns a value as expected from the functional interface Function:


public static int parseInt(String s) throws NumberFormatException { … }

Instance method of a particular object instance

Sometimes we need a way to refer to a method of an existing object instance. Here’s the basic syntax:


objectInstance::methodName

And here’s a simple example:


// Lambda Expression way
String prefix = "Mr. ";
names.stream().map(str -

Here are some common use cases where we might want to refer to a method of an existing object instance:


// String processing
String prefix = "Hello, ";
list.stream().map(prefix::concat);

// Collection methods
List result = new ArrayList();
list.forEach(result::add);

// Custom object
Formatter formatter = new Formatter();
data.stream().map(formatter::format);

That’s it! If you are calling a method on an existing object, you can use object::method rather than x -> object.method(x).

Next, we’ll consider a more complex scenario for instance method references.

Instance method of an arbitrary object of a particular type

In this scenario, rather than static methods, we use a method reference associated with instances of a specific class. This allows us to dynamically apply the method to various instances of that class. Such method references are used in functional programming to invoke instance-specific methods within functional interfaces, enhancing code flexibility and reuse.

For example, the toUpperCase() method of the String class does not take any parameters and returns a String. At first glance, this does not match the Function interface directly, as it is designed to take one input and produce one output. However, when you use a method reference like String::toUpperCase in the context of the map() function in a stream of String objects, Java treats it as an instance method reference of an arbitrary object of a particular type.

Elements of an instance method reference

Why does String::toUpperCase work in this context? Let’s break it down:

First, we have the type of String::toUpperCase. In this case, String::toUpperCase is an instance method reference. The instance method reference
refers to the toUpperCase() method that is called on instances of String.

Next, consider the expected signature. The map() function in Java’s Stream API expects a Function super T, ? extends R>. In the case of a stream of String objects, this becomes Function.

Then, we look at matching the signature:

  • The toUpperCase() method can be invoked on an instance of String and return another String.
  • In the functional interface Function, the apply method takes a String and returns a String.
  • When String::toUpperCase is used, each element of the stream (a String) is passed as the instance on which the toUpperCase() method is invoked. This effectively turns the method into one that matches Function, where the input String is the instance itself, and the output is the result of the toUpperCase() method.

Behind the scenes, when you write String::toUpperCase, Java binds each element of the stream (String object) to the toUpperCase() method. This means String::toUpperCase is interpreted as s -> s.toUpperCase() where s is each element of the stream.

Expected signatures in method references

Method references only work when the method’s signature is the same as the method in the expected functional interface. As an example, the map() function in Java’s Stream API expects a particular signature: Function super T, ? extends R>. In the case of a stream of String objects, this becomes Function.

Matching the signature

  • The toUpperCase() method can be invoked on an instance of String and return another String.
  • In the functional interface Function, the apply method takes a String and returns a String.
  • When String::toUpperCase is used, each element of the stream (a String) is passed as the instance on which the toUpperCase() method is invoked. This effectively turns the method into one that matches Function, where the input String is the instance itself, and the output is the result of the toUpperCase() method.

When you write String::toUpperCase, Java binds each element of the stream (String object) to the toUpperCase() method. This means String::toUpperCase is interpreted as s -> s.toUpperCase(), where s is each element of the stream.

This ability to implicitly treat instance methods as functions accepting their instance as a parameter is a powerful feature of Java’s lambda expressions and method references. It allows you to write very clean and expressive code. This example also shows the flexibility of Java’s type inference and method reference mechanics in the context of its functional programming capabilities.

Printing elements of a List

Printing list elements is another common use case that requires using an instance method of an arbitrary object of a particular type in Java. This time, we’ll use the List interface and its method .forEach in combination with the Consumer functional interface.

Suppose you have a list of Mascot objects. Each one has a method, printGreeting(), that prints the mascot’s name. You want to print the name of each person in the list. Here’s how to do it using a method reference:


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

public class Mascot {
    private String name;

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

    public void printGreeting() {
        System.out.println("Hello, I'm " + name + ", welcome to Java!");
    }

    public static void main(String[] args) {
        List mascots = Arrays.asList(new Mascot("Duke"), new Mascot("Juggy"));

        // Print a greeting from each mascot using a method reference
        mascots.forEach(Mascot::printGreeting);
    }
}

Note that the forEach method in Java’s Iterable interface (which List extends) expects a single argument that matches the Consumer functional interface you saw in a previous example. The Consumer interface is designed to execute an operation on a single input argument. In this case, the accept method takes one parameter of type T and returns no result (void), which means it operates via side effects (modifying state, printing output, etc.).

Matching the method reference

When you use Mascot::printGreeting as a method reference, you are referring to the printGreeting method defined within the Mascot class:


public void printGreeting() {
    System.out.println("Hello, I'm " + name + ", welcome to Java!");
}

This method takes no parameters and returns no value (void), and is called on instances of the Mascot class.

How Mascot::printGreeting fits Consumer

The forEach method is designed to work with a Consumer super T>, where T is the type of elements in the Iterable (in this case, Mascot). Let’s see how Mascot::printGreeting fits into this framework:

  • Type compatibility: The printGreeting method is an instance method that can be invoked on any object of type Mascot.
  • Functional interface match: The printGreeting method matches the accept method of the Consumer interface because it operates on a single instance of Mascot and matches the signature void accept(Mascot mascot).
  • Method reference adaptation: In the context of Mascot::printGreeting, Java treats this method reference as an implementation of Consumer. Each time forEach iterates over the list, it implicitly passes the current Mascot object to printGreeting.

Constructor references

Constructor references are a powerful feature often used with streams and collections. They provide a clean, concise way to create new objects from elements of streams or collections. Consider the following example, where constructor references simplify the process of converting data within collections.

In this example, we convert a List of names to user objects:


public class User {
    private String name;

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

   // Omitted getters and setters
}

List names = Arrays.asList("John",  "Mary",  "Bob");
List users = names.stream()
    .map(User::new)  // Creates a new User for each name
    .collect(Collectors.toList());
System.out.println(users);

Here’s the output from the above code:


[John, Mary, Bob]

Notice the elements of this code:

  • Data: You start with a list of names (["John", "Mary", "Bob"]).
  • Stream: The names are turned into a stream using names.stream().
  • User objects: User::new is a shortcut to call the User constructor for each name. This creates a new User object from each name.
  • Collect: The User objects are collected into a list using collect(Collectors.toList()).

This example shows how User::new makes the code cleaner by replacing more complex lambdas like name -> new User(name).

References to instance methods in superclass and current class types

In Java, the keywords super and this play crucial roles in contextually referring to the current object and its superclass. When used with method references, these keywords give you a clear way to refer to methods of the current instance or methods of the superclass instance. This can be particularly useful in cases where method names are overridden, or when you want to preserve the semantics of your code for readability and maintenance.

‘this’ with method references

We use this within method references to refer to the method of the current instance of the class. It’s particularly useful when you need to pass an instance method of the current object as a parameter to another method, often done in event handling or callback scenarios.

Here’s a simple example where the this keyword is used in a method reference within the context of the current class:


public class Greeter {
    public void greet() {
        System.out.println("Hello, welcome!");
    }

    public void performGreet(Runnable action) {
        action.run();
    }

    public void executeGreeting() {
        // Using 'this' with method reference to refer to the current instance's method
        performGreet(this::greet);
    }

    public static void main(String[] args) {
        new Greeter().executeGreeting();
    }
}

In this example:

  • this::greet is a method reference that points to the greet method of the current Greeter instance.
  • The method executeGreeting uses this method reference to pass the greet method as a Runnable to performGreet.

‘super’ with method references

Using super with method references allows you to refer specifically to methods of a superclass that might have been overridden in the current class. This is useful in subclass contexts where you need to invoke the superclass’s version of a method.

Let’s consider a situation where a class overrides a method from its superclass and wants to refer to the superclass’s method version using a method reference:


class Base {
    public void show() {
        System.out.println("Base class show method.");
    }
}

public class Derived extends Base {
    @Override
    public void show() {
        System.out.println("Derived class show method.");
    }

    public void performShow(Runnable action) {
        action.run();
    }

    public void executeShow() {
        // Using 'super' with method reference to refer to the superclass's method
        performShow(super::show);
    }

    public static void main(String[] args) {
        new Derived().executeShow();
    }
}

In this example:

  • super::show is a method reference that points to the show method of the superclass Base from the subclass Derived.
  • The method executeShow uses this method reference to ensure that the superclass method is used, even though it is overridden in the current class.

Practical applications of method references

You can use method references wherever functional interfaces are applicable. They are particularly useful in stream operations, event listener setups, and scenarios involving methods from functional programming interfaces like Function, Consumer, Predicate, and Supplier. Here are some examples:

  • Stream operations: In the Java Streams API, method references are commonly used in map, filter, forEach, and other operations. For example, stream.map(String::toUpperCase) uses a method reference to transform each element of a stream to uppercase.
  • Event listeners: In GUI applications or event-driven programming, method references simplify the attachment of behavior to events. As an example, button.addActionListener(this::methodName) binds the method directly as an event listener.
  • Constructors and factories: When using Java Streams and other APIs that require object generation, constructor references provide a shorthand for object creation, such as Stream.generate(ClassName::new).

Advantages and disadvantages of using method references

Method references enhance the expressive power of Java, allowing developers to write more functional-style programming. Their integration with Java’s existing features like streams and lambdas helps developers write clean, maintainable code. Features like method references are part of Java’s ongoing integration of functional programming patterns.

Using method references has a few advantages:

  • Readability: Code written using method references is clearer and more succinct.
  • Efficiency: Method references help reduce boilerplate code compared to traditional anonymous class implementations.
  • Maintainability: Method references are easier than lambdas to understand and modify.

While method references can simplify and clarify code, they also have potential downsides that can lead to less maintainable code:

  • Overuse in complex scenarios: Avoid using method references for methods with complex operations or when transformations on parameters are needed before method application. For instance, if a lambda involves modifying parameters before a method call, replacing it with a method reference might obscure what’s happening, as in stream.map(x -> process(x).save()) versus stream.map(MyClass::save).
  • Reduced readability: Method references should not be used if they will make your code harder to understand. This can happen if the method being referenced is not self-explanatory or involves side effects that are not immediately clear from the context of use.
  • Debugging challenges: Debugging might be slightly more challenging with method references because they do not provide as much inline detail as lambdas. When something goes wrong, it might be harder to pinpoint if a method reference is passing unexpected values.

By keeping these best practices and pitfalls in mind, developers can effectively use method references to write cleaner, more efficient Java code while avoiding common traps that might lead to code that is hard to read and maintain.

Are method references better than lambdas?

Method references can yield better performance than lambda expressions, especially in certain contexts. Here are the key points to consider regarding the performance benefits of method references:

  1. Reduced code overhead: Method references often result in cleaner and more concise code. They do not require the boilerplate code that lambdas might (such as explicit parameter declaration), which can make the resulting bytecode slightly more efficient. This reduction in code complexity can make optimization by the Java virtual machine easier, potentially leading to better performance.
  2. JVM optimization: The JVM has optimizations specifically tailored for method references, such as invoking invokevirtual or invokeinterface directly without the additional overhead of the lambda wrapper. These optimizations can potentially make method references faster or at least as fast as equivalent lambda expressions.
  3. Reuse of method reference instances: Unlike lambda expressions, method references do not capture values from the enclosing context unless explicitly required. This often allows the JVM to cache and reuse instances of method references more effectively than lambda expressions, which might capture different values and thus may require separate instances each time they are created.
  4. Specific scenarios: The performance advantage of method references can be more noticeable in specific scenarios where the same method is reused multiple times across a large dataset, such as in streams or repetitive method calls within loops. In these cases, the reduced overhead and potential caching benefits can contribute to better performance.
  5. Benchmarking is key: While theoretical advantages exist, the actual performance impact can vary depending on the specific use case, the complexity of the lambda expression, and the JVM implementation. It’s generally a good practice to benchmark performance if it is a critical factor.