Classi e oggetti

Fin qui abbiamo visto i tipi di dato primitivi che il linguaggio Java mette a disposizione.

Sebbene sia possibile scrivere programmi interessanti usando solo i numeri, i caratteri e i booleani, spesso i programmi hanno bisogno di manipolare tipi di dato più complessi che rappresentano più fedelmente le entità del mondo reale. Conti bancari, anagrafiche di dipendenti sono alcuni esempi di questo genere di dati.

Per questi motivi un linguaggio a oggetti mette a disposizione anche meccanismi per definire nuovi tipi di dato, basati sul concetto di classe (vedremo in dettaglio questo meccanismo nel secondo modulo di questo corso).

Una classe definisce un insieme di oggetti (per esempio i conti bancari), cioè definisce (descrive)

  • la struttura che gli oggetti devono avere,
  • le operazioni che possono operare su di essi.

Si possono creare oggetti di una classe con la primitiva new.

Ogni diverso oggetto, detto anche istanza, della classe ha la struttura descritta nella classe e può essere manipolato invocando su di essi i metodi di istanza definiti nella classe.

Ogni classe definisce un tipo di dato, i cui elementi sono le sue istanze. Quindi possiamo dichiarare variabili il cui tipo è una classe, e potremo assegnargli solo oggetti di quella classe.

Il linguaggio Java oltre a fornire i meccanismi necessari per definire nuove classi (questo però lo vediamo poi), mette a disposizione del programmatore un gran numero di classi predefinite (queste invece dobbiamo imparare a usarle adesso).

Riassumendo quindi, i dati che possono essere manipolati in un programma Java sono di due tipi:

  • elementi dei tipi di dato primitivi, e

  • gli oggetti (elementi delle classi).

N.B. Un linguaggio orientato agli oggetti puro avrebbe solo classi e oggetti ....




Esempio di oggetti: rettangoli

Tra le centinaia di classi fornite da Java, la classe java.awt.Rectangle definisce oggetti che rappresentano rettangoli.

La classe java.awt.Rectangle descrive il modo con il quale sarà rappresentato ciascun rettangolo: usando due variabili per descrivere la posizione nel piano del suo vertice superiore sinistro (x e y) e due variabili per descrivere la sua dimensione (height e width). Si noti che questa è una scelta realizzativa.

Ogni rettangolo (istanza della classe) sarà quindi rappresentato usando quattro variabili d'istanza.

Ad esempio, in un programma possiamo creare tre rettangoli con opportuni valori per le variabili d'istanza. Possiamo visualizzare ogni oggetto come uno scatolotto avente in alto il nome della classe, e sotto le variabili d'istanza.

Rappresentazione grafica dei tre oggetti a sinistra.




Metodi di istanza

Una classe definisce sia il modo in cui rappresentiamo una determinata collezione di oggetti sia le operazioni per manipolare tali oggetti (tramite i metodi di istanza) .

Se vogliamo rappresentare un conto corrente, ad esempio, le operazioni che possiamo voler definire possono essere:

  • leggere il saldo del conto,
  • prelevare una cifra dal conto,
  • depositare una cifra sul conto,
  • modificare il tasso di interesse applicato.

Nel caso dei rettangoli un' operazione che può essere utile per manipolarli è l'operazione che permette di translare (spostare) un rettangolo nel piano.

La classe Rectangle [locale, Medialab, Sun] descrive sia il modo in cui costruire un rettangolo con i valori desiderati per la variabili d'istanza sia un metodo d'istanza translate che sposta il rettangolo nel piano (assieme a molti altri).

Il metodo translate ha due parametri formali che rappresentano lo spostamento lungo l'asse x e quello lungo l'asse y.




Un semplice programma

Supponiamo adesso di voler rappresentare un rettangolo di coordinate x = 5 e y = 10 e di dimensioni height = 20 e width = 30 e di volerlo spostare di 15 unità lungo l'asse x e di 25 lungo l'asse y;

Scriviamo quindi un programma che crea un oggetto (un particolare rettangolo) con le dimensioni desiderate e successivamente lo sposta come desiderato.

I passi da intraprendere sono:

  • Creare un rettangolo di coordinate x = 5 e y = 10 e di dimensioni height = 20 e width = 30;

  • Stampare lo stato del rettangolo;

  • Spostare il rettangolo di 15 unità lungo l'asse x e di 25 lungo l'asse y;

  • Stampare il nuovo stato del rettangolo.

Il programma risultante è il seguente:

import java.awt.Rectangle;

public class MoveRectangle {

    public static void main(String[] args) {
        Rectangle rect;
        rect = new Rectangle(5, 10, 20, 30);
        System.out.println(rect);
        rect.translate(15, 25);
        System.out.println(rect);
   }

}




Analizziamo il programma

