Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
217 views
in Technique[技术] by (71.8m points)

java - 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?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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 reference String::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 method a.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:

  1. A static method (ClassName::methodName)
  2. An instance method of a particular object (instanceRef::methodName)
  3. A super method of a particular object (super::methodName)
  4. An instance method of an arbitrary object of a particular type (ClassName::methodName)
  5. A class constructor reference (ClassName::new)
  6. 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 a List<String> expects a Comparator<String>
  • Comparator<String> is a functional interface with the method int sort(String, String), which is equivalent to BiFunction<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 a String argument, so it is compatible with the above lambda: String a becomes the receiver and String b becomes the method argument

Edit: After input from the OP I have added a low level example that explains the desugaring


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...