tifyty

pure Java, what else ?

Többszörös öröklődés implementálása Java-ban

Van-e többszörös öröklődés Java-ban? Van. Interfész öröklődés van, de implementáció öröklődés nincs. Ha valami olyasmit szeretnénk csinálni, mint a többszörös öröklődés, akkor

  • Az okosok szerint valamit elrontottunk a kód tervezése során, és ebben valóban lehet valami mert mióta Java-zom nem sokszor éreztem a hiányát, de azért néha mégis, és erre meg azt mondják a szintén okos emberek, hogy
  • leszármazás helyett használjunk kompozíciót!

Oké, ha nincs ló, a szamár is jó (mondják a birkapásztorok), akkor csináljunk kompozíciót. Használjunk delegátor pattern-t! Ennél mi sem egyszerűbb:

package com.verhas.multiple.inheritance.examples;

class Example1 {
	public class Super1 {
		public void s1() {
		}
	}

	public class Super2 {
		public void s2() {
		}
	}

	public class Child extends Super1 {
		private Super2 super2 = new Super2();

		public void s2() {
			super2.s2();
		}
	}
}

Készen vagyunk? Ennyire egyszerű lenne? Valószínűleg nem, hiszen ez nem érne meg egy blogbejegyzést. Több gond is van. Például az, hogy a Child az leszármazás szerint nem terjeszti ki a Super2 osztályt, ezért nem is használható Super2 s = new Child(); hozzárendelésben. Nem fog lefordulni. Azt persze ne várjuk, hogy két meglevő osztályból varázsolok egy olyant, amelyik mind a kettőnek leszármazottja. Ezt a Java nem tudja. De interfészekkel sok minden megoldható.

package com.verhas.multiple.inheritance.examples;

class Example2 {
	public interface Supi1 {
		void s1();
	}

	public class Super1 implements Supi1 {
		public void s1() {
		}
	}

	public interface Supi2 {
		void s2();
	}

	public class Super2 implements Supi2 {
		public void s2() {
		}
	}

	public class Child extends Super1 implements Supi2 {
		private Super2 super2 = new Super2();

		public void s2() {
			super2.s2();
		}
	}
}

Bár a Super2 s = new Child(); még mindig hibás, de a Supi2 s = new Child(); már nem! Hurrá. Ezzel máris egy kicsit beljebb vagyunk, de még mindig nem vagyunk készen. Lehet ezen a dolgon tovább reszelni. Mi van akkor, ha a két osztály, amiből örökölni akarok absztakt? Van benne néhány olyan metódus, amit nem definiál, csak deklarál, és a nem absztrakt metódusokban használ. Ezeket a metódusokat a leszármazott osztályban kell definiálni. De mit tegyünk két, vagy több osztályból való leszármazás esetén?

Az egyik ősosztálytól tudunk örökölni, de a másikat ezzel ki is zártuk az öröklődésből. De ez nem baj. Ahogy az előbb megoldottuk a dolgot delegációval, most is meg tudjuk ezt tenni:

package com.verhas.multiple.inheritance.examples;

class Example3 {
	public interface Supi1 {
		void a1();

		void s1();
	}

	public abstract class Super1 implements Supi1 {
		abstract public void a1();

		public void s1() {
			a1();
		}
	}

	public interface Supi2 {
		void a2();

		void s2();
	}

	public abstract class Super2 implements Supi2 {
		abstract public void a2();

		public void s2() {
			a2();
		}
	}

	public class Super22 extends Super2 {
		private Supi2 supi;

		public Super22(Supi2 supi) {
			this.supi = supi;
		}

		public void a2() {
			supi.a2();
		}
	}

	public class Child extends Super1 implements Supi2 {
		private Super2 super2 = new Super22(this);

		public void a2() {

		}

		public void a1() {
		}

		public void s2() {
			super2.s2();
		}
	}
}

