Tüübiteisendus pärilikkuse taustal
Sissejuhatus
Pärimise kontekstis tähendab tüübiteisendus (casting) liikumist ülemklassi ja alamklassi tüüpide vahel. See erineb primitiivsete tüüpide teisendamisest, millega olete varasemalt juba kokku puutunud. Siin ei teisendata andmeid, vaid muudetakse seda, kuidas kompilaator näeb juba mälus olevat objekti.
Sellest tulenevalt jaguneb tüübiteisendus siin kaheks:
- upcasting ehk alamklassi objekti käsitlemine ülemklassi tüübina
- downcasting ehk ülemklassi muutuja teisendamine alamklassi tüüpi
Upcasting
Upcasting esineb, kui alamklassi objekt omistatakse ülemklassi tüüpi muutujale. See on alati ohutu ja toimub automaatselt ehk eraldi tüübiteisenduse süntaksit siin kasutama ei pea. Näiteks:
Car car = new Car("Toyota", 120, 4);
Vehicle v = car; // upcast — no cast syntax needed
See töötab kehtiva "IS-A" suhte tõttu - Car is a Vehicle.
Objekt mälus on ikka Car tüüpi, ainult muutuja tüüp muutub.
Läbi tekkinud muutuja v on võimalik ligi pääseda ainult meetoditele, mis on Vehicle klassis olemas:
v.drive(); // OK — defined in Vehicle
v.getBrand(); // OK — defined in Vehicle
// v.getDoors(); // Error — getDoors() is not part of Vehicle
// v.honk(); // Error — honk() is not part of Vehicle
Upcasting võimaldab polümorfismi - see ei muuda objekti ennast, vaid ainult seda, millise tüübi kaudu sellele viidatakse.
Selle kaudu on võimalik näiteks luua List<Vehicle>, milles saab hoida Car, Truck ja muid alamklassi objekte ühes kollektsioonis koos.
Downcasting
Downcasting on vastupidi. Sellega anname kompilaatorile märku, et antud muutuja viitab tegelikult alamklassi objektile ning seda võib käsitleda alamklassi tüübina. Selleks on vaja ka eraldi süntaksi kasutada, näiteks:
Vehicle v = new Car("Toyota", 120, 4);
Car car = (Car) v; // explicit downcast
car.getDoors(); // 4 — now accessible
car.honk(); // Toyota beeps!
(Car) süntaks annab kompilaatorile märku, et seda muutujat tuleks kohelda ette antud tüübina.
Antud juhul vastutus langeb programmeerija peale, kompilaator laseb selle koodi läbi, kuid on risk, et võivad programmi käitusajal vead tekkida.
Downcasting on ohutu ainult siis, kui objekt on tegelikult alamklassi tüüpi. Teisisõnu, objekt peab olema loodud selle alamklassi kaudu:
Vehicle v = new Car(...); // OK to downcast later
Vehicle v = new Truck(...); // NOT safe to cast to Car
ClassCastException
Kui antud objekti pole võimalik soovitud tüüpi teisendada, tekib ClassCastException erind.
Näiteks:
Vehicle v = new Truck("Volvo", 90, 20.0);
Car car = (Car) v; // compiles fine
// ClassCastException at runtime: Truck cannot be cast to Car
Antud kood kompileerub, kuna kompilaatoril pole võimalik alati teada, mida antud muutuja tegelikult sisaldab. Tegelik kontroll toimub programmi töö ajal.
Vea vältimiseks tasub alati eelnevalt veenduda, kas on võimalik tüübiteisendust teha.
Seda saab teha läbi instanceof märksõna:
if (v instanceof Car) {
Car car = (Car) v;
System.out.println(car.getDoors());
}
Antud juhul kui muutuja v pole Car tüüpi, siis järgnevat koodi ei panda töölegi.
Java 16+ instanceof
Alates Java 16st on võimalik tüüpi kontrolli ja teisendust kokku panna ühele reale:
if (v instanceof Car car) {
System.out.println(car.getDoors()); // car is already available, no explicit cast needed
}
Tekkinud car muutuja skoop kehtib ainult if-ploki raames.
Selle tõttu pole vajadust eraldi tüübiteisendust teha ning väheneb ka instanceof kontrolli ununemise oht.
Võrdlus vana ja uue tüübikontrolli vahel:
// Before Java 16
if (v instanceof Car) {
Car car = (Car) v;
System.out.println(car.getDoors());
}
// Java 16+
if (v instanceof Car car) {
System.out.println(car.getDoors());
}
Igal juhul peaks eelistama uut viisi tüübikontrolli ja -teisenduse läbi viimiseks.
Vahetu tüübiteisendus (inline casting)
Lisaks on võimalik teha tüübiteisendust ja meetodi väljakutset ka ühel real, ilma muutujat tekitamata. Selleks tuleb teisendatav objekt mähkida sulgudesse koos tüübiga ning peale sulge kutsuda vajalik meetod välja. Näiteks:
System.out.println(
((Car) v).getDoors()
);
Sisemised sulud (Car) v teostavad tüübiteisendust ning väljaspool sulge .getDoors() kutsub meetodi välja saadud tulemuse peal.
See on kasulik juhul, kui on korraks vaja tüübiteisendust teha mingi tegevuse teostamiseks, kuid liigne kasutus vähendab koodi loetavust.
Kompilaator lubab sellise teisenduse läbi, kuid tegelik tüübikontroll toimub alles programmi töö ajal ning vale tüübi korral tekib ClassCastException.