tifyty

pure Java, what else ?

A Java8 default metódus hazugsága

Mi az a default metódus?

A Java 8 megjelenésével lehetővé vált, hogy egy interfészt olyan módon módosítsunk új metódus beszúrásával, hogy az közben kompatibilis maradjon az őt implementáló osztályokkal. A Java 8 előtt ez nem volt lehetséges, mert ha egy új metódus került az interfészbe, akkor azt minden olyan osztálynak implementálnia kellett, amelyik az interfészt implementálta és maga nem volt absztrakt. Így ha egy interfészbe új metódus került egy library-ban, akkor csak úgy lehetett az új verzióra áttérni, hogy minden ilyen változást követni kellett a library-t használó kódokban. Ha tehát lett egy computeIfAbsent() metódus az interfészben, akkor azt az alkalmazásban az osztálynak is implementálnia kellett, még akkor is, ha semmit nem csinált vele, nem is használta semmilyen kód.

A Java 8 megjelenésével ez már nem szükséges. És mégis

Ha egy interfészhez egy default metódust adunk hozzá néhány, az interfészt implementáló osztály használhatatlanná válhat.

Először nézzük meg a default metódusok finomságait.

A Java 8-ban lehetőségünk van arra, hogy egy metódust az interfészben implementáljunk. (Statikus metódusok is lehetnek interfészekben, de ez most egy másik történet.) Azt a metódust, amit egy interfészben implementálunk ‘default’ metódusnak hívunk, és a default módosítóval jelöljük meg, jelezve, hogy nem absztrakt. (Amúgy maga a tény, hogy van törzse, és az argumentumot záró zárójele után nem pontosvessző jön eléggé kifejezi, hogy default metódussal van dolgunk, de a Java mindig is redundáns volt kicsit az olvashatóság kedvéért.) Az osztályok, amelyek implementálják ezt az interfészt öröklik a default metódusok megvalósítását is. Ha az osztálynak másképp van szüksége a metódusra semmi nem áll útjában, hogy felüldefiniálja (interfészben metódus nem lehet final).

Ha tehát van egy osztályunk, amelyik implementál egy interfészt, és ebbe az interfészbe belekerül később egy új default metódus az osztályunk továbbra is működőképes.

Többszörös öröklődés?

A dolog akkor kezd bonyolódni amikor a többszörös öröklődés kerül képbe. A Java mindig tartotta magát, hogy többszörös öröklődés csak definíciós szinten van, implementáció esetén nem lehet többszörös öröklődés (ellentétben a C++-szal, ahol van, és elég nagy görcsölés.) Mi történik akkor, ha egy osztály több (legyen az egyszerűség kedvéért kető) interface-t is implementál amelyek mindegyikében megtalálható az m() metódus. Ha mindegyik interface-ben abstrakt a metódus, akkor úgy mint az eddigi Java verziókban az osztálynak implementálnia kell a metódust. Ebben nincs változás. De mi van akkor ha pontosan egy interfész ad default implementációt? Nos a Java 8 válasza az, hogy ebben az esetben is implementálnia kell az osztálynak a metódust, különben nem fog lefordulni. Ezek után mondani sem kell, hogy ha több default implementációt is örököl az osztály, akkor ugyanez a helyzet.

Megjegyzés: Ha az interfészek a default implementációt úgy adják, hogy öröklődésen keresztül végül is ugyanaz az egy default implementáció jelenik meg az osztályban örököltként, akkor az nem probléma. Azaz ha az interfész A implementálja m()-t, majd B és C interfészek kiterjesztik A-t és a D osztály implementálja B és C-t, akkor örökli az m() implementációját is. Úgy érdemes fogalmazni, hogy egy osztály pontosan egy default implementációt tud örökölni, akár több leszármazási úton keresztül de csak akkor ha ugyanezt a metódust nem örökli absztaktként is.

Természetesen ha az osztálynak implementálnia kell a metódust, akkor az történhet magában az osztályban, de örökölheti az implementációt kiterjesztett osztályból is.

Még egy megjegyzés: Bármennyire is vonzó lehetne default implementációt adni például a toString() metódusra: nem lehet. Ami az Object-ből öröklődik, az érinthetetlen. És ez jobb így.

No de akkor most térjünk vissza az előző heti rövid poszthoz

Ha egy C osztály implementál egy I interfészt, és I-ben van egy absztrakt metódus, amit C nem implementál, akkor C nem fordul le, de futni azért még futhat.

valamint amit az előbb írtam, és amin esetleg átsiklott a tisztelt olvasó: “…ebben az esetben is implementálnia kell az osztálynak a metódust, különben nem fog lefordulni…“.

Nem fog lefordulni. De futni fog? Mondjuk ha már korábban lefordult olyan interfész verziókkal, amelyek nem okoztak gondot a fordítónak. A válasz az, hogy fog futni. Vagy nem. A Java 8 nem igazán konzisztens ebben az esetben. Amúgy meg van az oka, de ebbe nem érdemes belemenni, mert a release kijött, és mindenkinek meg volt a lehetősége beleszólni korábban. Ha pedig nem tette, most már késő. (Én sem tettem, de nem is értek annyira a Java-hoz meg a programozáshoz, hogy meg mertem volna tenni.)

  • Mondjuk van két interfészünk, és egy osztály, amelyik implementálja mind a két interfészt.
  • Az egyik interfész implementálja a default metódust m().
  • Az osztály és az interfészek lefordulnak.
  • Változtassuk meg az interfészt, amelyik nem tartalmazta eddig az m() metódust, hogy deklarálja azt absztraktként.
  • Fordítsuk le csak a módosított interfészt.
  • Futtassuk az osztályt.

