Corso Java #6: principi della programmazione a oggetti

DiMarco Nisticò

PUBBLICATO IL 5 Gen 2024 ALLE 13:22 - AGGIORNATO IL 31 Dicembre 2023 ALLE 15:59 #java, #programmazione

Dopo diverso tempo riprendiamo in mano il corso Java, affrontando alcuni dei concetti forse più fondamentali di questo linguaggio di programmazione (e molti altri), ovvero ereditarietà, polimorfismo e incapsulamento. Questi termini vengono spesso associati al cosiddetti paradigma Object Oriented, o semplicemente programmazione orientata agli oggetti (OOP), in quanto si basano proprio sull’utilizzare classi, interfacce ed entità astratte per compiere determinate operazioni.

Durante il nostro corso abbiamo cercato di racchiudere in pochi articoli le basi della programmazione, partendo dai concetti più basilari fino a spiegare la sintassi principale che sta alla base dei concetti che tratteremo oggi. Non siamo voluti andare su argomenti troppo tecnici in quanto chi parte da zero potrebbe trovarsi spiazzato, ma sicuramente da qui in avanti avremo a disposizione tutti gli strumenti per poter comprendere a pieno anche sintassi più elaborate. Prima di proseguire, vi invitiamo a recuperare i precedenti articoli:

Detto questo, possiamo cominciare.

Classe e oggetto: cosa rappresentano

La classe è uno dei concetti fondamentali della programmazione e un elemento su cui si basano praticamente tutti i linguaggi di programmazione, in quanto permette la rappresentazione e il flusso di dati più o meno complessi, oltre che essere necessaria per un’architettura di progetto efficiente. A livello di definizione, una classe definisce le proprietà e i metodi principali per una tipologia di oggetto, intendendo per oggetto un’istanza di una classe. Dopo aver definito una classe all’interno del programma, è possibile definire un nuovo oggetto che ha come tipo il nome della classe stesso, potendo quindi inizializzare le proprietà definite al suo interno o richiamare eventualmente i metodi e funzioni implementate. Vediamo un esempio pratico:

public class Quadrato {
      private int valoreLato;

      public double calcoloAreaQuadrato (int valoreLato){
             return valoreLato*valoreLato;
      }

      public double calcoloVolumeQuadrato (int valoreLato){
             return valoreLato*valoreLato*valoreLato;
      }
}

In questo caso abbiamo definito una classe chiamata Quadrato, al cui interno abbiamo dichiarato una sola proprietà valoreLato e due metodi per il calcolo dell’area e del volume del quadrato. Questa classe potrà essere richiamata per poter utilizzare tutti i metodi definiti al suo interno ed eseguire specifiche operazioni. Ecco come:

import package.name.Quadrato

public static void main(String[] args){
      Quadrato quadrato = new Quadrato();
      double areaQuadrato = quadrato.calcoloAreaQuadrato(3)
      double volumeQuadrato = quadrato.calcoloVolumeQuadrato(3)
      System.out.println("Area del quadrato: " + areaQuadrato);
      System.out.println("Volume del quadrato: " + volumeQuadrato );
}

In questo secondo esempio abbiamo innanzitutto importato la classe all’interno del file utilizzando la parola chiave import, che permette di rendere visibile la classe all’interno di un altra. Successivamente, all’interno del metodo principale main abbiamo creato una nuova istanza dell’oggetto Quadrato, su cui poi richiamare i metodi definiti precedentemente. Nell’esempio sopra, l’oggetto è stato definito con il nome quadrato, su cui abbiamo calcolato l’area e il volume impostando come valoreLato il valore 3.

Differenza tra public, private e protected

