Java 8 default metoder i interfaces: I lang tid op til Java 8 releasen den 18 Marts 2014 og også efter har der været snakket og skrevet mangt og meget om de nye features. Især Lambda udtryk har fået meget fokus. I denne artikel har Jan Schoubo i stedet valgt at sætte fokus på et knap så omfangsrigt, men lige så vidtgående emne – default metoder i interfaces.

Problemet kender du nok: Du skal udvide funktionaliteten af et programbibliotek, der består af nogle klasser og interfaces, som allerede bruges mange steder. Du er nødt til at lave udvidelsen, så den er 100% bagudkompatibel.

Antag at du vil udvide en klasse med en metode. Det er nemt nok – du kan blot tilføje metoden med tilhørende krop. Den rettede klasse kan stadig bruges uden videre – andre klasser kan instantiere den rettede klasse eller nedarve fra den som hidtil. Og har de andre klasser i forvejen en metode med samme signatur vil den blot override den nye. Alle gamle klasser virker som før, men hvis de havde den nye metode i forvejen, vil der dog optræde en ny warning, da de jo nu overrider en metode, der ikke eksisterede før. Men det er kun en warning, og den oversatte kode virker som hidtil.

Men hvad nu hvis det er et interface, der skal udvides? Så er det straks en anden situation. Her vil alle klasser, der implementerer interfacet, nu mangle en implementering af den pågældende metode selvfølgelig, for du har udvidet ”kontrakten” til at skulle omfatte en metode mere.

Det er der en løsning på:
I Java 8 kan interfaces have default metode-implementeringer.

Syntaksen er såre simpel. Husk at interfaces egentlig ”bare” er at betragte som helt abstrakte klasser, der kun består af abstrakte metoder. Nu er det blevet lempet lidt, så et interface kan have kode på en metode og altså ikke være abstrakt længere. Man skriver blot sin metode med en ny modifier default foran.

Forestil dig, at du har et interface, KanSendesMedPosten, der skal have en ny metode, beregnBrevPorto().

Før:

public interface KanSendesMedPosten {
    public long beregnPakkePorto();
}

Efter:

public interface KanSendesMedPosten {
    public long beregnPakkePorto();
    public default long beregnBrevPorto(){
        throw new RuntimeException("Kan IKKE sendes med brev");
    }
}

Nu behøver du ikke ændre i de klasser, der allerede implementerede interfacet:

Før og efter:

public class Elefant implements KanSendesMedPosten {
    @Override public long beregnPakkePorto() { return 1_200_000_00; }
}

new Elefant().beregnPakkePorto() returnerer et frygteligt stort tal – fordi Elefant har metoden.

new Elefant().beregnBrevPorto() giver en oversætterfejl (før) eller kaster en RuntimeException(efter) – fordi Elefant ikke har metoden.

De klasser, som du ønsker skal implementere hele det nye interface, lader du implemente den nye metode, ganske som du plejer:

Før:

public class KæresteBrev implements KanSendesMedPosten {
    @Override public long beregnPakkePorto() { return 45_00; }
}

Efter:

public class KæresteBrev implements KanSendesMedPosten {
    @Override public long beregnPakkePorto() { return 45_00; }
    @Override public long beregnBrevPorto() { return 9_00; }
}

Implementering

Ellers opfører interfaces med default metoder sig ganske som andre interfaces.

Ikke-default metoder SKAL implementeres, default-metoder KAN implementeres.

Næsten! For hvad sker der, hvis den samme default metode optræder i flere interfaces? Det var jo ikke noget problem før – om du implementerede den ene eller den anden metode med samme signatur kunne jo være lige meget – du havde stadig overholdt ”kontrakten”. Men den går ikke længere. Hvis begge interfaces har en default metode med samme signatur, og det ene interface ikke bare er et subinterface af det andet, så får du nu en oversætterfejl.

public interface KanSendesMedFedEx{
    public default long beregnBrevPorto(){
        throw new RuntimeException("Kan IKKE sendes med brev af FedEx");
    }
}

public interface KanSendesMedUPS{
    public default long beregnBrevPorto(){
        throw new RuntimeException("Kan IKKE sendes med brev af UPS");
    }
}

Så kan du IKKE implementere begge interfaces:

// KAN IKKE OVERSÆTTES
public class Rykker implements KanSendesMedUPS, KanSendesMedFedEx{
    ......
}

Hvis Rykker var en eksisterende klasse, så ser det jo ud som om ændringen ikke er bagud kompatibel? Det skyldes, at vi lod klassen implementere to interfaces med samme default metode. Løsningen kunne bestå i at flytte default metoden op i et fælles interface KanSendesMedPrivatOmdeler. Så vil klassen Rykker kunne oversættes igen. Endelig kan man jo også overveje om opgaven er bedre løst med (abstrakte) klasser – hvis det alligevel kun giver mening at sende med een type post-besørger – eller med komposition i stedet for arv – hvis man skal have et sæt af mulige post-besørgere.

Default metoder på interfaces kan altså lette opgaven med at tilføje funktionalitet til eksisterende interfaces, men fordi mekanismen minder om multiple nedarvning, så kan det give problemer, når interfaces, man typisk bruger samtidig, får tilføjet overlappende default metoder. Dette problem kan kun løses ved at tilføje nye metoder via nye interfaces i stedet for via eksisterende. I praksis er problemet måske ikke så stort…?

Polymorfi

Hvilken metode bliver så faktisk kørt? Polymorfi-reglerne er udvidet lidt.

Polymorfi før Java 8:

  • Nærmeste ikke-abstrakte metode opad i klasse-hierarkiet bliver brugt.

Polymorfi fra Java 8:

  • Nærmeste ikke-abstrakte metode opad i klasse-hierarkiet bliver brugt, hvis den findes.
  • Nærmeste default metode opad i interface-hierarkierne bliver brugt.

Polymorfi-reglerne er altså stadig skruet sådan sammen, at hvis oversætteren har sagt god for koden, så er der altid netop een metode et eller andet sted opad i klasse- eller interface-hierarkierne, der kan kaldes.

Bemærk at problemet med enslydende default metoder netop forekommer, hvis b) kan resultere i mere end een default metode via forskellige interface-hierarkier. Netop denne tvetydighed gør, at situationen skal resultere i en oversætter-fejl.

Eksempler på polymorfi:

new KæresteBrev().beregnBrevPorto() returnerer 900 – fordi KæresteBrev har metoden beregnBrevPorto.

new Elefant().beregnBrevPorto() kaster RuntimeException – fordi Elefant ikke har metoden.

public interface KanSendesMedKurér extends KanSendesMedPosten{
    public default long beregnPakkePorto() { 
        return 500_00; 
    }
    public default long beregnBrevPorto() { 
        return 400_00; 
    }
}

public class ValentineHilsen implements KanSendesMedKurér{
}

new ValentineHilsen().beregnBrevPorto() returnerer 40000 – fordi klassen ikke har metoden, men nærmeste interface har.