default method multiple inheritance
Ebben az esetben az osztály fut. Nem tudjuk újra fordítani a módosított interfésszel, de mivel a régivel már le lett fordítva: fut. Most

  • módosítsuk az interfészt amelyikben az absztrakt metódus van, és készítsünk az m() metódushoz egy default implementációt.
  • Fordítsuk le csak a módosított interfészt.
  • Futtassuk az osztály: hiba!

Ha két olyan interfész is van az öröklődési láncban, amelyik default implementációt ad egy metódusra, akkor az implementáló osztályban nem lehet meghívni a metódust, ha az nincs explicite az osztályban vagy egy ősosztályban definiálva.

invalid multiple inharitance of default methods
Ugyanakkor a lefordított .class továbbra is kompatibilis. Be lehet tölteni, és el lehet indítani a futtatását mindaddig, míg nem történik hívás arra a metódusra, amelyet többszörösen definiálnak az interfészek.

Minta kód

directory structure of test

A fentiek demonstrálására készítettem, egy test könyvtárat a C.java osztály számára, és három alkönyvtárat a két I1.java és I2.java interfészek különböző verzióihoz. A base könyvtár tartalmazza azt az interfész verziót, amelyik jó a fordításhoz is, és a futtatáshoz is. I1 tartalmazza az m() metódust default implementációval. Az interfész I2 ebben a változatban nem tartalmaz semmilyen metódust.

Az osztály tartalmaz egy main metódust, így futtatható egyszerűen a tesztünk során. A teszt ellenőrzi, hogy van-e valamilyen parancssori argumentum, így később meg tudjuk hívni úgy is, hogy meghívja az m() metódust, és úgy is, hogy ne.

~/github/test$ cat C.java 
public class C implements I1, I2 {
  public static void main(String[] args) {
    C c = new C();
    if( args.length == 0 ){
      c.m();
    }
  }
}
~/github/test$ cat base/I1.java 
public interface I1 {
  default void m(){
    System.out.println("hello interface 1");
  }	
}
~/github/test$ cat base/I2.java 
public interface I2 {
}

Ezeket a programokat le tudjuk fordítani, és le is tudjuk futtatni:

~/github/test$ javac -cp .:base C.java
~/github/test$ java -cp .:base C
hello interface 1

A compatible könyvtár egy olyan verziót tartalmaz az I2 interfészből, amelyik deklarálja de nem implementálja az m() metódust. Az I1.java tartalma mindegyik könyvtárban ugyanaz.

~/github/test$ cat compatible/I2.java 

public interface I2 {
  void m();
}

This can not be used to compile the class C:

~/github/test$ javac -cp .:compatible C.java 
C.java:1: error: C is not abstract and does not override abstract method m() in I2
public class C implements I1, I2 {
       ^
1 error

A hibaüzenet nagyon precíz. Mivel van egy C.class fájlunk az előző fordításból ha lefordítjuk csak az interfészeket a compatible könyvtárban, akkor még mindig tudjuk futtatni az osztályt:

~/github/test$ javac compatible/I*.java
~/github/test$ java -cp .:compatible C
hello interface 1

A harmadik wrong nevű könyvtár az I2 olyan verzióját tartalmazza, amelyik definiálja az m() metódust:

~/github/test$ cat wrong/I2.java 
public interface I2 {
  default void m(){
    System.out.println("hello interface 2");
  }
}

Azt gondolom, hogy a C osztályt ezekkel az interfészekkel lefordítani megpróbálni sem kell. De ha lefordítjuk az interfészeket, és elindítjuk a kódot az fut mindaddig, míg meg nem akarja hívni a m() metódust. Ennek demonstrálására használjuk a parancssori argumentumot:

~/github/test$ javac wrong/*.java
~/github/test$ java -cp .:wrong C
Exception in thread "main" java.lang.IncompatibleClassChangeError: Conflicting default methods: I1.m I2.m
	at C.m(C.java)
	at C.main(C.java:5)
~/github/test$ java -cp .:wrong C x
~/github/test$

Összefoglalás

Ha most kezded el, kedves olvasó, Java 8 kompatibilisíteni a könyvtáradat, és default metódusokat adsz az interfészekhez, akkor valószínűleg ez nem fog problémát okozni. Legalábbis ebben reménykedtek a JDK8 fejlesztő is, amikor funkcionális metódusokat adtak például a gyűjtemény interfészekhez. Az alkalmazások a Java 7 vagy korábbi implementációkon nyugszanak, és ezekben nincsenek default metódusok, így ha nem akarják újrafordítani az alkalmazásokat, akkor futni fognak még ütközés esetén is. De ha több olyan library is van, amelyik már Java 8 “kompatibilissé” lett téve, akkor azért van esély, hogy két metódus neve, szignatúrája ugyanaz lesz. Hogyan kell elkerülni?

Tervezd meg a library API-t előre. Ne számíts rá, hogy majd a default metódus megoldja később a gondokat ha ki kell egészíteni az API-t. A default metódusok csak az utolsó menedék. Gondosan válaszd meg a metódus neveket. Aztán majd idővel megtanuljuk, hogy a default metódusokat hogyan lehet és érdemes használni Java-ban.

2 responses to “A Java8 default metódus hazugsága

Vélemény, hozzászólás?

Adatok megadása vagy bejelentkezés valamelyik ikonnal:

WordPress.com Logo

Hozzászólhat a WordPress.com felhasználói fiók használatával. Kilépés / Módosítás )

Twitter kép

Hozzászólhat a Twitter felhasználói fiók használatával. Kilépés / Módosítás )

Facebook kép

Hozzászólhat a Facebook felhasználói fiók használatával. Kilépés / Módosítás )

Google+ kép

Hozzászólhat a Google+ felhasználói fiók használatával. Kilépés / Módosítás )

Kapcsolódás: %s

%d blogger ezt kedveli: