Polimorfism în Java

Polimorfismul înseamnă că același apel (f.arie()) execută metoda clasei reale a obiectului, chiar dacă variabila are tipul superclasei.

Sintaxă
Forma f = new Cerc(2);   // tipul variabilei: Forma, tipul obiectului: Cerc
f.arie();                // se apelează Cerc.arie()

Ce face Polimorfism

O variabilă de tipul superclasei sau al unei interfețe poate ține obiecte din orice subclasă. Când apelezi o metodă rescrisă, Java alege la rulare versiunea din clasa reală a obiectului (legare dinamică). Codul care lucrează cu Forma nu trebuie să știe dacă are un cerc sau un dreptunghi.

Compilatorul se uită doar la tipul variabilei: prin Forma f poți apela numai metodele declarate în Forma. Pentru metodele specifice subclasei ai nevoie de o conversie, de preferat cu instanceof cu pattern.

Supraîncărcarea (overloading, metode cu același nume și parametri diferiți) este altceva: versiunea se alege la compilare, după tipul declarat al argumentelor. Rescrierea (overriding) se alege la rulare, după obiect.

Exemple

Exemplul 1: Același apel, comportamente diferite

Bucla nu știe ce formă are în față. Fiecare obiect răspunde cu propria metodă arie().

abstract class Forma {
    abstract double arie();

    String nume() {
        return getClass().getSimpleName();
    }
}

class Cerc extends Forma {
    final double raza;
    Cerc(double raza) { this.raza = raza; }

    @Override
    double arie() { return Math.PI * raza * raza; }
}

class Dreptunghi extends Forma {
    final double l, h;
    Dreptunghi(double l, double h) { this.l = l; this.h = h; }

    @Override
    double arie() { return l * h; }
}

class Patrat extends Dreptunghi {
    Patrat(double latura) { super(latura, latura); }
}

public class Main {
    public static void main(String[] args) {
        Forma[] forme = {new Cerc(1.5), new Dreptunghi(3, 4), new Patrat(2.5)};
        double total = 0;
        for (Forma f : forme) {
            System.out.printf("%-10s %8.2f m²%n", f.nume(), f.arie());
            total += f.arie();
        }
        System.out.printf("%-10s %8.2f m²%n", "Total", total);
    }
}

Rezultat

Cerc           7.07 m²
Dreptunghi    12.00 m²
Patrat         6.25 m²
Total         25.32 m²

Exemplul 2: Rescriere (la rulare) și supraîncărcare (la compilare)

Variabila n are tipul Notificare, dar obiectul este Sms: trimite() alege versiunea din Sms. Metoda supraîncărcată descrie se alege după tipul variabilei, deci se apelează varianta pentru Notificare.

class Notificare {
    String trimite() { return "notificare generică"; }
}

class Sms extends Notificare {
    @Override
    String trimite() { return "SMS către 07xx xxx xxx"; }
}

class Email extends Notificare {
    @Override
    String trimite() { return "email către ana@exemplu.ro"; }
}

public class Main {
    static String descrie(Notificare n) { return "descrie(Notificare)"; }
    static String descrie(Sms s) { return "descrie(Sms)"; }

    public static void main(String[] args) {
        Notificare n = new Sms();
        System.out.println(n.trimite());
        System.out.println(descrie(n));
        System.out.println(descrie(new Sms()));

        Notificare[] toate = {new Notificare(), new Sms(), new Email()};
        for (Notificare x : toate) {
            System.out.println("-> " + x.trimite());
        }
    }
}

Rezultat

SMS către 07xx xxx xxx
descrie(Notificare)
descrie(Sms)
-> notificare generică
-> SMS către 07xx xxx xxx
-> email către ana@exemplu.ro

Greșeli frecvente

  • Apelezi prin variabila superclasei o metodă care există doar în subclasă: forma.getRaza() dă cannot find symbol. Verifică tipul cu if (forma instanceof Cerc c) și folosește c.
  • Te aștepți ca metodele static să fie polimorfe. Ele nu se rescriu, ci se ascund: se alege versiunea după tipul variabilei, nu al obiectului.
  • Confunzi supraîncărcarea cu rescrierea. Dacă parametrul diferă, nu ai rescris nimic; @Override te avertizează.

Comenzi înrudite

  • Moștenire (extends): Cu extends o clasă moștenește câmpurile și metodele altei clase și le poate extinde sau rescrie; Java permite o singură superclasă.
  • Interfețe (interface, implements): O interfață (interface) descrie ce metode trebuie să aibă o clasă; clasa o promite cu implements și poate implementa mai multe interfețe.
  • Clase abstracte (abstract): O clasă abstract nu poate fi instanțiată direct; poate avea metode abstracte, pe care subclasele concrete sunt obligate să le scrie.
  • instanceof: instanceof verifică dacă un obiect este de un anumit tip; în forma nouă declară și o variabilă deja convertită la acel tip.
  • Pattern matching (instanceof și switch): Pattern matching testează tipul (și forma) unui obiect și îi extrage părțile într-un singur pas, în instanceof sau în switch.