7.3 Method References: Four Varieties and Edge Cases
Key Takeaways
- Method references provide compact, readable syntax using the double-colon operator (::) to delegate execution directly to an existing method or constructor matching a target functional interface.
- The four distinct kinds of method references are static methods (ClassName::staticMethod), bound instance methods (expr::instanceMethod), unbound instance methods (ClassName::instanceMethod), and constructor references (ClassName::new or Type[]::new).
- In unbound instance method references (ClassName::instanceMethod), the first argument of the target functional interface serves as the invocation receiver instance, with subsequent arguments passed as method parameters.
- Target typing and compiler overload resolution determine the matching method signature at compile time; ambiguous method references across overloaded methods or conflicting static/instance signatures cause compilation errors.
Method References: Four Varieties and Edge Cases
Method references (introduced via the double-colon :: operator) provide an even more concise syntax than lambda expressions when the lambda body merely forwards its arguments to an existing method or constructor. For the 1Z0-830 exam, you must be capable of immediately translating between method references and equivalent lambda expressions, categorizing the four distinct varieties, correctly mapping parameter dispatch rules (especially for unbound instance method references), and diagnosing compiler ambiguity issues.
1. Classification of Method References
Every method reference falls into one of four distinct categories:
+-----------------------------------------------------------------------------------------------------+
| THE FOUR METHOD REFERENCE VARIETIES |
| |
| 1. Static Method Reference : ClassName::staticMethod |
| 2. Bound Instance Method Reference : instanceRef::instanceMethod |
| 3. Unbound Instance Method Reference : ClassName::instanceMethod |
| 4. Constructor Reference : ClassName::new OR Type[]::new |
+-----------------------------------------------------------------------------------------------------+
2. Variety 1: Static Method References (ClassName::staticMethod)
In a static method reference, the target method is a static member of the named class. All parameters of the functional interface SAM method are passed directly as arguments to the static method in identical order.
// Example 1: Single Parameter
Function<String, Integer> parseIntLambda = s -> Integer.parseInt(s);
Function<String, Integer> parseIntRef = Integer::parseInt;
int parsed = parseIntRef.apply("42"); // 42
// Example 2: Two Parameters
BinaryOperator<Double> maxLambda = (a, b) -> Math.max(a, b);
BinaryOperator<Double> maxRef = Math::max;
double higher = maxRef.apply(12.5, 19.3); // 19.3
// Example 3: Primitive Specialization
IntBinaryOperator sumRef = Integer::sum;
int total = sumRef.applyAsInt(10, 20); // 30
3. Variety 2: Bound Instance Method References (expr::instanceMethod)
In a bound instance method reference, the method reference is tied to a specific object instance known at the time the method reference expression is evaluated. The parameters of the functional interface are passed as arguments to the instance method invoked on that fixed object.
String prefix = "Oracle_";
// Bound to the local variable 'prefix'
Function<String, String> prefixerLambda = s -> prefix.concat(s);
Function<String, String> prefixerRef = prefix::concat;
String res = prefixerRef.apply("Java21"); // "Oracle_Java21"
// Bound to static field System.out
Consumer<Object> printLambda = x -> System.out.println(x);
Consumer<Object> printRef = System.out::println;
printRef.accept("Bound Instance Reference");
[!NOTE] Receiver Evaluation Timing: In a bound instance method reference (
expr::method), the expressionexpris evaluated immediately when the method reference is created, not when the functional interface SAM method is later invoked. Ifexprevaluates tonullat creation time, aNullPointerExceptionis thrown immediately.
4. Variety 3: Unbound Instance Method References (ClassName::instanceMethod)
In an unbound instance method reference, the syntax specifies a class name (ClassName::instanceMethod), but the method referenced is an instance method (not static).
The Receiver Parameter Shift Rule
- The first parameter ($a_1$) of the functional interface serves as the receiver instance (the object on which the method is invoked).
- Any subsequent parameters ($a_2, \dots, a_n$) are passed as arguments to the method.
// Example 1: 1 Parameter SAM -> 0 Parameter Instance Method on Target
// Target type: Function<String, Integer> (SAM: Integer apply(String s))
Function<String, Integer> lengthLambda = s -> s.length();
Function<String, Integer> lengthRef = String::length;
int len = lengthRef.apply("Java"); // Invokes "Java".length() -> 4
// Example 2: 1 Parameter SAM -> 0 Parameter boolean Instance Method
// Target type: Predicate<String> (SAM: boolean test(String s))
Predicate<String> isEmptyLambda = s -> s.isEmpty();
Predicate<String> isEmptyRef = String::isEmpty;
boolean empty = isEmptyRef.test(""); // Invokes "".isEmpty() -> true
// Example 3: 2 Parameter SAM -> 1 Parameter Instance Method on First Target
// Target type: BiPredicate<String, String> (SAM: boolean test(String s1, String s2))
BiPredicate<String, String> startsWithLambda = (s1, s2) -> s1.startsWith(s2);
BiPredicate<String, String> startsWithRef = String::startsWith;
boolean match = startsWithRef.test("Enterprise", "Enter"); // "Enterprise".startsWith("Enter") -> true
// Example 4: 2 Parameter SAM -> 1 Parameter int Instance Method on First Target
// Target type: Comparator<String> / ToIntBiFunction<String, String>
ToIntBiFunction<String, String> compLambda = (s1, s2) -> s1.compareToIgnoreCase(s2);
ToIntBiFunction<String, String> compRef = String::compareToIgnoreCase;
int compRes = compRef.applyAsInt("JAVA", "java"); // 0
+-----------------------------------------------------------------------------------------+
| UNBOUND INSTANCE METHOD REFERENCE PARAMETER DISPATCH |
| |
| BiFunction<String, String, String> concatRef = String::concat; |
| |
| SAM Invocation: concatRef.apply( "Hello", " World" ); |
| │ │ |
| ▼ ▼ |
| Dispatched as : "Hello".concat( " World" ) |
| ─────── ───────── |
| Receiver Argument |
+-----------------------------------------------------------------------------------------+
5. Variety 4: Constructor References (ClassName::new & Type[]::new)
Constructor references instantiate new objects or arrays by invoking a constructor that matches the parameter signature of the target functional interface.
Object Constructor References
// 0 Parameters -> Supplier<T>
Supplier<List<String>> listSupplierLambda = () -> new ArrayList<>();
Supplier<List<String>> listSupplierRef = ArrayList::new;
List<String> list = listSupplierRef.get();
// 1 Parameter (initial capacity) -> IntFunction<List<String>> / Function<Integer, List<String>>
IntFunction<List<String>> listCapacityLambda = cap -> new ArrayList<>(cap);
IntFunction<List<String>> listCapacityRef = ArrayList::new;
List<String> sizedList = listCapacityRef.apply(50); // new ArrayList<>(50)
// 2 Parameters -> BiFunction<T, U, R>
class Employee {
String name;
int age;
public Employee(String name, int age) { this.name = name; this.age = age; }
}
BiFunction<String, Integer, Employee> empCreatorLambda = (n, a) -> new Employee(n, a);
BiFunction<String, Integer, Employee> empCreatorRef = Employee::new;
Employee emp = empCreatorRef.apply("Duke", 28);
Array Constructor References
Array constructors require an int size parameter matching IntFunction<T[]> or Function<Integer, T[]>:
// IntFunction<String[]> for Array Creation
IntFunction<String[]> arrayCreatorLambda = size -> new String[size];
IntFunction<String[]> arrayCreatorRef = String[]::new;
String[] names = arrayCreatorRef.apply(10); // new String[10]
// Stream toArray Usage
List<String> words = List.of("Java", "SE", "21");
String[] wordArray = words.stream().toArray(String[]::new);
6. Deep Comparison Matrix: The 4 Varieties
| Variety | Syntax Pattern | Equivalent Lambda Form | Target Interface Example |
|---|---|---|---|
| Static | Math::abs | x -> Math.abs(x) | IntUnaryOperator |
| Bound Instance | myObj::doWork | (a, b) -> myObj.doWork(a, b) | BiConsumer<T, U> |
| Unbound Instance | String::toUpperCase | s -> s.toUpperCase() | Function<String, String> |
| Unbound Instance | String::concat | (s1, s2) -> s1.concat(s2) | BiFunction<String, String, String> |
| Constructor (Class) | TreeMap::new | () -> new TreeMap<>() | Supplier<Map<K, V>> |
| Constructor (Array) | int[]::new | size -> new int[size] | IntFunction<int[]> |
7. Edge Cases: Overload Resolution & Ambiguity
The compiler resolves method references by matching the parameter and return types of the functional interface's abstract method against available method overloads.
1. Static vs. Unbound Instance Collision
If a class defines both a static method and an instance method with the same name that could match a given SAM signature, the compiler rejects the reference due to ambiguity:
class CollisionDemo {
public static boolean check(CollisionDemo cd) { return true; }
public boolean check() { return true; }
}
// Target: Predicate<CollisionDemo> (SAM: boolean test(CollisionDemo cd))
// Could mean static: cd -> CollisionDemo.check(cd)
// Could mean unbound: cd -> cd.check()
// Predicate<CollisionDemo> p = CollisionDemo::check; // COMPILE ERROR: Ambiguous method reference
2. Overload Resolution Based on Context
class Printer {
public void print(String s) { System.out.println("String: " + s); }
public void print(Integer i) { System.out.println("Int: " + i); }
}
Printer p = new Printer();
Consumer<String> stringConsumer = p::print; // Dispatches to print(String)
Consumer<Integer> intConsumer = p::print; // Dispatches to print(Integer)
3. Explicit Type Arguments for Generic Method References
If the method being referenced is generic, type arguments can be supplied explicitly between the :: operator and the method name:
class Utilities {
public static <T> void consume(T item) {}
}
Consumer<String> c = Utilities::<String>consume;
Which of the following functional interface assignments represents an unbound instance method reference where the first argument of the functional interface becomes the receiver?
Given the following stream pipeline: Stream<String> words = Stream.of("Java", "SE", "21"); String[] array = words.toArray(String[]::new); What functional interface does the constructor reference String[]::new target in the toArray method?
Examine the following code snippet: String text = "Hello"; Supplier<String> supplier = text::toUpperCase; text = "World"; String result = supplier.get(); What is the compilation and execution outcome?
Consider the following class definition: class TransformUtil { public static String process(TransformUtil t) { return "Static: " + t; } public String process() { return "Instance: " + this; } } What happens when attempting to compile the following line? Function<TransformUtil, String> func = TransformUtil::process;