Hur fungerar metodreferenser i Java 8

Hur kan egentligen String-metoden int compareToIgnoreCase(String) likställas med Comparator-metoden int compare(T param1, T param2)

För ett tag sedan svarade jag på just den frågan på StackOverflow och svaret passar som ett inlägg här på Stacktrace.

Frågeställningen kan illustreras med följande kodsnutt:

List<String> names = new ArrayList<>(); names.add("johnny"); 
names.add("ronny"); 
names.add("sonny"); 

// Detta fungerar fint 
Comparator<String> nameComparator = new MyCustomNameComparator<>(); 
Collections.sort(names, nameComparator::compare); 

// Men, varför fungerar detta egentligen? 
// compareToIgnoreCase har ju en annan metod-signatur än Comparator.compare 
Collections.sort(names, String::compareToIgnoreCase);

Hur kan detta då komma sig, metodsignaturerna är ju olika? För att bena ut det hela börjar vi med att reda ut hur :: fungerar.

Metodreferenser kan användas på följande sätt:

  • Som en statisk metod (ClassName::methodName)
  • En instansmetod för ett specifikt objekt (instanceRef::methodName)
  • En metod från en basklass (super::methodName)
  • En instansmetod på godtyckligt objekt med en specificerad typ (ClassName::methodName)
  • En konstruktor-referens (ClassName::new)
  • Allokering av en array (TypeName[]::new)

I fallet med String.compareToIgnoreCase(String) så är ju det en instansmetod för ett specifikt objekt enligt ovan dvs instanceRef::methodName. Detta kan översättas till följande pseudo-uttryck: (instanceRef, param) -> instanceRef.methodName(param) {return int}.

Metoden Comparator.compare(param1, param2) faller under samma kategori dvs instanceRef::methodName. Skillnaden är att det är två parametrar. Men uttrycket kan pseudo-beskrivas så här: (param1, param2) -> instanceRef.methodName(param1, param2) {return int}

Så, dessa metoder kan beskrivas med samma syntax. Följande exempel illustrerar detta ytterligare:

 // 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;

Men vad händer då bakom kulisserna? Jo, en process som kallas ”desugaring”. Det finns bland annat fin läsning om desugaring i detta dokument av Brian Goetz. Exempelvis, om du antar följande kod:

class A { 
   public void foo() { 
      List<String> list = ... list.forEach( s -> { System.out.println(s); }); 
   } 
}

Koden ovan kommer att konverteras till något i stil med:

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); 
   }
}

Och, som Brian Goetz skriver i artikeln:
”if the desugared method is an instance method, the receiver is considered to be the first argument”. Brian förklarar vidare att the lambda’s remaining arguments are passed as arguments to the referred method.

Så, i fallet med Comparator-metoden så skickas helt enkelt parametrarna vidare och därför kan de bägge metodreferenserna likställas.

För än mer läsning i ämnet, kolla in detta blogginlägg från Moandji Ezana.