Liigu peamise sisu juurde

Polümorfism

Õppevideo antud teemal:

Sissejuhatus

Polümorfism (polymorphism) ehk mitmekujulisus on millegi esinemine mitmel eri kujul. Javas tähendab polümorfism võimet käsitleda erinevate alamklasside objekte ühise ülemklassi tüübi kaudu. Kuigi käsitleme objekte mingi üldistuse kaudu, siis objekt käitub siiski vastavalt oma tegelikule klassile.

Teile võib polümorfism olla tuttav juba varasematest õpingutest järgmise selgituse kaudu: Polümorfism on olukord, mille korral käitub meetod sõltuvalt klassist erinevalt. Polümorfism esineb siis, kui alamklass kasutab superklassis defineeritud meetodit teisel moel. Teiste sõnadega: kui superklassil ja alamklassil on olemas sama nimega meetodid, millel on sama palju argumente, ning mis teevad erinevat asja, siis on tegemist polümorfismiga.

See on pärilikkuse üks peamisi eeliseid: kui klassihierarhia on loodud, töötab ülemklassi tüüpi kasutav kood automaatselt ka kõigi selle alamklassidega.

Probleem ilma polümorfismita

Oletame, et koodis hoitakse kogumit sõidukitest ning kõikide puhul on vaja välja kutsuda drive() meetod. Ilma polümorfismita näeks see umbes selline välja:

List<Car> cars = List.of(new Car("Toyota", 120, 4));
List<Truck> trucks = List.of(new Truck("Volvo", 90, 20.0));

for (Car car : cars) { car.drive(); }
for (Truck truck : trucks) { truck.drive(); }

Iga uue sõidukitüübi puhul on vaja tekitada uus järjend ning järjekordne tsükkel.

Teisendamine ülemklassi tüübiks (upcasting)

Kuna Car on Vehicle ja Truck on Vehicle (IS-A suhe), siis mõlemat tüüpi sõidukeid saaks hoiustada ühes Vehicle tüübi kogumikus:

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(new Car("Toyota", 120, 4));
vehicles.add(new Truck("Volvo", 90, 20.0));
vehicles.add(new Car("Honda", 110, 2));

for (Vehicle v : vehicles) {
v.drive();
}

// Toyota is driving
// Volvo is driving
// Honda is driving

Antud võtte nimi on upcasting - alamklassi objektide hoiustamine ülemklassi tüüpi muutujas. See teisendamine toimub automaatselt ning eraldi andmetüübi teisendust pole vaja kirjutada.

Dünaamiline meetodivalik (Dynamic dispatch)

Java kasutab sisemiselt dynamic dispatch ehk dünaamilist meetodivalikut. Lühidalt, kui kutsuda välja Vehicle tüüpi muutuja drive() meetodit, siis Java ei otsusta, millist meetodit käivitada muutuja tüübi järgi, vaid objekti tegeliku tüübi järgi. Kui Car klass kirjutaks drive() meetodi loogikat üle, siis kasutataks selle versiooni drive() meetodist:

class Vehicle {
private String brand;

public Vehicle(String brand) { this.brand = brand; }
public String getBrand() { return brand; }

public void drive() {
System.out.println(brand + " is driving");
}
}

class Car extends Vehicle {
public Car(String brand) { super(brand); }

@Override
public void drive() {
System.out.println(getBrand() + " is driving smoothly");
}
}

class Truck extends Vehicle {
public Truck(String brand) { super(brand); }

@Override
public void drive() {
System.out.println(getBrand() + " is driving loudly");
}
}
List<Vehicle> vehicles = List.of(
new Vehicle("Generic"),
new Car("Toyota"),
new Truck("Volvo")
);

for (Vehicle v : vehicles) {
v.drive();
}

// Generic is driving
// Toyota is driving smoothly
// Volvo is driving loudly

Kuigi tsükkel on teadlik ainult Vehicle tüüpi objektidest, siis iga objekt seal kogumikus vastab drive() meetodi välja kutsumisele erinevalt. Kui tekiks vajadus lisada uus alamklass (nt: Motorcycle), siis selle lisamine ei nõuaks tsükklis mingeid muudatusi.

Upcasting'u ja dynamic dispatch'i kooslus võimaldab polümorfismi: kood töötab automaatselt kõikide alamklasside puhul, ilma et oleks vaja iga alamklassiga eraldi tegeleda.

Dynamic dispatch töötab tänu meetodite ülekirjutamisele. Selle teema kohta saate lähemalt lugeda siit.

Alamklassi käitumise rakendamine

Ülemklassi tüüpi kasutades muutujal on võimalik ligi pääseda ainult ülemklassi meetoditele. Kui alamklass defineerib lisaks muid meetodeid ning nendele oleks ligipääsu vaja, tuleb objekt teisendada tagasi alamklassi tüüpi. Selle võttega tutvume lähemalt Tüübiteisendus pärilikkuses peatükis, kuid järgnev on lühike näide sellest:

for (Vehicle v : vehicles) {
v.drive(); // works for all vehicles

// if v is of type Truck, we cast that type to it
// This line creates a temporary variable called "truck"
// NB: Applies to Java 16+
if (v instanceof Truck truck) {
// now we can access Truck class specific methods
System.out.println("Payload: " + truck.getPayloadTons() + " tons");
}

// In earlier versions it is done like this:
if (v instanceof Truck) {
Truck truck = (Truck) v; // explicit casting
System.out.println("Payload: " + truck.getPayloadTons() + " tons");
}
}
hoiatus

instanceof-i ja tüübi teisenduste sagedane kasutamine koodis on sageli märk sellest, et klassihierarhiat saaks paremini kujundada. Kui iga alamklass vajab erikohtlemist, tasub mõelda, kas see loogika võiks kuuluda hoopis üle kirjutatud meetodisse.