Oggetti e Classi

Giovedì, 12 Ottobre 2006

OBIETTIVI DELLA LEZIONE

In questa lezione:

  1. Capiremo il concetto di Oggetto e di Classe attraverso numerosi esempi
  2. Vedremo la differenza tra varibili o metodi di istanza e variabili o metodi di classe
  3. Analizzeremo alcuni errori comuni per evitarli.

Ancora sui metodi: overload

Abbiamo utilizzato:

System.out.println(3.0)
System.out.println("Benvenuto")
System.out.println(5)

Quella che abbiamo sfruttato senza saperlo è la possibilità che Java offre di "sovraccaricare" (to overload) un metodo. In pratica costruiamo metodi differenti con lo stesso nome, ma con una diversa lista di parametri formali. Il compilatore sceglie il metodo che corrisponde meglio ai parametri attuali scelti dall'utente allorquando utilizza il metodo. La scelta del metodo tra quelli disponibili avviene dunque durante la fase di compilazione (NON durante l'esecuzione!).

L'overload rende più leggibile il codice. Vediamo un esempio, un metodo che calcola il maggiore di due numeri:

public static int massimo(int a, int b) {
  int c = ( a >= b ? a : b );
  return c;
}

public static double massimo(double a, double b) {
  double c = ( a >= b ? a : b );
  return c;
}

Ragionare per oggetti

Che cosa è un oggetto software? E cosa una classe? Vediamo insieme in queste slides.

Cosa è un oggetto?

Per completare la nostra conoscenza di cosa sia un oggetto software, ecco alcune definizioni prese dal libro "Thinking in Java", 3rd ed. di Bruce Eckel. Il libro può essere scaricato direttamente dal sito dell'autore (è un ottimo libro).

"Alan Kay summarized five basic characteristics of Smalltalk, the first successful object-oriented language and one of the languages upon which Java is based. These characteristics represent a pure approach to objectoriented programming:

  1. Everything is an object. Think of an object as a fancy variable; it stores data, but you can “make requests” to that object, asking it to perform operations on itself. In theory, you can take any conceptual component in the problem you’re trying to solve (dogs, buildings, services, etc.) and represent it as an object in your program.
  2. A program is a bunch of objects telling each other what to do by sending messages. To make a request of an object, you “send a message” to that object. More concretely, you can think of a message as a request to call a method that belongs to a particular object.
  3. Each object has its own memory made up of other objects. Put another way, you create a new kind of object by making a package containing existing objects. Thus, you can build complexity into a program while hiding it behind the simplicity of objects.
  4. Every object has a type. Using the parlance, each object is an instance of a class, in which “class” is synonymous with “type.” The most important distinguishing characteristic of a class is “What messages can you send to it?”
  5. All objects of a particular type can receive the same messages. This is actually a loaded statement, as you will see later. Because an object of type “circle” is also an object of type “shape,” a circle is guaranteed to accept shape messages. This means you can write code that talks to shapes and automatically handle anything that fits the description of a shape. This substitutability is one of the powerful concepts in OOP.

Booch offers an even more succinct description of an object:

      "An object has state, behavior and identity".

This means that an object can have internal data (which gives it state), methods (to produce behavior), and each object can be uniquely distinguished from every other object—to put this in a concrete sense, each object has a unique address in memory

In Java abbiamo visto esistono tipi di dati primitivi e oggetti. In definitiva, i tipi di dati primitivi sono definiti dal linguaggio e di certo non ne possiamo aggiungere altri. Per scrivere i nostri programmi possiamo invece ideare nuovi oggetti!

Un primo esempio

class ContoBancario {
  // Variabili istanza:
  private double saldo;

  // Metodi istanza:
  // costruttore predefinito
  public ContoBancario() {
    this.saldo = 0.0;
  }
  // costruttore alternativo
  public ContoBancario(double sommaIniziale) {
    this.saldo = sommaIniziale;
  }

  public double saldoConto() {
    return this.saldo;
  }

  public void deposita(double sommaDepositata) {
    this.saldo += sommaDepositata;
  }

  public void preleva(double sommaPrelevata) {
    this.saldo -= sommaPrelevata;
  }
}
class ProvaConto0 {
  public static void main(String[] args) {
    ContoBancario contoLuigi = new ContoBancario();
    ContoBancario contoGino = new ContoBancario(1000.0);

    // Trasferiamo 100.0 dal conto di Gino a quello di Luigi
    contoGino.preleva(100.0);
    contoLuigi.deposita(100.0);

    System.out.println("Saldo di Gino: " + contoGino.saldoConto());
    System.out.println("Saldo di Luigi: " + contoLuigi.saldoConto());
  }
}

Analizziamo il primo esempio in queste slides. Una sintesi è riportata nei paragrafi che seguono. Questo che stiamo affrontando è un argomento importante: fate anche riferimento al libro di testo!

Variabili e metodi istanza

Lo stato di un oggetto è "contenuto" in particolari variabili dette campi o variabili istanza.

I metodi istanza sono l'interfaccia dell'oggetto verso altri oggetti. Ne definiscono il comportamento. Per invitare un oggetto a manifestare un comportamento (invocare un metodo), si utilizza la notazione:

    nomeOggetto.nomeMetodoIstanza(eventualiArgomentiDelMetodoIstanza);

Il nome dell'oggetto e quello del suo metodo che vorremmo venisse invocato sono separati da un . punto.

Un metodo istanza ha sempre:

Costruttori

Particolari metodi sono detti costruttori e hanno lo stesso nome della classe (identico). Sono metodi che sono preposti all'inizializzazione degli oggetti una volta che sono stati creati dalla JVM.

E' tipico utilizzare l'overload cioè avere più costruttori con lo stesso nome (anche perché non potrebbe essere diversamente, visto che il metodo costruttore deve avere lo stesso nome della classe) e con differenti parametri espliciti. Il compilatore Java selezionerà il metodo opportuno a partire dalla chiamata al metodo. Cioè: se il costruttore verrà invocato con nessun parametro, allora il compilatore selezionerà il primo, se il parametro sarà invece uno, verrà invocato il secondo.

All'interno dei costruttori, come nei metodi di istanza è possibile utilizzare il parametro implicito this.

Specificatori di Accesso

Costruendo il nostro oggetto non vogliamo che i suoi dati siano disponibili all'esterno ma che l'utente programmatore utilizzi di volta in volta i metodi offerti dalla classe per operare sull'oggetto.

Questa caratteristica tipica dei linguaggi OO si dice astrazione perché crea tipi di dati astratti (non ci interessa come fisicamente sono memorizzati ne come si debba operare su di essi).

Per incapsulare le variabili nell'oggetto (cioè non renderle disponibili all'esterno) basta utilizzare lo specificatore di accesso private.

Al contrario, generalmente vogliamo che i metodi siano disponibili all'esterno della classe, per questo utilizzaremo lo specificatore di accesso public.

Una classe utilizza un altra

Nell'esempio sopra le due classi possono essere in due file separati. Non c'è bisogno che siano nello stesso, ma andrebbe bene ugualmente. Come regola nel nostro corso li terremo tutti nella stessa directory. Attenzione: solo una classe per file può essere dichiarata public.

Un secondo esempio

class ContoBancario {
  // Variabili istanza:
  private double saldo;

  // Metodi istanza:
  // costruttore predefinito
  public ContoBancario() {
    this.saldo = 0.0;
  }
  // costruttore alternativo
  public ContoBancario(double sommaIniziale) {
    this.saldo = sommaIniziale;
  }

  public double saldoConto() {
    return this.saldo;
  }

  public void deposita(double sommaDepositata) {
    this.saldo += sommaDepositata;
  }

  public void preleva(double sommaPrelevata) {
    this.saldo -= sommaPrelevata;
  }

  // Metodo di classe
  public static void trasferisci(double sommaTrasferita,
    ContoBancario origine, ContoBancario destinatario) {
    origine.saldo -= sommaTrasferita;
    destinatario.saldo += sommaTrasferita;
  }
}
class ProvaConto1 {
  public static void main(String[] args) {
    ContoBancario contoLuigi = new ContoBancario();
    ContoBancario contoGino = new ContoBancario(1000.0);

    // Trasferiamo 100.0 dal conto di Gino a quello di Luigi
    ContoBancario.trasferisci(100.0, contoGino, contoLuigi);

    System.out.println("Saldo di Gino: " + contoGino.saldoConto());
    System.out.println("Saldo di Luigi: " + contoLuigi.saldoConto());
  }
}

Metodi e variabili di classe

Analizziamo il secondo esempio in queste altre slides.

Non sempre un metodo agisce direttamente solo su di un oggetto. Talvolta vengono coinvolti più oggetti, di tipo differente ad esempio. Anche per questo esistono i metodi di classe.

Per definire un metodo di classe va utilizzata la parola chiave static (deriva dal C, ma non ha un significato esplicito).

Inoltre è possibile definire anche delle variabili di classe utilizzando lo specificatore static. Questo vuol dire che ciascun oggetto della classe NON conterrà una copia della variabile, che verrà invece custodita altrove. Di questa variabile ne esisterà una copia sola.

Le variabili di classe vengono spesso utilizzate per definire dei valori costanti. Per farlo si utilizza lo specificatore final.Esempio:

public static final double PI = 3.14159;

Per convenzione le costanti hanno nomi tutti maiuscoli. Più parole si congiungono con un carattere "_". Esempio:

public static final int ALBERI_MELE = 20;

Metodi e variabili statiche vanno utilizzate il meno possibile!

Un terzo esempio

import javax.swing.*;

class Ellisse {
  // variabili istanza
  private double asseMaggiore, asseMinore;

  // costruttori
  public Ellisse() {
    this.asseMaggiore = 0.0;
    this.asseMinore = 0.0;
  }

  public Ellisse(double maggiore, double minore) {
    this.asseMaggiore = maggiore;
    this.asseMinore = minore;
  }

  // metodi istanza
  public void setAsseMaggiore(double maggiore) {
    this.asseMaggiore = maggiore;
  }

  public void setAsseMinore(double minore) {
    this.asseMinore = minore;
  }

  public double getAsseMaggiore() {
    return this.asseMaggiore;
  }

  public double getAsseMinore() {
    return this.asseMinore;
  }

  public double calcolaArea() {
    return Math.PI * this.asseMaggiore * this.asseMinore;
  }

}

public class AreaEllisse {
  public static void main(String[] args) {
    String stringaInput;
    Ellisse ellisseUtente = new Ellisse();

    stringaInput = JOptionPane.showInputDialog("Inserisci l'asse maggiore: ");
    ellisseUtente.setAsseMaggiore(Double.parseDouble(stringaInput));

    stringaInput = JOptionPane.showInputDialog("Inserisci l'asse minore: ");
    ellisseUtente.setAsseMinore(Double.parseDouble(stringaInput));

    JOptionPane.showMessageDialog(null, "L'area dell'ellisse avente asse minore " +
      ellisseUtente.getAsseMinore() + " e asse maggiore " + ellisseUtente.getAsseMaggiore() +
      " è " + ellisseUtente.calcolaArea() + ".");

    System.exit(0);
  }
}

null

Quando una variabile di tipo riferimento ad oggetto non viene inizializzata:

null è una costante letterale per le variabili di tipo riferimento ad oggetto.

Se abbiamo finito di utilizzare un oggetto, possiamo assegnare al suo riferimento il valore null. il meccanismo di garbage collection della JVM lo eliminerà dallo heap.

Errori comuni

Vediamo alcuni errori in cui è facile incappare lavorando con le classi Java.

Copiare i riferimenti agli oggetti

Abbiamo visto che in Java c'è una differenza sostanziale tra variabili ti tipo riferimento ad oggetto ed oggetti veri e propri. Capiamolo meglio. Consideriamo questa classe:

class Punto3D {
  private double x, y, z;

  public Punto3D(double x0, double y0, double z0) {
    this.x = x0;
    this.y = y0;
    this.z = z0;
  }

  public double getX() {
    return this.x;
  }

  public void setX(double nuovoX) {
    this.x = nuovoX;
  }

  public double getY() {
    return this.y;
  }

  public double getZ() {
    return this.z;
  }

}

A costo di essere noiosi, rivediamo in dettaglio cosa significa istanziare un nuovo oggetto Punto3D

Punto3D questo = new Punto3D(1.0,2.0,1.0);

Ecco uno schema dei quattro passi necessari:






Vediamo invece cosa succede quando dichiariamo una variabile di tipo primitivo:

double distanza = 10.0;







Riassumendo:

Attenzione che quando copiate una variabile oggetto viene copiato solo il riferimento all'oggetto (cioè ovviamente il contenuto della variabile). Esempio:

    Punto3D questo = new Punto3D(1.0,2.0,3.0);
    Punto3D quello;

    quello = questo; // attenzione! quello e quello contengono lo stesso riferimento!
    quello.setX(8.0);

    System.out.println(questo.getX()); //Stampa 8.0 non 1.0!

Confrontare i riferimenti agli oggetti

Quando confrontate due variabili di tipo riferimento ad oggetto non confrontate gli oggetti! Esempio:

    Punto3D questo = new Punto3D(1.0,2.0,3.0);
    Punto3D quello = new Punto3D(1.0,2.0,3.0);

    if(questo == quello) { // Attenzione stiamo confrontando i riferimenti NON gli oggetti!
      System.out.println("I due riferimenti coincidono");
    }

    if((questo.getX()==quello.getX()) &&
      (questo.getY()==quello.getY()) &&
      (questo.getZ()==quello.getZ())) { // Giusto
      System.out.println("I due punti coincidono");
    }

Una soluzione più efficace ed elegante è quella di sovrascrivere il metodo equals() della superclasse Object. Lo vedremo in seguito.

Dimenticarsi di chiamare il costruttore

Da quello che abbiamo visto prima se ci dimentichiamo di invocare il costruttore i passi 2, 3 e 4 non vengono effettuati. La variabile oggetto contiene un valore qualunque e il compilatore si lamenta con l'errore variable ... might not have been initialized. Esempi:

    Punto3D questo;
    questo.setX(10.0); // Sbagliato questo non è stata inizializzata

    Punto3D questo = null;
    questo.setX(10.0); // Sbagliato questo non si riferisce a nessun oggetto

    Punto3D quello = new Punto3D(1.0,2.0,3.0);
    quello.setX(20.0); // Giusto

Invocare il costruttore come metodo istanza

    Punto3D quello = new Punto3D(1.0,2.0,3.0);

    // Sbagliato non possiamo ricreare un oggetto!
    quello.Punto3D(6.0,0.0,1.0);

    // Giusto, creiamo un secondo oggetto e ne mettiamo il riferimento in quello.
    // Il vecchio oggetto viene eliminato prima o poi dal meccanismo automatico
    // di garbage collection.
    quello = new Punto3D(6.0,0.0,1.0);

Chiamare un metodo istanza senza specificare su quale oggetto deve operare

    Punto3D quello = new Punto3D(1.0,2.0,3.0);

    setX(10.0); // Sbagliato, su quale oggetto deve operare?

Lab

Esercizio 1: Classi [Soluzione]

Implementate la classe Lattina che contenga i metodi getAreaBase() e getVolume(). Ciascuna lattina deve essere caratterizzata da altezza e diametro. Nel costruttore specificate il diametro e l'altezza della lattina. Prevedete anche un costruttore predefinito per la lattina standard (altezza=10, diametro=5).

Esercizio 2: Classi [Soluzione]

Costruire la classe CalcolaLattina il cui metodo di classe main chieda all'utente le dimensioni della lattina e restituisca il volume e la superfice di base della lattina. Utilizzare la classe Lattina sviluppata nell'esercizio precedente.

Esercio 3: Metodi [Soluzione]

Sviluppare la classe ParolaPalindroma che contenga il metodo
public static boolean verificaParolaPalindroma(String parola) { ... }
che verifichi se la stringa ricevuta come argomento sia palindroma o meno. Una parola si dice palindroma quando la successione delle lettere è identica sia se la parola è letta da sinistra a destra che viceversa. Ingegni, ottetto e onorarono sono esempi di parole palindrome. Scrivete un metodo di classe main che chieda all'utente di inserire una parola e restituisca il risultato.

Esercio 4: Classi [Soluzione]

Anche riutilizzando parte del codice dell'esercizio della lezione 4, sviluppare la classe EquazioneSecondoGrado.

Lo stato di ciascun oggetto deve essere nascosto. La classe deve prevedere tutti i metodi setter e getter necessari. Inoltre deve prevedere un costruttore predefinito (a=b=c=0) e un costruttore che permetta di inizializzare opportunamente i parametri dell'equazione. Infine deve fornire i metodi di istanza pubblici delta() (che ritorni il valore b*b-4*a*c), risolvi() che ritorni un array di double con la/le soluzione/i (nel caso l'equazione non abbia soluzione utilizzate il valore Double.NaN), isRisolvibile() (che restituisca all'utente un valore boolean che valga true nel caso l'equazione ammetta soluzioni reali).

Quindi create una seconda classe che chieda all'utente i parametri dell'equazione, istanzi un opportuno oggetto di classe EquazioneSecondoGrado, verifichi se l'equazione ha soluzioni reali, ed, eventualmente, le calcoli.

©2006 Roberto Sassi