OOP in Java: classi, oggetti, incapsulamento, ereditarietà e astrazione
Impara la programmazione orientata agli oggetti in Java con esempi pratici. Padroneggia classi, oggetti, ereditarietà, incapsulamento e classi astratte utilizzando un sistema di menu per ristorante.
Java è costantemente tra le tre lingue più popolari al mondo. La sua adozione in ambiti come lo sviluppo software enterprise, le app Android e le applicazioni web su larga scala è impareggiabile. Il suo solido sistema di tipi, l’ecosistema esteso e la capacità di “write once, run anywhere” la rendono particolarmente allettante per costruire sistemi robusti e scalabili. In questo articolo esploreremo come le funzionalità di programmazione orientata agli oggetti di Java permettano agli sviluppatori di sfruttare efficacemente queste capacità, consentendo loro di creare applicazioni manutenibili e scalabili grazie a una corretta organizzazione e riuso del codice.
Una nota sull’organizzazione ed esecuzione del codice Java
Prima di iniziare a scrivere codice, facciamo un po’ di setup.
Come per la sua sintassi, Java ha regole rigide sull’organizzazione del codice.
Per prima cosa, ogni classe pubblica deve stare nel proprio file, chiamato esattamente come la classe ma con estensione .java. Quindi, se voglio scrivere una classe Laptop, il nome del file deve essere Laptop.java—sensibile alle maiuscole/minuscole. Puoi avere classi non pubbliche nello stesso file, ma è meglio separarle. So che stiamo andando avanti—parlando di come organizzare le classi prima ancora di scriverle—ma avere un’idea di massima su dove mettere le cose in anticipo è utile.
Tutti i progetti Java devono avere un file Main.java con la classe Main. Qui è dove testi le tue classi creando oggetti a partire da esse.
Per eseguire il codice Java, useremoIntelliJ IDEA, un IDE Java molto diffuso. Dopo aver installato IntelliJ:
Crea un nuovo progetto Java (File > New > Project)
Fai clic destro sulla cartella src per creare il file Main.java e incolla il seguente contenuto:
publicclassMain{publicstaticvoidmain(String[] args){// Create and tests objects here}}Offerto da
Quando parliamo di classi, scriviamo il codice in file diversi da Main.java. Ma se parliamo di creare e testare oggetti, torniamo a Main.java.
Per eseguire il programma, puoi cliccare sul pulsante verde di play accanto al metodo main:
L’output verrà mostrato nella finestra Run in basso.
Se sei completamente nuovo a Java, dai un’occhiata al nostrocorso Introduction to Java, che copre i fondamenti dei tipi di dato e del flusso di controllo in Java prima di proseguire.
Altrimenti, tuffiamoci subito.
Classi e oggetti in Java
Quindi, cosa sono esattamente le classi?
Le classi sono costrutti di programmazione in Java per rappresentare concetti del mondo reale. Per esempio, considera questa classe MenuItem (crea un file per scrivere questa classe nel tuo IDE):
publicclassMenuItem{publicString name;publicdouble price;}Offerto da
La classe ci fornisce un progetto o un modello per rappresentare vari elementi del menu di un ristorante. Modificando i due attributi della classe, name, e price, possiamo creare innumerevoli oggetti menu come un burger o un’insalata.
Quindi, per creare una classe in Java, inizi con una riga che descrive il livello di accesso della classe (private, public o protected) seguito dal nome della classe. Subito dopo le parentesi, definisci gli attributi della tua classe.
Ma come creiamo oggetti che appartengono a questa classe? Java lo consente tramite i metodi costruttori:
Un costruttore è un metodo speciale che viene chiamato quando creiamo un nuovo oggetto da una classe. Inizializza gli attributi dell’oggetto con i valori che forniamo. Nell’esempio sopra, il costruttore accetta i parametri name e price e li assegna ai campi dell’oggetto usando la parola chiave ‘this’ per riferirsi a una futura istanza dell’oggetto.
La sintassi del costruttore è diversa dagli altri metodi della classe perché non richiede di specificare un tipo di ritorno. Inoltre, il costruttore deve avere lo stesso nome della classe e dovrebbe avere lo stesso numero di attributi dichiarati dopo la definizione della classe. Sopra, il costruttore sta creando due attributi perché ne abbiamo dichiarati due dopo la definizione della classe: name e price.
Dopo aver scritto la classe e il suo costruttore, puoi crearne istanze (oggetti) nel tuo metodo main:
Qui sopra stiamo creando due oggetti MenuItem nelle variabili burger e salad. Come richiesto in Java, il tipo della variabile deve essere dichiarato, ovvero MenuItem. Poi, per creare un’istanza della nostra classe, scriviamo la parola chiave new seguita dall’invocazione del costruttore.
Oltre al costruttore, puoi creare metodi normali che danno comportamento alla tua classe. Per esempio, sotto aggiungiamo un metodo per calcolare il prezzo totale dopo le tasse:
publicclassMenuItem{publicString name;publicdouble price;// ConstructorpublicMenuItem(String name,double price){this.name = name;this.price = price;}// Method to calculate price after taxpublicdoublegetPriceAfterTax(){double taxRate =0.08;// 8% tax ratereturn price +(price * taxRate);}}Offerto da
Ora possiamo calcolare il prezzo totale, tasse incluse:
publicclassMain{publicstaticvoidmain(String[] args){MenuItem burger =newMenuItem("Burger",3.5);System.out.println("Price after tax: $"+ burger.getPriceAfterTax());}}Offerto da
Lo scopo delle classi è fornire un modello per creare oggetti. Questi oggetti verranno poi utilizzati da altri script o programmi. Per esempio, i nostri oggetti MenuItem possono essere usati da un’interfaccia utente che ne mostra nome, prezzo e immagine su uno schermo.
Per questo motivo, dobbiamo progettare le nostre classi in modo che le loro istanze possano essere usate solo come previsto. Al momento, la nostra classe MenuItem è molto basilare e soggetta a errori. Qualcuno potrebbe creare oggetti con attributi assurdi, come una apple pie a prezzo negativo o un sandwich da un milione di dollari:
// Inside Main.javaMenuItem applePie =newMenuItem("Apple Pie",-5.99);// Negative price!MenuItem sandwich =newMenuItem("Sandwich",1000000);// Unreasonably expensiveSystem.out.println("Apple pie price: $"+ applePie.price);System.out.println("Sandwich price: $"+ sandwich.price);Offerto da
Quindi, la prima cosa da fare dopo aver scritto una classe è proteggere i suoi attributi limitando come vengono creati e accessi. Per iniziare, vogliamo consentire solo valori positivi per price e impostare un valore massimo per evitare di mostrare per errore elementi ridicolmente costosi.
Java ci permette di farlo usando i metodi setter:
publicclassMenuItem{privateString name;privatedouble price;privatestaticfinaldoubleMAX_PRICE=100.0;publicMenuItem(String name,double price){this.name = name;setPrice(price);}publicvoidsetPrice(double price){if(price <0){thrownewIllegalArgumentException("Price cannot be negative");}if(price >MAX_PRICE){thrownewIllegalArgumentException("Price cannot exceed $"+MAX_PRICE);}this.price = price;}}Offerto da
Vediamo cosa c’è di nuovo nel blocco di codice sopra:
1. Abbiamo reso gli attributi privati aggiungendo la parola chiave private. Questo significa che possono essere accessibili solo all’interno della classe MenuItem. L’incapsulamento parte da questo passaggio cruciale.
2. Abbiamo aggiunto una nuova costante MAX_PRICE che è:
private (accessibile solo all’interno della classe)
static (condivisa tra tutte le istanze)
final (non può essere modificata dopo l’inizializzazione)
impostata a $100.0 come prezzo massimo ragionevole
3. Abbiamo aggiunto un metodo setPrice() che:
Accetta un parametro price
Valida che il prezzo non sia negativo
Valida che il prezzo non superi MAX_PRICE
Lancia IllegalArgumentException con messaggi descrittivi se la validazione fallisce
Imposta il prezzo solo se tutte le validazioni passano
4. Abbiamo modificato il costruttore per usare setPrice() invece di assegnare direttamente il prezzo. Questo assicura che la validazione del prezzo avvenga durante la creazione dell’oggetto.
Abbiamo appena implementato uno dei pilastri fondamentali del buon design orientato agli oggetti — l’incapsulamento. Questo paradigma impone la protezione dei dati e un accesso controllato agli attributi dell’oggetto, garantendo che i dettagli di implementazione interni siano protetti da interferenze esterne e possano essere modificati solo tramite interfacce ben definite.
Rendiamo il concetto ancora più chiaro applicando l’incapsulamento all’attributo name. Immagina un bar che serve solo latte, cappuccino, espresso, americano e mocha.
Quindi, i nomi dei nostri elementi del menu possono essere solo uno tra quelli in questa lista. Ecco come possiamo imporlo nel codice:
// Rest of the class here...privatestaticfinalString[]VALID_NAMES={"latte","cappuccino","espresso","americano","mocha"};privateString name;publicvoidsetName(String name){String lowercaseName = name.toLowerCase();for(String validName :VALID_NAMES){if(validName.equals(lowercaseName)){this.name = name;return;}}thrownewIllegalArgumentException("Invalid drink name. Must be one of: "+String.join(", ",VALID_NAMES));}Offerto da
Il codice sopra implementa la validazione del nome per gli elementi del menu di un bar. Vediamolo nel dettaglio:
1. Per prima cosa, definisce un array privato statico e finale VALID_NAMES che contiene gli unici nomi di bevande consentiti: latte, cappuccino, espresso, americano e mocha. Questo array è:
private: accessibile solo all’interno della classe
static: condiviso tra tutte le istanze
final: non può essere modificato dopo l’inizializzazione
2. Dichiara un campo privato String name per memorizzare il nome della bevanda
3. Il metodo setName() implementa la logica di validazione:
Accetta un parametro String name
Lo converte in minuscolo per rendere il confronto case-insensitive
Itera sull’array VALID_NAMES
Se trova una corrispondenza, imposta il nome e ritorna
Se non trova corrispondenze, lancia un’IllegalArgumentException con un messaggio descrittivo che elenca tutte le opzioni valide
Ecco la classe completa finora:
publicclassMenuItem{privateString name;privatedouble price;privatestaticfinalString[]VALID_NAMES={"latte","cappuccino","espresso","americano","mocha"};privatestaticfinaldoubleMAX_PRICE=100.0;publicMenuItem(String name,double price){setName(name);setPrice(price);}publicvoidsetName(String name){String lowercaseName = name.toLowerCase();for(String validName :VALID_NAMES){if(validName.equals(lowercaseName)){this.name = name;return;}}thrownewIllegalArgumentException("Invalid drink name. Must be one of: "+String.join(", ",VALID_NAMES));}publicvoidsetPrice(double price){if(price <0){thrownewIllegalArgumentException("Price cannot be negative");}if(price >MAX_PRICE){thrownewIllegalArgumentException("Price cannot exceed $"+MAX_PRICE);}this.price = price;}}Offerto da
Dopo aver protetto il modo in cui gli attributi vengono creati, vogliamo anche proteggere come vengono accessi. Questo si fa usando i metodi getter:
publicclassMenuItem{// Rest of the code here...publicStringgetName(){return name;}publicdoublegetPrice(){return price;}}Offerto da
I metodi getter forniscono accesso controllato agli attributi privati di una classe. Risolvono il problema dell’accesso diretto agli attributi che può portare a modifiche indesiderate e rompere l’incapsulamento.
Per esempio, senza getter, potremmo accedere agli attributi direttamente:
MenuItem item =newMenuItem("Latte",4.99);String name = item.name;// Direct access to attribute
item.name ="INVALID";// Can modify directly, bypassing validationOfferto da
Con i getter, imponiamo un accesso corretto:
MenuItem item =newMenuItem("Latte",4.99);String name = item.getName();// Controlled access through getter// item.name = "INVALID"; // Not allowed - must use setName() which validatesOfferto da
Questo incapsulamento:
Protegge l’integrità dei dati prevenendo modifiche non valide
Ci consente di cambiare l’implementazione interna senza influenzare il codice che usa la classe
Fornisce un unico punto di accesso che può includere logica aggiuntiva se necessario
Rende il codice più manutenibile e meno soggetto a bug
Ereditarietà
La nostra classe inizia a sembrare valida, ma presenta diversi problemi. Per esempio, per un ristorante grande che serve molti tipi di piatti e bevande, la classe non è abbastanza flessibile.
Se vogliamo aggiungere diversi tipi di alimenti, incontreremo varie sfide. Alcuni piatti possono essere preparati per l’asporto, mentre altri richiedono consumo immediato. Gli elementi del menu possono avere prezzi e sconti diversi. I piatti possono necessitare di monitoraggio della temperatura o conservazione speciale. Le bevande possono essere calde o fredde con ingredienti personalizzabili. Gli elementi possono richiedere informazioni sugli allergeni e opzioni di porzione. L’attuale sistema non gestisce questi requisiti variabili.
L’ereditarietà offre una soluzione elegante a tutti questi problemi. Ci permette di creare versioni specializzate degli elementi del menu definendo una classe base MenuItem con attributi comuni e poi creando classi figlie che ereditano queste basi aggiungendo funzionalità uniche.
Per esempio, potremmo avere una classe Drink per le bevande con opzioni di temperatura, una classe Food per elementi che richiedono consumo immediato e una classe Dessert per elementi con esigenze di conservazione speciale — tutte ereditano la funzionalità core degli elementi del menu.
Per definire una classe figlia che eredita da una classe genitore, usiamo la parola chiave extends dopo il nome della classe figlia seguita dalla classe genitore. Dopo la definizione della classe, definiamo i nuovi attributi che questa figlia possiede e implementiamo il suo costruttore.
Ma nota come dobbiamo ripetere l’inizializzazione di name e price insieme a isCold. Non è l’ideale perché la classe genitore potrebbe avere centinaia di attributi. Inoltre, il codice sopra genererà un errore in compilazione perché non è il modo corretto di inizializzare gli attributi della classe genitore. Il modo corretto è usare la parola chiave super:
La parola chiave super viene usata per chiamare il costruttore della classe genitore. In questo caso, super(name, price) chiama il costruttore di MenuItem per inizializzare quegli attributi, evitando duplicazione di codice. Dobbiamo solo inizializzare il nuovo attributo isCold specifico della classe Drink.
La parola chiave è molto flessibile perché puoi usarla per riferirti alla classe genitore in qualsiasi parte della classe figlia. Per esempio, per chiamare un metodo del genitore, usi super.methodName() mentre super.attributeName è per gli attributi.
Override dei metodi
Ora, supponiamo di voler aggiungere un nuovo metodo alle nostre classi per calcolare il prezzo totale dopo le tasse. Poiché diversi elementi del menu possono avere aliquote fiscali differenti (ad esempio, cibo preparato vs. bevande confezionate), possiamo usare l’override dei metodi per implementare calcoli fiscali specifici in ogni classe figlia mantenendo un nome di metodo comune nella classe genitore.
Ecco come si presenta:
publicclassMenuItem{// Rest of the MenuItem classpublicdoublecalculateTotalPrice(){// Default tax rate of 10%return price *1.10;}}publicclassFoodextendsMenuItem{privateboolean isVegetarian;publicFood(String name,double price,boolean isVegetarian){super(name, price);this.isVegetarian = isVegetarian;}@OverridepublicdoublecalculateTotalPrice(){// Food has 15% taxreturnsuper.getPrice()*1.15;}}publicclassDrinkextendsMenuItem{privateboolean isCold;publicDrink(String name,double price,boolean isCold){super(name, price);this.isCold = isCold;}@OverridepublicdoublecalculateTotalPrice(){// Drinks have 8% taxreturnsuper.getPrice()*1.08;}}Offerto da
In questo esempio, l’override dei metodi consente a ciascuna sottoclasse di fornire la propria implementazione di calculateTotalPrice():
La classe base MenuItem definisce un calcolo fiscale predefinito del 10%.
Quando le classi Food e Drink estendono MenuItem, sovrascrivono questo metodo per implementare le proprie aliquote fiscali:
Gli alimenti hanno un’aliquota più alta del 15%
Le bevande hanno un’aliquota più bassa dell’8%
L’annotazione @Override è usata per indicare esplicitamente che questi metodi stanno sovrascrivendo il metodo della classe genitore. Questo aiuta a intercettare errori se la firma del metodo non corrisponde a quella della classe genitore.
Ogni sottoclasse può comunque accedere al prezzo della classe genitore usando super.getPrice(), dimostrando come i metodi sovrascritti possano utilizzare la funzionalità della classe genitore aggiungendo il proprio comportamento.
In breve, l’override dei metodi è una parte integrante dell’ereditarietà che consente alle sottoclassi di fornire la propria implementazione dei metodi definiti nella classe genitore, abilitando comportamenti più specifici mantenendo la stessa firma del metodo.
Classi astratte
La nostra gerarchia di MenuItem funziona, ma c’è un problema: chiunque dovrebbe poter creare un semplice oggetto MenuItem? Dopotutto, nel nostro ristorante ogni elemento del menu è o un Food o un Drink — non esiste un “elemento di menu generico”.
Possiamo evitarlo rendendo MenuItem una classe astratta. Una classe astratta fornisce solo un blueprint di base — può essere usata solo come classe genitore per l’ereditarietà, non istanziata direttamente.
Per rendere MenuItem astratta, aggiungiamo la parola chiave abstract dopo il suo modificatore di accesso:
publicabstractclassMenuItem{privateString name;privatedouble price;publicMenuItem(String name,double price){setName(name);setPrice(price);}// Existing getters/setters remain the same// Make this method abstract - every subclass MUST implement itpublicabstractdoublecalculateTotalPrice();}Offerto da
Le classi astratte possono anche avere metodi astratti come calculateTotalPrice() qui sopra. Questi metodi astratti fungono da contratti che obbligano le sottoclassi a fornire le proprie implementazioni. In altre parole, qualsiasi metodo astratto in una classe astratta deve essere implementato dalle classi figlie.
Quindi, riscriviamo Food e Drink tenendo conto di queste modifiche:
Attraverso questa implementazione del sistema di menu, abbiamo visto come astrazione ed ereditarietà lavorino insieme per creare codice flessibile e manutenibile che può adattarsi facilmente a diversi requisiti di business.
Conclusione
Oggi abbiamo dato uno sguardo a ciò di cui Java è capace come linguaggio orientato agli oggetti. Abbiamo coperto le basi di classi, oggetti e alcuni pilastri chiave dell’OOP: incapsulamento, ereditarietà e astrazione tramite un sistema di menu per ristorante.
Per rendere questo sistema pronto per la produzione, ci sono ancora molte cose da imparare, come le interfacce (parte dell’astrazione), il polimorfismo e i design pattern OOP. Per approfondire questi concetti, consulta il nostro corsoIntroduction to OOP in Java.
Quali sono i prerequisiti per seguire questo tutorial OOP in Java?⌄
Dovresti avere conoscenze di base di programmazione in Java, inclusi tipi di dato, variabili, flusso di controllo (if/else, loop) e sintassi di base. Il nostro corso Introduction to Java copre questi fondamentali se ti serve un ripasso.
Perché usare un sistema di menu per ristorante per spiegare i concetti OOP?⌄
Un sistema di menu per ristorante è un esempio intuitivo che dimostra applicazioni reali dei principi OOP. Mostra naturalmente l’ereditarietà (diversi tipi di elementi del menu), l’incapsulamento (protezione dei valori di prezzo e nome) e le interfacce (programmi fedeltà, controllo della temperatura). La maggior parte delle persone capisce come funziona un ristorante, rendendo i concetti più facili da afferrare.
Qual è la differenza tra classi astratte e interfacce in Java?⌄
Una classe può estendere una sola classe astratta ma implementare più interfacce.
Le classi astratte possono avere campi e costruttori; le interfacce no.
Le classi astratte possono avere sia metodi astratti sia concreti; le interfacce tradizionalmente hanno solo metodi astratti (prima di Java 8).
Le classi astratte forniscono un’implementazione di base, mentre le interfacce definiscono un contratto.
Sono un creator di contenuti sulla data science con oltre 2 anni di esperienza e uno dei più seguiti su Medium. Mi piace scrivere articoli dettagliati su AI e ML con un pizzico di sarcasmo, perché bisogna pur fare qualcosa per renderli un po’ meno noiosi. Ho pubblicato oltre 130 articoli e un corso su DataCamp, con un altro in arrivo. I miei contenuti sono stati visti da oltre 5 milioni di persone, 20.000 delle quali sono diventate follower sia su Medium che su LinkedIn.
Scopri i 15 migliori server MCP remoti che stanno trasformando lo sviluppo AI nel 2026. Scopri come migliorano automazione, ragionamento, sicurezza e velocità dei workflow.
Java è costantemente tra le tre lingue più popolari al mondo. La sua adozione in ambiti come lo sviluppo software enterprise, le app Android e le applicazioni web su larga scala è impareggiabile. Il suo solido sistema di tipi, l’ecosistema esteso e la capacità di “write once, run anywhere” la rendono particolarmente allettante per costruire sistemi robusti e scalabili. In questo articolo esploreremo come le funzionalità di programmazione orientata agli oggetti di Java permettano agli sviluppatori di sfruttare efficacemente queste capacità, consentendo loro di creare applicazioni manutenibili e scalabili grazie a una corretta organizzazione e riuso del codice.
Una nota sull’organizzazione ed esecuzione del codice Java
Prima di iniziare a scrivere codice, facciamo un po’ di setup.
Come per la sua sintassi, Java ha regole rigide sull’organizzazione del codice.
Per prima cosa, ogni classe pubblica deve stare nel proprio file, chiamato esattamente come la classe ma con estensione
.java. Quindi, se voglio scrivere una classe Laptop, il nome del file deve essereLaptop.java—sensibile alle maiuscole/minuscole. Puoi avere classi non pubbliche nello stesso file, ma è meglio separarle. So che stiamo andando avanti—parlando di come organizzare le classi prima ancora di scriverle—ma avere un’idea di massima su dove mettere le cose in anticipo è utile.Tutti i progetti Java devono avere un file
Main.javacon la classe Main. Qui è dove testi le tue classi creando oggetti a partire da esse.Per eseguire il codice Java, useremo IntelliJ IDEA, un IDE Java molto diffuso. Dopo aver installato IntelliJ:
Main.javae incolla il seguente contenuto:Quando parliamo di classi, scriviamo il codice in file diversi da
Main.java. Ma se parliamo di creare e testare oggetti, torniamo aMain.java.Per eseguire il programma, puoi cliccare sul pulsante verde di play accanto al metodo main:
L’output verrà mostrato nella finestra Run in basso.
Se sei completamente nuovo a Java, dai un’occhiata al nostro corso Introduction to Java, che copre i fondamenti dei tipi di dato e del flusso di controllo in Java prima di proseguire.
Altrimenti, tuffiamoci subito.
Classi e oggetti in Java
Quindi, cosa sono esattamente le classi?
Le classi sono costrutti di programmazione in Java per rappresentare concetti del mondo reale. Per esempio, considera questa classe MenuItem (crea un file per scrivere questa classe nel tuo IDE):
La classe ci fornisce un progetto o un modello per rappresentare vari elementi del menu di un ristorante. Modificando i due attributi della classe,
name, eprice, possiamo creare innumerevoli oggetti menu come un burger o un’insalata.Quindi, per creare una classe in Java, inizi con una riga che descrive il livello di accesso della classe (
private,publicoprotected) seguito dal nome della classe. Subito dopo le parentesi, definisci gli attributi della tua classe.Ma come creiamo oggetti che appartengono a questa classe? Java lo consente tramite i metodi costruttori:
Un costruttore è un metodo speciale che viene chiamato quando creiamo un nuovo oggetto da una classe. Inizializza gli attributi dell’oggetto con i valori che forniamo. Nell’esempio sopra, il costruttore accetta i parametri name e price e li assegna ai campi dell’oggetto usando la parola chiave ‘this’ per riferirsi a una futura istanza dell’oggetto.
La sintassi del costruttore è diversa dagli altri metodi della classe perché non richiede di specificare un tipo di ritorno. Inoltre, il costruttore deve avere lo stesso nome della classe e dovrebbe avere lo stesso numero di attributi dichiarati dopo la definizione della classe. Sopra, il costruttore sta creando due attributi perché ne abbiamo dichiarati due dopo la definizione della classe:
nameeprice.Dopo aver scritto la classe e il suo costruttore, puoi crearne istanze (oggetti) nel tuo metodo main:
Output:
Qui sopra stiamo creando due oggetti
MenuItemnelle variabiliburgeresalad. Come richiesto in Java, il tipo della variabile deve essere dichiarato, ovveroMenuItem. Poi, per creare un’istanza della nostra classe, scriviamo la parola chiave new seguita dall’invocazione del costruttore.Oltre al costruttore, puoi creare metodi normali che danno comportamento alla tua classe. Per esempio, sotto aggiungiamo un metodo per calcolare il prezzo totale dopo le tasse:
Ora possiamo calcolare il prezzo totale, tasse incluse:
Output:
Incapsulamento
Lo scopo delle classi è fornire un modello per creare oggetti. Questi oggetti verranno poi utilizzati da altri script o programmi. Per esempio, i nostri oggetti
MenuItempossono essere usati da un’interfaccia utente che ne mostra nome, prezzo e immagine su uno schermo.Per questo motivo, dobbiamo progettare le nostre classi in modo che le loro istanze possano essere usate solo come previsto. Al momento, la nostra classe
MenuItemè molto basilare e soggetta a errori. Qualcuno potrebbe creare oggetti con attributi assurdi, come una apple pie a prezzo negativo o un sandwich da un milione di dollari:Quindi, la prima cosa da fare dopo aver scritto una classe è proteggere i suoi attributi limitando come vengono creati e accessi. Per iniziare, vogliamo consentire solo valori positivi per price e impostare un valore massimo per evitare di mostrare per errore elementi ridicolmente costosi.
Java ci permette di farlo usando i metodi setter:
Vediamo cosa c’è di nuovo nel blocco di codice sopra:
1. Abbiamo reso gli attributi privati aggiungendo la parola chiave
private. Questo significa che possono essere accessibili solo all’interno della classe MenuItem. L’incapsulamento parte da questo passaggio cruciale.2. Abbiamo aggiunto una nuova costante
MAX_PRICEche è:3. Abbiamo aggiunto un metodo
setPrice()che:4. Abbiamo modificato il costruttore per usare
setPrice()invece di assegnare direttamente il prezzo. Questo assicura che la validazione del prezzo avvenga durante la creazione dell’oggetto.Abbiamo appena implementato uno dei pilastri fondamentali del buon design orientato agli oggetti — l’incapsulamento. Questo paradigma impone la protezione dei dati e un accesso controllato agli attributi dell’oggetto, garantendo che i dettagli di implementazione interni siano protetti da interferenze esterne e possano essere modificati solo tramite interfacce ben definite.
Rendiamo il concetto ancora più chiaro applicando l’incapsulamento all’attributo name. Immagina un bar che serve solo latte, cappuccino, espresso, americano e mocha.
Quindi, i nomi dei nostri elementi del menu possono essere solo uno tra quelli in questa lista. Ecco come possiamo imporlo nel codice:
Il codice sopra implementa la validazione del nome per gli elementi del menu di un bar. Vediamolo nel dettaglio:
1. Per prima cosa, definisce un array privato statico e finale
VALID_NAMESche contiene gli unici nomi di bevande consentiti: latte, cappuccino, espresso, americano e mocha. Questo array è:2. Dichiara un campo privato String name per memorizzare il nome della bevanda
3. Il metodo
setName()implementa la logica di validazione:VALID_NAMESEcco la classe completa finora:
Dopo aver protetto il modo in cui gli attributi vengono creati, vogliamo anche proteggere come vengono accessi. Questo si fa usando i metodi getter:
I metodi getter forniscono accesso controllato agli attributi privati di una classe. Risolvono il problema dell’accesso diretto agli attributi che può portare a modifiche indesiderate e rompere l’incapsulamento.
Per esempio, senza getter, potremmo accedere agli attributi direttamente:
Con i getter, imponiamo un accesso corretto:
Questo incapsulamento:
Ereditarietà
La nostra classe inizia a sembrare valida, ma presenta diversi problemi. Per esempio, per un ristorante grande che serve molti tipi di piatti e bevande, la classe non è abbastanza flessibile.
Se vogliamo aggiungere diversi tipi di alimenti, incontreremo varie sfide. Alcuni piatti possono essere preparati per l’asporto, mentre altri richiedono consumo immediato. Gli elementi del menu possono avere prezzi e sconti diversi. I piatti possono necessitare di monitoraggio della temperatura o conservazione speciale. Le bevande possono essere calde o fredde con ingredienti personalizzabili. Gli elementi possono richiedere informazioni sugli allergeni e opzioni di porzione. L’attuale sistema non gestisce questi requisiti variabili.
L’ereditarietà offre una soluzione elegante a tutti questi problemi. Ci permette di creare versioni specializzate degli elementi del menu definendo una classe base
MenuItemcon attributi comuni e poi creando classi figlie che ereditano queste basi aggiungendo funzionalità uniche.Per esempio, potremmo avere una classe
Drinkper le bevande con opzioni di temperatura, una classeFoodper elementi che richiedono consumo immediato e una classeDessertper elementi con esigenze di conservazione speciale — tutte ereditano la funzionalità core degli elementi del menu.Estendere le classi
Implementiamo queste idee partendo da
Drink:Per definire una classe figlia che eredita da una classe genitore, usiamo la parola chiave
extendsdopo il nome della classe figlia seguita dalla classe genitore. Dopo la definizione della classe, definiamo i nuovi attributi che questa figlia possiede e implementiamo il suo costruttore.Ma nota come dobbiamo ripetere l’inizializzazione di
nameepriceinsieme aisCold. Non è l’ideale perché la classe genitore potrebbe avere centinaia di attributi. Inoltre, il codice sopra genererà un errore in compilazione perché non è il modo corretto di inizializzare gli attributi della classe genitore. Il modo corretto è usare la parola chiavesuper:La parola chiave
superviene usata per chiamare il costruttore della classe genitore. In questo caso,super(name, price)chiama il costruttore diMenuItemper inizializzare quegli attributi, evitando duplicazione di codice. Dobbiamo solo inizializzare il nuovo attributoisColdspecifico della classeDrink.La parola chiave è molto flessibile perché puoi usarla per riferirti alla classe genitore in qualsiasi parte della classe figlia. Per esempio, per chiamare un metodo del genitore, usi
super.methodName()mentresuper.attributeNameè per gli attributi.Override dei metodi
Ora, supponiamo di voler aggiungere un nuovo metodo alle nostre classi per calcolare il prezzo totale dopo le tasse. Poiché diversi elementi del menu possono avere aliquote fiscali differenti (ad esempio, cibo preparato vs. bevande confezionate), possiamo usare l’override dei metodi per implementare calcoli fiscali specifici in ogni classe figlia mantenendo un nome di metodo comune nella classe genitore.
Ecco come si presenta:
In questo esempio, l’override dei metodi consente a ciascuna sottoclasse di fornire la propria implementazione di
calculateTotalPrice():La classe base
MenuItemdefinisce un calcolo fiscale predefinito del 10%.Quando le classi
FoodeDrinkestendonoMenuItem, sovrascrivono questo metodo per implementare le proprie aliquote fiscali:L’annotazione
@Overrideè usata per indicare esplicitamente che questi metodi stanno sovrascrivendo il metodo della classe genitore. Questo aiuta a intercettare errori se la firma del metodo non corrisponde a quella della classe genitore.Ogni sottoclasse può comunque accedere al prezzo della classe genitore usando
super.getPrice(), dimostrando come i metodi sovrascritti possano utilizzare la funzionalità della classe genitore aggiungendo il proprio comportamento.In breve, l’override dei metodi è una parte integrante dell’ereditarietà che consente alle sottoclassi di fornire la propria implementazione dei metodi definiti nella classe genitore, abilitando comportamenti più specifici mantenendo la stessa firma del metodo.
Classi astratte
La nostra gerarchia di
MenuItemfunziona, ma c’è un problema: chiunque dovrebbe poter creare un semplice oggettoMenuItem? Dopotutto, nel nostro ristorante ogni elemento del menu è o un Food o un Drink — non esiste un “elemento di menu generico”.Possiamo evitarlo rendendo
MenuItemuna classe astratta. Una classe astratta fornisce solo un blueprint di base — può essere usata solo come classe genitore per l’ereditarietà, non istanziata direttamente.Per rendere
MenuItemastratta, aggiungiamo la parola chiaveabstractdopo il suo modificatore di accesso:Le classi astratte possono anche avere metodi astratti come
calculateTotalPrice()qui sopra. Questi metodi astratti fungono da contratti che obbligano le sottoclassi a fornire le proprie implementazioni. In altre parole, qualsiasi metodo astratto in una classe astratta deve essere implementato dalle classi figlie.Quindi, riscriviamo
FoodeDrinktenendo conto di queste modifiche:Attraverso questa implementazione del sistema di menu, abbiamo visto come astrazione ed ereditarietà lavorino insieme per creare codice flessibile e manutenibile che può adattarsi facilmente a diversi requisiti di business.
Conclusione
Oggi abbiamo dato uno sguardo a ciò di cui Java è capace come linguaggio orientato agli oggetti. Abbiamo coperto le basi di classi, oggetti e alcuni pilastri chiave dell’OOP: incapsulamento, ereditarietà e astrazione tramite un sistema di menu per ristorante.
Per rendere questo sistema pronto per la produzione, ci sono ancora molte cose da imparare, come le interfacce (parte dell’astrazione), il polimorfismo e i design pattern OOP. Per approfondire questi concetti, consulta il nostro corso Introduction to OOP in Java.
Se vuoi mettere alla prova le tue conoscenze di Java, prova a rispondere ad alcune delle domande presenti nel nostro articolo sulle domande di colloquio Java.