Reference to methods with different parameters in Java8
I'm wondering how does all this stuff with method references and functional interfaces works on lower level. The easiest example is where we have some List
List<String> list = new ArrayList<>();
list.add("b");
list.add("a");
list.add("c"):
Now we want to sort it with Collections class, so we can call:
Collections.sort(list, String::compareToIgnoreCase);
But if we define custom comparator it could be something like:
Comparator<String> customComp = new MyCustomOrderComparator<>();
Collections.sort(list, customComp::compare);
The problem is that Collections.sort
takes two parameters: List and Comparator. Since Comparator is functional interface it can be replaced with lambda expression or method reference with the same signature (parameters and return type). So how does passing the method reference String::compareToIgnoreCase
work when it refers to an instance method that takes only one parameter when the signatures of these methods don't match?
How are the method references translated in Java8?
From the Oracle method references tutorial:
Reference to an Instance Method of an Arbitrary Object of a Particular Type
The following is an example of a reference to an instance method of an arbitrary object of a particular type:
String[] stringArray = { "Barbara", "James", "Mary", "John", "Patricia", "Robert", "Michael", "Linda" };
Arrays.sort(stringArray, String::compareToIgnoreCase);
The equivalent lambda expression for the method referenceString::compareToIgnoreCase
would have the formal parameter list(String a, String b)
, where a and b are arbitrary names used to better describe this example. The method reference would invoke the methoda.compareToIgnoreCase(b)
.
But, what does the ::
operator really mean? Well, the ::
operator can be described like this (from this SO question):
Method Reference can be obtained in different styles, but they all mean the same:
- A static method (
ClassName::methodName
)- An instance method of a particular object (
instanceRef::methodName
)- A super method of a particular object (
super::methodName
)- An instance method of an arbitrary object of a particular type (
ClassName::methodName
)- A class constructor reference (
ClassName::new
)- An array constructor reference (
TypeName[]::new
)
So, that means that the method reference String::compareToIgnoreCase
falls under the second category (instanceRef::methodName
) which means that it can be translated to (a, b) -> a.compareToIgnoreCase(b)
.
I believe that the following examples illustrates this further. A Comparator<String>
contains one method that operates on two String
operands and returns an int
. That can be pseudo-described as (a, b) ==> return int
(where the operands are a
and b
). If you view it that way all of the following falls under that category:
// Trad anonymous inner class
// Operands: o1 and o2. Return value: int
Comparator<String> cTrad = new Comparator<String>() {
@Override
public int compare(final String o1, final String o2) {
return o1.compareToIgnoreCase(o2);
}
};
// Lambda-style
// Operands: o1 and o2. Return value: int
Comparator<String> cLambda = (o1, o2) -> o1.compareToIgnoreCase(o2);
// Method-reference à la bullet #2 above.
// The invokation can be translated to the two operands and the return value of type int.
// The first operand is the string instance, the second operand is the method-parameter to
// to the method compareToIgnoreCase and the return value is obviously an int. This means that it
// can be translated to "instanceRef::methodName".
Comparator<String> cMethodRef = String::compareToIgnoreCase;
This great SO-answer to explains how lambda functions are compiled. In that answer Jarandinor refers to the following passage from Brian Goetz excellent document that describes more about lambda translations.
Instead of generating bytecode to create the object that implements the lambda expression (such as calling a constructor for an inner class), we describe a recipe for constructing the lambda, and delegate the actual construction to the language runtime. That recipe is encoded in the static and dynamic argument lists of an invokedynamic instruction.
Basically what this means is that the native runtime decides how to translate the lambda.
Brian continues:
Method references are treated the same way as lambda expressions, except that most method references do not need to be desugared into a new method; we can simply load a constant method handle for the referenced method and pass that to the metafactory.
So, lambdas are desugared into a new method. E.g.
class A {
public void foo() {
List<String> list = ...
list.forEach( s -> { System.out.println(s); } );
}
}
The code above will be desugared to something like this:
class A {
public void foo() {
List<String> list = ...
list.forEach( [lambda for lambda$1 as Consumer] );
}
static void lambda$1(String s) {
System.out.println(s);
}
}
But, Brian also explains this in the document:
if the desugared method is an instance method, the receiver is considered to be the first argument
Brian continues to explan that the lambda’s remaining arguments are passed as arguments to the referred method.
So, with the help of this entry by Moandji Ezana, the desugaring of compareToIgnoreCase
as a Comparator<String>
can be broken down to the following steps:
-
Collections#sort
for aList<String>
expects aComparator<String>
-
Comparator<String>
is a functional interface with the methodint sort(String, String)
, which is equivalent toBiFunction<String, String, Integer>
- The comparator instance could therefore be supplied by a
BiFunction
-compatible lambda:(String a, String b) -> a.compareToIgnoreCase(b)
-
String::compareToIgnoreCase
refers to an instance method that takes aString
argument, so it is compatible with the above lambda:String a
becomes the receiver andString b
becomes the method argument
Edit: After input from the OP I have added a low level example that explains the desugaring