Hát, ez már nem olyan egyszerű, de azért nem kell megijedni. Most már két-két metódusunk van az interfészekben. Az egyiket implementálja az ősosztály, a másik absztrakt. Amelyik ősosztályt kiterjesztjük, ott minden rendeben, nincs szükség semmilyen trükkre. A másik osztályt viszont a delegációhoz példányosítani kell, és amennyiben az absztract ez nem megy. Ezért készítettünk belőle egy nem absztrakt Super22 leszármaztatott osztályt. Ez az osztály úgy implementálja az absztrakt a2() metódust, hogy delegálja azt egy valamilyen másik osztálynak, amelyik implementálja a Supi2 interfészt. A leszármazott osztályunkban pedig ezt, a Super22 osztályt úgy hozzuk létre, hogy a konstruktornak a delegáláshoz szükséges osztály paraméterben átadjuk a this-t. Így a delegálás megtörténik oda-vissza.

Nézzük meg akkor most általánosan, hogy mit is csináltunk. (Itt egy olyan rész jön, aminek a forrása ez a cikk. Ami itt következik, az nem fordítás, csak annak a cikknek egy fejezetét olvasva világosodtam meg a mixineket illetően.) Mixin-t gyártottunk. Mi az a mixin?

A mixin egy olyan osztály, amelyiknek a metódusait bele lehet keverni (érted: mix in!) egy leszármazott osztályba. Ahhoz, hogy egy osztály bekeverhető legyen, szükséges, hogy minden olyan metódus rendelkezésre álljon a leszármazott osztályban, amelyet a mixin használ, szükséges a működéséhez, de maga nem definiál, és máshonnan sem szerez meg. Ezek tipikusan a Java absztrakt metódusok. A mixin biztosít metódusokat (angolul provide), és megköveteli (angolul require) más metódusok rendelkezésre állását.

Ennek megfelelően egy mixin leprogramozásához két interfészt írhatunk: interface MProvides és interface MRequires. Az MProvides interfészt a mixin implementálja, az MRequires-t használja, tehát ahhoz szükséges egy olyan változó, amelyik olyan osztály példányára mutat, amelyik implementálja azt.

	public interface MRequires {
		void required();
	}

	public interface MProvides {
		void provided();
	}

	public class Mixin implements MProvides {
		private final MRequires parent;
		public Mixin(MRequires parent){
			this.parent = parent;
		}
		public void provided(){
			parent.required();
		}
	}

Ezzel egy kicsit kezd tisztulni a kép, alakul a Java mixin pattern. Ha van egy olyan osztályunk, amelyikbe szeretnénk ezt a Mixin-t beleilleszteni:

	public class Parent {
		public void required() {
		}
	}

akkor a következő leszármazott osztályt kell készítenünk:

	class Child extends Parent implements MRequires, MProvides {
		public Child() {
			super();
			this.mixin = new Mixin(this);
		}

		public void provided() {
			mixin.provided();
		}

		private final MProvides mixin;
	}

Egy kicsit sok az interfész, meg a mindenféle osztályok, de ezt már megszoktuk EJB2.0 idejében. Azért lehet egyszerűsíteni a dolgot. Kicsit. Az alap mixin pattern az amit itt leírtam.

Egy kis kitérő

Te Java programozó vagy, esetleg mazochista. Más változatot nem tudok elképzelni, ha idáig elolvastad. Legyen az a munkahipotézisünk, hogy Java programozó vagy. A mostani munkahelyemen, ha jelentkezel, simán előfordulhat, hogy én foglak interjúztatni. Ha most, ebben a pillanatban egy interjún azt a kérdést tenném fel neked, hogy lehet-e egy Java interfészben végrehajtható kód, akkor arra mit válaszolnál? Őszintén, csak magadnak.

A legjobb válasz eddig az volt, hogy “azt mondanám, hogy nem, de a helyes válasz az, hogy igen, mert különben miért kérdeznéd”. Azt azonban be kell vallanom, hogy a kérdést nem interjún tettem fel, és nem is fogom, mert ez egy szívatós kérdés. Azt meg interjún kerülni kell.

Kitérő vége

Ugyanezt a mixin implementációt átírhatjuk úgy is, hogy a mixin osztályt, mint egy interfész inner class-át implementáljuk. (Hááááá!!! Ezért volt a kitérő!!!! Őszintén? (Nem! Hazudj a szemembe!) Innen jött az egész cikk témája.)

package com.verhas.multiple.inheritance.examples;

public class Mixin2 {
	public interface MRequires {
		void required();
	}

	public interface MProvides {
		void provided();
	}

