Method reference in Java 8

  • Last Updated: August 30, 2023
  • By: javahandson
  • Series
img

Method reference in Java 8

Method reference in Java lets you reuse an existing method wherever a lambda would go. Learn the :: operator, all four reference types, and when to pick each one.

1. Introduction

You write a lambda. Then you look at it and realise it does nothing new. It just calls a method that already exists somewhere in your code.

That happens more often than you would think. A lambda that only forwards its arguments to another method is pure ceremony. It adds a line, adds a pair of brackets, and adds nothing else.

Java 8 gave us a shortcut for exactly that case. The double colon operator, written as ::, points straight at an existing method. No wrapper, no forwarding, no duplicated logic.

Think of it like giving someone a phone number instead of relaying the call yourself. You are not doing the work. You hand over a way to reach whoever already does it.

This shortcut shows up everywhere in modern Java. Stream pipelines lean on it heavily, and so do sorting, factories, and collectors. Once you can read it, a lot of real-world Java suddenly gets shorter and clearer.

1.1 What This Article Covers

We start from a plain lambda and shrink it step by step. From there we open up every form the :: operator can take. Here is the plan:

  • What a method reference is, and the lambda it replaces
  • The :: syntax, and what sits on each side of it
  • All four types, from static references to constructor references
  • Bound versus unbound, which trips up most beginners
  • The rules the compiler checks before it accepts your reference
  • Real stream examples you will actually write at work
  • When a plain lambda remains the better choice
  • Six pitfalls, then a full walkthrough and interview questions

2. What Is a Method Reference?

A method reference is a compact way to pass an existing method as behaviour. Instead of describing the work again inside a lambda, you name the method that already does it.

2.1 The Problem It Solves

Say we have a small class with an addition method that adds two integers.

package com.javahandson.method.reference;

public class Test {
    public static void addition(int a, int b) {
        System.out.println(a + b);
    }
}

Now we need a functional interface with a matching shape. Its single method also takes two integers.

@FunctionalInterface
interface Operation {
    void add(int a, int b);
}

The obvious move is a lambda. So we write one that adds the two numbers and prints the total.

public class Demo {
    public static void main(String[] args) {
        Operation operation = (int a, int b) -> System.out.println(a + b);
        operation.add(10, 15); // Output: 25
    }
}

Look closely and something feels off. Our lambda body repeats what Test.addition already does, character for character. We copied logic that had a perfectly good home.

2.2 From Lambda to Method Reference

So why write the logic twice? Point at the existing method instead.

public class Demo {
    public static void main(String[] args) {
        Operation operation = Test::addition;
        operation.add(10, 15); // Output: 25
    }
}

Same result, one clean line. The add method of Operation now routes straight to the addition method of Test. That routing is what the name “method reference” describes.

Notice the missing brackets after addition. We are not calling the method here. We hand over a way to call it later, and the interface decides when that happens.

2.3 The :: Syntax

Every method reference has the same shape. A target sits on the left of the ::, and a method name sits on the right.

Target :: methodName

ClassName    :: staticMethod      // static
instanceName :: instanceMethod    // bound to one object
ClassName    :: instanceMethod    // unbound, any object
ClassName    :: new               // constructor

The target tells Java where to look. A class name sends it to the class, while a variable sends it to that specific object.

Three rules cover the whole syntax:

  • Never put brackets after the method name, because you are not invoking anything
  • Skip the arguments too, since the functional interface supplies them at call time
  • Use the keyword new on the right when you want a constructor rather than a method

2.4 It Is Still a Functional Interface

A method reference is not a new kind of value. It produces an instance of a functional interface, exactly like a lambda does.

That means every rule you already know still applies. The target type must have exactly one abstract method, and that method decides which references fit.

You can spot this in the code above. Operation holds just one abstract method, so the line compiles. Add a second one and the same line breaks.

Shaky on that idea? Start with Custom Functional Interface in Java. The :: form makes far more sense once that piece clicks.

3. The Four Types of Method References

Many tutorials stop at two types. Java actually defines four, and the two instance forms behave very differently.

3.1 The Big Picture

Here are the four, in the order we will cover them:

  1. A static form, which points at a class
  2. A bound form, which points at one object
  3. An unbound form, which names a class but calls an instance method
  4. Last comes the constructor form, which builds a new object

Here is the syntax for each:

ClassName    :: staticMethod     // 1. static
instanceName :: instanceMethod   // 2. bound
ClassName    :: instanceMethod   // 3. unbound
ClassName    :: new              // 4. constructor

