Ripasso dei concetti chiave

In questo articolo andremo a ripassare quelli che sono i concetti chiave della certificazione OCA che sono fondamentali per lo studio ed il superamento dell’esame OCP.

Modificatori di accesso

Quali sono ? semplice : public, protected,private e default

Vediamo subito un pezzo di codice :

public static void main(String[] args) {
  BigCat cat = new BigCat();
  System.out.println(cat.name);
  System.out.println(cat.hasFur);
  System.out.println(cat.hasPaws);
  System.out.println(cat.id);
}

Supponiamo ora che tutte le seguenti classi possiedano un metodo main che proverà a stampare tutte e 4 le variabili, quali variabili è possibile accedere ?

package cat;
public class BigCat {
 public String name = "cat";
 protected boolean hasFur = true;
 boolean hasPaws = true;
 private int id;
}

package cat.species;
public class Lynx extends BigCat { }

package cat;
public class CatAdmirer { }

package mouse;
public class Mouse { }

Se ci fermiamo bene a ragionare, vediamo che l’unica classe che compila è BigCat, tutte le altre no, perchè ?

La linea che stampa cat.name è ok per tutte le classi perchè è definita come public, quindi accessibile a tutte le classi

La linea che stampa cat.id è ok solo per BigCat perchè definita private, quindi accessibile solo all’interno della classe che la dichiara.

La linea che stampa cat.hasPaws ha come accesso default, quindi visibile a tutte le classi all’interno dello stesso package, quindi BigCat e CatAdmirer

La linea che stampa cat.hasFur compila solo per BigCat e CatAdmirer, invece non compila per Lynx ! ma come ? protected non permette l’accesso ? si ma solo tramite ereditarità ! nel nostro esempio il main di Lynx accederebbe tramite variabile e non tramite ereditarità, sarebbe ok se all’interno del main fosse stato scritto : Lynx cat = new Lynx()

Rivediamo un sunto delle regole per i modificatori di accesso :

Puo accedere private default protected public
membri nella stessa classe si si si si
membri altra classe stesso package no si si si
membri in superclassi e package differenti no no si si
membri/metodi in altra classe (no superclasse) in package differenti no no no si

 

Overloading e Overriding

Ora ripassiamo la differenza tra overload e override. Riusciamo a evidenziare quali metodi sono un overload e quali sono un override nel seguente esempio ?

1: public class Bobcat {
2:   public void findDen() { }
3: }

1: public class BobcatKitten extends Bobcat {
2:   public void findDen() { }
3:   public void findDen(boolean b) { }
4:   public int findden() throws Exception { return 0; }
5: }

Il metodo alla linea 2 è un override corretto, poichè il metodo ha la stessa identica firma della superclasse

Il metodo alle linea 3 è un overload poichè ha lo stesso nome ma lista di parametri differenti

Il metodo alla linea 4 non è nulla, Java è case sensitive e quel metodo non è ne un overload ne un override.

Possiamo ricapitolare dicendo che le regole per un overload e override prevedano che il metodo abbia lo stesso nome, per l’override è richiesto che sia il nome del metodo che la lista dei parametri sia uguale, per l’overload deve cambiare solo la lista dei parametri.

Ricordiamo che in caso di overload multipli, Java invocherà quello con la lista parametri più compatibile e cercherà nel seguente ordine:

  • Esatto tipo di dati
  • Match con superclassi
  • Convertendo ad un primitivo più grande
  • Convertendo tramite autoboxed
  • Varargs

Per l’overriding ci sono da seguire le seguenti regole :

  • Il modificatore di accesso deve essere lo stesso o più permissivo
  • il tipo di ritorno deve essere lo stesso oppure un covariant return type (sottoclasse)
  • Se il metodo rilancia delle exception, il metodo overridato dovrà rilanciare la stessa exception o sottoclassi di essa

Sia per l’overload che per l’override i metodi non devono essere statici.

Classi Astratte

Andiamo ora a ripassare il concetto di classe astratta, vediamo se i concetto assimilati nella precedente certificazione sono chiari, ci sono 3 modi per permettere al seguente codice di compilare, quali sono ?

abstract class Cat {
  _____________________
}
class Lion extends Cat {
  void clean() {}
}

Il primo è sicuramente definire un metodo astratto come :

abstract void clean();

