7.3 Method References: Static, Bound, Unbound, and Constructor References

Key Takeaways

  • A method reference (::) is a compact syntax for a lambda that delegates directly to an existing method or constructor; it requires a target functional interface context and cannot be assigned to var or Object.

  • Static method references (ClassName::staticMethod) map parameters of the functional interface directly to the static method's argument list in order.

  • Bound instance method references (instanceRef::instanceMethod) bind to a specific pre-existing object evaluated when the reference is created; the object is not supplied as a functional interface parameter.

  • Unbound instance method references (ClassName::instanceMethod) use the first parameter of the functional interface as the target receiver on which the instance method is invoked.

  • Constructor references (ClassName::new and Type[]::new) invoke constructors; array constructor references take an integer size parameter and match IntFunction<Type[]>.

Last updated: October 2026

7.3 Method References: Static, Bound, Unbound, and Constructor References

Method references provide a compact, highly readable alternative to lambda expressions when an expression does nothing more than invoke an existing method or constructor by name. Using the double colon separator (::), method references allow developers to treat existing methods as first-class functional targets. For the 1Z0-829 exam, you must clearly distinguish between the four categories of method references, understand how the compiler differentiates bound from unbound references, and navigate constructor reference mechanics.


Syntax and Target Typing

A method reference consists of a target reference or type name, the double colon operator (::), and the method identifier or the new keyword:

Target::MethodOrConstructor\text{Target} :: \text{MethodOrConstructor}

Like lambda expressions, method references have no intrinsic standalone type. The compiler infers whether a method reference is compatible with a given target based on the expected functional interface's abstract method descriptor:

// Legal: compiler infers Consumer<String> from assignment context
Consumer<String> printer = System.out::println;

// COMPILE ERROR: Cannot infer type for method reference using var
// var ref = System.out::println;

// COMPILE ERROR: Object is not a functional interface
// Object obj = System.out::println;

// Legal: explicit cast establishes target functional interface context
Object obj = (Consumer<String>) System.out::println;

The Four Categories of Method References

The Java Language Specification defines four distinct forms of method references based on how the target method is invoked and whether an instance receiver is bound:

CategorySyntaxEquivalent Lambda ExpressionExample
1. Static Method ReferenceClassName::staticMethod(args) -> ClassName.staticMethod(args)Integer::parseInt, Math::max
2. Bound Instance ReferenceinstanceRef::instanceMethod(args) -> instanceRef.instanceMethod(args)System.out::println, "Java"::concat
3. Unbound Instance ReferenceClassName::instanceMethod(instance, args) -> instance.instanceMethod(args)String::toLowerCase, String::concat
4. Constructor ReferenceClassName::new / Type[]::new(args) -> new ClassName(args) / size -> new Type[size]ArrayList::new, String[]::new
                                +--------------------------+
                                | Method Reference Types   |
                                +--------------------------+
                                              |
         +--------------------+---------------+--------------------+--------------------+
         |                    |                                    |                    |
         v                    v                                    v                    v
  [Static Reference]   [Bound Instance]                    [Unbound Instance]   [Constructor Reference]
  ClassName::method    instance::method                    ClassName::method    ClassName::new / Type[]::new
  args -> Class.m(args) args -> instance.m(args)           (inst, args)->inst.m(args) args -> new Class(args)

1. Static Method References

Refers to a static method of a class. The parameters supplied to the functional interface are passed directly as the arguments to the static method in identical order:

// Static method taking 1 argument: Function<String, Integer>
Function<String, Integer> parser = Integer::parseInt; // s -> Integer.parseInt(s)
Integer val = parser.apply("123"); // 123

// Static method taking 2 arguments: BinaryOperator<Double>
BinaryOperator<Double> maxFinder = Math::max; // (a, b) -> Math.max(a, b)
Double greater = maxFinder.apply(10.5, 20.2); // 20.2

2. Bound Instance Method References

A bound instance method reference binds to a specific object instance that is evaluated when the method reference expression is created. The functional interface parameters do not include the target instance:

String prefix = "USER_";
// Bound reference: 'prefix' is evaluated now and captured
Function<String, String> tagger = prefix::concat; // s -> prefix.concat(s)
System.out.println(tagger.apply("42")); // "USER_42"

// Bound reference with zero arguments: Supplier<Integer>
Supplier<Integer> lenSupplier = prefix::length; // () -> prefix.length()
System.out.println(lenSupplier.get()); // 5

Warning

Null Evaluation Timing in Bound References When creating a bound method reference (instanceRef::method), instanceRef is evaluated immediately at declaration time. If instanceRef is null, a NullPointerException is thrown instantly when the reference is defined, not when the functional interface method is subsequently invoked!

3. Unbound Instance Method References

An unbound instance method reference specifies a class name rather than an existing instance. The first parameter of the target functional interface serves as the receiver object on which the instance method is invoked. Any subsequent parameters map to the method's arguments:

// Single parameter becomes the invocation target: Function<String, Integer>
Function<String, Integer> lengthFunc = String::length; // s -> s.length()
System.out.println(lengthFunc.apply("OpenJDK")); // 7

// Unbound reference matching Predicate<String>
Predicate<String> blankChecker = String::isBlank; // s -> s.isBlank()
System.out.println(blankChecker.test("   ")); // true

// Two parameters: 1st is target, 2nd is method argument: BiPredicate<String, String>
BiPredicate<String, String> startsWithChecker = String::startsWith; // (s1, s2) -> s1.startsWith(s2)
System.out.println(startsWithChecker.test("Java 17", "Java")); // true

