"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 NO NO NO
Pacchetto (nessun modificatore) NO NO
Protetto protetto NO
Pubblico pubblico

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:

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

Codice
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:

Codice
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
class Cat
{
 protected String getName()
 {
  return "Oscar";
 }
}
class Tiger extends Cat
{
 public String getName()
 {
  return "Oscar Tiggerman";
 }
}
Abbiamo ampliato la visibilità del metodo da protecteda public.
Codice Perché questo è «legale»
public static void main(String[] args)
{
 Cat cat = new Cat();
 cat.getName();
}
È tutto fantastico. Qui non sappiamo nemmeno che la visibilità è stata estesa in una classe discendente.
public static void main(String[] args)
{
 Tiger tiger = new Tiger();
 tiger.getName();
}
Qui chiamiamo il metodo la cui visibilità è stata estesa.

Se ciò non fosse possibile, potremmo sempre dichiarare un metodo in Tiger:
public String getPublicName()
{
super.getName(); //chiama il metodo protetto
}

In altre parole, non stiamo parlando di alcuna violazione della sicurezza.

public static void main(String[] args)
{
 Cat catTiger = new Tiger();
 catTiger.getName();
}
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
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Tiger getMyParent()
 {
  return (Tiger) this.parent;
 }
}
Abbiamo sovrascritto il metodo getMyParente ora restituisce un Tigeroggetto.
Codice Perché questo è «legale»
public static void main(String[] args)
{
 Cat parent = new Cat();

 Cat me = new Cat();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
È 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».

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Tiger me = new Tiger();
 me.setMyParent(parent);
 Tiger myParent = me.getMyParent();
}
Qui chiamiamo il metodo il cui tipo restituito è stato ristretto.

Se ciò non fosse possibile, potremmo sempre dichiarare un metodo in Tiger:
public Tiger getMyTigerParent()
{
return (Tiger) this.parent;
}

In altre parole, non ci sono violazioni della sicurezza e/o violazioni del casting di tipo.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
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
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Object getMyParent()
 {
  if (this.parent != null)
   return this.parent;
  else
   return "I'm an orphan";
 }
}
Abbiamo sovraccaricato il metodo getMyParent e ristretto il tipo del suo valore restituito.

Va tutto bene qui.

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 Cat myParent = me.getMyParent();
}
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."