08 - Incapsulamento e modificatori di accesso
Scopri come proteggere e controllare l'accesso ai dati nella tua classe.
L'incapsulamento consiste nel nascondere i dettagli interni di una classe e nel controllare le modalità di accesso e modifica dei suoi dati. Questo impedisce bug e rende il codice più mantenibile.
Il Problema senza Incapsulamento
public class Player {
public String name;
public int health;
public int maxHealth;
}
public class Main {
public static void main(String[] args) {
Player player = new Player();
player.health = 100;
player.maxHealth = 100;
// Oops! Qualcuno ha infranto le regole
player.health = 500; // Vita oltre il massimo!
player.health = -50; // Vita negativa!
player.name = ""; // Nome vuoto!
}
}Senza protezione, chiunque può impostare valori non validi!
Modificatori Di Accesso
Java ha keyword che controllano chi può accedere ai membri della classe:
| Modificatore | Classe | Pacchetto | Sottoclasse | Globale |
|---|---|---|---|---|
public | ✓ | ✓ | ✓ | ✓ |
protected | ✓ | ✓ | ✓ | ✗ |
| (nessuno) | ✓ | ✓ | ✗ | ✗ |
private | ✓ | ✗ | ✗ | ✗ |
Per ora, concentriamoci su:
public- Chiunque può accedereprivate- Solo questa classe può accedere
Rendere Proprietà Private
public class Player {
private String name;
private int health;
private int maxHealth;
public Player(String name, int maxHealth) {
this.name = name;
this.health = maxHealth;
this.maxHealth = maxHealth;
}
}Ora non puoi accedere direttamente alle proprietà:
Player player = new Player("Alice", 100);
player.health = 500; // ❌ Errore! health è privatoGetters e Setters
Per accedere alle proprietà private, crea metodi getter e setter:
public class Player {
private String name;
private int health;
private int maxHealth;
public Player(String name, int maxHealth) {
this.name = name;
this.health = maxHealth;
this.maxHealth = maxHealth;
}
// Getter - restituisce un valore
public int getHealth() {
return health;
}
// Setter - importa il valore e convalida
public void setHealth(int health) {
if (health < 0) {
this.health = 0;
} else if (health > maxHealth) {
this.health = maxHealth;
} else {
this.health = health;
}
}
public String getName() {
return name;
}
public int getMaxHealth() {
return maxHealth;
}
}Ora puoi interagire in modo sicuro con l'oggetto:
Player player = new Player("Alice", 100);
player.setHealth(150); // Automaticamente limitato a 100
System.out.println(player.getHealth()); // 100
player.setHealth(-20); // Automaticamente impostato a 0
System.out.println(player.getHealth()); // 0Seguire le convenzioni di nomenclatura di Java:
- Getter:
get+ nome della proprietà (in maiuscolo) - Setter:
set+ nome della proprietà (in maiuscolo) - Boolean:
is+ nome della proprietà (in maiuscolo)
private int health;
public int getHealth() { }
public void setHealth(int health) { }
private boolean alive;
public boolean isAlive() { }
public void setAlive(boolean alive) { }
private String name;
public String getName() { }
public void setName(String name) { }Benefici dell’incapsulamento
1. Convalida
public class Item {
private int durability;
private int maxDurability;
public void setDurability(int durability) {
if (durability < 0) {
this.durability = 0;
} else if (durability > maxDurability) {
this.durability = maxDurability;
} else {
this.durability = durability;
}
}
public boolean isBroken() {
return durability <= 0;
}
}2. Proprietà in Sola-Lettura
A volte non vuoi un setter:
public class Monster {
private String id; // Non dovrebbe mai cambiare
private int health;
public Monster(String id, int health) {
this.id = id;
this.health = health;
}
// Solo il Getter - nessun Setter!
public String getId() {
return id;
}
public int getHealth() {
return health;
}
public void setHealth(int health) {
this.health = health;
}
}3. Proprietà Calcolate
I Getter non devono restituire un membro della classe direttamente:
public class Player {
private int health;
private int maxHealth;
public int getHealth() {
return health;
}
// Proprietà calcolata
public double getHealthPercentage() {
return (health * 100.0) / maxHealth;
}
// Proprietà calcolata
public boolean isLowHealth() {
return getHealthPercentage() < 25;
}
}Esempi Pratici
Item con Durabilità
public class Tool {
private String name;
private int durability;
private int maxDurability;
private boolean broken;
public Tool(String name, int maxDurability) {
this.name = name;
this.durability = maxDurability;
this.maxDurability = maxDurability;
this.broken = false;
}
public void use() {
if (broken) {
System.out.println(name + " è rotto!");
return;
}
durability--;
System.out.println(name + " utilizzato. Durabilità: " + durability);
if (durability <= 0) {
broken = true;
System.out.println(name + " si è rotto!");
}
}
public void repair() {
durability = maxDurability;
broken = false;
System.out.println(name + " riparato!");
}
// Getters
public String getName() {
return name;
}
public int getDurability() {
return durability;
}
public boolean isBroken() {
return broken;
}
public double getDurabilityPercentage() {
return (durability * 100.0) / maxDurability;
}
}Esempio di un Conto Bancario
public class PlayerWallet {
private int gold;
private int silver;
public PlayerWallet() {
this.gold = 0;
this.silver = 0;
}
public void addGold(int amount) {
if (amount > 0) {
gold += amount;
System.out.println("Aggiunto " + amount + " di oro");
}
}
public boolean spendGold(int amount) {
if (amount > gold) {
System.out.println("Oro insufficiente!");
return false;
}
gold -= amount;
System.out.println("Hai speso " + amount + " di oro");
return true;
}
public int getGold() {
return gold;
}
public int getTotalValue() {
// 1 gold = 100 silver
return gold * 100 + silver;
}
}Sistema di Blocco Protetto
public class ProtectedBlock {
private int x, y, z;
private String type;
private String owner;
private boolean locked;
public ProtectedBlock(int x, int y, int z, String type, String owner) {
this.x = x;
this.y = y;
this.z = z;
this.type = type;
this.owner = owner;
this.locked = true;
}
public boolean canBreak(String playerName) {
if (!locked) {
return true;
}
return playerName.equals(owner);
}
public void unlock(String playerName) {
if (playerName.equals(owner)) {
locked = false;
System.out.println("Blocco sbloccato");
} else {
System.out.println("Non possiedi questo blocco!");
}
}
// solo Getters - posizione e proprietario non dovrebbero cambiare
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
public String getOwner() {
return owner;
}
public boolean isLocked() {
return locked;
}
}Quando utilizzare Private vs Public
Rendi privato per impostazione predefinita! Rendi pubbliche solamente le cose che sono necessarie da accedere dall'esterno.
Private:
- Dati interni (salute, posizione, inventario)
- Metodi Helper utilizzati solo all'interno della classe
- Tutto ciò che richiede convalida
Public:
- Metodi che definiscono il comportamento della classe
- Costruttore
- Metodi che altre classi devono chiamare
public class Example {
// Private - dati interni
private int internalCounter;
private String secretKey;
// Public - parte dell'interfaccia
public void doSomething() {
// Usa il metodo helper privato
validateData();
}
// Private - helper interno
private void validateData() {
// ...
}
}La keyword final
final significa che una variabile non può essere modificata dopo che è stata impostata:
public class Player {
private final String id; // Non modificabile dopo la creazione
private String name; // Modificabile
private int health; // Modificabile
public Player(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
// Nessun setId() - e' final!
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}La keyword static
Membri Statici
Una classe può definire due tipi di membri:
-
Membri dell'istanza — appartengono al singolo oggetto (ogni istanza ne ha la propria copia)
-
Membri statici — appartengono alla classe (una singola copia condivisa per l'intero tipo)
In parole povere: i membri dell' istanza appartengono agli oggetti; i membri statici appartengono alla classe stessa e sono condivisi da tutti gli oggetti di quel tipo.
Dichiarazione
/* (modificatore d'accesso) */ static ... memberName; Esempio
class Data {
public int x; // Membro istanziato
public static int y = 1000; // Membro statico
// Membro istanziato:
// può accedere sia a membri statici che non-statici
public void foo() {
x = 100; // OK - equivalente a this.x = 100;
y = 100; // OK - equivalente a Data.y = 200;
}
// Membro statico:
// non può accedere a membri non statici
public static void bar() {
x = 100; // Errore: la variable non statica x non può essere chiamata in un contesto statico
y = 100; // OK
}
}Accedere ai membri statici
Data data = new Data();
data.x = 1000; // OK
data.y = 1000; // più o meno OK - Non proprio suggerito, è meglio usare Data.y
Data.y = 1000; // OK - best practice
Data.x = 1000; // Errore: non è possibile accedere alle variabili d'istanza in un contesto staticoCampi statici
Uno campo statico o 'static field' rappresenta un dato di proprietà della classe piuttosto che dell'oggetto Uno campo statico o 'static field' rappresenta un dato di proprietà della classe piuttosto che dell'oggetto I campi statici sono anche memorizzati in una porzione di memoria specifica che viene condivisa tra tutte le istanze oggetto che vengono create. Uno campo statico o 'static field' rappresenta un dato di proprietà della classe piuttosto che dell'oggetto I campi statici sono anche memorizzati in una porzione di memoria specifica che viene condivisa tra tutte le istanze oggetto che vengono create.
Si dichiara come segue:
/* (modificatore di accesso) (facoltativo) */ static /* final/volatile (facoltativo) */ nomeDelCampo;Prendiamo lo stesso esempio di classe Data e aggiungiamo questo costruttore:
public Data() {
y++; // ricorda che questo equivale a: Data.y++;
}// Ogni instanza di Data avrà una copia privata (istanziata) del membro x
// Tuttavia punterà alla stessa posizione di memoria per il membro y
Data d1 = new Data(); // y = 1001
d1.x = 5;
Data d2 = new Data(); // y = 1002
d2.x = 25;
Data d3 = new Data(); // y = 1003
// ... e così viaMetodi statici
I metodi statici rappresentano essenzialmente una funzione di un certa classe
Dalla classe Data ricorda la funzione (metodo istantaneo) foo e (metodo statico) bar
Si può accedere a tali metodi tramite:
Data d1 = new Data();
d1.foo(); // Metodo instanziato: Accessibile SOLO dall'oggetto istanziato
Data.bar(); // Metodo statico: accessibile senza oggettoInizializzatore Statico
Usa un blocco static initializer per eseguire la logica d'inizializzazione quando la classe è caricata la prima volta:
class OtherData {
private static int a = 12;
private static int b;
private static String msg;
static {
msg = "Inizializzazione..."
System.out. println(msg);
b = 4;
// ... inizializzazione complessa che non può essere fatta in una singola espressione
}
}Esercizi Pratici
-
Crea una classe
BankAccount:- Proprietà private: accountNumber, balance
- Costruttore per impostare il numero di conto
- Metodi: deposit(), withdraw(), getBalance()
- Convalidazione: impossibile prelevare più del saldo
- Il numero di conto deve essere in sola lettura
-
Crea una classe
Door:- Proprietà private: isLocked, keyCode
- Costruttore per impostare il keyCode
- Metodi: lock(), unlock(String code), isLocked()
- unlock() funziona solo con il codice corretto
- Il codice dovrebbe essere privato (non esporlo!)
-
Crea una classe
PlayerStats:- Proprietà private: forza, difesa, velocità
- Costruttore per impostare tutte le statistiche
- Getter per tutte le statistiche
- Metodo: getPowerLevel() che restituisce forza + difesa + velocità
- Le statistiche non possono essere negative o superiori a 100
-
Esegui il refactoring di una classe: prendi una delle classi della lezione precedente e aggiungi un incapsulamento adeguato:
- Rendi private tutte le proprietà
- Aggiungi getter e setter appropriati
- Aggiungi convalida ove necessaria