Types 2 and 3 look almost alike on the page. One names an object, the other names a class. That single difference changes how arguments flow.

3.2 Type 1: Static Method Reference

This one is the easiest. You point at a static method through its class name.

import java.util.function.Function;

public class StaticRefDemo {
    public static void main(String[] args) {
        Function<String, Integer> parser = Integer::parseInt;

        int value = parser.apply("42");
        System.out.println(value + 8); // Output: 50
    }
}

The parseInt method takes a String and gives back an int. Function wants exactly that shape, so the reference fits without any glue.

Our earlier addition example belongs here too, since it carries the static keyword. A class name on the left is your clue.

3.3 Type 2: Bound Instance Method Reference

Sometimes you already hold the object you want to call. Put that variable on the left of the :: and the reference locks onto it.

import java.util.function.Supplier;

public class BoundRefDemo {
    public static void main(String[] args) {
        String greeting = "Hello Java";

        Supplier<String> shout = greeting::toUpperCase;
        System.out.println(shout.get()); // Output: HELLO JAVA
    }
}

The word “bound” says it plainly. This reference ties itself to the greeting object and never touches another one.

Count the inputs and the pattern jumps out. Supplier takes none at all, since the object is already fixed. Nothing is left for the caller to pass.

Your old Test example works the same way once addition drops the static keyword.

package com.javahandson.method.reference;

public class Test {
    void addition(int a, int b) {
        System.out.println(a + b);
    }
}

class Demo {
    public static void main(String[] args) {
        Test test = new Test();

        Operation operation = test::addition;
        operation.add(10, 15); // Output: 25
    }
}

Because addition now belongs to an object, we create that object first. Then the variable name test goes on the left instead of the class name.

3.4 Type 3: Unbound Instance Method Reference

Here is the form that confuses people. You write a class name on the left, yet the method on the right is an instance method.

import java.util.function.Function;

public class UnboundRefDemo {
    public static void main(String[] args) {
        Function<String, Integer> lengthOf = String::length;

        System.out.println(lengthOf.apply("javahandson")); // Output: 11
    }
}

The length method takes no inputs, so why does Function pass one? Because that first input becomes the object. Java quietly moves it to the left of the dot:

lengthOf.apply("javahandson")   // what you write
"javahandson".length()          // what Java calls

So the rule is simple. Input one supplies the object, and the rest go to the method itself.

import java.util.function.BiPredicate;

public class UnboundTwoArgs {
    public static void main(String[] args) {
        // "javahandson".startsWith("java")
        BiPredicate<String, String> startsWith = String::startsWith;

        System.out.println(startsWith.test("javahandson", "java")); // Output: true
    }
}

Read that comment carefully. The first input slides left of the dot, and the second stays inside the brackets. Every unbound reference follows this shift.

3.5 Type 4: Constructor Reference

Constructors get a reference form too. Write new on the right of the ::, and you have a factory.

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

public class ConstructorRefDemo {
    public static void main(String[] args) {
        Supplier<ArrayList<String>> maker = ArrayList::new;

        ArrayList<String> names = maker.get();
        names.add("Ravi");
        System.out.println(names); // Output: [Ravi]
    }
}

Constructors with parameters work just as smoothly. Pick a functional interface whose arguments match the constructor you want.

import java.util.function.Function;

class Employee {
    private final String name;

