Definizione di Classi in Java
- Le classi in Java definiscono nuovi tipi di dato e forniscono un modello per creare oggetti.
- Una classe contiene variabili d'istanza (o campi) e metodi, che insieme sono detti membri della classe.
- Gli oggetti sono istanze di classi e contengono le proprie copie delle variabili d'istanza.
La forma generale di una classe
L'aspetto più importante da comprendere riguardo a una classe è che essa definisce un nuovo tipo di dato. Una volta definito, questo nuovo tipo può essere utilizzato per creare oggetti di quel tipo. Pertanto, una classe è un modello per un oggetto e un oggetto è un'istanza di una classe. Poiché un oggetto è un'istanza di una classe, i termini oggetto e istanza vengono spesso usati in modo intercambiabile.
Quando si definisce una classe, se ne dichiara la forma e la struttura esatte. Questo avviene specificando i dati che essa contiene e il codice che opera su quei dati. Sebbene classi molto semplici possano contenere solo codice o solo dati, la maggior parte delle classi reali contiene entrambi. Come si vedrà, il codice di una classe definisce l'interfaccia ai suoi dati.
class nomeClasse {
tipo variabileIstanza1;
tipo variabileIstanza2;
// …
tipo variabileIstanzaN;
tipo nomeMetodo1(elencoParametri) {
// corpo del metodo
}
tipo nomeMetodo2(elencoParametri) {
// corpo del metodo
}
// …
tipo nomeMetodoN(elencoParametri) {
// corpo del metodo
}
}
I dati, ovvero le variabili, definiti all'interno di una classe sono detti variabili d'istanza o campi. Il codice è contenuto nei metodi.
Collettivamente, metodi e variabili definiti all'interno di una classe sono detti membri della classe. Nella maggior parte delle classi, le variabili d'istanza vengono manipolate e rese accessibili dai metodi della classe stessa. In generale, quindi, sono i metodi a determinare come possono essere utilizzati i dati di una classe.
Le variabili d'istanza si chiamano così perché ogni istanza della classe, ossia ogni suo oggetto, contiene la propria copia di tali variabili. Di conseguenza, i dati di un oggetto sono separati e unici rispetto ai dati di un altro. Si tornerà presto su questo concetto, che è fondamentale da apprendere fin da subito.
Tutti i metodi condividono la stessa forma generale del metodo main()
impiegato finora. Tuttavia, la maggior parte dei metodi non sarà dichiarata come static
o public
. Si noti che la forma generale di una classe non specifica un metodo main()
: alle classi Java non occorre obbligatoriamente un metodo di questo tipo. Se ne definisce uno soltanto se la classe rappresenta il punto di avvio del programma; inoltre, alcune applicazioni Java non ne richiedono affatto uno.
Esempio di classe
Come già affermato, una classe definisce un nuovo tipo di dato.
Come esempio illustrativo definiamo una classe Scatola
e quindi un nuovo tipo di dato.
class Scatola {
double larghezza;
double altezza;
double profondita;
}
Questa classe definisce un nuovo tipo di dato denominato Scatola
. La classe contiene tre variabili d'istanza: larghezza
, altezza
e profondita
. Queste variabili rappresentano le dimensioni della scatola. Si noti che, in Java, le variabili d'istanza sono sempre dichiarate all'interno della classe, ma al di fuori di qualsiasi metodo.
Questo nome verrà usato per dichiarare oggetti di tipo Scatola
. È importante ricordare che una dichiarazione di classe crea soltanto un modello; non crea oggetti reali. Il codice precedente, quindi, non fa nascere alcun oggetto di tipo Scatola
.
Per creare effettivamente un oggetto Scatola
, si utilizza un'istruzione quale la seguente:
Scatola miaScatola = new Scatola(); // crea un oggetto Scatola chiamato miaScatola
Dopo l'esecuzione di questa istruzione, miaScatola
farà riferimento a un'istanza di Scatola
, assumendo quindi una “realtà” fisica. Non è necessario soffermarsi ora sui dettagli di tale istruzione.
Ogni volta che viene creata un'istanza di una classe, si genera un oggetto che possiede la propria copia di ciascuna variabile d'istanza definita dalla classe. Pertanto, ogni oggetto Scatola
conterrà le proprie variabili larghezza
, altezza
e profondita
. Tali variabili si accedono tramite l'operatore punto (.
), che collega il nome dell'oggetto al nome della variabile d'istanza. Per assegnare alla variabile larghezza
di miaScatola
il valore 100, si può scrivere:
miaScatola.larghezza = 100;
Questa istruzione ordina al compilatore di assegnare alla copia di larghezza
contenuta in miaScatola
il valore 100. L'operatore punto si usa, in generale, tanto per accedere alle variabili d'istanza quanto ai metodi di un oggetto.
Un'ultima nota: benché comunemente denominato operatore punto, la specifica formale di Java classifica il simbolo .
come separatore. Tuttavia, l'espressione operatore punto è talmente diffusa da essere impiegata anche qui.
Di seguito un programma completo che utilizza la classe Scatola
:
/* Un programma che utilizza la classe Scatola.
Chiamare questo file DimostrazioneScatola.java
*/
class Scatola {
double larghezza;
double altezza;
double profondita;
}
// Questa classe dichiara un oggetto di tipo Scatola.
class DimostrazioneScatola {
public static void principale(String[] args) {
Scatola miaScatola = new Scatola();
double volume;
// assegna valori alle variabili d'istanza di miaScatola
miaScatola.larghezza = 10;
miaScatola.altezza = 20;
miaScatola.profondita = 15;
// calcola il volume della scatola
volume = miaScatola.larghezza * miaScatola.altezza * miaScatola.profondita;
System.out.println("Il volume è " + volume);
}
}
Si dovrebbe chiamare il file che contiene questo programma DimostrazioneScatola.java, poiché il metodo main()
si trova nella classe denominata DimostrazioneScatola, non nella classe denominata Scatola.
Quando il programma viene compilato, si noterà che sono stati creati due file .class
, uno per Scatola e uno per DimostrazioneScatola. Il compilatore Java inserisce automaticamente ogni classe nel proprio file .class
. Non è necessario che le classi Scatola e DimostrazioneScatola si trovino effettivamente nello stesso file sorgente; ognuna potrebbe stare nel proprio file, rispettivamente Scatola.java e DimostrazioneScatola.java.
Per eseguire questo programma occorre avviare DimostrazioneScatola.class. L'esecuzione produrrà il seguente output:
$ java DimostrazioneScatola
Il volume è 3000.0
Come già affermato, ogni oggetto possiede le proprie copie delle variabili d'istanza. Ciò significa che, disponendo di due oggetti Scatola
, ciascuno avrà la propria copia di profondita
, larghezza
e altezza
. È importante comprendere che le modifiche alle variabili d'istanza di un oggetto non influiscono su quelle di un altro. Ad esempio, il programma seguente dichiara due oggetti Scatola:
// Questo programma dichiara due oggetti Scatola.
class Scatola {
double larghezza;
double altezza;
double profondita;
}
class DimostrazioneScatola2 {
public static void principale(String[] args) {
Scatola miaScatola1 = new Scatola();
Scatola miaScatola2 = new Scatola();
double volume;
// assegna valori alle variabili d'istanza di miaScatola1
miaScatola1.larghezza = 10;
miaScatola1.altezza = 20;
miaScatola1.profondita = 15;
/* assegna valori differenti alle variabili
d'istanza di miaScatola2 */
miaScatola2.larghezza = 3;
miaScatola2.altezza = 6;
miaScatola2.profondita = 9;
// calcola il volume della prima scatola
volume = miaScatola1.larghezza * miaScatola1.altezza * miaScatola1.profondita;
System.out.println("Il volume è " + volume);
// calcola il volume della seconda scatola
volume = miaScatola2.larghezza * miaScatola2.altezza * miaScatola2.profondita;
System.out.println("Il volume è " + volume);
}
}
L'output generato da questo programma è il seguente:
Il volume è 3000.0
Il volume è 162.0
Come si può vedere, i dati di miaScatola1
sono completamente separati dai dati contenuti in miaScatola2
.
Dichiarare oggetti
Come appena spiegato, creando una classe si crea un nuovo tipo di dato, che può essere usato per dichiarare oggetti di quel tipo.
Ottenere oggetti di una classe richiede però due passaggi. Prima, si deve dichiarare una variabile del tipo della classe. Questa variabile non definisce un oggetto; è semplicemente una variabile che può fare riferimento a un oggetto.
Secondo, si deve acquisire una copia fisica dell'oggetto e assegnarla a tale variabile. Ciò si ottiene usando l'operatore new
. L'operatore new
alloca dinamicamente (ovvero in fase di esecuzione) la memoria per un oggetto e restituisce un riferimento ad esso. Questo riferimento è, in sostanza, l'indirizzo di memoria dell'oggetto allocato da new
, che viene poi memorizzato nella variabile. In Java, pertanto, tutti gli oggetti di classe devono essere allocati dinamicamente. Esaminiamo ora i dettagli della procedura.
Nei programmi di esempio precedenti, viene usata una riga simile alla seguente per dichiarare un oggetto di tipo Scatola
:
Scatola miaScatola = new Scatola();
Questa istruzione combina i due passaggi appena descritti. Può essere riscritta così, per mostrare più chiaramente ogni fase:
Scatola miaScatola; // dichiara riferimento a oggetto
miaScatola = new Scatola(); // alloca un oggetto Scatola
La prima riga dichiara miaScatola
come riferimento a un oggetto di tipo Scatola
. A questo punto, miaScatola
non fa ancora riferimento a un oggetto reale.
La riga successiva alloca un oggetto e assegna un riferimento a esso nella variabile miaScatola
. Dopo l'esecuzione della seconda riga, miaScatola
può essere usata come se fosse un oggetto Scatola
. In realtà, miaScatola contiene, in sostanza, l'indirizzo di memoria dell'effettivo oggetto Scatola. La sequenza di eventi appena descritta è illustrata nella figura che segue:
L'operatore new
Come già spiegato, l'operatore new
alloca dinamicamente memoria per un oggetto.
Nel contesto di un'assegnazione assume la forma generale seguente:
varClasse = new nomeClasse();
Qui, varClasse
è una variabile del tipo di classe che si sta creando. nomeClasse
è il nome della classe che viene istanziata.
Il nome della classe seguito da parentesi specifica il costruttore della classe. Un costruttore definisce ciò che avviene quando un oggetto della classe viene creato. I costruttori sono una parte fondamentale di tutte le classi e possiedono numerosi attributi importanti.
La maggior parte delle classi reali definisce esplicitamente i propri costruttori all'interno della definizione di classe. Tuttavia, se non viene specificato alcun costruttore, Java fornirà automaticamente un costruttore predefinito. È il caso di Scatola
. Per il momento si userà il costruttore predefinito; più avanti si vedrà come definire costruttori personalizzati.
A questo punto potrebbe sorgere il dubbio sul motivo per cui new
non è necessario per tipi quali interi o caratteri. La risposta è che i tipi primitivi di Java non sono implementati come oggetti, bensì come variabili “normali”, per ragioni di efficienza.
Gli oggetti, infatti, dispongono di molte funzionalità e attributi che richiedono un trattamento differente rispetto ai tipi primitivi; evitando di applicare ai primitivi l'overhead previsto per gli oggetti, Java può implementarli con maggiore efficienza. Più avanti saranno presentate versioni oggetto dei tipi primitivi, utili nei casi in cui servano oggetti completi di tali tipi.
È importante comprendere che new
alloca memoria per un oggetto durante l'esecuzione. Il vantaggio è che il programma può creare tanti oggetti quanti ne occorrono, o nessuno, in base alle necessità. Tuttavia, la memoria è finita: è possibile che new
non riesca ad allocare memoria perché insufficiente, generando un'eccezione a run-time (la gestione delle eccezioni sarà trattata nelle lezioni future).
Si riveda la distinzione fra classe e oggetto. Una classe crea un nuovo tipo di dato che può essere usato per creare oggetti, ovvero fornisce lo schema logico che definisce le relazioni fra i suoi membri. Dichiarare un oggetto di una classe significa creare un'istanza di quella classe. La classe, quindi, è un costrutto logico; l'oggetto ha realtà fisica (occupa spazio in memoria). È importante mantenere chiara questa distinzione.
Assegnare variabili di riferimento a oggetti
Le variabili di riferimento a oggetti si comportano in modo diverso da quanto ci si potrebbe attendere durante un'assegnazione. Si consideri il frammento seguente:
Scatola s1 = new Scatola();
Scatola s2 = s1;
Si potrebbe pensare che s2
riceva un riferimento a una copia dell'oggetto a cui fa riferimento s1
, e quindi che s1
e s2
si riferiscano a oggetti distinti. Non è così: dopo l'esecuzione del frammento, s1
e s2
faranno riferimento allo stesso oggetto. L'assegnazione di s1
a s2
non alloca memoria né copia alcuna parte dell'oggetto originale; fa semplicemente sì che s2
riferisca lo stesso oggetto di s1
. Di conseguenza, qualsiasi modifica apportata all'oggetto tramite s2
influenzerà l'oggetto a cui fa riferimento s1
, poiché si tratta dello stesso oggetto:
Sebbene s1
e s2
riferiscano lo stesso oggetto, non sono collegate in altro modo. Un'assegnazione successiva a s1 staccherà s1 dall'oggetto originale senza modificare l'oggetto né influenzare s2. Per esempio:
Scatola s1 = new Scatola();
Scatola s2 = s1;
// ...
s1 = null;
In questo caso, s1
viene impostata a null
, ma s2
continua a puntare all'oggetto originale.
Riferimenti a oggetti
Assegnare una variabile di riferimento a oggetti a un'altra variabile di riferimento non crea una copia dell'oggetto; si crea solo una copia del riferimento.