// Two parameters: 1st is target, 2nd is method argument: BiFunction<String, String, String>
BiFunction<String, String, String> concatFunc = String::concat; // (s1, s2) -> s1.concat(s2)
System.out.println(concatFunc.apply("Open", "JDK")); // "OpenJDK"

Comparing Bound vs. Unbound Syntax

Consider the method String.concat(String str):

  • Bound: "Hello"::concat binds to "Hello". It accepts 1 parameter (String) and returns String. Target: Function<String, String>.
  • Unbound: String::concat has no bound instance. It requires 2 parameters (target String and argument String) and returns String. Target: BiFunction<String, String, String>.

Notice that both static method references and unbound instance method references use the syntax ClassName::methodName. The compiler resolves whether it is static or unbound by checking if methodName is static or instance, and comparing the arity of the target functional interface.


Constructor References

Constructor references instantiate new objects or arrays using the new keyword.

Class Constructor References (ClassName::new)

The compiler selects the appropriate constructor based on the functional interface's abstract method parameters:

// Zero arguments: Supplier<List<String>> invokes new ArrayList<>()
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();

// One argument: Function<String, StringBuilder> invokes new StringBuilder(String)
Function<String, StringBuilder> sbFactory = StringBuilder::new;
StringBuilder sb = sbFactory.apply("Initial");

// Two arguments: BiFunction<String, Integer, CustomRecord>
record Employee(String name, int id) {}
BiFunction<String, Integer, Employee> empFactory = Employee::new;
Employee emp = empFactory.apply("Alice", 101);

Array Constructor References (Type[]::new)

Array constructor references allow creating arrays of specified lengths. The functional interface must accept a single integer argument representing the array length and return the array type, matching IntFunction<Type[]>:

// Array constructor reference: size -> new String[size]
IntFunction<String[]> stringArrayFactory = String[]::new;
String[] words = stringArrayFactory.apply(10); // Array of length 10

// Primary use case: Stream.toArray(IntFunction<A[]> generator)
List<String> fruits = List.of("Apple", "Banana", "Cherry");
String[] fruitArray = fruits.stream().toArray(String[]::new);

Overload Resolution and Compiler Disambiguation

When a class contains overloaded methods, the compiler inspects the target functional interface to identify a unique matching method:

public class OverloadDemo {
    public static void print(String s) { System.out.println("String: " + s); }
    public static void print(Integer i) { System.out.println("Integer: " + i); }
}

// Disambiguated by target parameter type:
Consumer<String> strConsumer = OverloadDemo::print;   // Calls print(String)
Consumer<Integer> intConsumer = OverloadDemo::print; // Calls print(Integer)

Ambiguity Errors

If the compiler cannot uniquely identify a single most specific method, compilation fails with an ambiguity error:

// Integer declares a static toString(int) AND an instance toString().
// For Function<Integer, String>, both fit: toString(int) as a static reference,
// and toString() as an unbound reference whose receiver is the Integer argument.
// COMPILE ERROR: incompatible types: invalid method reference
//                reference to toString is ambiguous
// Function<Integer, String> f = Integer::toString;

To resolve the ambiguity, use an explicit lambda such as i -> Integer.toString(i) or i -> i.toString(). A different failure occurs with a raw target such as Function rawFunc = SomeClass::process; when the only overloads accept String and Integer: the raw function type passes an Object, so javac reports that no suitable method is found.


Restrictions on Method References

  1. No Explicit Arguments: You cannot pass arguments directly inside method reference syntax: Math::max(10, 20) is invalid syntax.
  2. Target Interface Required: Method references cannot be used with var or assigned to Object without an explicit cast.
  3. Generics Type Witnesses: If a method is generic, type witnesses appear after the :: operator: Collections::<String>emptyList.
Test Your Knowledge

Consider the following code snippet:

String leader = "Java";
Function<String, String> f1 = leader::concat;
BiFunction<String, String, String> f2 = String::concat;

System.out.println(f1.apply(" 17") + " & " + f2.apply("Open", "JDK"));

What is printed when this code is executed?

A

Compilation fails because leader::concat requires two arguments.

B

Compilation fails because String::concat cannot be assigned to BiFunction.

C

Java 17 & Java OpenJDK

D

Java 17 & OpenJDK

Test Your Knowledge

Consider the following code:

java.util.function.IntFunction<String[]> creator = String[]::new;
String[] items = creator.apply(3);
System.out.println(items.length + " " + items[0]);

What is the result of compiling and executing this code?

A

Throws a ClassCastException at runtime.

B

Compiles and prints 3 null.

C

Compilation fails because array constructor references cannot be assigned to IntFunction.

D

Compiles and prints 0 null.

Test Your Knowledge

Given the following class definition:

public class Dispatcher {
    public static void route(Integer id) { System.out.print("INT:" + id); }
    public static void route(String name) { System.out.print("STR:" + name); }

    public static void main(String[] args) {
        java.util.function.Consumer<Integer> c = Dispatcher::route;
        c.accept(100);
    }
}

What is the result of attempting to compile and run this program?

A

Compilation fails because the method reference Dispatcher::route is ambiguous between the two overloads.

B

Compilation fails because static methods cannot be assigned to Consumer.

C

Compiles cleanly and prints INT:100.

D

Compiles cleanly and prints STR:100 due to type coercion.

Test Your Knowledge

Consider the following method declarations inside a class:

public void checkReferences() {
    var ref1 = (java.util.function.Supplier<String>) "hello"::toUpperCase; // Line 1
    var ref2 = String::trim;                                               // Line 2
    Object ref3 = System.out::println;                                     // Line 3
}

Which lines fail compilation?

A

Line 1 only

B

Line 2 and Line 3 only

C

Line 1 and Line 2 only

D

Line 1, Line 2, and Line 3

Sections you finish are checked off in the contents.