3. Classi e interfacce
3.1. L'argomento spiegato con esempi
3.1.1. Panoramica
Esploreremo ora la programmazione orientata agli oggetti attraverso alcuni esempi. Un oggetto è un'entità che contiene dati che ne definiscono lo stato (chiamati attributi o proprietà) e funzioni (chiamate metodi). Un oggetto viene creato sulla base di un modello chiamato classe:
public class C1{
type1 p1; // property p1
type2 p2; // property p2
…
type3 m3(…){ // m3 method
…
}
type4 m4(…){ // m4 method
…
}
…
}
Dalla classe precedente C1, possiamo creare molti oggetti O1, O2, … Tutti avranno le proprietà p1, p2, … e i metodi m3, m4, … Avranno valori diversi per le loro proprietà pi, quindi ciascuno avrà il proprio stato.
Se O1 è un oggetto di tipo C1, allora O1.p1 si riferisce alla proprietà p1 di O1, e O1.m1 si riferisce al metodo m1 di O1.
Consideriamo un primo modello a oggetti: la classe Person.
3.1.2. Definizione della classe Person
La definizione della classe Person è la seguente:
import java.io.*;
public class personne{
// attributes
private String prenom;
private String nom;
private int age;
// method
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// method
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Qui abbiamo la definizione di una classe, che è un tipo di dati. Quando creiamo variabili di questo tipo, le chiamiamo oggetti. Una classe è quindi un modello a partire dal quale vengono costruiti gli oggetti.
I membri o campi di una classe possono essere dati o metodi (funzioni). Questi campi possono avere uno dei seguenti tre attributi:
private: un campo privato è accessibile solo dai metodi interni della classe
public: un campo pubblico è accessibile da qualsiasi funzione, indipendentemente dal fatto che sia definita all'interno della classe
protected: un campo protected è accessibile solo dai metodi interni della classe o da un oggetto derivato (vedere il concetto di ereditarietà più avanti).
In genere, i dati di una classe sono dichiarati privati, mentre i suoi metodi sono dichiarati pubblici. Ciò significa che l’utente di un oggetto (il programmatore)
a: non avrà accesso diretto ai dati privati dell'oggetto
b: potrà chiamare i metodi pubblici dell'oggetto, compresi quelli che forniscono l'accesso ai suoi dati privati.
La sintassi per dichiarare un oggetto è la seguente:
public class nomClasse{
private donnée ou méthode privée
public donnée ou méthode publique
protected donnée ou méthode protégée
}
Note
- L'ordine in cui vengono dichiarati gli attributi privati, protetti e pubblici è arbitrario.
3.1.3. Il metodo initialize
Torniamo alla nostra classe Person dichiarata come:
import java.io.*;
public class personne{
// attributes
private String prenom;
private String nom;
private int age;
// method
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// method
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
Qual è il ruolo del metodo initialize? Poiché lastName, firstName e age sono membri privati della classe Person, le istruzioni
non sono validi. Dobbiamo inizializzare un oggetto di tipo Person utilizzando un metodo pubblico. Questo è il ruolo del metodo initialize. Scriviamo:
La sintassi p1.initialize è valida perché initialize è pubblico.
3.1.4. L'operatore new
La sequenza di istruzioni
è errata. L'affermazione
dichiara p1 come riferimento a un oggetto di tipo persona. Questo oggetto non esiste ancora, quindi p1 non viene inizializzato. È come se avessimo scritto:
dove indichiamo esplicitamente con la parola chiave null che la variabile p1 non fa ancora riferimento ad alcun oggetto.
Quando poi scriviamo
stiamo chiamando il metodo initialize dell'oggetto a cui fa riferimento p1. Tuttavia, questo oggetto non esiste ancora e il compilatore segnalerà un errore. Affinché p1 faccia riferimento a un oggetto, dobbiamo scrivere:
Questo crea un oggetto Person non inizializzato: gli attributi name e first_name, che sono riferimenti a oggetti String, avranno il valore null, mentre age avrà il valore 0. Esiste quindi un'inizializzazione predefinita. Ora che p1 fa riferimento a un oggetto, l'istruzione di inizializzazione per questo oggetto
è valida.
3.1.5. La parola chiave this
Diamo un'occhiata al codice del metodo initialize:
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
L'istruzione this.firstName = P significa che all'attributo firstName dell'oggetto corrente (this) viene assegnato il valore P. La parola chiave this si riferisce all'oggetto corrente: quello in cui risiede il metodo in esecuzione. Come lo sappiamo? Vediamo come viene inizializzato l'oggetto a cui fa riferimento p1 nel programma chiamante:
Viene chiamato il metodo initialize dell'oggetto p1. Quando facciamo riferimento all'oggetto this all'interno di questo metodo, in realtà stiamo facendo riferimento all'oggetto p1. Il metodo initialize avrebbe potuto essere scritto anche come segue:
public void initialise(String P, String N, int age){
prenom=P;
nom=N;
this.age=age;
}
Quando un metodo di un oggetto fa riferimento a un attributo A di quell'oggetto, la notazione this.A è implicita. Deve essere utilizzata esplicitamente quando c'è un conflitto di identificatori. Questo è il caso dell'istruzione:
this.age=age;
dove age si riferisce sia a un attributo dell'oggetto corrente che al parametro age ricevuto dal metodo. L'ambiguità deve quindi essere risolta facendo riferimento all'attributo age come this.age.
3.1.6. Un programma di prova
Ecco un programma di prova:
public class test1{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
p1.identifie();
}
}
La classe Person è definita nel file sorgente person.java ed è compilata:
E:\data\serge\JAVA\BASES\OBJETS\2>javac personne.java
E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002 09:21 473 personne.java
10/06/2002 09:22 835 personne.class
10/06/2002 09:23 165 test1.java
Facciamo lo stesso per il programma di test:
E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java
E:\data\serge\JAVA\BASES\OBJETS\2>dir
10/06/2002 09:21 473 personne.java
10/06/2002 09:22 835 personne.class
10/06/2002 09:23 165 test1.java
10/06/2002 09:25 418 test1.class
Potrebbe sembrare sorprendente che il programma test1.java non importi la classe person con un'istruzione:
Quando il compilatore incontra nel codice sorgente un riferimento a una classe che non è definita nello stesso file sorgente, cerca la classe in varie posizioni:
- nei pacchetti importati dalle istruzioni import
- nella directory da cui è stato avviato il compilatore
Nel nostro esempio, il compilatore è stato avviato dalla directory contenente il file personne.class, il che spiega perché ha trovato la definizione della classe personne. In questo scenario, l'aggiunta di un'istruzione import causa un errore di compilazione:
E:\data\serge\JAVA\BASES\OBJETS\2>javac test1.java
test1.java:1: '.' expected
import personne;
^
1 error
Per evitare questo errore ma garantire che la classe Person venga importata, in futuro scriveremo quanto segue all'inizio del programma:
Ora possiamo eseguire il file test1.class:
È possibile combinare più classi in un unico file sorgente. Combiniamo le classi person* e test1 nel file sorgente test2.java*. La classe test1 viene rinominata test2 per riflettere la modifica nel nome del file sorgente:
// imported packages
import java.io.*;
class personne{
// attributes
private String prenom; // first name
private String nom; // its name
private int age; // his age
// method
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}//initialize
// method
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}//identifies
}//class
public class test2{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
p1.identifie();
}
}
Si noti che la classe Person non ha più l'attributo public. Infatti, in un file sorgente Java, solo una classe può avere l'attributo public. Si tratta della classe che contiene il metodo main. Inoltre, il file sorgente deve prendere il nome da questa classe. Compiliamo il file test2.java:
E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002 09:36 633 test2.java
E:\data\serge\JAVA\BASES\OBJETS\3>javac test2.java
E:\data\serge\JAVA\BASES\OBJETS\3>dir
10/06/2002 09:36 633 test2.java
10/06/2002 09:41 832 personne.class
10/06/2002 09:41 418 test2.class
Si noti che è stato generato un file .class per ciascuna delle classi presenti nel file sorgente. Ora eseguiamo il file test2.class:
D'ora in poi, useremo entrambi i metodi in modo intercambiabile:
- classi raggruppate in un unico file sorgente
- una classe per file sorgente
3.1.7. Un altro metodo inizializza
Continuiamo con la classe Person e aggiungiamo il seguente metodo:
public void initialise(personne P){
prenom=P.prenom;
nom=P.nom;
this.age=P.age;
}
Ora abbiamo due metodi denominati *initialize*: ciò è consentito purché accettino parametri diversi. È proprio questo il caso. Il parametro è ora un riferimento P a una persona. Gli attributi della persona P vengono quindi assegnati all'oggetto corrente (this). Si noti che il metodo initialize ha accesso diretto agli attributi dell'oggetto P anche se sono di tipo privato. Questo è sempre vero: i metodi di un oggetto O1 di una classe C hanno sempre accesso agli attributi privati di altri oggetti della stessa classe C.
Ecco un test della nuova classe Person:
// import nobody;
import java.io.*;
public class test1{
public static void main(String arg[]){
personne p1=new personne();
p1.initialise("Jean","Dupont",30);
System.out.print("p1=");
p1.identifie();
personne p2=new personne();
p2.initialise(p1);
System.out.print("p2=");
p2.identifie();
}
}
e i relativi risultati:
3.1.8. Costruttori della classe Person
Un costruttore è un metodo che porta il nome della classe e viene chiamato quando l'oggetto viene creato. Viene generalmente utilizzato per inizializzare l'oggetto. È un metodo che può accettare argomenti ma non restituisce alcun risultato. Il suo prototipo o definizione non è preceduto da alcun tipo (nemmeno void).
Se una classe ha un costruttore che accetta n argomenti args, la dichiarazione e l'inizializzazione di un oggetto di quella classe possono essere effettuate come segue:
classe objet =new classe(arg1,arg2, ... argn);
oppure
classe objet;
…
objet=new classe(arg1,arg2, ... argn);
Quando una classe ha uno o più costruttori, uno di questi deve essere utilizzato per creare un oggetto di quella classe. Se una classe C non ha costruttori, ha un costruttore predefinito, ovvero il costruttore senza parametri: public C(). Gli attributi dell'oggetto vengono quindi inizializzati con i valori predefiniti. Questo è ciò che è successo quando, nei programmi precedenti, abbiamo scritto:
Creiamo due costruttori per la nostra classe Person:
public class personne{
// attributes
private String prenom;
private String nom;
private int age;
// manufacturers
public personne(String P, String N, int age){
initialise(P,N,age);
}
public personne(personne P){
initialise(P);
}
// method
public void initialise(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
public void initialise(personne P){
this.prenom=P.prenom;
this.nom=P.nom;
this.age=P.age;
}
// method
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
}
I nostri due costruttori chiamano semplicemente i corrispondenti metodi initialize. Ricordiamo che quando, in un costruttore, troviamo la notazione initialize(P), ad esempio, il compilatore la traduce in this.initialize(P). Nel costruttore, il metodo initialize viene quindi chiamato per operare sull'oggetto a cui fa riferimento this, ovvero l'oggetto corrente, quello che si sta costruendo.
Ecco un programma di prova:
// import nobody;
import java.io.*;
public class test1{
public static void main(String arg[]){
personne p1=new personne("Jean","Dupont",30);
System.out.print("p1=");
p1.identifie();
personne p2=new personne(p1);
System.out.print("p2=");
p2.identifie();
}
}
e i risultati ottenuti:
3.1.9. Riferimenti agli oggetti
Stiamo ancora utilizzando la stessa classe Person. Il programma di test diventa il seguente:
// import nobody;
import java.io.*;
public class test1{
public static void main(String arg[]){
// p1
personne p1=new personne("Jean","Dupont",30);
System.out.print("p1="); p1.identifie();
// p2 references the same object as p1
personne p2=p1;
System.out.print("p2="); p2.identifie();
// p3 references an object that will be a copy of the object referenced by p1
personne p3=new personne(p1);
System.out.print("p3="); p3.identifie();
// change the state of the object referenced by p1
p1.initialise("Micheline","Benoît",67);
System.out.print("p1="); p1.identifie();
// as p2=p1, the object referenced by p2 must have changed state
System.out.print("p2="); p2.identifie();
// as p3 does not reference the same object as p1, the object referenced by p3 must not have changed
System.out.print("p3="); p3.identifie();
}
}
I risultati sono i seguenti:
p1=Jean,Dupont,30
p2=Jean,Dupont,30
p3=Jean,Dupont,30
p1=Micheline,Benoît,67
p2=Micheline,Benoît,67
p3=Jean,Dupont,30
Quando si dichiara la variabile p1 utilizzando
p1 fa riferimento all'oggetto personne("Jean","Dupont",30) ma non è l'oggetto stesso. In C, diremmo che si tratta di un puntatore, ovvero l'indirizzo dell'oggetto creato. Se poi scriviamo:
non è l'oggetto person("Jean","Dupont",30) a essere modificato, bensì è il riferimento p1 a cambiare il proprio valore. L'oggetto person("Jean","Dupont",30) andrà "perso" se non è referenziato da nessun'altra variabile.
Quando scriviamo:
inizializziamo il puntatore p2: esso "punta" allo stesso oggetto (fa riferimento allo stesso oggetto) del puntatore p1. Pertanto, se modifichiamo l'oggetto "indicato" (o a cui fa riferimento) da p1, modifichiamo quello a cui fa riferimento p2.
Quando scriviamo:
viene creato un nuovo oggetto, che è una copia dell'oggetto a cui fa riferimento p1. Questo nuovo oggetto sarà referenziato da p3. Se modifichi l'oggetto "indicato" (o referenziato) da p1, non modifichi in alcun modo quello referenziato da p3. Questo è ciò che mostrano i risultati.
3.1.10. Oggetti temporanei
In un'espressione, è possibile chiamare esplicitamente il costruttore di un oggetto: l'oggetto viene creato, ma non è possibile accedervi (ad esempio per modificarlo). Questo oggetto temporaneo viene creato allo scopo di valutare l'espressione e poi scartato. Lo spazio di memoria che occupava verrà automaticamente recuperato in seguito da un programma chiamato "garbage collector", il cui ruolo è quello di recuperare lo spazio di memoria occupato da oggetti che non sono più referenziati dai dati del programma.
Si consideri il seguente esempio:
// import nobody;
public class test1{
public static void main(String arg[]){
new personne(new personne("Jean","Dupont",30)).identifie();
}
}
e modifichiamo i costruttori della classe Person in modo che visualizzino un messaggio:
// manufacturers
public personne(String P, String N, int age){
System.out.println("Constructeur personne(String, String, int)");
initialise(P,N,age);
}
public personne(personne P){
System.out.println("Constructeur personne(personne)");
initialise(P);
}
Otteniamo i seguenti risultati:
che mostra la costruzione successiva dei due oggetti temporanei.
3.1.11. Metodi per la lettura e la scrittura degli attributi privati
Aggiungiamo alla classe Person i metodi necessari per leggere o modificare lo stato degli attributi degli oggetti:
public class personne{
private String prenom;
private String nom;
private int age;
public personne(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
public personne(personne P){
this.prenom=P.prenom;
this.nom=P.nom;
this.age=P.age;
}
public void identifie(){
System.out.println(prenom+","+nom+","+age);
}
// accessors
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//modifiers
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
Testiamo la nuova classe con il seguente programma:
// import nobody;
public class test1{
public static void main(String[] arg){
personne P=new personne("Jean","Michelin",34);
System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
P.setAge(56);
System.out.println("P=("+P.getPrenom()+","+P.getNom()+","+P.getAge()+")");
}
}
e otteniamo i seguenti risultati:
3.1.12. Metodi e attributi di classe
Supponiamo di voler contare il numero di oggetti Person creati in un'applicazione. Potremmo gestire noi stessi un contatore, ma rischiamo di dimenticare oggetti temporanei creati qua e là. Sembrerebbe più sicuro includere un'istruzione nei costruttori della classe Person che incrementi un contatore. Il problema è passare un riferimento a questo contatore in modo che il costruttore possa incrementarlo: dobbiamo passare loro un nuovo parametro. Possiamo anche includere il contatore nella definizione della classe. Poiché si tratta di un attributo della classe stessa e non di un particolare oggetto di quella classe, lo dichiariamo in modo diverso utilizzando la parola chiave static:
Per farvi riferimento, scriviamo person.nbPeople per indicare che si tratta di un attributo della classe Person stessa. In questo caso, abbiamo creato un attributo privato a cui non è possibile accedere direttamente dall'esterno della classe. Creiamo quindi un metodo pubblico per fornire l'accesso all'attributo di classe nbPersonnes. Per restituire il valore di nbPersonnes, il metodo non necessita di un oggetto specifico: infatti, nbPersonnes non è l'attributo di un oggetto specifico; è l'attributo dell'intera classe. Pertanto, abbiamo bisogno di un metodo di classe che sia anche dichiarato statico:
che verrà chiamato dall'esterno utilizzando la sintassi person.getNbPeople(). Ecco un esempio.
La classe Person diventa la seguente:
public class personne{
// class attribute
private static long nbPersonnes=0;
// object attributes
…
// manufacturers
public personne(String P, String N, int age){
initialise(P,N,age);
nbPersonnes++;
}
public personne(personne P){
initialise(P);
nbPersonnes++;
}
// method
…
// class method
public static long getNbPersonnes(){
return nbPersonnes;
}
}// class
Con il seguente programma:
// import nobody;
public class test1{
public static void main(String arg[]){
personne p1=new personne("Jean","Dupont",30);
personne p2=new personne(p1);
new personne(p1);
System.out.println("Nombre de personnes créées : "+personne.getNbPersonnes());
}// hand
}//test1
Otteniamo i seguenti risultati:
3.1.13. Passaggio di un oggetto a una funzione
Abbiamo già menzionato che Java passa i parametri effettivi della funzione per valore: i valori dei parametri effettivi vengono copiati nei parametri formali. Una funzione non può quindi modificare i parametri effettivi.
Nel caso di un oggetto, non bisogna lasciarsi fuorviare dall'uso improprio del linguaggio che si verifica quando le persone si riferiscono sistematicamente a un "oggetto" invece che a un "riferimento a un oggetto". Un oggetto viene manipolato solo tramite un riferimento (un puntatore) ad esso. Ciò che viene passato a una funzione, quindi, non è l'oggetto stesso ma un riferimento a quell'oggetto. È quindi il valore del riferimento — non il valore dell'oggetto stesso — che viene copiato nel parametro formale: non viene creato alcun nuovo oggetto.
Se un riferimento a un oggetto R1 viene passato a una funzione, verrà copiato nel corrispondente parametro formale R2. Pertanto, i riferimenti R2 e R1 puntano allo stesso oggetto. Se la funzione modifica l'oggetto a cui punta R2, ovviamente modifica anche quello a cui fa riferimento R1, poiché sono lo stesso.

Ciò è illustrato dal seguente esempio:
// import nobody;
public class test1{
public static void main(String arg[]){
personne p1=new personne("Jean","Dupont",30);
System.out.print("Paramètre effectif avant modification : ");
p1.identifie();
modifie(p1);
System.out.print("Paramètre effectif après modification : ");
p1.identifie();
}// hand
private static void modifie(personne P){
System.out.print("Paramètre formel avant modification : ");
P.identifie();
P.initialise("Sylvie","Vartan",52);
System.out.print("Paramètre formel après modification : ");
P.identifie();
}// modify
}// class
Il metodo modify è dichiarato statico perché è un metodo di classe: non è necessario anteporgli un oggetto per chiamarlo. I risultati ottenuti sono i seguenti:
Constructeur personne(String, String, int)
Paramètre effectif avant modification : Jean,Dupont,30
Paramètre formel avant modification : Jean,Dupont,30
Paramètre formel après modification : Sylvie,Vartan,52
Paramètre effectif après modification : Sylvie,Vartan,52
Possiamo vedere che viene costruito un solo oggetto: quello della persona p1 nella funzione principale, e che l'oggetto è stato effettivamente modificato dalla funzione modify.
3.1.14. Incapsulamento dei parametri di output di una funzione in un oggetto
Poiché i parametri vengono passati per valore, non è possibile scrivere una funzione Java con parametri di output di tipo int, ad esempio, poiché non possiamo passare un riferimento a un tipo int che non sia un oggetto. Possiamo quindi creare una classe che incapsuli il tipo int:
public class entieres{
private int valeur;
public entieres(int valeur){
this.valeur=valeur;
}
public void setValue(int valeur){
this.valeur=valeur;
}
public int getValue(){
return valeur;
}
}
La classe precedente dispone di un costruttore per l'inizializzazione di un numero intero e di due metodi per la lettura e la modifica del valore di tale numero intero. Testiamo questa classe con il seguente programma:
// import integer;
public class test2{
public static void main(String[] arg){
entieres I=new entieres(12);
System.out.println("I="+I.getValue());
change(I);
System.out.println("I="+I.getValue());
}
private static void change(entieres entier){
entier.setValue(15);
}
}
e otteniamo i seguenti risultati:
3.1.15. Un gruppo di persone
Un oggetto è un dato come un altro e, in quanto tale, più oggetti possono essere raggruppati in un array:
// import nobody;
public class test1{
public static void main(String arg[]){
personne[] amis=new personne[3];
System.out.println("----------------");
amis[0]=new personne("Jean","Dupont",30);
amis[1]=new personne("Sylvie","Vartan",52);
amis[2]=new personne("Neil","Armstrong",66);
int i;
for(i=0;i<amis.length;i++)
amis[i].identifie();
}
}
L'istruzione person[] friends = new person[3]; crea un array di 3 elementi di tipo person. Questi 3 elementi vengono inizializzati qui con il valore null, il che significa che non fanno riferimento ad alcun oggetto. Ancora una volta, in senso tecnico, ci riferiamo a un "array di oggetti" quando in realtà si tratta semplicemente di un array di riferimenti a oggetti. La creazione dell'array di oggetti — un array che è esso stesso un oggetto (come indicato dall'uso di new) — non crea, di per sé, alcun oggetto del tipo dei suoi elementi: ciò deve essere fatto successivamente.
Si ottengono i seguenti risultati:
----------------
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Constructeur personne(String, String, int)
Jean,Dupont,30
Sylvie,Vartan,52
Neil,Armstrong,66
3.2. L'ereditarietà attraverso esempi
3.2.1. Panoramica
Qui discutiamo il concetto di ereditarietà. Lo scopo dell'ereditarietà è quello di "personalizzare" una classe esistente in modo che soddisfi le nostre esigenze. Supponiamo di voler creare una classe Insegnante: un insegnante è un tipo specifico di persona. Ha attributi che altre persone non hanno: la materia che insegna, per esempio. Ma ha anche gli attributi di qualsiasi persona: nome, cognome ed età. Un insegnante è quindi un membro a pieno titolo della classe Persona, ma possiede attributi aggiuntivi. Piuttosto che scrivere una classe Insegnante da zero, preferiremmo partire dalla classe Persona esistente e adattarla alle caratteristiche specifiche degli insegnanti. È il concetto di ereditarietà che ci permette di farlo.
Per esprimere che la classe Insegnante eredita le proprietà della classe Persona, scriveremmo:
public class enseignant extends personne
Person è detta classe padre (o base), mentre Teacher è la classe derivata (o figlia). Un oggetto Teacher possiede tutte le caratteristiche di un oggetto Person: ha gli stessi attributi e metodi. Questi attributi e metodi della classe padre non vengono ripetuti nella definizione della classe figlia; ci limitiamo a specificare gli attributi e i metodi aggiunti dalla classe figlia:
class enseignant extends personne{
// attributes
private int section;
// manufacturer
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
}
Supponiamo che la classe Person sia definita come segue:
public class personne{
private String prenom;
private String nom;
private int age;
public personne(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
public personne(personne P){
this.prenom=P.prenom;
this.nom=P.nom;
this.age=P.age;
}
public String identite(){
return "personne("+prenom+","+nom+","+age+")";
}
// accessors
public String getPrenom(){
return prenom;
}
public String getNom(){
return nom;
}
public int getAge(){
return age;
}
//modifiers
public void setPrenom(String P){
this.prenom=P;
}
public void setNom(String N){
this.nom=N;
}
public void setAge(int age){
this.age=age;
}
}
Il metodo identifiant è stato leggermente modificato per restituire una stringa che identifica la persona e ora si chiama identite. Qui, la classe Teacher si aggiunge ai metodi e agli attributi della classe Person:
- un attributo `section`, che è il numero della sezione a cui appartiene l'insegnante all'interno del corpo docente (in pratica una sezione per materia)
- un nuovo costruttore che inizializza tutti gli attributi di un insegnante
3.2.2. Creazione di un oggetto insegnante
Il costruttore della classe Teacher è il seguente:
// constructeur
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
L'istruzione super(P,N,age) è una chiamata al costruttore della classe padre, in questo caso la classe Person. Sappiamo che questo costruttore inizializza i campi first_name, last_name e age dell'oggetto Person contenuto all'interno dell'oggetto Student. Questo sembra piuttosto complicato, e potremmo preferire scrivere:
// constructeur
public enseignant(String P, String N, int age,int section){
this.prenom=P;
this.nom=N
this.age=age
this.section=section;
}
Questo è impossibile. La classe Person ha dichiarato i suoi tre campi — first_name, last_name e age — come privati. Solo gli oggetti della stessa classe hanno accesso diretto a questi campi. Tutti gli altri oggetti, compresi gli oggetti figli come in questo caso, devono utilizzare metodi pubblici per accedervi. La situazione sarebbe stata diversa se la classe Person avesse dichiarato i tre campi come protetti: in tal caso avrebbe consentito alle classi derivate di avere accesso diretto ai tre campi. Nel nostro esempio, l'utilizzo del costruttore della classe padre era quindi la soluzione corretta, e questo è l'approccio standard: quando si costruisce un oggetto figlio, si chiama prima il costruttore dell'oggetto padre e poi si completano le inizializzazioni specifiche dell'oggetto figlio (sezione nel nostro esempio).
Proviamo un primo programma:
// import nobody;
// import teacher;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
Questo programma crea semplicemente un oggetto Teacher (new) e ne identifica l'identità. La classe Teacher non dispone di un metodo di identificazione, ma la sua classe padre ne possiede uno, anch'esso pubblico: grazie all'ereditarietà, esso diventa un metodo pubblico della classe Teacher.
I file sorgente delle classi vengono collocati nella stessa directory e quindi compilati:
E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002 10:00 765 personne.java
10/06/2002 10:00 212 enseignant.java
10/06/2002 10:01 192 test1.java
E:\data\serge\JAVA\BASES\OBJETS\4>javac *.java
E:\data\serge\JAVA\BASES\OBJETS\4>dir
10/06/2002 10:00 765 personne.java
10/06/2002 10:00 212 enseignant.java
10/06/2002 10:01 192 test1.java
10/06/2002 10:02 316 enseignant.class
10/06/2002 10:02 1 146 personne.class
10/06/2002 10:02 550 test1.class
Il file test1.class viene eseguito:
3.2.3. Sovraccarico dei metodi
Nell'esempio precedente, l'identità della persona faceva parte dell'insegnante, ma mancavano alcune informazioni specifiche della classe Teacher (la sezione). Dobbiamo quindi scrivere un metodo per identificare l'insegnante:
class enseignant extends personne{
int section;
public enseignant(String P, String N, int age,int section){
super(P,N,age);
this.section=section;
}
public String identite(){
return "enseignant("+super.identite()+","+section+")";
}
}
Il metodo identity della classe Teacher si basa sul metodo identity della sua classe padre (super.identity) per visualizzare la parte "person", quindi aggiunge il campo section, che è specifico della classe Teacher.
La classe Teacher dispone ora di due metodi identity:
- quello ereditato dalla classe padre Person
- il proprio
Se E è un oggetto Teacher, E.identity fa riferimento al metodo identity della classe Teacher. Si dice che il metodo identity della classe padre è "sovrascritto" dal metodo identity della classe figlia. In generale, se O è un oggetto e M è un metodo, per eseguire il metodo O.M, il sistema cerca un metodo M nel seguente ordine:
- nella classe dell'oggetto O
- nella sua classe padre, se ne ha una
- nella classe padre della sua classe padre, se esiste
- e così via…
L'ereditarietà consente quindi di sovrascrivere nella classe figlia i metodi con lo stesso nome presenti nella classe padre. È questo che permette alla classe figlia di essere adattata alle proprie esigenze. In combinazione con il polimorfismo, di cui parleremo tra poco, la sovrascrittura dei metodi è il principale vantaggio dell'ereditarietà.
Prendiamo in esame lo stesso esempio di prima:
// import nobody;
// import teacher;
public class test1{
public static void main(String arg[]){
System.out.println(new enseignant("Jean","Dupont",30,27).identite());
}
}
I risultati ottenuti questa volta sono i seguenti:
3.2.4. Polimorfismo
Si consideri una gerarchia di classi: C0 C1 C2 … Cn
dove Ci Cj indica che la classe Cj deriva dalla classe Ci . Ciò implica che la classe Cj possiede tutte le caratteristiche della classe Ci più altre. Siano Oi oggetti di tipo Ci . È corretto scrivere:
Infatti, per eredità, la classe Cj possiede tutte le caratteristiche della classe Ci più quelle aggiuntive. Pertanto, un oggetto Oj di tipo Cj contiene al suo interno un oggetto di tipo Ci. L'operazione
significa che Oi è un riferimento all'oggetto di tipo Ci contenuto nell'oggetto Oj.
Il fatto che una variabile Oi della classe Ci possa in realtà riferirsi non solo a un oggetto della classe Ci, ma a qualsiasi oggetto derivato dalla classe Ci, è chiamato polimorfismo: la capacità di una variabile di riferirsi a diversi tipi di oggetti.
Facciamo un esempio e consideriamo la seguente funzione, che è indipendente da qualsiasi classe:
La classe Object è la "classe padre" di tutte le classi Java. Quindi, quando scriviamo:
stiamo implicitamente scrivendo:
Pertanto, ogni oggetto Java contiene una componente *Object*. Di conseguenza, possiamo scrivere:
Il parametro formale di tipo Object nella funzione display riceverà un valore di tipo Teacher. Poiché Teacher deriva da Object, ciò è corretto.
3.2.5. Sovraccarico e polimorfismo
Completiamo la nostra funzione display:
Il metodo obj.toString() restituisce una stringa che identifica l'oggetto obj nella forma class_name@object_address. Cosa succede nel caso del nostro esempio precedente:
Il sistema deve eseguire l'istruzione System.out.println(e.toString()), dove e è un oggetto Teacher. Cerca un metodo toString nella gerarchia delle classi che porta alla classe Teacher, partendo dall'ultima:
- nella classe Teacher, non trova un metodo toString()
- nella classe padre Person, non trova un metodo toString()
- nella classe padre Object, trova il metodo toString() e lo esegue
Questo è ciò che dimostra il seguente programma:
// import nobody;
// import teacher;
public class test1{
public static void main(String arg[]){
enseignant e=new enseignant("Lucile","Dumas",56,61);
affiche(e);
personne p=new personne("Jean","Dupont",30);
affiche(p);
}
public static void affiche(Object obj){
System.out.println(obj.toString());
}
}
I risultati sono i seguenti:
Cioè, nome_classe@indirizzo_oggetto. Poiché questo non è molto chiaro, potremmo essere tentati di definire un metodo toString per le classi Person e Student che sovrascriva il metodo toString della classe padre Object. Anziché scrivere metodi simili ai metodi di identità già esistenti nelle classi Person e Teacher, rinominiamo semplicemente quei metodi di identità in toString:
public class personne{
...
public String toString(){
return "personne("+prenom+","+nom+","+age+")";
}
...
}
class enseignant extends personne{
int section;
…
public String toString(){
return "enseignant("+super.toString()+","+section+")";
}
}
Utilizzando lo stesso programma di test di prima, i risultati sono i seguenti:
3.3. Classi interne
Una classe può contenere la definizione di un'altra classe. Si consideri il seguente esempio:
// imported classes
import java.io.*;
public class test1{
// internal class
private class article{
// we define the structure
private String code;
private String nom;
private double prix;
private int stockActuel;
private int stockMinimum;
// manufacturer
public article(String code, String nom, double prix, int stockActuel, int stockMinimum){
// attribute initialization
this.code=code;
this.nom=nom;
this.prix=prix;
this.stockActuel=stockActuel;
this.stockMinimum=stockMinimum;
}//manufacturer
//toString
public String toString(){
return "article("+code+","+nom+","+prix+","+stockActuel+","+stockMinimum+")";
}//toString
}//item class
// local data
private article art=null;
// manufacturer
public test1(String code, String nom, double prix, int stockActuel, int stockMinimum){
// attribute definition
art=new article(code, nom, prix, stockActuel,stockMinimum);
}//test1
// accessor
public article getArticle(){
return art;
}//getArticle
public static void main(String arg[]){
// create a test1 instance
test1 t1=new test1("a100","velo",1000,10,5);
// display test1.art
System.out.println("art="+t1.getArticle());
}//hand
}// fin class
La classe test1 contiene la definizione di un'altra classe, la classe article. Si dice che article è una classe interna della classe test1. Ciò può essere utile quando la classe interna è necessaria solo all'interno della classe che la contiene. Compilando il codice sorgente test1.java sopra riportato, si ottengono due file .class:
E:\data\serge\JAVA\classes\interne>dir
05/06/2002 17:26 1 362 test1.java
05/06/2002 17:26 941 test1$article.class
05/06/2002 17:26 1 020 test1.class
È stato generato un file test1$article.class per la classe article, che è interna alla classe test1. Se eseguiamo il programma sopra riportato, otteniamo i seguenti risultati:
3.4. Interfacce
Un'interfaccia è un insieme di prototipi di metodi o proprietà che costituisce un contratto. Una classe che decide di implementare un'interfaccia si impegna a fornire un'implementazione di tutti i metodi definiti nell'interfaccia. Il compilatore verifica tale implementazione.
Ecco un esempio della definizione dell'interfaccia java.util.Enumeration:
Riepilogo dei metodi | ||
boolean | hasMoreElements() Verifica se questa enumerazione contiene altri elementi. | |
Oggetto | nextElement() Restituisce l'elemento successivo di questa enumerazione se l'oggetto di enumerazione ha almeno un altro elemento da fornire. | |
Qualsiasi classe che implementi questa interfaccia sarà dichiarata come
I metodi hasMoreElements() e nextElement() devono essere definiti nella classe C.
Si consideri il seguente codice che definisce una classe studente che specifica il nome di uno studente e il suo voto in una materia:
// a student class
public class élève{
// public attributes
public String nom;
public double note;
// manufacturer
public élève(String NOM, double NOTE){
nom=NOM;
note=NOTE;
}//manufacturer
}//student
Definiamo una classe notes che raccoglie i voti di tutti gli studenti in una materia:
// imported classes
// student import
// class notes
public class notes{
// attributes
protected String matière;
protected élève[] élèves;
// manufacturer
public notes (String MATIERE, élève[] ELEVES){
// student & subject memorization
matière=MATIERE;
élèves=ELEVES;
}//notes
// toString
public String toString(){
String valeur="matière="+matière +", notes=(";
int i;
// concatenate all the notes
for (i=0;i<élèves.length-1;i++){
valeur+="["+élèves[i].nom+","+élèves[i].note+"],";
};
//final note
if(élèves.length!=0){ valeur+="["+élèves[i].nom+","+élèves[i].note+"]";}
valeur+=")";
// end
return valeur;
}//toString
}//class
Gli attributi subject e students sono dichiarati protected in modo da poter essere accessibili da una classe derivata. Decidiamo di derivare la classe notes in una classe notesStats che avrà due attributi aggiuntivi: la media e la deviazione standard dei voti:
public class notesStats extends notes implements Istats {
// attributes
private double _moyenne;
private double _écartType;
La classe notesStats deriva dalla classe notes e implementa la seguente interfaccia Istats:
// an interface
public interface Istats{
double moyenne();
double écartType();
}//
Ciò significa che la classe notesStats deve avere due metodi denominati average e standardDeviation con le firme specificate nell'interfaccia Istats. La classe notesStats è la seguente:
// imported classes
// import notes;
// import Istats;
// student import;
public class notesStats extends notes implements Istats {
// attributes
private double _moyenne;
private double _écartType;
// manufacturer
public notesStats (String MATIERE, élève[] ELEVES){
// parent class construction
super(MATIERE,ELEVES);
// average score calculation
double somme=0;
for (int i=0;i<élèves.length;i++){
somme+=élèves[i].note;
}
if(élèves.length!=0) _moyenne=somme/élèves.length;
else _moyenne=-1;
// standard deviation
double carrés=0;
for (int i=0;i<élèves.length;i++){
carrés+=Math.pow((élèves[i].note-_moyenne),2);
}//for
if(élèves.length!=0) _écartType=Math.sqrt(carrés/élèves.length);
else _écartType=-1;
}//manufacturer
// ToString
public String toString(){
return super.toString()+",moyenne="+_moyenne+",écart-type="+_écartType;
}//ToString
// istats interface methods
public double moyenne(){
// makes the average score
return _moyenne;
}//average
public double écartType(){
// makes the standard deviation
return _écartType;
}//écartType
}//class
La media _mean e la deviazione standard _standardDev vengono calcolate al momento della creazione dell'oggetto. Pertanto, i metodi mean e standardDev restituiscono semplicemente i valori degli attributi _mean e _standardDev. Entrambi i metodi restituiscono -1 se l'array degli studenti è vuoto.
La seguente classe di test:
// imported classes
// student import;
// import Istats;
// import notes;
// import notesStats;
// test class
public class test{
public static void main(String[] args){
// some students & notes
élève[] ELEVES=new élève[] { new élève("paul",14),new élève("nicole",16), new élève("jacques",18)};
// recorded in a notes object
notes anglais=new notes("anglais",ELEVES);
// and display
System.out.println(""+anglais);
// idem with mean and standard deviation
anglais=new notesStats("anglais",ELEVES);
System.out.println(""+anglais);
}//hand
}//class
restituisce i risultati:
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0])
matière=anglais, notes=([paul,14.0],[nicole,16.0],[jacques,18.0]),moyenne=16.0,écart-type=1.632993161855452
Le diverse classi in questo esempio sono tutte contenute in file sorgente separati:
E:\data\serge\JAVA\interfaces\notes>dir
06/06/2002 14:06 707 notes.java
06/06/2002 14:06 878 notes.class
06/06/2002 14:07 1 160 notesStats.java
06/06/2002 14:02 101 Istats.java
06/06/2002 14:02 138 Istats.class
06/06/2002 14:05 247 élève.java
06/06/2002 14:05 309 élève.class
06/06/2002 14:07 1 103 notesStats.class
06/06/2002 14:10 597 test.java
06/06/2002 14:10 931 test.class
La classe notesStats avrebbe potuto benissimo implementare i metodi average e standardDev autonomamente, senza specificare di implementare l'interfaccia Istats. Allora, a cosa servono le interfacce? Ecco a cosa: una funzione può accettare come parametro un valore di tipo I. Qualsiasi oggetto della classe C che implementi l'interfaccia I può quindi essere un parametro di quella funzione. Si consideri la seguente interfaccia:
// an Iexample interface
public interface Iexemple{
int ajouter(int i,int j);
int soustraire(int i,int j);
}//interface
L'interfaccia Iexemple definisce due metodi: add e subtract. Le seguenti classi, class1 e class2, implementano questa interfaccia.
// imported classes
// import Iexample;
public class classe1 implements Iexemple{
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
}//class
// imported classes
// import Iexample;
public class classe2 implements Iexemple{
public int ajouter(int a, int b){
return a+b+100;
}
public int soustraire(int a, int b){
return a-b+200;
}
}//class
Per semplificare l'esempio, le classi non fanno altro che implementare l'interfaccia Iexample. Consideriamo ora il seguente esempio:
// imported classes
// import class1;
// import class2;
// test class
public class test{
// a static function
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//calculate
// the main function
public static void main(String[] arg){
// creation of two objects class1 and class2
classe1 c1=new classe1();
classe2 c2=new classe2();
// static function calls calculate
calculer(4,3,c1);
calculer(14,13,c2);
}//hand
}//test class
La funzione statica calculate accetta come parametro un elemento di tipo Iexample. Può quindi ricevere come parametro un oggetto di tipo class1 o di tipo class2. Questo è ciò che viene fatto nella funzione main con i seguenti risultati:
Possiamo quindi notare che questa proprietà è simile al polimorfismo osservato con le classi. Pertanto, se un insieme di classi Ci che non sono correlate tra loro tramite ereditarietà (e quindi non possono utilizzare il polimorfismo basato sull'ereditarietà) presenta un insieme di metodi con la stessa firma, può essere utile raggruppare tali metodi in un'interfaccia I da cui tutte le classi pertinenti erediterebbero. Le istanze di queste classi Ci possono quindi essere utilizzate come parametri per funzioni che accettano un parametro di tipo I, ovvero funzioni che utilizzano solo i metodi degli oggetti Ci definiti nell'interfaccia I e non gli attributi e i metodi specifici delle varie classi Ci.
Nell'esempio precedente, ogni classe o interfaccia era oggetto di un file sorgente separato:
E:\data\serge\JAVA\interfaces\opérations>dir
06/06/2002 14:33 128 Iexemple.java
06/06/2002 14:34 218 classe1.java
06/06/2002 14:32 220 classe2.java
06/06/2002 14:33 144 Iexemple.class
06/06/2002 14:34 325 classe1.class
06/06/2002 14:34 326 classe2.class
06/06/2002 14:36 583 test.java
06/06/2002 14:36 628 test.class
Infine, si noti che l'ereditarietà delle interfacce può essere multipla, ovvero è possibile scrivere
dove ij sono interfacce.
3.5. Classi anonime
Nell'esempio precedente, le classi class1 e class2 avrebbero potuto essere omesse. Si consideri il seguente programma, che fa essenzialmente la stessa cosa del precedente ma senza definire esplicitamente le classi class1 e class2:
// imported classes
// import Iexample;
// test class
public class test2{
// an internal class
private static class classe3 implements Iexemple{
public int ajouter(int a, int b){
return a+b+1000;
}
public int soustraire(int a, int b){
return a-b+2000;
}
};//definition class3
// a static function
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//calculate
// the main function
public static void main(String[] arg){
// creation of two objects implementing the Iexemple interface
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
};//definition i1
Iexemple i2=new Iexemple(){
public int ajouter(int a, int b){
return a+b+100;
}
public int soustraire(int a, int b){
return a-b+200;
}
};//definition i2
// another object Iexample
Iexemple i3=new classe3();
// static function calls calculate
calculer(4,3,i1);
calculer(14,13,i2);
calculer(24,23,i3);
}//hand
}//test class
La caratteristica principale è nel codice:
// creation of two objects implementing the Iexemple interface
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
return a+b+10;
}
public int soustraire(int a, int b){
return a-b+20;
}
};//definition i1
Creiamo un oggetto i1 il cui unico scopo è implementare l'interfaccia Iexample. Questo oggetto è di tipo Iexample. Possiamo quindi creare oggetti di tipo interfaccia. Molti metodi delle classi Java restituiscono oggetti di tipo interfaccia, ovvero oggetti il cui unico scopo è implementare i metodi di un'interfaccia. Per creare l'oggetto i1, si potrebbe essere tentati di scrivere:
Iexemple i1=new Iexemple()
Tuttavia, un'interfaccia non può essere istanziata. Solo una classe che implementa tale interfaccia può essere istanziata. Qui, definiamo una classe di questo tipo "al volo" all'interno del corpo della definizione dell'oggetto i1:
Iexemple i1=new Iexemple(){
public int ajouter(int a, int b){
// définition de ajouter
}
public int soustraire(int a, int b){
// définition de soustraire
}
};//définition i1
Il significato di tale istruzione è analogo alla sequenza:
public class test2{
................
// an internal class
private static class classe1 implements Iexemple{
public int ajouter(int a, int b){
// definition of add
}
public int soustraire(int a, int b){
// definition of subtract
}
};//definition class1
.................
public static void main(String[] arg){
...........
Iexemple i1=new classe1();
}//hand
}//class
Nell'esempio sopra riportato, stiamo effettivamente istanziando una classe, non un'interfaccia. Una classe definita "al volo" viene chiamata classe anonima. Si tratta di un metodo spesso utilizzato per istanziare oggetti il cui unico scopo è quello di implementare un'interfaccia.
L'esecuzione del programma precedente produce i seguenti risultati:
L'esempio precedente utilizzava classi anonime per implementare un'interfaccia. Queste possono essere utilizzate anche per derivare classi che non dispongono di costruttori parametrizzati. Si consideri il seguente esempio:
// imported classes
// import Iexample;
class classe3 implements Iexemple{
public int ajouter(int a, int b){
return a+b+1000;
}
public int soustraire(int a, int b){
return a-b+2000;
}
};//definition class3
public class test4{
// a static function
private static void calculer(int i, int j, Iexemple inter){
System.out.println(inter.ajouter(i,j));
System.out.println(inter.soustraire(i,j));
}//calculate
// hand method
public static void main(String args[]){
// definition of an anonymized class deriving from class3
// to redefine subtract
classe3 i1=new classe3(){
public int ajouter(int a, int b){
return a+b+10000;
}//subtract
};//i1
// static function calls calculate
calculer(4,3,i1);
}//hand
}//class
Qui abbiamo una classe classe3 che implementa l'interfaccia Iexemple. Nella funzione main, definiamo una variabile i1 di un tipo derivato da classe3. Questa classe derivata viene definita "al volo" in una classe anonima e sovrascrive il metodo ajouter della classe classe3. La sintassi è identica a quella della classe anonima che implementa un'interfaccia. Tuttavia, qui il compilatore rileva che classe3 non è un'interfaccia ma una classe. Per il compilatore, si tratta quindi di una derivazione di classe. Tutti i metodi presenti nel corpo della classe anonima sovrascriveranno i metodi con lo stesso nome nella classe base.
L'esecuzione del programma precedente produce i seguenti risultati:
3.6. P acchetti
3.6.1. Creazione di classi in un pacchetto
Per stampare una riga sullo schermo, usiamo l'istruzione
Se osserviamo la definizione della classe System, vediamo che in realtà si chiama java.lang.System:

Verifichiamolo con un esempio:
public class test1{
public static void main(String[] args){
java.lang.System.out.println("Coucou");
}//hand
}//class
Compiliamo ed eseguiamo questo programma:
E:\data\serge\JAVA\classes\paquetages>javac test1.java
E:\data\serge\JAVA\classes\paquetages>dir
06/06/2002 15:40 127 test1.java
06/06/2002 15:40 410 test1.class
E:\data\serge\JAVA\classes\paquetages>java test1
Coucou
Perché, allora, possiamo scrivere
System.out.println("Coucou");
invece di
java.lang.System.out.println("Coucou");
Questo perché, per impostazione predefinita, ogni programma Java importa automaticamente il pacchetto java.lang. È quindi come se all'inizio di ogni programma fosse presente la seguente istruzione:
Cosa significa questa istruzione? Fornisce l'accesso a tutte le classi nel pacchetto java.lang. Il compilatore troverà lì il file System.class, che definisce la classe System. Non sappiamo ancora dove il compilatore troverà il pacchetto java.lang o che aspetto abbia un pacchetto. Ci torneremo più avanti. Per creare una classe in un pacchetto, scriviamo:
Per questo esempio, creiamo la nostra classe Person studiata in precedenza all'interno di un pacchetto. Sceglieremo istia.st come nome del pacchetto. La classe Person diventa:
// name of the package in which the person class will be created
package istia.st;
// class person
public class personne{
// last name, first name, age
private String prenom;
private String nom;
private int age;
// builder 1
public personne(String P, String N, int age){
this.prenom=P;
this.nom=N;
this.age=age;
}
// toString
public String toString(){
return "personne("+prenom+","+nom+","+age+")";
}
}//class
Questa classe viene compilata e poi inserita nella directory istia\st della directory corrente. Perché istia\st? Perché il pacchetto si chiama istia.st.
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:04 <DIR> istia
E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002 16:04 <DIR> st
E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002 16:28 675 personne.class
Ora utilizziamo la classe person in una prima classe di test:
public class test{
public static void main(String[] args){
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
System.out.println("p1="+p1);
}//hand
}//test class
Si noti che la classe Person è ora preceduta dal nome del suo pacchetto, istia.st. Dove troverà il compilatore la classe istia.st.Person? Il compilatore cerca le classi di cui ha bisogno in un elenco predefinito di directory e in una struttura di directory che parte dalla directory corrente. In questo caso, cercherà la classe istia.st.personne in un file denominato istia\st\personne.class. Ecco perché abbiamo collocato il file personne.class nella directory istia\st. Compiliamo e poi eseguiamo il programma di test:
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:06 246 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
E:\data\serge\JAVA\classes\paquetages\personne>java test
p1=personne(Jean,Dupont,20)
Per evitare di scrivere
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
è possibile importare la classe istia.st.personne utilizzando un'istruzione import:
import istia.st.personne;
A questo punto possiamo scrivere
personne p1=new personne("Jean","Dupont",20);
e il compilatore tradurrà questo in
istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);
Il programma di test diventa quindi il seguente:
// imported namespaces
import istia.st.personne;
public class test2{
public static void main(String[] args){
personne p1=new personne("Jean","Dupont",20);
System.out.println("p1="+p1);
}//hand
}//test2 class
Compiliamo ed eseguiamo questo nuovo programma:
E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:06 246 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 16:50 740 test2.class
E:\data\serge\JAVA\classes\paquetages\personne>java test2
p1=personne(Jean,Dupont,20)
Abbiamo inserito il pacchetto istia.st nella directory corrente. Non è obbligatorio. Spostiamolo in una cartella denominata mesClasses, sempre all'interno della directory corrente. Ricordiamo che le classi del pacchetto istia.st si trovano in una cartella denominata istia\st. L'albero delle directory della directory corrente è il seguente:
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 16:06 246 test.java
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 16:50 740 test2.class
06/06/2002 16:21 <DIR> mesClasses
E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses
06/06/2002 16:22 <DIR> istia
E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia
06/06/2002 16:22 <DIR> st
E:\data\serge\JAVA\classes\paquetages\personne>dir mesClasses\istia\st
06/06/2002 16:01 1 153 personne.class
Ora ricompiliamo il programma test2.java:
E:\data\serge\JAVA\classes\paquetages\personne>javac test2.java
test2.java:2: package istia.st does not exist
import istia.st.personne;
Il compilatore non riesce più a trovare il pacchetto istia.st poiché lo abbiamo spostato. Si noti che lo cerca a causa dell'istruzione import. Per impostazione predefinita, lo cerca nella directory corrente in una cartella denominata istia\st, che non esiste più. Esaminiamo le opzioni del compilatore:
E:\data\serge\JAVA\classes\paquetages\personne>javac
Usage: javac <options> <source files>
where possible options include:
-g Generate all debugging info
-g:none Generate no debugging info
-g:{lines,vars,source} Generate only some debugging info
-O Optimize; may hinder debugging or enlarge class file
-nowarn Generate no warnings
-verbose Output messages about what the compiler is doing
-deprecation Output source locations where deprecated APIs are used
-classpath <path> Specify where to find user class files
-sourcepath <path> Specify where to find input source files
-bootclasspath <path> Override location of bootstrap class files
-extdirs <dirs> Override location of installed extensions
-d <directory> Specify where to place generated class files
-encoding <encoding> Specify character encoding used by source files
-source <release> Provide source compatibility with specified release
-target <release> Generate class files for specific VM version
-help Print a synopsis of standard options
In questo caso, l'opzione -classpath può essere utile. Consente di indicare al compilatore dove cercare le classi e i pacchetti. Proviamo. Compiliamo indicando al compilatore che il pacchetto istia.st si trova ora nella cartella mesClasses:
E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath mesClasses test2.java
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:47 236 test2.java
06/06/2002 17:03 740 test2.class
06/06/2002 16:21 <DIR> mesClasses
Questa volta, la compilazione si completa senza problemi. Eseguiamo il programma test2.class:
E:\data\serge\JAVA\classes\paquetages\personne>java test2
Exception in thread "main" java.lang.NoClassDefFoundError: istia/st/personne
at test2.main(test2.java:6)
Ora è il turno della Java Virtual Machine di non riuscire a trovare la classe istia/st/personne. La sta cercando nella directory corrente, mentre ora si trova nella directory mesClasses. Diamo un'occhiata alle opzioni della Java Virtual Machine:
E:\data\serge\JAVA\classes\paquetages\personne>java
Usage: java [-options] class [args...]
(to execute a class)
or java -jar [-options] jarfile [args...]
(to execute a jar file)
where options include:
-client to select the "client" VM
-server to select the "server" VM
-hotspot is a synonym for the "client" VM [deprecated]
The default VM is client.
-cp -classpath <directories and zip/jar files separated by ;>
set search path for application classes and resources
-D<name>=<value>
set a system property
-verbose[:class|gc|jni]
enable verbose output
-version print product version and exit
-showversion print product version and continue
-? -help print this help message
-X print help on non-standard options
-ea[:<packagename>...|:<classname>]
-enableassertions[:<packagename>...|:<classname>]
enable assertions
-da[:<packagename>...|:<classname>]
-disableassertions[:<packagename>...|:<classname>]
disable assertions
-esa | -enablesystemassertions
enable system assertions
-dsa | -disablesystemassertions
disable system assertions
Possiamo notare che anche la JVM dispone di un'opzione classpath, proprio come il compilatore. Usiamola per indicare alla JVM dove si trova il pacchetto istia.st:
E:\data\serge\JAVA\classes\paquetages\personne>java.bat -classpath mesClasses test2
Exception in thread "main" java.lang.NoClassDefFoundError: test2
Non abbiamo fatto molti progressi. Ora non si riesce a trovare la classe test2 stessa. Ecco perché: quando la parola chiave classpath è assente, la directory corrente viene sistematicamente cercata per le classi, ma non quando è presente. Di conseguenza, il file test2.class situato nella directory corrente non viene trovato. La soluzione? Aggiungere la directory corrente al classpath. La directory corrente è rappresentata dal simbolo .
E:\data\serge\JAVA\classes\paquetages\personne>java -classpath mesClasses;. test2
p1=personne(Jean,Dupont,20)
Perché tutta questa complessità? Lo scopo dei pacchetti è quello di evitare conflitti di nomi tra le classi. Consideriamo due aziende, E1 ed E2, che distribuiscono classi impacchettate rispettivamente nei pacchetti com.e1 e com.e2. Supponiamo che un cliente, C, acquisti entrambi i set di classi, in cui entrambe le aziende hanno definito una classe denominata Person. Il cliente C farà riferimento alla classe Person dell'azienda E1 come com.e1.Person e a quella dell'azienda E2 come com.e2.Person, evitando così un conflitto di nomi.
3.6.2. Ricerca dei pacchetti
Quando scriviamo in un programma
per accedere a tutte le classi del pacchetto java.util, dove si trova? Abbiamo detto che i pacchetti vengono cercati per impostazione predefinita nella directory corrente o nell'elenco delle directory specificate nell'opzione classpath del compilatore o della JVM, se tale opzione è presente. Vengono cercati anche nelle directory lib della directory di installazione del JDK. Consideriamo questa directory:

In questo esempio, le directory jdk14\lib e jdk14\jre\lib verranno cercate per i file .class o per i file .jar o .zip, che sono archivi di classi. Ad esempio, cerchiamo i file .jar situati nella directory jdk14 menzionata sopra:

Ce ne sono diverse dozzine. Un file .jar può essere aperto con l'utilità WinZip. Apriamo il file rt.jar sopra indicato (rt = RunTime). Contiene diverse centinaia di file .class, compresi quelli appartenenti al pacchetto java.util:

Un modo semplice per gestire i pacchetti consiste nel collocarli nella directory <jdk>\jre\lib, dove <jdk> è la directory di installazione del JDK. In genere, un pacchetto contiene diverse classi ed è pratico raggrupparle in un unico file .jar (JAR = Java ARchive file). L'eseguibile jar.exe si trova nella cartella <jdk>\bin:
E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\bin\jar.exe"
07/02/2002 12:52 28 752 jar.exe
È possibile ottenere la guida sull'uso del programma jar richiamandolo senza alcun parametro:
E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar.exe"
Syntaxe : jar {ctxu}[vfm0M] [fichier-jar] [fichier-manifest] [rÚp -C] fichiers ...
Options :
-c crÚer un nouveau fichier d'archives
-t gÚnÚrer la table des matiÞres du fichier d'archives
-x extraire les fichiers nommÚs (ou tous les fichiers) du fichier d'archives
-u mettre Ó jour le fichier d'existing archives
-v gÚnÚrer des informations verbeuses sur la sortie standard
-f spÚcifier le nom du fichier d'archives
-m inclure les informations manifest provenant du fichier manifest spÚcifiÚ
-0 stocker seulement ; ne pas utiliser la compression ZIP
-M ne pas crÚer de fichier manifest pour les entrÚes
-i gÚnÚrer l'index for jar files spÚcifiÚs
-C passer au rÚpertoire spÚcifiÚ et inclure le fichier suivant
Si un rÚpertoire est spÚcifiÚ, il est traitÚ rÚcursivement.
Les noms des fichiers manifest et d'archives must be spÚcifiÚs
dans l'order of ''m'' and ''f'' indicators.
Exemple 1 : pour archiver deux fichiers de classe dans le fichier d'archives classes.jar :
jar cvf classes.jar Foo.class Bar.class
Exemple 2 : utilisez le fichier manifest existant 'mymanifest'' to archive all files in the
rÚpertoire foo/ dans 'classes.jar'':
jar cvfm classes.jar monmanifest -C foo/ .
Torniamo alla classe *person.class creata in precedenza nel pacchetto *istia.st:
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 17:36 195 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 18:15 740 test2.class
E:\data\serge\JAVA\classes\paquetages\personne>dir istia
06/06/2002 16:04 <DIR> st
E:\data\serge\JAVA\classes\paquetages\personne>dir istia\st
06/06/2002 16:28 675 personne.class
Creiamo un file denominato istia.st.jar che archivi tutte le classi nel pacchetto istia.st, ovvero tutte le classi nell'albero di directory istia\st mostrato sopra:
E:\data\serge\JAVA\classes\paquetages\personne>"e:\program files\jdk14\bin\jar" cvf istia.st.jar istia\st\*
E:\data\serge\JAVA\classes\paquetages\personne>dir
06/06/2002 16:28 467 personne.java
06/06/2002 17:36 195 test.java
06/06/2002 16:04 <DIR> istia
06/06/2002 16:06 738 test.class
06/06/2002 16:47 236 test2.java
06/06/2002 18:15 740 test2.class
06/06/2002 18:08 874 istia.st.jar
Esaminiamo il contenuto del file istia.st.jar utilizzando WinZip:

Mettiamo il file istia.st.jar nella directory <jdk>\jre\lib\perso:
E:\data\serge\JAVA\classes\paquetages\personne>dir "e:\program files\jdk14\jre\lib\perso"
06/06/2002 18:08 874 istia.st.jar
Ora compiliamo il programma test2.java e poi lo eseguiamo:
E:\data\serge\JAVA\classes\paquetages\personne>javac -classpath istia.st.jar test2.java
E:\data\serge\JAVA\classes\paquetages\personne>java -classpath istia.st.jar;. test2
p1=personne(Jean,Dupont,20)
Si noti che è stato sufficiente specificare il nome dell'archivio da cercare senza dover indicarne esplicitamente la posizione. Tutte le directory nell'albero <jdk>\jre\lib vengono esaminate per trovare il file .jar richiesto.
3.7. Esempio di calcolo delle imposte
Riprendiamo il calcolo delle imposte già trattato nel capitolo precedente ed elaboriamolo utilizzando una classe. Ricordiamo il problema:
Consideriamo il caso semplificato di un contribuente che deve dichiarare solo il proprio stipendio:
- calcoliamo il numero di scaglioni fiscali del dipendente nbParts = nbEnfants/2 + 1 se è celibe, nbEnfants/2 + 2 se è coniugato, dove nbEnfants è il numero di figli.
- Se ha almeno tre figli, riceve una quota aggiuntiva pari a metà
- Calcoliamo il suo reddito imponibile R = 0,72 * S, dove S è il suo stipendio annuale
- Calcoliamo il suo coefficiente familiare QF = R / nbParts
- Calcoliamo la sua imposta I. Si consideri la seguente tabella:
12620,0 | 0 | 0 |
13.190 | 0,05 | 631 |
15.640 | 0,1 | 1.290,5 |
24.740 | 0,15 | 2.072,5 |
31.810 | 0,2 | 3.309,5 |
39.970 | 0,25 | 4.900 |
48.360 | 0,3 | 6.898,5 |
55.790 | 0,35 | 9.316,5 |
92.970 | 0,4 | 12.106 |
127.860 | 0,45 | 16.754,5 |
151.250 | 0,50 | 23.147,5 |
172.040 | 0,55 | 30.710 |
195.000 | 0,60 | 39.312 |
0 | 0,65 | 49.062 |
Ogni riga ha 3 campi. Per calcolare l'imposta I, trova la prima riga in cui QF <= campo1. Ad esempio, se QF = 23.000, la riga trovata sarà
L'imposta I è quindi pari a 0,15*R - 2072,5*nbParts. Se QF è tale che la condizione QF<=field1 non è mai soddisfatta, vengono utilizzati i coefficienti dell'ultima riga. Qui:
il che dà come risultato l'imposta I = 0,65*R - 49062*nbParts.
La classe impots sarà definita come segue:
// creation of an impots class
public class impots{
// data required for tax calculation
// come from an external source
private double[] limites, coeffR, coeffN;
// manufacturer
public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
// check that the 3 arrays have the same size
boolean OK=LIMITES.length==COEFFR.length && LIMITES.length==COEFFN.length;
if (! OK) throw new Exception ("Les 3 tableaux fournis n'ont pas la même taille("+
LIMITES.length+","+COEFFR.length+","+COEFFN.length+")");
// it's good
this.limites=LIMITES;
this.coeffR=COEFFR;
this.coeffN=COEFFN;
}//manufacturer
// tAX CALCULATION
public long calculer(boolean marié, int nbEnfants, int salaire){
// calculating the number of shares
double nbParts;
if (marié) nbParts=(double)nbEnfants/2+2;
else nbParts=(double)nbEnfants/2+1;
if (nbEnfants>=3) nbParts+=0.5;
// calculation of taxable income & family quota
double revenu=0.72*salaire;
double QF=revenu/nbParts;
// tAX CALCULATION
limites[limites.length-1]=QF+1;
int i=0;
while(QF>limites[i]) i++;
// return result
return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
}//calculate
}//class
Viene creato un oggetto fiscale con i dati necessari per calcolare l'imposta di un contribuente. Questa è la parte stabile dell'oggetto. Una volta creato questo oggetto, il suo metodo calculate può essere chiamato ripetutamente per calcolare l'imposta del contribuente in base al suo stato civile (coniugato o meno), al numero di figli e allo stipendio annuale.
Un programma di prova potrebbe apparire così:
//imported classes
// import impots;
import java.io.*;
public class test
{
public static void main(String[] arg) throws IOException
{
// interactive tax calculator
// the user enters three data points on the keyboard: married nbEnfants salary
// the program then displays the tax payable
final String syntaxe="syntaxe : marié nbEnfants salaire\n"
+"marié : o pour marié, n pour non marié\n"
+"nbEnfants : nombre d'enfants\n"
+"salaire : salaire annuel en F";
// data tables required for tax calculation
double[] limites=new double[] {12620,13190,15640,24740,31810,39970,48360,55790,92970,127860,151250,172040,195000,0};
double[] coeffR=new double[] {0,0.05,0.1,0.15,0.2,0.25,0.3,0.35,0.4,0.45,0.5,0.55,0.6,0.65};
double[] coeffN=new double[] {0,631,1290.5,2072.5,3309.5,4900,6898.5,9316.5,12106,16754.5,23147.5,30710,39312,49062};
// create a reading flow
BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
// tax object creation
impots objImpôt=null;
try{
objImpôt=new impots(limites,coeffR,coeffN);
}catch (Exception ex){
System.err.println("L'erreur suivante s'est produite : " + ex.getMessage());
System.exit(1);
}//try-catch
// infinite loop
while(true){
// tax calculation parameters are requested
System.out.print("Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :");
String paramètres=IN.readLine().trim();
// anything to do?
if(paramètres==null || paramètres.equals("")) break;
// check the number of arguments in the input line
String[] args=paramètres.split("\\s+");
int nbParamètres=args.length;
if (nbParamètres!=3){
System.err.println(syntaxe);
continue;
}//if
// checking the validity of parameters
// married
String marié=args[0].toLowerCase();
if (! marié.equals("o") && ! marié.equals("n")){
System.err.println(syntaxe+"\nArgument marié incorrect : tapez o ou n");
continue;
}//if
// nbEnfants
int nbEnfants=0;
try{
nbEnfants=Integer.parseInt(args[1]);
if(nbEnfants<0) throw new Exception();
}catch (Exception ex){
System.err.println(syntaxe+"\nArgument nbEnfants incorrect : tapez un entier positif ou nul");
continue;
}//if
// salary
int salaire=0;
try{
salaire=Integer.parseInt(args[2]);
if(salaire<0) throw new Exception();
}catch (Exception ex){
System.err.println(syntaxe+"\nArgument salaire incorrect : tapez un entier positif ou nul");
continue;
}//if
// parameters are correct - tax is calculated
System.out.println("impôt="+objImpôt.calculer(marié.equals("o"),nbEnfants,salaire)+" F");
// next taxpayer
}//while
}//hand
}//class
Ecco un esempio del programma precedente in azione:
E:\data\serge\MSNET\c#\impots\3>java test
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument marié incorrect : tapez o ou n
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o s d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument nbEnfants incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 d
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Argument salaire incorrect : tapez un entier positif ou nul
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :q s d f
syntaxe : marié nbEnfants salaire
marié : o pour marié, n pour non marié
nbEnfants : nombre d'enfants
salaire : salaire annuel en F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :o 2 200000
impôt=22504 F
Paramètres du calcul de l'impôt au format marié nbEnfants salaire ou rien pour arrêter :