Ereditarietà in Java

Un concetto molto importante della programmazione ad oggetti è l'ereditarietà, vediamo come sfruttarlo in Java.

Ereditarietà

Per ereditarietà si intende che una classe può essere figlia di un'altra, assimilando attributi e metodi della classe madre.

Si può dire che un quadrato sia un rettangolo con larghezza e altezza identiche; Dato che la classe Rettangolo l'abbiamo già scritta proviamo a implementare una classe Quadrato ereditando da Rettangolo.

public class Rettangolo {

    //attributi
    private double lunghezza;
    private double altezza;

    //costruttore
    public Rettangolo(double lunghezza, double altezza){

        this.lunghezza = lunghezza;
        this.altezza = altezza;
    }

    //metodi
    public double area() {
        return lunghezza * altezza;
    }

    public double aerimetro() {
        return (lunghezza + altezza) * 2;
    }
}

public class Quadrato extends Rettangolo {

    public Quadrato(double lato){
        super(lato, lato);
    }
}

Nella prima riga della classe Quadrato usiamo la keyword extends, per far sapere che stiamo ereditando dalla classe Rettangolo.

Nel Costruttore di Quadrato usiamo la parola chiave super, questa ci permette di chiamare il costruttore della classe che stiamo ereditando, chiamata anche superclasse, nel nostro caso Rettangolo.

Proviamo adesso ad utilizzarla nel Main:

public class Main {

    public static void main(String[] args) { 

        Quadrato q = new Quadrato(20);

        System.out.println("Area: " + q.area() + " Perimetro: "+ q.perimetro());

    }
}
Area: 400.0 Perimetro: 80.0

uml

Nei diagrammi UML si usa la freccia triangolare per indicare un legame di ereditarietà, la direzione della freccia va dalla sottoclasse alla superclasse.

Classi Astratte

Esistono uno speciali tipi di classe che non possono essere istanziate, si chiamano classi astratte e possono tornarci molto utili per generalizzare il nostro codice. Immaginiamo di voler calcolare l'area di diversi tipi di figure geometriche. Come facciamo a scrivere del codice generico al punto giusto? Con una classe Astratta!

public abstract class Forma {

    //metodi
    public abstract double area();

    public abstract double perimetro();
}  

Ecco qua la nostra classe astratta, con la keyword abstract alla riga 1. Anche i metodi area() e perimetro() sono abstract: questo significa che quando estendiamo la classe dovremo farne l'override dei metodi, cioè implementarli.

Scriviamo altre due classi Rettangolo e Cerchio:

public class Rettangolo extends Forma {

    private double lunghezza;
    private double altezza;

    public Rettangolo(double lunghezza, double altezza){

        this.lunghezza = lunghezza;
        this.altezza = altezza;
    }

    public double area() {
        return lunghezza * altezza;
    }

    public double perimetro() {
        return (lunghezza + altezza) * 2;
    }
}

public class Cerchio extends Forma {

    private double raggio;

    public Cerchio(double raggio){

        this.raggio = raggio;
    }

    public double area() {
        return Math.PI * Math.pow(raggio, 2);
    }

    public double perimetro() {
        return 2 * Math.PI * raggio;
    }
}

Scriviamo il main:

public class Main {

    public static void main(String[] args) {

        Forma f1 = new Cerchio(20);

        System.out.println("Area: " + f1.area() + " Perimetro: "+ f1.perimetro());

        Forma f2 = new Rettangolo(10, 5);

        System.out.println("Area: " + f1.area() + " Perimetro: "+ f1.perimetro());

    }
}

Come puoi vedere il tipo di f1 e f2 è Forma, anche se istanzio un Cerchio e un Rettangolo. Questo è possibile grazie all'ereditarietà. Il fatto che Forma sia una classe astratta mi aiuta a non commettere errori istanziandola direttamente, ma mi costringe ad usare le sue sottoclassi, Cerchio e Rettangolo.

uml

Nei diagrammi UML una classe astratta si nota dal nome scritto in corsivo. Anche i metodi astratti vengono scritti in corsivo.

Interfacce

Un'interfaccia ci permette di definire un contratto, formato da una serie di metodi che la classe che la implementa deve rispettare. Questo può tornarci utile quando ci aspettiamo le stesse caratteristiche da oggetti molto diversi tra loro, oppure quando bisogna mantenere uno standard tra diversi programmatori.

Vediamo un esempio:

public interface Orologio {

    String getOra();
}

public class Sveglia implements Orologio {

    private int ore;
    private int minuti;

    private int oreAllarme;
    private int minutiAllarme;

    public Sveglia(int ore, int minuti){
        this.ore = ore;
        this.minuti = minuti;
    }

    public void setAllarme(int oreAllarme, int minutiAllarme){
        this.oreAllarme = oreAllarme;
        this.minutiAllarme = minutiAllarme;
    }

    public String getAllarme(){
        return this.oreAllarme + ":" + this.minutiAllarme;
    }

    public String getOra(){
        return this.ore + ":" + this.minuti;
    }

    public void suona(){
        //fai suonare l'allarme
    }
}

public class OrologioDaPolso implements Orologio {

    private int ore;
    private int minuti;

    public OrologioDaPolso(int ore, int minuti){
        this.ore = ore;
        this.minuti = minuti;
    }

    public String getOra(){
        return this.ore + ":" + this.minuti;
    }

    public void carica(){
        //ricarica l'orologio
    }
}

public class Meridiana implements Orologio {

    public Meridiana(){

    }

    public String getOra(){
        
        //... calcola l'ora in base alla posizione del sole
        return "11:59";
    }
}

Per usare correttamente un'interfaccia basta mettere la keyword implements nella classe interessata e fare l'override di tutti dei metodi.

public class Main {

    public static void main(String[] args) {

        Orologio o1 = new Meridiana();
        Orologio o2 = new OrologioDaPolso(10,20);
        Orologio o3 = new Sveglia(7,30);

        System.out.println("Ora: " + o1.getOra());
        System.out.println("Ora: " + o2.getOra());
        System.out.println("Ora: " + o3.getOra());

    }
}

Possiamo notare che anche qui, in modo simile a Forma, utilizziamo Orologio e poi istanziamo oggetti diversi. Questo è possibile sempre grazie all'ereditarietà. Le interfacce risultano più flessibili poiché applicabili senza alterare la gerarchia delle classi.

uml

Nei diagrammi UML un'interfaccia si riconosce dalla scritta <<interface>> posta prima del nome, per indicare che una classe sta implementando un'interfaccia si utilizza la freccia triangolare e tratteggiata. La direzione della freccia va dalla classe all'interfaccia.

© Nicola Bovolato 2020