Il secondo, semplice, è definire il metodo come non astratto, poichè nella classe Lion si tratta di override corretto:

void clean() {}

Il terzo ? questo è più difficile.. sembra .. ma se ci pensiamo bene, basta non definire nulla, lasciamo vuoto e queste classi compileranno correttamente, perchè una classe astratta può contenere tranquillamente 0 metodi astratti.

Una classe astratta può contenere un numero indefinito di metodi astratti, anche zero, I metodi possono essere astratti o concreti, ricordiamo che la prima classe concreta deve implementare tutti i metodi astratti rimanenti (perchè alcuni potrebbero essere stati definiti nelle superclassi)

Static e Final

Andiamo a fare un breve ripasso dei modificatori static e final, prendiamo il seguente esempio, in quali linee di codice possiamo aggiungere i modificatori static o final senza incorrere in errori di compilazione ?

1: abstract class Cat {
2:   String name = "The Cat";
3:   void clean() { }
4: }
5: class Lion extends Cat {
6:   void clean() { }
7: }

Se aggiungiamo static e final alla linea 2 è corretto, questo farà si che potremmo accedere alla variabile name tramite il nome della classe Cat.name e impedirà di modificarne il valore.

Non possiamo aggiungere static alle linee 3 e 6 perchè il metodo è overridato, se lo facciamo dobbiamo farlo in entrambe le linee ma non sarebbe più un override.

Non possiamo aggiungere final alla linea 3 poichè impediremmo alla linea 6 di overridare il metodo

Riassumendo:

  • final previene alla variabile di riferimento di essere riassegnata, e ad un metodo o classe di essere overridato o estesa
  • static permette alla variabile di essere condivisa tra le varie istanze della classe e possono essere utilizzate tramite il nome della classe
  • final e static possono essere utilizzate anche nelle classi, final per una classe impedisce a questa di essere estesa, in un metodo impedisce che sia ridefinito, attenzione, non possiamo mai dichiarare un metodo come abstract e final ! ovvio no ?
  • static può essere utilizzato per una classe interna, lo vedremo più avanti.

instaceof

Questo forse è uno dei concetti nuovi, nella OCA abbiamo visto gli operatori ==, <, > etc etc, consideriamo anche instanceof come un operatore.

Questo operatore si applica a istanze di classe e restituisce un valore booleano

a instaceof B
  • restituisce true se a è di tipo B
  • restituisce true se a è una sottoclasse di B
  • restituisce true se a implementa l’interfaccia B

Vediamo un esempio pratico :

class HeavyAnimal { }
class Hippo extends HeavyAnimal { }
class Elephant extends HeavyAnimal { }

Date se seguenti istruzioni :

12: HeavyAnimal hippo = new Hippo();
13: boolean b1 = hippo instanceof Hippo; // true
14: boolean b2 = hippo instanceof HeavyAnimal; // true
15: boolean b3 = hippo instanceof Elephant; // false

13 restituisce true poichè hippo è di tipo Hippo
14 restituisce true poichè hippo è una sottoclasse di HeavyAnimal
15 restituisce false poichè hippo non ha nulla a che fare con Elephant !

Ricordiamo che in Java tutte le classe estendono Object, di fatto a instaceof Object restituirà sempre true, tranne che in un caso … null !!! se un riferimento è null, qualsiasi chiamata a instanceof restituirà false.

Ricordiamo inoltre che il compilatore Java è abbastanza intelligente da emettere un errore di compilazione in caso di utilizzo di instanceof su tipi scorrelati, ma attenzione, il compilatore controlla la congruenza solo se è chiamato su una classe, nel caso di interfacce il controllo verrà fatto a runtime, perchè il compilatore non può avere la certezza, vediamo un esempio di questo :

public interface Mother {}
class Hippo extends HeavyAnimal { }

E questo codice compila :

42: HeavyAnimal hippo = new Hippo();
43: boolean b6 = hippo instanceof Mother;

Il compilatore permette questa istruzione poichè più avanti  potresti aver definito :

class MotherHippo extends Hippo implements Mother { }

L’operatore instanceof è utilizzato tipicamente per verificare un tipo prima di applicare il cast esplicito:

public void feedAnimal(Animal animal) {
 if(animal instanceof Cow) {
   ((Cow)animal).addHay();
 } else if(animal instanceof Bird) {
   ((Bird)animal).addSeed();
 } else if(animal instanceof Lion) {
   ((Lion)animal).addMeat();
 } else {
   throw new RuntimeException("Unsupported animal");
 }
} 

In questo esempio, utilizziamo questo operatore per verificare se il nostro animale è effettivamente un istanza di una sottoclasse

Virtual Method invocation

Il virtual method invocation fa riferimento all’effettivo metodo invocato tramite il polimorfismo, vediamo un esempio pratico per chiarire il concetto :

abstract class Animal {
 public abstract void feed(); }
}
class Cow extends Animal {
 public void feed() { addHay(); }
 private void addHay() { }
}
class Bird extends Animal {
 public void feed() { addSeed(); }
 private void addSeed() { }
}
class Lion extends Animal {
 public void feed() { addMeat(); }
 private void addMeat() { }
}

Le classi concrete di Animal devono overridare il metodo feed(), vediamo che ogni tipo di animale mangia un differente dipo di alimento, se utilizziamo in un nostro metodo l’oggetto generico Animal, dovremmo essere in grado di invocare il metodo corretto anche tramite la superclasse :

public void feedAnimal(Animal a) { 
  animal.feed();
}

Quale metodo feed() sarà invocato ? semplice, Java invocherà il metodo feed() proprio dell’oggetto passato come riferimento di Animal, se ad esempio avessimo :

Animal a = new Lion();
feedAnimal(a);

verrebbe invocato feed() di Lion e non feed di Animal !

Questo funziona per i metodi, ma non per le variabili membro !

abstract class Animal {
 String name = "???";
 public void printName() {
 System.out.println(name);
 }
}
class Lion extends Animal {
 String name = "Leo";
}
public class PlayWithAnimal {
 public static void main(String... args) {
    Animal animal = new Lion();
    animal.printName();
  }
}

Questo codice stamperà ??? ! La variabile name definita in Lion verrà utilizzata solo se sarà referenziata all’interno di Lion, vediamo  un altro esempio per chiarire questo concetto :

abstract class Animal {
 public void careFor() {
   play();
 }
 public void play() {
   System.out.println("pet animal");
 } }
class Lion extends Animal {
 public void play() {
   System.out.println("toss in meat");
 } }
public class PlayWithAnimal {
 public static void main(String... args) {
 Animal animal = new Lion();
 animal.careFor();
 } }

Questo stamperà “toss in meat” ! perchè ? il metodo main crea un istanza di tipo Lion e invoca careFor, la superclasse Animal possiede il metodo careFor e verrà invocato, questo metodo al suo interno invoca play(), sia Animal che Lion possiedono play(), Java invocherà quello relativo al tipo della variabile, in questo caso Lion, quindi verrà invocato play() di Lion.

Annotare i metodi Overridden

Sai già come overridare un metodo, ma Java ti fornisce uno strumento comodo per indicare al compilatore (e ai tuoi colleghi) per specificare che questo metodo è un override, e lo si fa tramite l’annotation @Override

Questo specifica che tu stai per overridare un dato metodo, eccone un esempio :

1: class Bobcat {
2:   public void findDen() { }
3: }
4: class BobcatMother extends Bobcat {
5:   @Override
6:   public void findDen() { }
7: }

Alla linea 5 stai dicendo al compilatore che stai overridato il metodo findDen() della superclasse Bobcat, se commetti qualche errore durante l’override, questo non genererà più un metodo overloadato come succedeva prima, bensi incorrerai in un errore di compilazione :

1: class Bobcat {
2:   public void findDen() { }
3: }
4: class BobcatMother extends Bobcat {
5:   @Override
6:   public void findDen(boolean b) { } // DOES NOT COMPILE
7: }

Non compila perchè findDen(boolean b) è un overload e non un override !

Questo è molto comodo per evitare errori di digitazione o per far conoscere esplicitamente ai tuoi colleghi che questo metodo deve essere overridato, ricorda per, questa annotation può essere utilizzata solo per i metodi e non per i membri !

Implementare hashCode(), equals() e toString()

Tutte le classi in java ereditano da Object, ereditandone tutti i metodi, tra qui hashCode() equals() e toString(), questi sono metodi molto diffusi e utili, vediamo come ridefinirli correttamente :

