"Ti parlerò dei « modificatori di accesso ». Ne ho già parlato una volta, ma la ripetizione è un pilastro dell'apprendimento."
Puoi controllare l'accesso (visibilità) che altre classi hanno ai metodi e alle variabili della tua classe. Un modificatore di accesso risponde alla domanda «Chi può accedere a questo metodo/variabile?». È possibile specificare un solo modificatore per ogni metodo o variabile.
1) modificatore « public ».
È possibile accedere a una variabile, metodo o classe contrassegnata con il modificatore public da qualsiasi punto del programma. Questo è il più alto grado di apertura: non ci sono restrizioni.
2) Modificatore « privato ».
È possibile accedere a una variabile, metodo o classe contrassegnata con il modificatore private solo nella classe in cui è dichiarata. Il metodo o la variabile contrassegnati è nascosto da tutte le altre classi. Questo è il massimo grado di privacy: accessibile solo dalla tua classe. Tali metodi non vengono ereditati e non possono essere sovrascritti. Inoltre, non è possibile accedervi in una classe discendente.
3) « Modificatore predefinito ».
Se una variabile o un metodo non è contrassegnato con alcun modificatore, viene considerato contrassegnato con il modificatore "predefinito". Le variabili ei metodi con questo modificatore sono visibili a tutte le classi nel pacchetto in cui sono dichiarati e solo a quelle classi. Questo modificatore è chiamato anche accesso " pacchetto " o " pacchetto privato ", alludendo al fatto che l'accesso a variabili e metodi è aperto all'intero pacchetto che contiene la classe.
4) modificatore « protetto ».
Questo livello di accesso è leggermente più ampio del pacchetto . È possibile accedere a una variabile, metodo o classe contrassegnata con il modificatore protected dal relativo pacchetto (come "pacchetto") e da tutte le classi ereditate.
Questa tabella spiega tutto:
Tipo di visibilità | Parola chiave | Accesso | |||
---|---|---|---|---|---|
La tua classe | Il tuo pacco | Discendente | Tutte le classi | ||
Privato | privato | SÌ | NO | NO | NO |
Pacchetto | (nessun modificatore) | SÌ | SÌ | NO | NO |
Protetto | protetto | SÌ | SÌ | SÌ | NO |
Pubblico | pubblico | SÌ | SÌ | SÌ | SÌ |
C'è un modo per ricordare facilmente questa tabella. Immagina di scrivere un testamento. Stai dividendo tutte le tue cose in quattro categorie. Chi può usare le tue cose?
Chi ha accesso | Modificatore | Esempio |
---|---|---|
Solo io | privato | Diario personale |
Famiglia | (nessun modificatore) | Foto di famiglia |
Famiglia ed eredi | protetto | Immobile di famiglia |
Tutti | pubblico | Memorie |
"È un po' come immaginare che le classi dello stesso pacchetto facciano parte di un'unica famiglia."
"Voglio anche dirti alcune sfumature interessanti sui metodi di override."
1) Implementazione implicita di un metodo astratto.
Diciamo che hai il seguente codice:
class Cat
{
public String getName()
{
return "Oscar";
}
}
E hai deciso di creare una classe Tiger che erediti questa classe e aggiungere un'interfaccia alla nuova classe
class Cat
{
public String getName()
{
return "Oscar";
}
}
interface HasName
{
String getName();
int getWeight();
}
class Tiger extends Cat implements HasName
{
public int getWeight()
{
return 115;
}
}
Se implementi solo tutti i metodi mancanti che IntelliJ IDEA ti dice di implementare, in seguito potresti finire per dedicare molto tempo alla ricerca di un bug.
Si scopre che la classe Tiger ha un metodo getName ereditato da Cat, che verrà preso come implementazione del metodo getName per l'interfaccia HasName.
"Non ci vedo niente di terribile in questo."
"Non è poi così male, è un posto probabile in cui gli errori si insinuano."
Ma può essere anche peggio:
interface HasWeight
{
int getValue();
}
interface HasSize
{
int getValue();
}
class Tiger extends Cat implements HasWeight, HasSize
{
public int getValue()
{
return 115;
}
}
Si scopre che non puoi sempre ereditare da più interfacce. Più precisamente, puoi ereditarli, ma non puoi implementarli correttamente. Guarda l'esempio. Entrambe le interfacce richiedono l'implementazione del metodo getValue(), ma non è chiaro cosa dovrebbe restituire: il peso o la dimensione? Questo è abbastanza spiacevole da affrontare.
"Sono d'accordo. Vuoi implementare un metodo, ma non puoi. Hai già ereditato un metodo con lo stesso nome dalla classe base. È rotto."
"Ma ci sono buone notizie."
2) Espansione della visibilità. Quando erediti un tipo, puoi espandere la visibilità di un metodo. Ecco come appare:
codice java | Descrizione |
---|---|
|
|
|
Abbiamo ampliato la visibilità del metodo da protected a public . |
Codice | Perché questo è «legale» |
---|---|
|
È tutto fantastico. Qui non sappiamo nemmeno che la visibilità è stata estesa in una classe discendente. |
|
Qui chiamiamo il metodo la cui visibilità è stata estesa.
Se ciò non fosse possibile, potremmo sempre dichiarare un metodo in Tiger: In altre parole, non stiamo parlando di alcuna violazione della sicurezza. |
|
Se tutte le condizioni necessarie per chiamare un metodo in una classe base ( Cat ) sono soddisfatte, allora sono certamente soddisfatte per chiamare il metodo sul tipo discendente ( Tiger ) . Perché le restrizioni sulla chiamata al metodo erano deboli, non forti. |
"Non sono sicuro di aver capito completamente, ma ricorderò che questo è possibile."
3) Restringere il tipo restituito.
In un metodo sottoposto a override, possiamo modificare il tipo restituito in un tipo di riferimento ristretto.
codice java | Descrizione |
---|---|
|
|
|
Abbiamo sovrascritto il metodo getMyParent e ora restituisce un Tiger oggetto. |
Codice | Perché questo è «legale» |
---|---|
|
È tutto fantastico. Qui non sappiamo nemmeno che il tipo restituito dal metodo getMyParent è stato ampliato nella classe discendente.
Come funzionava e funziona il «vecchio codice». |
|
Qui chiamiamo il metodo il cui tipo restituito è stato ristretto.
Se ciò non fosse possibile, potremmo sempre dichiarare un metodo in Tiger: In altre parole, non ci sono violazioni della sicurezza e/o violazioni del casting di tipo. |
|
E tutto funziona bene qui, anche se abbiamo ampliato il tipo di variabili alla classe base (Cat).
A causa dell'override, viene chiamato il metodo setMyParent corretto. E non c'è nulla di cui preoccuparsi quando si chiama il metodo getMyParent , perché il valore restituito, sebbene della classe Tiger, può comunque essere assegnato alla variabile myParent della classe base (Cat) senza problemi. Gli oggetti Tiger possono essere archiviati in modo sicuro sia nelle variabili Tiger che nelle variabili Cat. |
"Sì. Capito. Quando sovrascrivi i metodi, devi essere consapevole di come funziona tutto questo se passiamo i nostri oggetti al codice che può gestire solo la classe base e non sa nulla della nostra classe. "
"Esattamente! Allora la grande domanda è perché non possiamo restringere il tipo del valore restituito quando sovrascriviamo un metodo?"
"È ovvio che in questo caso il codice nella classe base smetterebbe di funzionare:"
codice java | Spiegazione del problema |
---|---|
|
|
|
Abbiamo sovraccaricato il metodo getMyParent e ristretto il tipo del suo valore restituito.
Va tutto bene qui. |
|
Quindi questo codice smetterà di funzionare.
Il metodo getMyParent può restituire qualsiasi istanza di un oggetto, perché in realtà viene chiamato su un oggetto Tiger. E non abbiamo un assegno prima dell'incarico. Pertanto, è del tutto possibile che la variabile myParent di tipo Cat memorizzi un riferimento String. |
"Splendido esempio, Amigo!"
In Java, prima che venga chiamato un metodo, non viene verificato se l'oggetto ha tale metodo. Tutti i controlli si verificano in fase di esecuzione. E una chiamata [ipotetica] a un metodo mancante molto probabilmente farebbe sì che il programma tenti di eseguire un bytecode inesistente. Ciò alla fine porterebbe a un errore fatale e il sistema operativo chiuderebbe forzatamente il programma.
"Ehi. Adesso lo so."
GO TO FULL VERSION