Dichiariamo di voler usare la classe Rectangle del package java.awt:
import java.awt.Rectangle;
     ...

Dichiariamo una variabile rect di tipo Rectangle e le assegnamo un nuovo oggetto con i valori desiderati per le variabili d'istanza:
     ...
public class MoveRectangle {

    public static void main(String[] args) {
        Rectangle rect;
        rect = new Rectangle(5, 10, 20, 30);
     ...

Con new Rectangle(5, 10, 20, 30) creiamo un nuovo oggetto (istanza) della classe Rectangle con i valori iniziali desiderati.

Dopo l'assegnamento rect= new Rectangle(5, 10, 20, 30) possiamo riferirci al nuovo oggetto creato usando il nome rect. In particolare l'effetto dell'assegnamento consiste nell'assegnare un riferimento al nuovo oggetto creato alla variabile rect.


Stampiamo lo stato del rettangolo:
     ...
        System.out.println(rect);
     ...

Output: java.awt.Rectangle[x=5,y=10,width=20,height=30]

Invochiamo il metodo d'istanza translate sull'oggetto rect per spostarlo della quantità voluta:
     ...
        rect.translate(15, 25);
     ...


Infine stampiamo lo stato finale:
     ...
        System.out.println(rect);
   }
}

Output: java.awt.Rectangle[x=20,y=35,width=20,height=30]





Dichiarazione e creazione di oggetti

Abbiamo visto che i dati che vengono manipolati in un programma Java sono divisi in due categorie:

  • gli elementi dei tipi di dati primitivi
  • gli oggetti.

Le variabili corrispondenti a queste due categorie di dati hanno comportamenti diversi.

Una variabile che si riferisce a un dato primitivo contiene direttamente il valore del dato, mentre una variabile che si riferisce a un oggetto contiene solo un riferimento alla zona di memoria dove si trova l'oggetto.

double capitale;
capitale
 
La dichiarazione della variabile capitale alloca la memoria necessaria per contenere un double.
Rectangle  rect;
rect
 
La dichiarazione della variabile rect alloca la memoria necessaria per un riferimento ad un'oggetto.
capitale = 1756;
capitale
1756
Il valore 1756 viene scritto nella variabile capitale.
rect =  new  Rectangle(5, 10, 20, 30)
Viene creato l'oggetto di tipo Rectangle (con i valori per le variabili di istanza richiesti) e nella variabile rect viene scritto un riferimento ad esso.



Assegnamento di oggetti

Un effetto del diverso comportamento delle variabili di tipi primitivi e di oggetti si riscontra nell'assegnamento tra variabili.

Un assegnamento tra variabili di tipi primitivi causa una copia del valore.

double capitale = 1756;

double altroCapitale;

capitale
1756

altroCapitale
 

 
altroCapitale = capitale;
capitale
1756

altroCapitale
1756

Il valore 1756 viene copiato nella variabile altroCapitale.  
capitale = capitale * 2;
capitale
3512

altroCapitale
1756

La variabile capitale viene aggiornata con il valore 3512

Invece un assegnamento tra variabili di tipi oggetto causa una copia del riferimento, e quindi una condivisione dell'oggetto.

Rectangle rect =  new  Rectangle(5, 10, 20, 30);

Rectangle altroRect;

 
altroRect = rect;
Non viene copiato l'oggetto, ma solo il riferimento ad esso.
rect.translate(15,25);
L'oggetto a cui fa riferimento rect viene spostato: vengono aggiornate le variabili di istanza dell'oggetto a cui rect fa riferimento. Di conseguenza anche altroRect viene traslato.



Confronto tra oggetti

Una conseguenza del fatto che quando assegniamo un oggetto ad una variabile l'effetto è quello di copiare in tale variabile un riferimento all'oggetto, è che l'opearatore di confronto "==" non si comporta (con gli oggetti) come uno si potrebbe aspettare....

Infatti (obj1 == obj2) vale true solo se obj1 e obj2 sono (riferimenti al) lo stesso oggetto

Esempio:


Rectangle rectUno = new Rectangle(5,10,20,30);
Rectangle rectDue = rectUno; 
Rectangle rectTre = new Rectangle(5,10,20,30);
 
 
boolean b = (rectUno == rectDue);
b = true
boolean b1 = (rectUno == rectTre);
b1 = false

Il modo per confrontare due oggetti (e non i loro riferimenti) è quello di utilizzare il metodo di istanza equals che è definito (con le modalità qui descritte) per tutte le classi della libreria di Java che vedremo in questo semestre.

obj1.equals(obj2) normalmente vale true se i due oggetti hanno "la stessa struttura" (anche se non coincidono fisicamente in memoria).

Esempio:

boolean b2 = rectUno.equals(rectTre);
b2 = true

Parlaremo ancora del diverso comportamento delle variabili di tipi primitivi e di oggetti a proposito del passaggio dei parametri.