Sintassi della composizione

Prima di ora, la composizione è stata usata abbastanza frequentemente. Semplicemente metti il riferimento ad un oggetto all'interno di nuove classi. Per esempio, supponiamo ti serva un oggetto che contiene molti oggetti di tipo stringa, una coppia di primitive, e un oggetto di un'altra classe. Per gli oggetti non primitivi, tu metti il riferimento all'interno della tua nuova classe, ma definisci le primitive direttamente:

//: c06:SprinklerSystem.java
// Composition for code reuse.
import com.bruceeckel.simpletest.*;

class WaterSource {
  private String s;
  WaterSource() {
    System.out.println("WaterSource()");
    s = new String("Constructed");
  }
  public String toString() { return s; }
}

public class SprinklerSystem {
  private static Test monitor = new Test();
  private String valve1, valve2, valve3, valve4;
  private WaterSource source;
  private int i;
  private float f;
  public String toString() {
    return
      "valve1 = " + valve1 + "\n" +
      "valve2 = " + valve2 + "\n" +
      "valve3 = " + valve3 + "\n" +
      "valve4 = " + valve4 + "\n" +
      "i = " + i + "\n" +
      "f = " + f + "\n" +
      "source = " + source;
  }
  public static void main(String[] args) {
    SprinklerSystem sprinklers = new SprinklerSystem();
    System.out.println(sprinklers);
    monitor.expect(new String[] {
      "valve1 = null",
      "valve2 = null",
      "valve3 = null",
      "valve4 = null",
      "i = 0",
      "f = 0.0",
      "source = null"
    });
  }
} ///:~

Uno dei metodi definiti in entrambe le classi è speciale: toString(). Impareremo dopo che ogni oggetto non primitivo ha un metodo toString(), e esso è chiamato in situazioni particolari quando il compilatore vuole una String ma  ha un oggetto. Nell'espressione in  SprinklerSystem.toString():

  "source = " + source;

Il compilatore vede che tu stai provando ad aggiungere un oggetto String ("source = ") ad un WaterSource. Poiché tu puoi solo "aggiungere " un String ad un altro Strign, esso dice "Io convertirò source in un String chiamando toString()!". Dopo aver fatto questo esso può combinare i due String e passare il risultante String a System.out.println(). Ogni volta che vuoi usare questo metodo non devi far altro che scriverlo nella classe.

Le primitive che sono  campi in una classe sono automaticamente inizializzati a zero. Ma il riferimento ad un oggetto è inizializzato a NULL, e se provi a chiamare metodi per uno di questi, incorrerai in una eccezione. Attualmente è buono che tu puoi ancora stamparli senza lanciare un eccezione.

Ha senso il fatto che il compilatore non crea un oggetto di default per ogni riferimento, perché questo potrebbe portare ad overhead in molti casi. Se vuoi un riferimento inizializzato, puoi fartelo da solo:

1) Nel punto in cui gli oggetti sono definiti. Questo significa che essi saranno inizializzati prima della chiamata al costruttore. Cioé a compile time.
2) Nel costruttore della classe.
3)Appena prima di quando tu abbia bisogno di usare l'oggetto. Ciò è spesso chiamato lazy inizialization. Essa può ridurre l'overhead in stuazioni dove la creazione di  oggetti è costosa e gli oggetti non hanno bisogno sempre di essere creati.

Tutti e tre gli approcci sono mostrati qui di seguito:

//: c06:Bath.java
// Constructor initialization with composition.
import com.bruceeckel.simpletest.*;

class Soap {
  private String s;
  Soap() {
    System.out.println("Soap()");
    s = new String("Constructed");
  }
  public String toString() { return s; }
}

public class Bath {
  private static Test monitor = new Test();
  private String // Initializing at point of definition:
    s1 = new String("Happy"),
    s2 = "Happy",
    s3, s4;
  private Soap castille;
  private int i;
  private float toy;
  public Bath() {
    System.out.println("Inside Bath()");
    s3 = new String("Joy");
    i = 47;
    toy = 3.14f;
    castille = new Soap();
  }
  public String toString() {
    if(s4 == null) // Delayed initialization:
      s4 = new String("Joy");
    return
      "s1 = " + s1 + "\n" +
      "s2 = " + s2 + "\n" +
      "s3 = " + s3 + "\n" +
      "s4 = " + s4 + "\n" +
      "i = " + i + "\n" +
      "toy = " + toy + "\n" +
      "castille = " + castille;
  }
  public static void main(String[] args) {
    Bath b = new Bath();
    System.out.println(b);
    monitor.expect(new String[] {
      "Inside Bath()",
      "Soap()",
      "s1 = Happy",
      "s2 = Happy",
      "s3 = Joy",
      "s4 = Joy",
      "i = 47",
      "toy = 3.14",
      "castille = Constructed"
    });
  }
} ///:~

Nota che nel costruttore Bath, uno statement è eseguito prima che abbia luogo una qualsiasi inizializzazione. Quando non inizializzi nella definizione, non ci sono ancora garanzie che l'inizializzazione venga fatta prima che tu spedisca un messaggio al riferimento all'oggetto, eccetto che per l'inevitabile eccezione run-time.

Quando chiami toString() esso carica s4, in questo modo i campi sono tutti inizializzati nel momento in cui sono usati.

Sintassi ereditarietà

L'ereditarietà è parte integrante di java (e di tutti i linguaggi OOP (Object Oriented Paradigm)). Quando tu crei una classe già hai usato questa proprietà, perché senza che tu esplicitamente erediti da alcune altre classi, implicitamente eredita dalla classe standard Object di Java.

La sintassi per la composizione è ovvia, ma per usare l'ereditarietà c'è un altro modo. Quando tu erediti, tu dici "Questa nuova classe è come la vecchia classe". Tu riesci a fare questo nel codice dando il nome alla classe come di solito, ma prima della parentesi graffa che apre il corpo della classe, metti la keyword extends seguita dal nome della classe base. Quando hai fatto questo, automaticamente metti tutti i campi e i metodi nella classe base. Qui c'è un esempio:

//: c06:Detergent.java
// Inheritance syntax & properties.
import com.bruceeckel.simpletest.*;

class Cleanser {
  protected static Test monitor = new Test();
  private String s = new String("Cleanser");
  public void append(String a) { s += a; }
  public void dilute() { append(" dilute()"); }
  public void apply() { append(" apply()"); }
  public void scrub() { append(" scrub()"); }
  public String toString() { return s; }
  public static void main(String[] args) {
    Cleanser x = new Cleanser();
    x.dilute(); x.apply(); x.scrub();
    System.out.println(x);
    monitor.expect(new String[] {
      "Cleanser dilute() apply() scrub()"
    });
  }
}

public class Detergent extends Cleanser {
  // Change a method:
  public void scrub() {
    append(" Detergent.scrub()");
    super.scrub(); // Call base-class version
  }
  // Add methods to the interface:
  public void foam() { append(" foam()"); }
  // Test the new class:
  public static void main(String[] args) {
    Detergent x = new Detergent();
    x.dilute();
    x.apply();
    x.scrub();
    x.foam();
    System.out.println(x);
    System.out.println("Testing base class:");
    monitor.expect(new String[] {
      "Cleanser dilute() apply() " +
      "Detergent.scrub() scrub() foam()",
      "Testing base class:",
    });
    Cleanser.main(args);
  }
} ///:~
Questo mostra anche altre cose. Primo, nel metodo Cleanser append(), gli String sono concatenati ad s usando l'operatore +=, che è uno degli operatori (accanto a "+") che i creatori di Java hanno "overloadato" per lavorare con gli oggetti String.

Secondo, sia Cleanser che Detergent contengono un metodo main(). Tu puoi creare un main() per ognuna delle tue classi, ed è spesso raccomandato farlo perché in questo modo il tuo codice di testing è impacchettato nella classe stessa. Anche se hai molte classi in un programma, solo il main() della classe invocata nella linea di comando sarà chiamato. In questo caso, quando scrivi java Detergent, Detergent.main() sarà chiamato. Ma puoi anche scrivere java Cleanser per invocare Cleanser.main(), anche se Cleanser non è una classe pubblica. Questa tecnica di mettere un main()  in ogni classe, permette un facile testing di ogni classe. E non hai bisogno di togliere il main() quando hai finito il test; puoi lasciarlo lì per successivi test.

Qui tu puoi vedere che Detergent.main() chiama esplicitamente Cleanser.main(), passandogli gli stessi argomenti dalla linea di comando (comunque, potresti passargli un qualsiasi array di String).

E' importante che tutti i metodi in Cleanser siano  public. Ricorda che se non metti i modificatori, il campo ha di default l'accesso package, che permette l'accesso solo ai membri del package. In questo modo, all'interno di  questo package, chiunque potrebbe usare questi metodi se non ci sono modificatori di accesso. Detergent non avrà problemi, per esempio. Comunque, se una classe da un qualche altro package ereditasse da Cleanser, esso potrebbe accedere solo a membri public. Allora per progettare l'ereditarietà, di solito si mettono tutti i campi private e tutti i metodi public. (i membri protected anche pemettono l'accesso da classi derivate; vedremo poi in seguito). Naturalmente, in casi particolari devi fare delle modifiche, ma questo è un modo utilizzabile.

Nota che Cleanser ha un insieme di metodi nella sua interfaccia: append(), dilute(), apply(), scrub(), and toString(). Poiché Detergent è derivato da Cleanser (grazie alla keyword extends), esso automaticamente prende tutti questi metodi nella sua interfaccia, anche se non li vedi esplicitamente definiti in Detergent. Puoi pensare all'ereditarietà, poi, riusando la classe.

Come visto in scrub(), è possibile prendere un metodo che è definito nella classe base e modificarlo. In questo caso, tu potresti voler chiamare il metodo della classe base nella nuova versione. Ma dentro scrub(), non puoi semplicemente chiamare scrub(), altrimenti crei una chiamata ricorsiva, che non è chiò che vuoi. Per risolvere questo problema, Java ha la keyword super che riferisce alla "superclasse" da cui la classe corrente ha ereditato. In questo modo l'espressione super.scrub() chiama la versione classe-base del metodo scrub().

Quando erediti non sei costretto ad usare il metodo della classe base. Tu puoi anche aggiungere nuovi metodi alla classe derivata solo scrivendoli al suo interno. Il metodo foam() è un esempio di ciò.

In Detergent.main() tu puoi vedere che per un oggetto Detergent, tu puoi chiamare tutti i metodi disponibili in  Cleanser così come in Detergent.

Inizializzare la classe base

Quando ci sono due classi coinvolte tra loro - la classe base e la classe derivata - invece di una sola, potrebbe esserci un pò di confusione a provare ad immaginare l'oggetto risultante prodotto da una classe derivata. D'altra parte, sembra che la nuova classe abbia la stessa interfaccia della classe base e  forse metodi e campi in più. Ma l'ereditarietà non fa solo si che venga copiata l'interfaccia della classe. Quando crei un oggetto della classe derivata, esso contiene al suo interno un sottoggetto della classe base. Questo sottoggetto è lo stesso se tu hai creato un oggettto della classe base da se stesso. E' solo che dall'esterno, il sottoggetto della classe base è avvolto all'interno dell'oggetto della classe derivata.

Naturalmente, è fondamentale che l'oggetto della classe base sia inizializzato correttamente, e c'è solo un modo per garantire questo: fare l'inizializzazione nel costruttore  chiamando il costruttore della classe base, che deve avere tutte le appropriate conoscenze e privilegi per farlo. Java automaticamente inserisce chiama il costruttore della classe base quando si crea un oggetto della classe derivata. Questo esempio mostra questo lavoro con tre livelli di ereditarietà:

//: c06:Cartoon.java
// Constructor calls during inheritance.
import com.bruceeckel.simpletest.*;

class Art {
  Art() {
    System.out.println("Art constructor");
  }
}

class Drawing extends Art {
  Drawing() {
    System.out.println("Drawing constructor");
  }
}

public class Cartoon extends Drawing {
  private static Test monitor = new Test();
  public Cartoon() {
    System.out.println("Cartoon constructor");
  }
  public static void main(String[] args) {
    Cartoon x = new Cartoon();
    monitor.expect(new String[] {
      "Art constructor",
      "Drawing constructor",
      "Cartoon constructor"
    });
  }
} ///:~

Puoi vedere che la costruzione avviene dal basso, in modo che la classe base inizializza l'oggetto prima che i costruttori della classe derivata possano accedere ad esso. Anche se non crei un costruttore per Cartoon(), il compilatore chiamerà un costruttore di default che chiami il costruttore della classe base.

Costruttori con argomenti

L'esempio precedente ha costruttori di default; ma essi non hanno argomenti. E' facile per il compilatore chiamarli poiché non ci sono problemi per vedere che argomenti passano. Se la tua classe non ha argomenti di default, oppure vuoi chiamare un costruttore della classe base che ha un argomento, devi esplicitamente scrivere la chiamata a questo costruttore usando la parola chiave super e l'appropriata lista di argomenti:

//: c06:Chess.java
// Inheritance, constructors and arguments.
import com.bruceeckel.simpletest.*;

class Game {
  Game(int i) {
    System.out.println("Game constructor");
  }
}

class BoardGame extends Game {
  BoardGame(int i) {
    super(i);
    System.out.println("BoardGame constructor");
  }
}

public class Chess extends BoardGame {
  private static Test monitor = new Test();
  Chess() {
    super(11);
    System.out.println("Chess constructor");
  }
  public static void main(String[] args) {
    Chess x = new Chess();
    monitor.expect(new String[] {
      "Game constructor",
      "BoardGame constructor",
      "Chess constructor"
    });
  }
} ///:~

Se non chiami il costruttore della classe base in BoardGame(), il compilatore si lamenterà che non riesce a trovare un costruttore  Game(). In più, la chiamata al costruttore della classe base deve essere la prima cosa che scrivi nel costruttore della classe derivata. (Il compilatore te lo ricorderà se sbagli).

Afferrare le eccezioni del costruttore base

Come giusta nota, il compilatore ti forza a mettere la chiamata al costruttore della classe base nel corpo del costruttore della classe derivata. Questo significa che niente potrà apparire prima di essa. Come vedrai nel capitolo 9, questo previene dal avere eccezioni nel costruttore della classe derivata che provengono dalla classe base. Può essere un inconveniente a volte.

Combinare composizione ed ereditarietà

E' molto comune usare composizione e ereditarietà insieme. L'esempio seguente mostra la creazione di una classe più complessa, usando sia ereditarietà che composizione, accanto alla necessaria inizializzazione del  costruttore:

//: c06:PlaceSetting.java
// Combining composition & inheritance.
import com.bruceeckel.simpletest.*;

class Plate {
  Plate(int i) {
    System.out.println("Plate constructor");
  }
}

class DinnerPlate extends Plate {
  DinnerPlate(int i) {
    super(i);
    System.out.println("DinnerPlate constructor");
  }
}

class Utensil {
  Utensil(int i) {
    System.out.println("Utensil constructor");
  }
}

class Spoon extends Utensil {
  Spoon(int i) {
    super(i);
    System.out.println("Spoon constructor");
  }
}

class Fork extends Utensil {
  Fork(int i) {
    super(i);
    System.out.println("Fork constructor");
  }
}

class Knife extends Utensil {
  Knife(int i) {
    super(i);
    System.out.println("Knife constructor");
  }
}

// A cultural way of doing something:
class Custom {
  Custom(int i) {
    System.out.println("Custom constructor");
  }
}

public class PlaceSetting extends Custom {
  private static Test monitor = new Test();
  private Spoon sp;
  private Fork frk;
  private Knife kn;
  private DinnerPlate pl;
  public PlaceSetting(int i) {
    super(i + 1);
    sp = new Spoon(i + 2);
    frk = new Fork(i + 3);
    kn = new Knife(i + 4);
    pl = new DinnerPlate(i + 5);
    System.out.println("PlaceSetting constructor");
  }
  public static void main(String[] args) {
    PlaceSetting x = new PlaceSetting(9);
    monitor.expect(new String[] {
      "Custom constructor",
      "Utensil constructor",
      "Spoon constructor",
      "Utensil constructor",
      "Fork constructor",
      "Utensil constructor",
      "Knife constructor",
      "Plate constructor",
      "DinnerPlate constructor",
      "PlaceSetting constructor"
    });
  }
} ///:~

Sebbene il compilatore ti forzi ad inizializzare la classe base, e richieda che tu lo faccia appena all'inizio del costruttoe, esso non controlla che tu abbia inizializzato i membri dell'oggetto (giustamente che se ne fotte!). Quindi devi stare attento e farci attenzione tu.

Garantire corretta pulizia

Java non ha il concetto di C++ di un distruttore, un metodo che è automaticamente chiamato quando un oggetto viene distrutto. La ragione è probabilmente che in java, la pratica è semplicemente di dimenticare l'oggetto più che distruggerlo, permettendo al garbage collector di ripulire la memoria se necessario.

Questo è buono, ma a volte la tua classe ha bisogno durante il suo tempo di vita di operazioni di pulizia. Come menzionato nel capitolo 4, non si sa quando viene chiamato il garbage collector, o se viene chiamato. Quindi se vuoi eliminare qualcosa da una classe, devi scrivere un metodo che lo faccia, e rassicurarti che il programmatore sappia che può chiamarlo. Prima di fare ciò - come descritto nel capitolo 9 ("Gestione degli errori con le eccezioni") - ti devi guardare le spalle da un eccezione mettendo ogni pulitore in una clausola finally .

Consideriamo un esempio di un computer-aided-design system che disegna lo schermo:

//: c06:CADSystem.java
// Ensuring proper cleanup.
package c06;
import com.bruceeckel.simpletest.*;
import java.util.*;

class Shape {
  Shape(int i) {
    System.out.println("Shape constructor");
  }
  void dispose() {
    System.out.println("Shape dispose");
  }
}

class Circle extends Shape {
  Circle(int i) {
    super(i);
    System.out.println("Drawing Circle");
  }
  void dispose() {
    System.out.println("Erasing Circle");
    super.dispose();
  }
}

class Triangle extends Shape {
  Triangle(int i) {
    super(i);
    System.out.println("Drawing Triangle");
  }
  void dispose() {
    System.out.println("Erasing Triangle");
    super.dispose();
  }
}

class Line extends Shape {
  private int start, end;
  Line(int start, int end) {
    super(start);
    this.start = start;
    this.end = end;
    System.out.println("Drawing Line: "+ start+ ", "+ end);
  }
  void dispose() {
    System.out.println("Erasing Line: "+ start+ ", "+ end);
    super.dispose();
  }
}

public class CADSystem extends Shape {
  private static Test monitor = new Test();
  private Circle c;
  private Triangle t;
  private Line[] lines = new Line[5];
  public CADSystem(int i) {
    super(i + 1);
    for(int j = 0; j < lines.length; j++)
      lines[j] = new Line(j, j*j);
    c = new Circle(1);
    t = new Triangle(1);
    System.out.println("Combined constructor");
  }
  public void dispose() {
    System.out.println("CADSystem.dispose()");
    // The order of cleanup is the reverse
    // of the order of initialization
    t.dispose();
    c.dispose();
    for(int i = lines.length - 1; i >= 0; i--)
      lines[i].dispose();
    super.dispose();
  }
  public static void main(String[] args) {
    CADSystem x = new CADSystem(47);
    try {
      // Code and exception handling...
    } finally {
      x.dispose();
    }
    monitor.expect(new String[] {
      "Shape constructor",
      "Shape constructor",
      "Drawing Line: 0, 0",
      "Shape constructor",
      "Drawing Line: 1, 1",
      "Shape constructor",
      "Drawing Line: 2, 4",
      "Shape constructor",
      "Drawing Line: 3, 9",
      "Shape constructor",
      "Drawing Line: 4, 16",
      "Shape constructor",
      "Drawing Circle",
      "Shape constructor",
      "Drawing Triangle",
      "Combined constructor",
      "CADSystem.dispose()",
      "Erasing Triangle",
      "Shape dispose",
      "Erasing Circle",
      "Shape dispose",
      "Erasing Line: 4, 16",
      "Shape dispose",
      "Erasing Line: 3, 9",
      "Shape dispose",
      "Erasing Line: 2, 4",
      "Shape dispose",
      "Erasing Line: 1, 1",
      "Shape dispose",
      "Erasing Line: 0, 0",
      "Shape dispose",
      "Shape dispose"
    });
  }
} ///:~

  Ogni cosa in questo è un qualche tipo di Shape (che è esso stesso un tipo di Object, che eredita implicitamente dalla classe radice).