	public interface MixinInterface extends MRequires, MProvides {
		class Mixin implements MixinInterface {
			private final MRequires parent;

			public Mixin(MRequires parent) {
				this.parent = parent;
			}

			public void provided() {
				required();
			}

			public void required() {
				parent.required();
			}
		}

	}

	public class Parent {
		public void required() {
		}
	}

	class Child extends Parent implements MRequires, MProvides {
		public Child() {
			super();
			this.mixin = new MixinInterface.Mixin(this);
		}

		public void provided() {
			mixin.provided();
		}

		private final MProvides mixin;
	}
}

Persze itt még didaktikai okok miatt megtartottam az MRequires és MProvides interfészeket, de nem feltétlenül kellenek. Lehet egyszerűsíteni a kódot, ha tudjuk, hogy mit is csinálunk.

package com.verhas.multiple.inheritance.examples;

public class Mixin3 {

	public interface MixinInterface {
		void required();

		void provided();

		public class Mixin implements MixinInterface {
			private final MixinInterface parent;

			public Mixin(MixinInterface parent) {
				this.parent = parent;
			}

			public void provided() {
				required();
			}

			public void required() {
				parent.required();
			}
		}
	}

	public class Parent {
		public void required() {
		}
	}

	class Child extends Parent implements MixinInterface {
		public Child() {
			super();
			this.mixin = new MixinInterface.Mixin(this);
		}

		public void provided() {
			mixin.provided();
		}

		private final MixinInterface mixin;
	}
}

Csak az a fránya delegálás, és a sok delegáló metódus… Azokat le kell generáltatni. Eclipse, NetBeans le tudja generálni, de akkor ott van benne a kódban, keveredik a kézzel írt kód és a generált, ez pedig növeli a karbantartás költségeit. Vagy esetleg lehet lombok-ot használni:

package com.verhas.multiple.inheritance.examples;

import lombok.AllArgsConstructor;
import lombok.Delegate;

public class Mixin4 {
	interface MixInto {
		void required();
	};

	public interface MixinInterface extends MixInto {
		void provided();

		@AllArgsConstructor
		public abstract class Mixin implements MixinInterface {
			@Delegate(types = MixInto.class)
			private final MixInto parent;

			public void provided() {
				required();
			}
		}
	}

	public class Parent {
		public void required() {
		}
	}

	@AllArgsConstructor
	public class Child extends Parent implements MixinInterface {
		@Delegate(types = MixinInterface.class, excludes = MixInto.class)
		private final MixinInterface mixin;

		@Override
		public void required() {
		}
	}
}

Tiszta, egyszerű, és világos pattern! Vagy nem? Delombok után persze egy kicsit hosszabb, de hogy lássuk, hogy pontosan mit is csinálnak “forrás szinten” ezek az annotációk:

// Generated by delombok at Mon Nov 19 11:38:19 CET 2012
package com.verhas.multiple.inheritance.examples;

public class Mixin5 {
	
	
	interface MixInto {
		
		void required();
	}
	
	public interface MixinInterface extends MixInto {
		
		void provided();
		
		abstract class Mixin implements MixinInterface {
			private final MixInto parent;
			
			public void provided() {
				required();
			}
			
			@java.beans.ConstructorProperties({"parent"})
			@java.lang.SuppressWarnings("all")
			public Mixin(final MixInto parent) {
				
				this.parent = parent;
			}
			
			@java.lang.SuppressWarnings("all")
			public void required() {
				this.parent.required();
			}
		}
	}
	
	public class Parent {
		
		
		public void required() {
		}
	}
	
	public class Child extends Parent implements MixinInterface {
		private final MixinInterface mixin;
		
		@Override
		public void required() {
		}
		
		@java.beans.ConstructorProperties({"mixin"})
		@java.lang.SuppressWarnings("all")
		public Child(final MixinInterface mixin) {
			
			this.mixin = mixin;
		}
		
		@java.lang.SuppressWarnings("all")
		public void provided() {
			this.mixin.provided();
		}
	}
}

Mindenki döntse el maga, hogy mennyire szereti ezt a pattern-t.

Most, hogy írom ezt a cikket, elolvastam a megformázott változatot, a színes kód blokkokat, és annak ellenére, hogy én írtam… Egyre jobban elmegy a kedvem a többszörös öröklődéstől Java-ban. Nem kell. Ha kell, akkor inkább másképp fogalmazom meg a feladatot, vagy a megoldást.