    Employee(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class EmployeeFactory {
    public static void main(String[] args) {
        Function<String, Employee> factory = Employee::new;

        Employee ravi = factory.apply("Ravi");
        System.out.println(ravi.getName()); // Output: Ravi
    }
}

Which constructor runs? Java picks the one matching the interface arguments. A single String argument selects the single String constructor.

3.6 All Four Side by Side

TypeWritten asExampleEquivalent lambda
StaticClass::staticMethodInteger::parseInts -> Integer.parseInt(s)
Bound instanceobject::instanceMethodgreeting::toUpperCase() -> greeting.toUpperCase()
Unbound instanceClass::instanceMethodString::lengths -> s.length()
ConstructorClass::newArrayList::new() -> new ArrayList()

Keep the last column handy. Whenever a reference puzzles you, expand it back into its lambda and the argument flow becomes visible again.

4. The Matching Rules

The compiler checks a short list before it accepts your reference. Some parts must match exactly, and others enjoy a little slack.

4.1 Arguments Must Line Up

This rule has no flexibility. The referenced method must accept the arguments the interface hands over.

public class Test {
    public static int addition(int a, int b) {
        return a + b;
    }
}

@FunctionalInterface
interface Operation {
    int add(int a, int b, int c); // three arguments
}

class Demo {
    public static void main(String[] args) {
        Operation operation = Test::addition; // compile error

        System.out.println(operation.add(10, 15, 20));
    }
}

/*
java: incompatible types: invalid method reference
    method addition in class Test cannot be applied to given types
      required: int,int
      found:    int,int,int
      reason: actual and formal argument lists differ in length
*/

Operation promises three integers while addition accepts two. Java refuses at compile time, which beats discovering the mismatch in production.

4.2 Return Types Can Widen

Return types get gentler treatment. A narrower primitive can flow into a wider one through normal widening conversion.

public class Test {
    public static int addition(int a, int b) {
        return a + b; // returns int
    }
}

@FunctionalInterface
interface Operation {
    float add(int a, int b); // wants float
}

class Demo {
    public static void main(String[] args) {
        Operation operation = Test::addition;

        System.out.println(operation.add(10, 15)); // Output: 25.0
    }
}

An int widens to a float without complaint, so the reference compiles. Note the println wrapper. Without it nothing reaches the console, because addition returns a value rather than printing one.

Turn the rule around and it fails. A method returning float cannot satisfy an interface that promises int, since that direction loses data.

4.3 A Returned Value Can Fill a void Slot

Here is a handy asymmetry. A method that returns something can satisfy a void abstract method, and Java simply discards the result.

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

public class VoidSlotDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();

        // List.add returns boolean, Consumer wants void
        Consumer<String> adder = names::add;

        adder.accept("Ravi");
        adder.accept("Anjali");
        System.out.println(names); // Output: [Ravi, Anjali]
    }
}

List.add hands back a boolean, yet Consumer declares void accept. The boolean quietly disappears and the code still compiles.

Try the reverse and you hit a wall. A void method can never fill a slot that demands a value, because there is nothing to hand back.

4.4 Access Modifiers Follow Normal Rules

The modifier on the referenced method does not have to match the interface. A private method can back a public abstract method without any trouble.

@FunctionalInterface
interface Operation {
    float add(int a, int b); // implicitly public
}

public class Demo {
    private static int addition(int a, int b) { // private
        return a + b;
    }

    public static void main(String[] args) {
        Operation operation = Demo::addition;

        System.out.println(operation.add(10, 15)); // Output: 25.0
    }
}

One catch deserves attention. Ordinary visibility rules still bind you, so this compiles only because main sits inside Demo. Move that reference to another class and the private method disappears from view.

5. Method References in Stream Code

Textbook examples are fine, but streams are where you will meet the :: operator daily. A few patterns cover most real code.

5.1 Printing With System.out::println

This is the reference every Java developer types first.

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

public class PrintDemo {
    public static void main(String[] args) {
        List<String> cities = Arrays.asList("Pune", "Delhi", "Kochi");

        cities.forEach(System.out::println);
    }
}

/*
Pune
Delhi
Kochi
*/

Which of the four types is this? Here System.out names an object, not a class. So we have a bound reference, and every item goes to that one stream.

5.2 Transforming With String::toUpperCase

Mapping is the natural home for unbound references. Each item arrives and takes its turn as the object.

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

public class MapDemo {
    public static void main(String[] args) {
        List<String> cities = Arrays.asList("Pune", "Delhi", "Kochi");

        List<String> loud = cities.stream()
                .map(String::toUpperCase)
                .collect(Collectors.toList());

        System.out.println(loud); // Output: [PUNE, DELHI, KOCHI]
    }
}

Compare the two styles side by side. The lambda form says the same thing with more noise, while the reference states the intent and stops.

5.3 Sorting With a Comparator

Sorting needs a key, and a reference names that key in one short phrase.

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

public class SortDemo {
    public static void main(String[] args) {
        List<String> cities = Arrays.asList("Pune", "Delhi", "Kochi");

        cities.sort(Comparator.comparing(String::length));
        System.out.println(cities); // Output: [Pune, Delhi, Kochi]

        cities.sort(String::compareToIgnoreCase);
        System.out.println(cities); // Output: [Delhi, Kochi, Pune]
    }
}

The second sort hides a neat trick. Comparator needs two inputs, yet the compare method takes just one. Unbound rules save the day here, since input one becomes the object and input two goes inside.