toString()

Questo metodo serve per poter rappresentare in forma leggibile lo stato di un oggetto, e come ben sai, viene invocato di default quando provi a stampare un oggetto tramite System.out

Alcune classi di default implementano toString come ArrayList ma molte altre no, come ad esempio un array classico.

la firma corretta del metodo è :

@Override
public String toString() {}

equals()

Questo è uno dei metodi più utilizzati da Java, tu sai come confrontare i tipi primitivi tramite l’operatore == oppure lo utilizzi per verificare se la variabile riferimento di due oggetti è la medesima, ma se vuoi implementare una tua logica per confrontare due oggetti, devi overridare il metodo equals.

String s1 = new String("lion");
String s2 = new String("lion");
System.out.println(s1.equals(s2)); // true
StringBuilder sb1 = new StringBuilder("lion");
StringBuilder sb2 = new StringBuilder("lion");
System.out.println(sb1.equals(sb2)); // false

String implementa equals per permetterti di verificare se il contenuto di due oggetti stringa è il medesimo, mentre StringBuilder no, di fatto utilizza equals di Object che si limita a verificare se i due riferimenti sono i medesimi.

1: public class Lion {
2:   private int idNumber;
3:   private int age;
4:   private String name;
5:   public Lion(int idNumber, int age, String name) {
6:     this.idNumber = idNumber;
7:     this.age = age;
8:     this.name = name;
9:   }
10:  @Override public boolean equals(Object obj) {
11:     if ( !(obj instanceof Lion)) return false;
12:     Lion otherLion = (Lion) obj;
13:     return this.idNumber == otherLion.idNumber;
14:   }
15: }

Fai attenzione alla firma del metodo alla linea 10 ! equal vuole in input un Object !

public boolean equals(Object o) { ... }

Subito dopo viene verificato se l’oggetto passato è compatibile, e lo fa tramite l’operatore instanceof, se non è compatibile il risultato deve essere false.

Dopo questa verifica puoi utilizzare la tua logica per dire se due oggetti sono uguali.

Il contratto del metodo equals

Molto importante, segui e impara queste regole per la corretta definizione del metodo equals

  • proprietà riflessiva : per ogni istanza non nulla, x.equals(x) == true
  • proprietà simmetrica : per ogni istanza non nulla di x e y allora x.equals(y) == true , y.equals(x) == true
  • proprietà transitiva : per ogni istanza non nulla di x, y e z, se x.equals(y) == true e y.equals(x) == true allora anche x.equals(z) sarà true
  • proprietà consistente : per ogni istanza non nulla di x e y invocazioni multiple di x.equals(y) restituirà sempre true o false a meno che non cambi  lo stato dell’oggetto verificato
  • per ogni istanza null deve restituire sempre false : x.equals(null) == false

Sapete dirmi cosa c’è di sbagliato in questo metodo ?

public boolean equals(Lion obj) {
 if (obj == null) return false;
 return this.idNumber == obj.idNumber;
}

Apparentemente sembra manchi il controllo sulla compatibilità del tipo, ma non è cosi ! questo non è un override del metodo equals() ! guardate la firma del metodo, vuole in input un Lion ! ma equas vuole un Object !

hashCode()

Ogni volta che si definisce il metodo equals dovrebbe essere ridefinito anche il metodo hashCode, questo metodo viene utilizzato quando si memorizza l’oggetto in una mappa.

Ricorda che tutte le variabili di istanza non devono essere utilizzate in un metodo hashCode (). È comune non includere variabili booleane e char nel codice hash

Di seguito i punti chiave del contratto di hashCode :

  • All’interno dello stesso programma, il risultato di hashCode () non deve cambiare. Ciò significa che non devi includere variabili che cambiano nel capire il codice hash.
  • Se equals () restituisce true quando viene chiamato con due oggetti, la chiamata a hashCode () su ognuno di questi oggetti deve restituire lo stesso risultato. Ciò significa che hashCode () può utilizzare un sottoinsieme delle variabili che equals () usa.
  • Se equals () restituisce false quando viene chiamato con due oggetti, chiamando hashCode () su ognuno di questi oggetti non è necessario restituire un risultato diverso. Ciò significa che i risultati hashCode () non devono essere univoci quando vengono richiamati oggetti non uguali.