EDIT: ho risposto a questa domanda perché ci sono un sacco di persone che imparano a programmare chiedendo questo, e la maggior parte delle risposte sono molto tecnicamente competenti, ma non sono così facili da capire se sei un principiante. Eravamo tutti principianti, quindi ho pensato di provare la mia risposta a una risposta più amichevole per i principianti.
I due principali sono il polimorfismo e la validazione. Anche se è solo una stupida struttura di dati.
Diciamo che abbiamo questa semplice classe:
public class Bottle {
public int amountOfWaterMl;
public int capacityMl;
}
Una classe molto semplice che contiene la quantità di liquido contenuta e la sua capacità (in millilitri).
Cosa succede quando lo faccio:
Bottle bot = new Bottle();
bot.amountOfWaterMl = 1500;
bot.capacityMl = 1000;
Bene, non ti aspetteresti che funzioni, giusto? Volete che ci sia una specie di controllo di sanità mentale. E peggio ancora, se non avessi mai specificato la capacità massima? Oh caro, abbiamo un problema.
Ma c'è anche un altro problema. E se le bottiglie fossero solo un tipo di contenitore? E se avessimo diversi contenitori, tutti con capacità e quantità di liquido riempito? Se potessimo solo creare un'interfaccia, potremmo permettere al resto del nostro programma di accettare quell'interfaccia, e bottiglie, taniche e ogni altra cosa funzionerebbero semplicemente in modo intercambiabile. Non sarebbe meglio? Poiché le interfacce richiedono metodi, anche questa è una buona cosa.
Finiremmo con qualcosa del tipo:
public interface LiquidContainer {
public int getAmountMl();
public void setAmountMl(int amountMl);
public int getCapacityMl();
}
Grande! E ora cambiamo solo Bottle in questo:
public class Bottle extends LiquidContainer {
private int capacityMl;
private int amountFilledMl;
public Bottle(int capacityMl, int amountFilledMl) {
this.capacityMl = capacityMl;
this.amountFilledMl = amountFilledMl;
checkNotOverFlow();
}
public int getAmountMl() {
return amountFilledMl;
}
public void setAmountMl(int amountMl) {
this.amountFilled = amountMl;
checkNotOverFlow();
}
public int getCapacityMl() {
return capacityMl;
}
private void checkNotOverFlow() {
if(amountOfWaterMl > capacityMl) {
throw new BottleOverflowException();
}
}
Lascerò la definizione di BottleOverflowException come esercizio per il lettore.
Ora nota quanto è più robusto. Ora possiamo gestire qualsiasi tipo di contenitore nel nostro codice accettando LiquidContainer anziché Bottle. E come queste bottiglie gestiscono questo tipo di cose possono differire. Puoi avere bottiglie che scrivono il loro stato su disco quando cambia, o bottiglie che salvano su database SQL o GNU sa cos'altro.
E tutti questi possono avere modi diversi di gestire varie whoopsie. Il Bottle controlla solo e se trabocca genera una RuntimeException. Ma potrebbe essere la cosa sbagliata da fare. (C'è una discussione utile da fare sulla gestione degli errori, ma lo sto tenendo molto semplice qui apposta. Le persone nei commenti indicheranno probabilmente i difetti di questo approccio semplicistico.;)
E sì, sembra che passiamo da un'idea molto semplice a ottenere rapidamente risposte molto migliori.
Si noti inoltre che non è possibile modificare la capacità di una bottiglia. Ora è incastonato nella pietra. Potresti farlo con un int dichiarandolo finale. Ma se questo fosse un elenco, potresti svuotarlo, aggiungere nuove cose e così via. Non puoi limitare l'accesso a toccare le viscere.
C'è anche la terza cosa che non tutti hanno affrontato: getter e setter usano le chiamate di metodo. Ciò significa che sembrano metodi normali ovunque. Invece di avere strana sintassi specifica per DTO e roba del genere, hai la stessa cosa ovunque.