Nell’ambito della programmazione, avrete sempre a che fare con alcune parole chiave che rappresentano la visibilità di metodi e variabili all’interno del codice sorgente. Nei linguaggi più comuni si parla di public, private e protected (su C# è presente anche interna). Con public si intende una variabile o un metodo che è accessibile in ogni punto del codice, quindi richiamabile e valorizzabile in ogni classe senza alcun limite. Al contrario, utilizzando private si limita la visibilità di un metodo o una variabile solamente all’interno della classe stessa che definisce l’elemento. Infine protected è una via di mezzo tra le due precedenti, in quanto rende metodo/variabile sì accessibile in ogni punto del codice ma soltanto all’interno dello stesso package, che in Java equivale alla cartella dove andiamo a creare tutti i vari file .java in cui scriviamo il codice. Facendo un esempio pratico, se abbiamo due file Quadrato.java e Forme.java, il primo dentro il percorso java/forme/Forme.java e il secondo dentro java/quadrato/Quadrato.java, la classe Quadrato potrà accedere alla classe Forme solamente se quest’ultima è definita come public in quanto appartenente a un altro package mentre se invece una terza classe Triangolo.java è posizionata dentro java/forme/Triangolo.java potrà accedere alla classe Forme.java in quanto appartenente allo stesso package.

NOTA: generalmente le proprietà di una classe sono definite come private.

Differenza tra classe, classe astratta e interfaccia

Dopo aver definito il concetto di classe e le tipologie di visibilità, è doveroso parlare delle tipologie di classe presenti in Java, ognuna con la propria utilità e necessità di utilizzo. Oltre alla classe in sé e per sé, composta da proprietà e metodi ben specifici e di cui abbiamo già definito le caratteristiche peculiari nei paragrafi precedenti, esistono anche le interfacce e le classi astratte. Con il termine interfaccia si definisce una particolare classe che contiene solamente la definizione di proprietà e metodi senza la loro implementazione e a livello di codice si definisce con la parola chiave interface come:

public interface Quadrato{
    private int lato;
    public int area(int lato);
    public int volume(int lato);
}

Come potete vedere, l’interfaccia offre solamente una panoramica dei metodi relativi all’oggetto Quadrato e perciò non può essere istanziata. Quindi in questo caso non potremo creare nel main un oggetto Quadrato e richiamare quindi i vari metodi annessi. Per poter sfruttare i metodi dell’interfaccia bisognerà prima implementarli attraverso una nuova classe che richiama l’interfaccia:

public class QuadratoImpl implements Quadrato {
      public int area (int lato){
           //corpo del metodo
      }
      public int volume (int lato){
           //corpo del metodo
      }
} 

In questo caso la parola chiave sarà implements, che dovrà essere posizionata subito dopo il nome della classe. La regola principale di questa classe è che deve implementare TUTTI i metodi e proprietà definiti nell’interfaccia, nessuno escluso. A questo punto è possibile creare un nuovo oggetto QuadratoImpl e applicare tutti i metodi appena implementati.

public static void main(String[] args){
      QuadratoImpl quadrato = new QuadratoImpl ();
      double areaQuadrato = quadrato.area(3)
      double volumeQuadrato = quadrato.volume(3)
      System.out.println("Area del quadrato: " + areaQuadrato);
      System.out.println("Volume del quadrato: " + volumeQuadrato );
}

Infine abbiamo le classi astratte, che sono una via di mezzo tra la classe standard e l’interfaccia. In questo caso abbiamo sia metodi implementati che non e viene definita con la parola chiave abstract:

public abstract class Quadrato {
      public int area (int lato){
           //corpo del metodo
      }
      public int volume (int lato);
} 

Rispetto all’esempio dell’interfaccia, qui il metodo “area” viene già implementato mentre il metodo “volume” viene solamente definito. Anche per le classi astratte vige la regola di non poter essere istanziate, in quanto contengono metodi non implementati, quindi è necessario creare una nuova classe che questa volta “estende” quella astratta e che implementi i metodi definiti inizialmente. Lato codice scriveremo:

public class QuadratoExt extends Quadrato{
      public int volume (int lato){
           //corpo del metodo
      }
} 

La classe QuadratoExt.java conterrà le implementazioni di tutti i metodi solamente definiti nella classe astratta, ed eventualmente nuove implementazioni per metodi già implementati nella classe astratta. In questo caso la parola chiave è extends, che andrà posizionata tra il nome della classe che estende e quello della classe estesa. Questa definizione introduce un concetto fondamentale, quello dell’ereditarietà.

NOTA: per poter implementare (o estendere) correttamente un’interfaccia (o una classe astratta) è importante che i metodi e le proprietà siano definiti come public o al massimo protected.

OOP: ereditarietà

L’ereditarietà il primo paradigma fondamentale della programmazione a oggetti e permette di estendere le caratteristiche di una classe generica con altrettante classi al fine di avere una classe comune a più classi che aggiungono proprietà e metodi distinti. Solitamente la classe che estende quella generale, detta solitamente classe padre, è detta classe figlia (o sottoclasse). Vediamo subito un esempio concreto:

//CLASSE PADRE
public class Quadrato{
      public int lato;
      public int area (int lato){
           //corpo del metodo
      }
} 

//CLASSE FIGLIA
public class QuadratoVolume extends Quadrato{
      public int volume (int lato){
           //corpo del metodo
      }
} 

In questo semplice codice, abbiamo creato una classe padre chiamata Quadrato, che al suo interno contiene una sola proprietà e un solo metodo. Successivamente abbiamo esteso questa classe con una seconda classe QuadratoVolume dove abbiamo implementato un secondo metodo. Nel momento in cui andremo a istanziare un oggetto di tipo QuadratoVolume, potremo applicare sia i metodi della classe QuadratoVolume che della classe Quadrato. Perciò il codice seguente risulterà perfettamente lecito:

public static void main(String[] args){
      QuadratoVolume quadrato = new QuadratoVolume ();
      double areaQuadrato = quadrato.area(3)
      double volumeQuadrato = quadrato.volume(3)
      System.out.println("Area del quadrato: " + areaQuadrato);
      System.out.println("Volume del quadrato: " + volumeQuadrato );
}

Nell’esempio mostrato sopra, il codice delle due classi è racchiuso in un unico blocco ma solitamente vengono creati due file distinti per le due classi (es. Quadrato.java e QuadratoVolume.java) così da mantenere un codice pulito e su cui è più facile fare manutenzione e modifiche successive.

A questo punto la domanda che potrebbe sorgere spontanea è: “E’ possibile estendere più di una classe contemporaneamente?”. La risposta purtroppo è no, in quanto Java non supporta l’ereditarietà multipla (come tanti altri linguaggi a differenza di Python), però esiste un compromesso piuttosto intelligente che permette di aggirare questo limite e che riguarda l’implementazione di più interfacce. Infatti, a differenza delle classi, su Java è possibile creare una classe che implementa più interfacce simultaneamente, con l’obbligo ovviamente di implementare tutti i metodi di tutte le interfacce coinvolte. Per applicare questa definizione, bisogna scrivere cosi:

public class Triangolo implements Forma, Geometria, Matematica {
      //implementazione di metodi e funzioni delle interfacce
}

Il concetto è lo stesso dell’implementazione di una singola interfaccia, con la differenza che bisogna scrivere tutte le interfacce separate da virgola appena dopo la parola chiave implements.

OOP: polimorfismo (override e overload)

Nell’esempio che vi abbiamo appena mostrato, abbiamo esteso una classe con un’altra aggiungendo un nuovo metodo che andasse ad ampliare le caratteristiche dell’oggetto Quadrato. In alcuni casi, però, può essere necessario solamente richiamare nuovamente gli stessi identici metodi della classe madre modificandone, però, l’implementazione. Ecco quindi che è importante definire i concetti di override e overload, che fanno parte del secondo paradigma della programmazione a oggetti detto polimorfismo (metodo che assume valori diversi in base al tipo fornito).

Con il termine override si intende l’implementazione di uno stesso metodo mantenendo la stessa firma (parametri) del metodo originario. Prendiamo come esempio la classe Forma, definita come segue:

//CLASSE PADRE
public class Forma{
      public int base;
      public int altezza;
      public int area (int base, int altezza){
           base*altezza;
      }
} 

Per il metodo public int area la firma è caratterizzata dai parametri int base e int altezza, che in caso di override dovrà rimanere la stessa:

//CLASSE FIGLIA
public class Triangolo extends Forma{
      public int volume (int lato){
           //corpo del metodo
      }
      public int area (int base, int altezza){
            (base*altezza)/2;
      }
} 

Come potete notare, la classe Triangolo estende la classe Forma con un nuovo metodo volume ma effettua anche l’override del metodo area applicando un calcolo leggermente diverso in quanto l’area del Triangolo è diversa rispetto a quella di un oggetto Forma generico. Oltre alla firma, dovrà rimanere invariato anche il tipo di ritorno del metodo, altrimenti verrà generato un errore in fase di compilazione.

Con il termine overload, invece, andiamo sempre ad effettuare un’implementazione di uno stesso metodo della classe padre ma stavolta modificando anche la firma del metodo stesso, magari per includere/rimuovere specifici parametri. Consideriamo ad esempio il caso di una classe Quadrato che estende Forma:

//CLASSE FIGLIA
public class Quadrato extends Forma{
      public int volume (int lato){
           //corpo del metodo
      }
      public int area (int base){
            base*base;
      }
} 

Nel caso del Quadrato, siccome l’area è data dal prodotto del lato per se stesso (in quanto base e altezza hanno lo stesso valore) possiamo riscrivere il metodo applicando un solo parametro. Questi esempi sono chiaramente semplificati per esprimere il concetto in maniera più semplice possibile, però l’override e l’overload sono operazioni molto potenti che permettono il riutilizzo del codice in modo intelligente.

OOP: incapsulamento

Veniamo al terzo e ultimo paradigma della programmazione a oggetti, ovvero l’incapsulamento. Il concetto di incapsulamento lo possiamo definire come la capacità di nascondere verso l’esterno i dettagli implementativi di un oggetto e le sue funzionalità. Per chi ci ha seguito finora potrà riconoscere questa definizione nel semplice concetto di visibilità private di una proprietà o di un metodo. Vediamo un esempio:

public class Cubo
{
    // Dichiarazione delle proprietà: si noti che sono definite tutte private.
    private int lunghezza;
    private int larghezza;
    private int altezza;
    // Metodi "Setter" per la modifica delle proprietà
    public void setLunghezza(int lun)
    {
        lunghezza = lun;
    }
    public void setLarghezza(int lar)
    {
        larghezza = lar;
    }
    public void setAltezza(int alt)
    {
        altezza = alt;
    }
    // Metodi "Getter" per ricavare i valori delle proprietà
    public int getLunghezza()
    {
        return lunghezza;
    }
    public int getLarghezza()
    {
        return larghezza;
    }
    public int getAltezza()
    {
        return altezza;
    }
    // Metodo pubblico che visualizza il volume del cubo, usando le proprietà
     interne della classe
    public void visualizzaVolume()
    {
        System.out.println(lunghezza * larghezza * altezza);
    }
}

Le proprietà della classe Cubo sono definite tutte come private, quindi non sono accessibili da nessun’altra classe, anche se si tratta di una classe derivata che estende la classe Cubo. In questo modo si circoscrivono tali proprietà a un solo oggetto. E’ possibile, però, dichiarare dei metodi pubblici interni alla classe che permettano di accedere a tali informazioni private e potendoli richiamare in altre classi perché pubblici. Tale procedura è caratterizzata dai metodi Getter e Setter, i primi per restituire i valori delle variabili e i secondi per assegnare tali valori. Le proprietà interne di una classe possono anche essere usare per implementare metodi funzionali per l’oggetto e che permettono di eseguire calcoli senza mettere in mostra le proprietà iniziali. Ad esempio, una classe Triangolo che estende la classe Forma con il metodo pubblico calcolaArea() definito con un solo parametro privato potrà comunque accedere a questo metodo e utilizzarlo per il calcolo dell’area senza conoscere nello specifico la/le proprietà che viene passata al metodo.

Con questo articolo abbiamo concluso il nostro corso base sul linguaggio Java, che vi permetterà già da subito di iniziare a realizzare i vostri primi programmi, anche quelli leggermente più complessi e che sfruttano a pieno il paradigma a oggetti.

Di Marco Nisticò

Sviluppatore informatico, cerco sempre di stare al passo con i tempi in un mondo ormai circondato dalla tecnologia.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.