Mondjuk Scala-ban?

8 responses to “Többszörös öröklődés implementálása Java-ban

  1. Gábor Lipták november 28, 2012 5:54 du.

    Nemrég csináltam kicsit hasonló dolgot. Egy XMLStreamReader fölött volt egy üzleti logika rétegem (az XML alapján generált üzleti objektumokat, amit aztán Iterátorként visszaadott). Az iterátor természetesen belül egy XML fájlt nyitva kellett hogy tartson, de azt akartam, hogy legalább warning legyen, ha nem zárom be. Szóval csináltam egy ilyet:

    public interface ClosableIterator extends Iterator<T>, Closeable {
    }

    Így a felhasználó osztály írója emlékezni fog, hogy a close metódust meg kéne hívni.

  2. Péter november 29, 2012 11:48 de.

    Bár én nem vagyok java programozó, szeretném megérteni mit is csinálsz pontosan.

    Még az elején írtad:
    “Bár a Super2 s = new Child(); még mindig hibás, de a Supi2 s = new Child(); már nem! Hurrá. Ezzel máris egy kicsit beljebb vagyunk, de még mindig nem vagyunk készen. Lehet ezen a dolgon tovább reszelni.”

    A következő “Super22-es” már tudja ezt? (Super2 s = new Child();)

    • v november 29, 2012 1:14 du.

      Nem, hiszen a Child a Super1 osztályt terjeszti ki amelyik a Supi1 interfészt implementálja. Így egy Child típusú objektum csak Child, Super1, Supi1 vagy Object típusú változóban tárolható.

      • Péter november 29, 2012 1:23 du.

        + Supi2

        “Azt persze ne várjuk, hogy két meglevő osztályból varázsolok egy olyant, amelyik mind a kettőnek leszármazottja.”
        Közben erre a mondatodra – újra – rátaláltam. 🙂

        ok. Köszi.

  3. tamasrev december 2, 2012 8:32 du.

    Az effective C++-ban van szó arról, hogy a többszörös öröklődés legtöbbször nem éri meg, kivéve ha:
    – interfészeket örökölsz többszörösen (C++-ul ezt úgy mondják, hogy csupa pure virtual függvénye van az osztálynak)
    – csak 1 darab publikus öröklődése van, ami is-a kapcsolatot modellez, a többi pedig privát öröklődés
    – a privát, többszörös öröklődéssel csak az újra begépelendő kódmennyiséget csökkented – ez olyasmi, mint a mixin.

    Szerintem amúgy kicsit fordítva ülünk a lóra: szívessen látnék egy valós problémát, amire jó megoldás a mixin.
    Mondjuk hogy a szokásos java.io-s osztályok egymásbaágyazásai helyett lehessen valami ilyet mondani:
    BufferedReader br = new FileInputStream(“example.txt”) with InputStreamReader with BufferedReader;
    Enélkül még egy kicsit öncélúnak tűnik ez a hókusz-pókusz

  4. LSM február 19, 2013 6:40 du.

    A kiterohoz: lehet statikus metodust implementalni interface-ben a szabvany szerint, de meg sose lattam elesben, es ez klasszikus felvetelizteto szivato kerdes.

    • v február 19, 2013 8:41 du.

      Szerintem csak statikus nested class-ban (ami egyébiránt by default static és nem is lehet más), vagy konstans mezőhöz rendelt anonymous osztályban példányosítva, ami természetéből adódóan singleton. Ez azonban már nem egy statikus metódus, mert itt van egy példány. Persze a class maga is objektum, és így a statikus metódus is kapcsolódik egy objektum példányhoz, aminek a típusa Class, és akkor jött létre, amikor az osztály betöltődött, de ezt nem szoktuk így nevezni.

  5. Molnár Péter december 9, 2015 1:38 du.

    Hmmmm…végigolvastam. Tényleg az a baj ezekkel az (egyébként kiváló) okfejtésekkel, hogy a valós problémák szinte soha nem kívánnak egy ilyen osztálystruktúrát. Ha meg igen, akkor fejlesztők túlnyomó része inkább favágó(bb) megoldásokat implementál.

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: