Classi

Giovedì, 6 Ottobre 2005

OBIETTIVI DELLA LEZIONE

In questa lezione:

  1. Capiremo il concetto di Classe attraverso numerosi esempi
  2. Capiremo il concetto di ereditarietà

Cosa è un oggetto?

Alcune definizioni prese dal libro "Thinking in Java", 3rd ed. di Bruce Eckel. Il libro può essere scaricato direttamente dal sito dell'autore. E' 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

Classi e Oggetti

In Java abbiamo visto esistono tipi di dati primitivi e oggetti. 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.

Sappiamo già che per avere a disposizione (da oggi diremo istanziare) un oggetto nei nostri programmi scrivevamo:

  String frase = "Questa è una stringa";
  double[] movimenti = new double[10];

Queste istruzioni:

Cosa è una classe? E' uno strumento che Java ci fornisce per ideare nuovi oggetti (attenzione ideare, non costruire! capiremo adesso la differenza).

Una ditta che produce sedie dovrà sicuramente avere tutti i macchinari ed i materiali per costruire delle sedie vere e proprie. Ma per costruirle gli operai e le macchine a controllo numerico devono seguire dei progetti, ben definiti e preparati a priori (magari dopo analisi di mercato o di stile). Inoltre saranno stabilite delle procedure per effettuare determinati tipi di azione (ad esempio una procedura per verniciare le seggiole).

La stessa cosa vale per i nostri oggetti Java. Per costruirli servono due cose: una serie di meccanismi per fare in modo che vengano effettivamente creati nella memoria del calcolatore e un progetto in base al quale vengano costruiti.

Una classe è proprio questo progetto per ideare un nuovo oggetto. La classe inoltre contiene una serie di procedure per operare sugli oggetti: sono dette metodi

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());
  }
}

Variabili e metodi istanza

L'operatore new costruisce "fisicamente" l'oggetto in memoria seguendo le indicazione date dalla classe. L'indicazione fondamentale che segue è legata ad esempio al tipo di oggetti o tipi primitivi che l'oggetto deve contenere.

Le variabili che ciascun oggetto contiene sono chiamate variabili istanza.

I metodi istanza operano sull'oggetto a cui appartengono: ogni oggetto non solo contiene delle variabili ma anche dei metodi! Per far si che un oggetto segua la procedura contenuta in uno dei suoi metodi, dobbiamo richiederlo all'oggetto stesso. La notazione è sempre la seguente:

    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 istanza 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.

Nell'esempio sopra c'è però una novità. Abbiamo due metodi costruttori che hanno lo stesso nome 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. Questa proprietà di Java è detta polimorfismo.

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 di classe

Non sempre un metodo agisce direttamente solo su di un oggetto. Talvolta vengono coinvolti più oggetti, di tipo differente ad esempio. Per questo esistono i metodi di classe. Vanno utilizzati il meno possibile.

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

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);
  }
}

Ereditarietà

class ContoCorrenteBancario extends ContoBancario {
  // variabili istanza:
  private double tassoInteresse;

  // costruttori(sono metodi istanza)
  public ContoCorrenteBancario() {
    super(5.0);
    this.tassoInteresse = 0.01;
  }

  public ContoCorrenteBancario(double tassoInteresseIniziale) {
    this();
    this.tassoInteresse = tassoInteresseIniziale;
  }

  public ContoCorrenteBancario(double sommaIniziale, double tassoInteresseIniziale) {
    super(sommaIniziale);
    this.tassoInteresse = tassoInteresseIniziale;
  }

  // metodi istanza
  public void aggiungiInteressi(int numeroMesi) {
    double interessiMaturati = (numeroMesi / 12.0) * this.tassoInteresse * this.saldoConto();
    this.deposita(interessiMaturati);
  }
}
class ProvaConto2 {
  public static void main(String[] args) {
    ContoCorrenteBancario contoLuigi = new ContoCorrenteBancario();
    ContoCorrenteBancario contoGino = new ContoCorrenteBancario(1000.0, 0.02);

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

    // E' trascorso un anno, Luigi e Gino maturano interessi. 
    contoLuigi.aggiungiInteressi(12);
    contoGino.aggiungiInteressi(12);

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

Perchè costruire classi? Per riutilizzarle espandendole (non modificandone il codice!). Per espandere una classe si utilizza la parola chiave extends

Cosa significa espandere una classe? Data una classe iniziale, che chiamiamo superclasse, costruiamo una classe derivata, detta sottoclasse, che la estenda e la specializzi. La sottoclasse eredita tutte le variabili ed i metodi della superclasse. Non dobbiamo riscriverli!

La sottoclasse può (deve nel caso del costruttore) ridefinire alcuni metodi perché agiscano con le nuove variabili introdotte.

Nell'esempio abbiamo visto due nuovi metodi:

Ultimo esempio

  class ContoCorrenteBancarioNumerato extends ContoCorrenteBancario {
  // variabile istanza:
  private int numeroConto;
  // variabile classe:
  private static int ultimoNumeroContoAssegnato = 0;

  // costruttori(sono metodi istanza)
  public ContoCorrenteBancarioNumerato() {
    super();
    ultimoNumeroContoAssegnato++;
    this.numeroConto = ultimoNumeroContoAssegnato;
  }

  public ContoCorrenteBancarioNumerato(double tassoInteresseIniziale) {
    super(tassoInteresseIniziale);
    ultimoNumeroContoAssegnato++;
    this.numeroConto = ultimoNumeroContoAssegnato;
  }

  public ContoCorrenteBancarioNumerato(double sommaIniziale, double tassoInteresseIniziale) {
    super(sommaIniziale, tassoInteresseIniziale);
    ultimoNumeroContoAssegnato++;
    this.numeroConto = ultimoNumeroContoAssegnato;
  }

  // metodi istanza
  public int getNumeroConto() {
    return this.numeroConto;
  }
}
class ProvaConto3 {
  public static void main(String[] args) {
    ContoCorrenteBancarioNumerato contoLuigi = new ContoCorrenteBancarioNumerato(0.03);
    ContoCorrenteBancarioNumerato contoMario = new ContoCorrenteBancarioNumerato();
    ContoCorrenteBancario contoGino = new ContoCorrenteBancario(1000.0, 0.02);

    // 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 del conto numero " + contoLuigi.getNumeroConto() +
      " di Luigi: " + contoLuigi.saldoConto());
    System.out.println("Saldo del conto numero " + contoMario.getNumeroConto() +
      " di Mario: " + contoMario.saldoConto());
  }
}

In questo ultimo esempio abbiamo visto che è 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.

Anche le variabili statiche, come i metodi statici del resto, vanno utilizzate il meno possibile!

Lab

Esercizio 1: Classi

Implementate una classe Lattina che contenga i metodi getSuperfice e getVolume. Nel costruttore specificate il diametro e l'altezza della lattina. Prevedete anche un costruttore predefinito per la lattina standard da 33cc (altezza=10, diametro=5).

Esercizio 2: Classi

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

Esercio 3: Metodi

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.

Esercizio 4: Classi

Scrivete una nuova classe Cerchio che estende la classe Ellisse dell'esempio visto a lezione. Scrivete gli opportuni costruttori e introducete i nuovi metodi di istanza getRaggio e setRaggio. Costuite la classe di prova ProvaRaggio che chieda all'utente il raggio e restituisca l'area.

©2005 Roberto Sassi