6. Lambda or Method Reference?

The :: form is not always the better one. Reach for it when the code truly reads more clearly.

6.1 When the Method Reference Wins

  • Your lambda body holds a single call and nothing else
  • The inputs travel straight through, in the same order
  • Some method with a good name already does the work
  • Naming it reads better than spelling out its body again

6.2 When the Lambda Wins

  • You need to flip a result, as with a not sign
  • Inputs arrive in the wrong order and need swapping
  • More than one statement belongs inside the body
  • Fixed values or outside variables join the call
  • Readers would have to stop and decode the short form

6.3 A Quick Comparison

AspectLambda expressionMethod reference
ShapeParameters, arrow, bodyTarget, ::, method name
LogicYou write it inlineIt already lives elsewhere
Extra logicNegation, maths, branchingNot allowed
Argument orderYou choose freelyMust pass straight through
ReuseCopy the body aroundOne method, many call sites
Best forAnything with real logicPlain forwarding calls

Notice that both columns end up at the same place. Each one builds an instance of the interface, so you choose on style, not on power.

7. Benefits of Method References

Why bother learning a second syntax? Four payoffs make it worthwhile:

  1. Shorter code, since a whole lambda body shrinks to one name
  2. Less noise, because the arrow and its inputs drop away
  3. Clearer code, as a good method name explains itself
  4. One home for the logic, so a fix lands in a single place

That last point carries the most weight. Copied lambda bodies drift apart over time, and a shared method cannot.

8. Common Mistakes and Pitfalls

These six trip up almost everyone at least once. Learn them here rather than during a code review.

8.1 Adding Brackets After the Method Name

Your fingers add brackets out of habit. Resist them.

Function<String, Integer> wrong = Integer::parseInt();  // compile error
Function<String, Integer> right = Integer::parseInt;   // correct

Brackets mean “run this now”. A method reference means “here is how to run it later”, so the two ideas cannot mix.

8.2 A null Receiver Fails Early

A bound reference evaluates its target immediately, not at call time. That timing surprises people.

String text = null;

Supplier<String> shout = text::toUpperCase; // NullPointerException right here

System.out.println(shout.get());           // never reaches this line

Java grabs the object the moment you build the reference. So a null target blows up on that very line, long before anyone calls it.

8.3 Mixing Up Bound and Unbound

The two instance forms look alike but act very differently. Count the inputs to tell them apart.

String greeting = "Hello";

Supplier<String> bound = greeting::toUpperCase;         // zero arguments
Function<String, String> unbound = String::toUpperCase; // one argument

System.out.println(bound.get());              // Output: HELLO
System.out.println(unbound.apply("world"));   // Output: WORLD

A small letter on the left usually means a variable, so the reference is bound. A capital means a class name, so the caller must pass the object.

8.4 Argument Counts That Do Not Match

We saw this in section 4.1. It earns a second mention, because the error text throws beginners off. Look for the words “invalid method reference”, then the required and found lines below it.

Those two lines tell you everything. Compare the counts, fix whichever side is wrong, and move on.

8.5 Ambiguity Between Static and Instance

Give one class both a static and an instance method that share a name, and Java can no longer tell which one you mean.

class Util {
    static String clean(String s) { return s.trim(); }
    String clean() { return "instance"; }
}

// Util::clean -> compile error: reference to clean is ambiguous

Both readings fit, so the compiler stops and asks. Rename one method and the problem goes away.

8.6 Forcing a Reference Where Logic Belongs

Some lambdas should stay as they are. They carry real logic, and forcing them into :: form only hurts.

// Fine as a lambda, because of the negation
.filter(name -> !name.isEmpty())

// Fine as a lambda, because of the extra maths
.map(price -> price * 1.18)

Neither line has an equivalent reference. Keep the lambda and let the code stay honest.

9. A Practical Walkthrough

Let us pull the ideas together in one small program. We will clean a messy list of usernames.

9.1 The Messy Input

Real data is rarely tidy. Ours has stray spaces, mixed case, and a couple of blank rows.

The job breaks into four steps:

  • Trim the padding from every name
  • Drop anything that turns out empty
  • Raise the survivors to uppercase
  • Sort them, then print one per line

9.2 Building the Pipeline

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

public class UserReport {
    public static void main(String[] args) {
        List<String> raw = Arrays.asList("  ravi ", "ANJALI", "", "  meera", "kabir  ", "   ");

        List<String> clean = raw.stream()
                .map(String::trim)              // unbound instance
                .filter(name -> !name.isEmpty()) // lambda: negation
                .map(String::toUpperCase)        // unbound instance
                .sorted(String::compareTo)       // unbound instance
                .collect(Collectors.toList());

        clean.forEach(System.out::println);      // bound instance
    }
}

/*
ANJALI
KABIR
MEERA
RAVI
*/

9.3 Reading the Result

Four of the five steps use a method reference. Walk through what each one does:

  • Trim runs first, in unbound form, and strips the spaces around each name
  • The filter stays a lambda, since a not sign has no short form
  • Next comes the case change, again through the unbound form
  • Sorting uses compare, which takes its first input as the object
  • Printing binds to one stream and sends out each line

Notice how the chain reads almost like the four-step plan we wrote above. That closeness between plan and code is the real payoff.

One honest note sits in the middle. Mixing lambdas and short forms in one chain is fine, so do not fight for a pure pipeline.

[IMAGE PLACEHOLDER: diagram showing the four method reference types, with arrows from each :: form to its equivalent lambda]

10. Interview Questions

Q: What is a method reference in Java?

A: A method reference is a shorthand for a lambda that does nothing but call one existing method. You write a target, then ::, then the method name. It creates a functional interface instance exactly as a lambda would, so Integer::parseInt and s -> Integer.parseInt(s) mean the same thing.

Q: How many types of method references does Java support?

A: Four. The static form points at a class, the bound form points at one object, the unbound form names a class but calls an instance method, and the constructor form ends in new. Many tutorials list only two, because they merge the bound and unbound forms. Those two behave very differently, so it pays to keep them apart.

Q: What is the difference between a bound and an unbound method reference?

A: A bound reference names a specific object, so that object always receives the call and the interface passes no extra argument for it. An unbound reference names a class, so the first argument at call time becomes the receiver. Compare greeting::toUpperCase, which needs zero arguments, with String::toUpperCase, which needs one.

Q: Can a method reference have a different return type from the functional interface?

A: Yes, as long as the conversion widens. A method returning int satisfies an interface method returning float, because int widens to float. A method returning a value can also satisfy a void abstract method, and Java discards the result. The reverse never works, so a void method cannot fill a slot that expects a value.

Q: Can the arguments differ between the method and the functional interface?

A: No. The referenced method must accept what the interface hands over, in the same order. A mismatch produces a compile error reading “invalid method reference”, with required and found lines showing both signatures. Remember that an unbound reference spends its first argument on the receiver.

Q: Why does a method reference have no brackets after the method name?

A: Brackets would invoke the method immediately. A method reference only points at the method so the functional interface can invoke it later, which is why Integer::parseInt compiles and Integer::parseInt() does not.

Q: What is a constructor reference?

A: A constructor reference uses the keyword new on the right of the ::, as in ArrayList::new. It turns a constructor into a factory you can pass around. Java picks whichever constructor matches the argument list of the functional interface, so Function<String, Employee> selects the single String constructor.

Q: When should I use a lambda instead of a method reference?

A: Keep the lambda whenever the body does more than forward a call. Negation such as name -> !name.isEmpty(), extra arithmetic, reordered arguments, and multi-statement bodies all rule out a reference. Readability decides the rest, so pick whichever version a teammate grasps faster.

Q: What happens if the object in a bound method reference is null?

A: Java throws a NullPointerException at the line that creates the reference, not at the line that calls it. The receiver expression evaluates eagerly when you build the reference, so text::toUpperCase fails on the assignment itself when text holds null.

Q: Are method references faster than lambdas?

A: Treat them as equal in speed. Both compile down to invokedynamic and both create a functional interface instance at runtime. A method reference occasionally skips one synthetic wrapper method, but that difference never shows up in real benchmarks, so choose on clarity alone.

11. Conclusion

Let us wrap up what we covered. A method reference replaces a lambda whose whole job is calling one method that already exists.

The syntax puts a target on the left of the :: and a method name on the right. Brackets never follow that name, because you are pointing rather than calling.

Four forms cover every case. Static references name a class, bound references name an object, unbound references name a class but call an instance method, and constructor references end in new.

Arguments must line up exactly, while return types enjoy some slack. An int widens into a float, and a returned value can fill a void slot.

One last tip to carry with you. Expand any confusing reference back into its lambda. The moment you see where each argument lands, the :: form stops looking cryptic and starts looking obvious.

Further Reading

Leave a Comment