Skip to content

3. Classi e interfacce

3.1. L'oggetto attraverso l'esempio

3.1.1. Nozioni generali

Affrontiamo ora, attraverso un esempio, la programmazione orientata agli oggetti. Un oggetto è un'entità che contiene dati che ne definiscono lo stato (chiamati attributi o proprietà) e funzioni (chiamate metodi). Un oggetto viene creato secondo un modello chiamato classe:

public class C1{
    type1 p1;            // proprietà p1
    type2 p2;            // proprietà p2
    
    type3 m3(){        // metodo m3
        
    }
    type4 m4(){        // metodo m4
        
    }
    
}

Partendo dalla classe precedente C1, è possibile creare numerosi oggetti O1, O2,… Tutti avranno le proprietà p1, p2,… e i metodi m3, m4, … Avranno valori diversi per le loro proprietà pi, avendo così ciascuno uno stato proprio.

Se O1 è un oggetto di tipo C1, O1.p1 indica la proprietà p1 di O1 e O1.m1 indica il metodo m1 di O1.

Consideriamo un primo modello di oggetto: la classe personne.

3.1.2. Definizione della classe “persona”

La definizione della classe personne sarà la seguente:


import java.io.*;

public class personne{
  // attributi
  private String prenom;
  private String nom;
  private int age;
  
  // metodo
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }

  // metodo
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
}

Qui abbiamo la definizione di una classe, ovvero un tipo di dati. Quando creeremo variabili di questo tipo, le chiameremo oggetti. Una classe è quindi uno stampo 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 tre attributi seguenti:

privé: un campo privato (private) è accessibile solo dai metodi interni della classe

public: un campo pubblico è accessibile da qualsiasi funzione, definita o meno all’interno della classe

protégé: un campo protetto (protected) è accessibile solo dai metodi interni della classe o da un oggetto derivato (si veda in seguito il concetto di ereditarietà).

In generale, 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à ricorrere ai metodi pubblici dell’oggetto e in particolare a quelli che consentiranno l’accesso ai suoi dati privati.

La sintassi per la dichiarazione di 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
}

Remarques

  • L'ordine di dichiarazione degli attributi private, protected e public è arbitrario.

3.1.3. Il metodo initialise

Torniamo alla nostra classe **persona** dichiarata come:


import java.io.*;

public class personne{
  // attributi
  private String prenom;
  private String nom;
  private int age;
  
  // metodo
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }

  // metodo
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
}

Qual è la funzione del metodo initialise? Poiché cognome, nome e età sono dati privati della classe persona, le istruzioni

personne p1;
p1.prenom="Jean";
p1.nom="Dupont";
p1.age=30;

sono illegali. Dobbiamo inizializzare un oggetto di tipo personne tramite un metodo pubblico. Questo è il ruolo del metodo initialise. Scriveremo:

personne p1;
p1.initialise("Jean","Dupont",30);

La sintassi p1.initialise è corretta poiché initialise è di accesso pubblico.

3.1.4. L'operatore new

La sequenza di istruzioni

personne p1;
p1.initialise("Jean","Dupont",30);

non è corretta. L'istruzione

    personne p1;

dichiara p1 come riferimento a un oggetto di tipo personne. Questo oggetto non esiste ancora e quindi p1 non è inizializzato. È come se si scrivesse:

personne p1=null;

dove si indica esplicitamente con la parola chiave null che la variabile p1 non fa ancora riferimento ad alcun oggetto.

Quando poi si scrive

p1.initialise("Jean","Dupont",30);

si richiama il metodo initialise dell’oggetto a cui fa riferimento p1. Tuttavia, tale oggetto non esiste ancora e il compilatore segnalerà l’errore. Affinché p1 faccia riferimento a un oggetto, è necessario scrivere:

personne p1=new personne();

Ciò ha l'effetto di creare un oggetto di tipo personne non ancora inizializzato: gli attributi nom e prenom, che sono riferimenti a oggetti di tipo String, avranno il valore null, e age assumerà il valore 0. Esiste quindi un’inizializzazione predefinita. Ora che p1 fa riferimento a un oggetto, l’istruzione di inizializzazione di tale oggetto

p1.initialise("Jean","Dupont",30);

è valida.

3.1.5. La parola chiave this

Diamo un'occhiata al codice del metodo initialise:


public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }

L'istruzione this.prenom=P significa che l'attributo prenom dell'oggetto corrente (this) riceve il valore P. La parola chiave this indica l'oggetto corrente: quello in cui si trova il metodo eseguito. Come lo si riconosce? Vediamo come avviene l'inizializzazione dell'oggetto a cui fa riferimento p1 nel programma chiamante:

p1.initialise("Jean","Dupont",30);

Viene chiamato il metodo initialise dell’oggetto p1. Quando in questo metodo si fa riferimento all’oggetto this, in realtà si fa riferimento all’oggetto p1. Il metodo initialise 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 tale oggetto, la sintassi this.A è implicita. È necessario utilizzarla esplicitamente in caso di conflitto tra identificatori. È il caso dell'istruzione:


this.age=age;

dove age indica sia un attributo dell’oggetto corrente sia il parametro age ricevuto dal metodo. È quindi necessario risolvere l’ambiguità indicando l’attributo age con this.age.

3.1.6. Un programma di prova

Ecco un programma di test:


public class test1{
  public static void main(String arg[]){
    personne p1=new personne();
    p1.initialise("Jean","Dupont",30);
    p1.identifie();
  }
}

La classe personne è definita nel file sorgente personne.java ed è stata 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

Procediamo allo stesso modo 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

È sorprendente che il programma test1.java non importi la classe personne con un'istruzione:

import personne;

Quando il compilatore incontra nel codice sorgente un riferimento a una classe non definita nello stesso file sorgente, cerca la classe in diversi punti:

  • 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é abbia trovato la definizione della classe personne. In questo caso, inserire un'istruzione import provoca 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 e ricordare che la classe Person deve essere importata, in futuro scriveremo all'inizio del programma:

// classi importate
// import persona;

Ora possiamo eseguire il file test1.class:

E:\data\serge\JAVA\BASES\OBJETS\2>java test1
Jean,Dupont,30

È possibile raggruppare più classi in un unico file sorgente. Raggruppiamo quindi le classi personne e test1 nel file sorgente test2.java. La classe test1 viene rinominata test2 per tenere conto della modifica del nome del file sorgente:

// pacchetti importati
import java.io.*;

class personne{
   // attributi
  private String prenom;    // nome della mia persona
  private String nom;            // il suo cognome
  private int age;                // la sua età

   // metodo
  public void initialise(String P, String N, int age){
    this.prenom=P;
    this.nom=N;
    this.age=age;
  }//inizializza

   // metodo
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }//identifica
}//classe
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 personne non possiede più l'attributo public. Infatti, in un file sorgente Java, solo una classe può avere l'attributo public. Si tratta di quella che contiene la funzione main. Inoltre, il file sorgente deve portare il nome di quest'ultima. 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. Eseguiamo ora il file test2.class:

E:\data\serge\JAVA\BASES\OBJETS\2>java test2
Jean,Dupont,30

In seguito, si utilizzeranno indifferentemente i due metodi:

  • classi raggruppate in un unico file sorgente
  • una classe per file sorgente

3.1.7. Un altro metodo di inizializzazione

Consideriamo ancora la classe personne e aggiungiamo ad essa il seguente metodo:


public void initialise(personne P){
    prenom=P.prenom;
    nom=P.nom;
    this.age=P.age;
  }

Ora abbiamo due metodi con il nome initialise: è 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 initialise ha accesso diretto agli attributi dell’oggetto P, sebbene questi siano di tipo private. Questo vale sempre: i metodi di un oggetto O1 di una classe C hanno sempre accesso agli attributi privati degli altri oggetti della stessa classe C.

Ecco un test della nuova classe personne:


// import persona;
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:

p1=Jean,Dupont,30
p2=Jean,Dupont,30

3.1.8. Costruttori della classe persona

Un costruttore è un metodo che porta il nome della classe e che viene chiamato al momento della creazione dell'oggetto. Generalmente viene utilizzato per inizializzarlo. È un metodo che può accettare argomenti ma che non restituisce alcun risultato. Il suo prototipo o la sua definizione non sono preceduti da alcun tipo (nemmeno void).

Se una classe ha un costruttore che accetta n argomenti argi, la dichiarazione e l’inizializzazione di un oggetto di tale classe potranno avvenire nella forma:


        classe objet =new classe(arg1,arg2, ... argn);

oppure


        classe objet;

        objet=new classe(arg1,arg2, ... argn);

Quando una classe ha uno o più costruttori, è obbligatorio utilizzare uno di questi costruttori per creare un oggetto di quella classe. Se una classe C non ha alcun costruttore, ne ha uno predefinito, ovvero il costruttore senza parametri: public C(). Gli attributi dell'oggetto vengono quindi inizializzati con i valori predefiniti. È ciò che è accaduto quando, nei programmi precedenti, avevamo scritto:

    personne p1;
    p1=new personne();

Creiamo due costruttori per la nostra classe personne:


public class personne{
  // attributi
  private String prenom;
  private String nom;
  private int age;
  
  // costruttori
  public personne(String P, String N, int age){
    initialise(P,N,age);
  }
  public personne(personne P){
    initialise(P);
  }

  // metodo
  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;
  }

  // metodo
  public void identifie(){
    System.out.println(prenom+","+nom+","+age);
  }
}

I nostri due costruttori si limitano a richiamare i metodi initialise corrispondenti. Ricordiamo che quando in un costruttore si trova, ad esempio, la notazione initialise(P), il compilatore la traduce in this.initialise(P). All’interno del costruttore, il metodo initialise 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 persona;
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:

p1=Jean,Dupont,30
p2=Jean,Dupont,30

3.1.9. I riferimenti agli oggetti

Utilizziamo sempre la stessa classe personne. Il programma di test diventa il seguente:


// import persona;
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 fa riferimento allo stesso oggetto di p1
    personne p2=p1;
    System.out.print("p2="); p2.identifie();
    // p3 fa riferimento a un oggetto che sarà una copia dell'oggetto a cui fa riferimento p1
    personne p3=new personne(p1);
    System.out.print("p3=");  p3.identifie();
    // si modifica lo stato dell'oggetto a cui fa riferimento p1
    p1.initialise("Micheline","Benoît",67);
    System.out.print("p1=");  p1.identifie();
    // poiché p2=p1, l'oggetto a cui fa riferimento p2 deve aver cambiato stato
    System.out.print("p2=");  p2.identifie();
    // poiché p3 non fa riferimento allo stesso oggetto di p1, l'oggetto a cui fa riferimento p3 non deve essere cambiato
    System.out.print("p3=");  p3.identifie();
  }
}

I risultati ottenuti 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 tramite

personne p1=new personne("Jean","Dupont",30);

p1 fa riferimento all’oggetto personne("Jean","Dupont",30) ma non è l’oggetto stesso. In C, si direbbe che si tratta di un puntatore, c.a.d, all’indirizzo dell’oggetto creato. Se poi si scrive:

    p1=null

Non è l'oggetto personne("Jean","Dupont",30) a essere modificato, bensì è il riferimento p1 a cambiare valore. L'oggetto persona("Jean","Dupont",30) andrà "perso" se non è referenziato da nessun'altra variabile.

Quando si scrive:

personne p2=p1;

si inizializza il puntatore p2: esso “punta” allo stesso oggetto (indica lo stesso oggetto) del puntatore p1. Pertanto, se si modifica l’oggetto “puntato” (o referenziato) da p1, si modifica anche quello referenziato da p2.

Quando si scrive:

personne p3=new personne(p1);

viene creato un nuovo oggetto, copia dell’oggetto a cui fa riferimento p1. Questo nuovo oggetto sarà referenziato da p3. Se si modifica l’oggetto “puntato” (o a cui si fa riferimento) da p1, non si modifica in alcun modo quello a cui fa riferimento p3. È quanto dimostrano i risultati ottenuti.

3.1.10. Gli oggetti temporanei

In un'espressione è possibile richiamare esplicitamente il costruttore di un oggetto: quest'ultimo viene creato, ma non vi abbiamo accesso (ad esempio per modificarlo). Questo oggetto temporaneo viene creato per le esigenze di valutazione dell'espressione e poi abbandonato. 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.

Consideriamo il seguente esempio:


// import persona;

public class test1{
  public static void main(String arg[]){
    new personne(new personne("Jean","Dupont",30)).identifie();
  }
}

e modifichiamo i costruttori della classe personne in modo che visualizzino un messaggio:


// produttori
  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:

Constructeur personne(String, String, int)
Constructeur personne(personne)
Jean,Dupont,30

che mostrano la creazione progressiva dei due oggetti temporanei.

3.1.11. Metodi di lettura e scrittura degli attributi privati

Aggiungiamo alla classe personne 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);
  }

  // accessori
  public String getPrenom(){
    return prenom;
  }
  public String getNom(){
    return nom;
  }
  public int getAge(){
    return age;
  }

  //modificatori
  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:


// importa persona;

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:

P=(Jean,Michelin,34)
P=(Jean,Michelin,56)

3.1.12. Metodi e attributi di classe

Supponiamo di voler contare il numero di oggetti personne creati in un'applicazione. Si potrebbe gestire manualmente un contatore, ma si rischia di tralasciare gli oggetti temporanei creati qua e là. Sembrerebbe più sicuro includere nei costruttori della classe personne un’istruzione che incrementi un contatore. Il problema è passare un riferimento a questo contatore affinché il costruttore possa incrementarlo: è necessario passare loro un nuovo parametro. Si può anche includere il contatore nella definizione della classe. Poiché si tratta di un attributo della classe stessa e non di un oggetto specifico di tale classe, lo si dichiara in modo diverso con la parola chiave static:

    private static long nbPersonnes;        // numero di persone create

Per fare riferimento ad esso, si scrive personne.nbPersonnes per indicare che si tratta di un attributo della classe personne stessa. In questo caso, abbiamo creato un attributo privato a cui non si avrà accesso diretto al di fuori della classe. Creiamo quindi un metodo pubblico per consentire l’accesso all’attributo della classe nbPersonnes. Per ottenere il valore di nbPersonnes, il metodo non necessita di un oggetto specifico: infatti nbPersonnes non è l’attributo di un oggetto specifico, ma è l’attributo di un’intera classe. È quindi necessario un metodo di classe, anch’esso denominato static:

public static long getNbPersonnes(){
    return nbPersonnes;
}

che dall’esterno verrà chiamato con la sintassi personne.getNbPersonnes(). Ecco un esempio.

La classe personne diventa la seguente:


public class personne{
  
  // attributo di classe
  private static long nbPersonnes=0;

  // attributi degli oggetti

  
  // costruttori
  public personne(String P, String N, int age){
    initialise(P,N,age);
    nbPersonnes++;
  }
  public personne(personne P){
    initialise(P);
    nbPersonnes++;
  }

  // metodo


  // metodo di classe
  public static long getNbPersonnes(){
    return nbPersonnes;
  }

}// classe

Con il seguente programma:


// import persona;

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

si ottengono i seguenti risultati:

    Nombre de personnes créées : 3

3.1.13. Passaggio di un oggetto a una funzione

Abbiamo già detto che in Java i parametri effettivi di una funzione vengono passati 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 ingannare dall’uso improprio del termine che viene sistematicamente fatto quando si parla di “oggetto” invece che di “riferimento all’oggetto”. Un oggetto viene manipolato solo tramite un riferimento (un puntatore) ad esso. Ciò che viene quindi passato a una funzione non è l’oggetto stesso, ma un riferimento a tale oggetto. È quindi il valore del riferimento, e non il valore dell’oggetto stesso, che viene duplicato nel parametro formale: non viene creato alcun nuovo oggetto.

Se un riferimento all’oggetto R1 viene passato a una funzione, verrà copiato nel parametro formale corrispondente R2. Pertanto, i riferimenti R2 e R1 indicano lo stesso oggetto. Se la funzione modifica l’oggetto a cui punta R2, modifica ovviamente anche quello a cui fa riferimento R1, poiché si tratta dello stesso oggetto.

Image

È quanto illustra il seguente esempio:


// import persona;

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

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

Il metodo modifie viene dichiarato come static poiché si tratta di un metodo di classe: non è necessario anteporvi un oggetto per richiamarlo. 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

Si nota che viene creato un solo oggetto: quello della persona p1 dalla funzione main e che l'oggetto è stato effettivamente modificato dalla funzione modifie.

3.1.14. Incapsulare i parametri di output di una funzione in un oggetto

A causa del passaggio dei parametri per valore, in Java non è possibile scrivere una funzione che abbia, ad esempio, parametri di output di tipo int, poiché non è possibile passare il riferimento a un tipo int che non è un oggetto. È quindi possibile 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 che consente di inizializzare un numero intero e di due metodi che consentono di leggere e modificare il valore di tale numero intero. Si verifica il funzionamento di questa classe con il seguente programma:


// import interi;

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 si ottengono i seguenti risultati:

I=12
I=15

3.1.15. Un array di persone

Un oggetto è un dato come un altro e, in quanto tale, più oggetti possono essere raggruppati in un array:


// import persona;

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 persona[] amici = new persona[3]; crea un array di 3 elementi di tipo personne. Questi 3 elementi vengono inizializzati qui con i valori null e c.a.d, che non fanno riferimento ad alcun oggetto. Ancora una volta, per improprio uso del termine, si parla di array di oggetti mentre si tratta semplicemente di un array di riferimenti a oggetti. La creazione dell’array di oggetti, che è esso stesso un oggetto (presenza di new), non crea quindi di per sé alcun oggetto del tipo dei suoi elementi: ciò va fatto in seguito.

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'eredità attraverso l'esempio

3.2.1. Introduzione

Qui affrontiamo il concetto di ereditarietà. Lo scopo dell'ereditarietà è quello di "personalizzare" una classe esistente affinché soddisfi le nostre esigenze. Supponiamo di voler creare una classe enseignant: un insegnante è una persona particolare. Ha degli attributi che un'altra persona non avrà: la materia che insegna, ad esempio. Ma possiede anche gli attributi di ogni persona: nome, cognome ed età. Un insegnante fa quindi pienamente parte della classe personne, ma presenta attributi aggiuntivi. Anziché creare una classe enseignant partendo da zero, preferiremmo riutilizzare le caratteristiche della classe personne, adattandole alle peculiarità degli insegnanti. È il concetto di ereditarietà che ci permette di farlo.

Per indicare che la classe enseignant eredita le proprietà della classe personne, scriveremo:


    public class enseignant extends personne

personne è detta classe padre (o madre) e enseignant classe derivata (o figlia). Un oggetto enseignant possiede tutte le caratteristiche di un oggetto personne: ha gli stessi attributi e gli stessi metodi. Questi attributi e metodi della classe genitrice non vengono ripetuti nella definizione della classe figlia: ci si limita a indicare gli attributi e i metodi aggiunti dalla classe figlia:


class enseignant extends personne{
// attributi
  private int section;

// costruttore
  public enseignant(String P, String N, int age,int section){
    super(P,N,age);
    this.section=section;
  }
}

Supponiamo che la classe personne 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+")";
  }

  // accessori
  public String getPrenom(){
    return prenom;
  }
  public String getNom(){
    return nom;
  }
  public int getAge(){
    return age;
  }

  //modificatori
  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 identifie è stato leggermente modificato per restituire una stringa di caratteri che identifica la persona e ora porta il nome identite. In questo caso, la classe enseignant aggiunge ai metodi e agli attributi della classe personne:

  • un attributo section che rappresenta il numero di sezione a cui appartiene il docente all’interno del corpo docente (in linea di massima, una sezione per disciplina)
  • un nuovo costruttore che consente di inizializzare tutti gli attributi di un docente

3.2.2. Creazione di un oggetto docente

Il costruttore della classe enseignant è il seguente:


// costruttore
  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 personne. Sappiamo che questo costruttore inizializza i campi «prenom», «nom» e «age» dell’oggetto personne contenuto all’interno dell’oggetto étudiant. Sembra piuttosto complicato e si potrebbe preferire scrivere:


// costruttore
  public enseignant(String P, String N, int age,int section){
    this.prenom=P;
        this.nom=N
        this.age=age
      this.section=section;
  }

Non è possibile. La classe personne ha dichiarato privati (private) i suoi tre campi prenom, nom e age. Solo gli oggetti della stessa classe hanno accesso diretto a questi campi. Tutti gli altri oggetti, compresi gli oggetti derivati come in questo caso, devono passare attraverso metodi pubblici per accedervi. La situazione sarebbe stata diversa se la classe personne avesse dichiarato protetti (protected) i tre campi: in tal caso avrebbe consentito alle classi derivate di avere accesso diretto ai tre campi. Nel nostro esempio, utilizzare il costruttore della classe padre era quindi la soluzione corretta ed è il metodo usuale: durante la creazione di un oggetto figlio, si chiama prima il costruttore dell’oggetto padre, poi si completano le inizializzazioni specifiche dell’oggetto figlio (section nel nostro esempio).

Proviamo a scrivere un primo programma:


// importa persona;
// importa insegnante;

public class test1{
  public static void main(String arg[]){
    System.out.println(new enseignant("Jean","Dupont",30,27).identite());
  }
}

Questo programma si limita a creare un oggetto enseignant (new) e a identificarlo. La classe enseignant non dispone del metodo identité, ma la sua classe padre ne possiede uno che, per di più, è pubblico: per eredità, esso diventa un metodo pubblico della classe enseignant.

I file sorgente delle classi vengono raccolti in un'unica directory e poi 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:

E:\data\serge\JAVA\BASES\OBJETS\4>java test1
personne(Jean,Dupont,30)

3.2.3. Sovraccarico di un metodo

Nell'esempio precedente, abbiamo ottenuto l'identità della parte personne relativa al docente, ma mancano alcune informazioni specifiche della classe enseignant (la sezione). È quindi necessario scrivere un metodo che consenta di identificare il docente:


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 identite della classe enseignant si basa sul metodo identite della sua classe padre (super.identite) per visualizzare la sua parte «personne», quindi la completa con il campo section, specifico della classe enseignant.

La classe enseignant dispone ora di due metodi identite:

  • quello ereditato dalla classe padre personne
  • quello proprio

Se E è un oggetto enseignant, E.identite indica il metodo identite della classe enseignant. Si dice che il metodo identite della classe madre sia “sovraccaricato” dal metodo identite 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 madre della sua classe madre, se esiste
  • ecc…

L’ereditarietà consente quindi di sovrascrivere nella classe figlia i metodi con lo stesso nome presenti nella classe madre. È questo che permette di adattare la classe figlia alle proprie esigenze. Insieme al polimorfismo, di cui parleremo più avanti, la sovrascrittura dei metodi rappresenta il principale vantaggio dell’ereditarietà.

Consideriamo lo stesso esempio di prima:


// import persona;
// importa insegnante;

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:

    enseignant(personne(Jean,Dupont,30),27)

3.2.4. Il polimorfismo

Consideriamo una gerarchia di classi: C0 C1 C2 … Cn

dove Ci Cj indica che la classe Cj deriva dalla classe Ci. Ne consegue che la classe Cj possiede tutte le caratteristiche della classe Ci più altre. Siano Oi oggetti di tipo Ci. È lecito scrivere:

    Oi=Oj avec j>i

Infatti, per ereditarietà, la classe Cj possiede tutte le caratteristiche della classe Ci più altre. Pertanto, un oggetto Oj di tipo Cj contiene al suo interno un oggetto di tipo Ci. L’operazione

    Oi=Oj

fa sì che Oi sia un riferimento all’oggetto di tipo Ci contenuto nell’oggetto Oj.

Il fatto cheuna variabile Oi della classe Ci possa di fatto fare riferimento non solo a un oggetto della classe Ci, ma a qualsiasi oggetto derivato dalla classe Ci è chiamato polimorfismo: la capacità di una variabile di fare riferimento a diversi tipi di oggetti.

Facciamo un esempio e consideriamo la seguente funzione indipendente da qualsiasi classe:

    public static void affiche(Object obj){
        ….
    }

La classe Object è la «classe madre» di tutte le classi Java. Pertanto, quando si scrive:

    public class personne

si scrive implicitamente:

    public class personne extends Object

Pertanto, ogni oggetto Java contiene al suo interno una parte di tipo Object. Si potrà quindi scrivere:

    enseignant e;
    affiche(e);

Il parametro formale di tipo Object della funzione affiche riceverà un valore di tipo enseignant. Poiché enseignant deriva da Object, ciò è consentito.

3.2.5. Sovraccarico e polimorfismo

Completiamo la nostra funzione affiche:

    public static void affiche(Object obj){
        System.out.println(obj.toString());
    }

Il metodo obj.toString() restituisce una stringa che identifica l'oggetto obj nella forma nom_de_la_classe@adresse_de_l'oggetto. Cosa succede nel caso del nostro esempio precedente:

    enseignant e=new enseignant(...);
    affiche(e);

Il sistema dovrà eseguire l’istruzione System.out.println(e.toString()), dove e è un oggetto insegnante. Cerca un metodo toString nella gerarchia delle classi che porta alla classe enseignant, partendo dall'ultima:

  • nella classe enseignant non trova il metodo toString()
  • nella classe padre personne, non trova il metodo toString()
  • nella classe padre Object, trova il metodo toString() e lo esegue

Questo è quanto mostra il seguente programma:


// importa persona;
// importa insegnante;

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 ottenuti sono i seguenti:

enseignant@1ee789
personne@1ee770

Vale a dire l'oggetto nom_de_la_classe@adresse_de_l. Poiché non è molto esplicito, si è tentati di definire un metodo toString per le classi personne e etudiant che sovrascriverebbe il metodo toString della classe madre Object. Anziché scrivere metodi simili a quelli identite già esistenti nelle classi personne e enseignant, limitiamoci a rinominare toString questi metodi identite:


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

Con lo stesso programma di test di prima, i risultati ottenuti sono i seguenti:

enseignant(personne(Lucile,Dumas,56),61)
personne(Jean,Dupont,30)

3.3. Classi interne

Una classe può contenere la definizione di un'altra classe. Consideriamo il seguente esempio:

// classi importate
import java.io.*;

public class test1{

    // classe interna
    private class article{
         // si definisce la struttura
        private String code;
        private String nom;
        private double prix;
        private int stockActuel;
        private int stockMinimum;

     // costruttore
    public article(String code, String nom, double prix, int stockActuel, int stockMinimum){
         // inizializzazione degli attributi
      this.code=code;
      this.nom=nom;
      this.prix=prix;
      this.stockActuel=stockActuel;
      this.stockMinimum=stockMinimum;
    }//costruttore

    //toString
    public String toString(){
        return "article("+code+","+nom+","+prix+","+stockActuel+","+stockMinimum+")";
    }//toString
  }//classe articolo

   // dati locali
  private article art=null;

   // produttore
  public test1(String code, String nom, double prix, int stockActuel, int stockMinimum){
       // definizione attributo
    art=new article(code, nom, prix, stockActuel,stockMinimum);
  }//test1

   // accessore
  public article getArticle(){
      return art;
  }//getArticle

    public static void main(String arg[]){
      // creazione di un'istanza test1
      test1 t1=new test1("a100","velo",1000,10,5);
    // visualizzazione test1.art
    System.out.println("art="+t1.getArticle());
  }//main

}// fine classe

La classe test1 contiene la definizione di un'altra classe, la classe article. Si dice che article sia una classe interna alla classe test1. Ciò può essere utile quando la classe interna ha utilità solo all’interno della classe che la contiene. Durante la compilazione del 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 interna alla classe test1. Eseguendo il programma sopra riportato, si ottengono i seguenti risultati:

E:\data\serge\JAVA\classes\interne>java test1
art=article(a100,velo,1000.0,10,5)

3.4. Le 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 a verificare tale implementazione.

Ecco, ad esempio, la 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 verrà dichiarata come

public class C : Enumeration{
    ...
    boolean hasMoreElements(){....}
    Object nextElement(){...}
}

I metodi hasMoreElements() e nextElement() dovranno essere definiti nella classe C.

Consideriamo il codice seguente che definisce una classe élève, la quale definisce il nome di uno studente e il suo voto in una materia:


     // una classe studente
public class élève{
     // attributi pubblici
    public String nom;
    public double note;
     // costruttore
    public élève(String NOM, double NOTE){
        nom=NOM;
        note=NOTE;
    }//costruttore
}//studente  

Definiamo una classe notes che raggruppa i voti di tutti gli studenti in una materia:


// classi importate
// importazione studente

// classe voti
public class notes{

     // attributi
    protected String matière;
    protected élève[] élèves;

     // costruttore
    public notes (String MATIERE, élève[] ELEVES){
         // memorizzazione studenti e materie
        matière=MATIERE;
        élèves=ELEVES;
    }//voti

    // toString
    public String toString(){
        String valeur="matière="+matière +", notes=(";
        int i;
         // si concatenano tutti i voti
        for (i=0;i<élèves.length-1;i++){
            valeur+="["+élèves[i].nom+","+élèves[i].note+"],";
        };
         //ultimo voto
        if(élèves.length!=0){ valeur+="["+élèves[i].nom+","+élèves[i].note+"]";}
        valeur+=")";
         // fine
        return valeur;
    }//toString
}//classe

Gli attributi matière e élèves vengono dichiarati come protected per renderli accessibili da una classe derivata. Decidiamo di derivare la classe notes in una classe notesStats che avrebbe due attributi aggiuntivi, la media e la deviazione standard dei voti:


public class notesStats extends notes implements Istats {
     // attributi
    private double _moyenne;
    private double _écartType;

La classe notesStats deriva dalla classe notes e implementa la seguente interfaccia Istats:


// un'interfaccia
public interface Istats{
    double moyenne();
    double écartType();
}//

Ciò significa che la classe notesStats deve avere due metodi denominati moyenne e écartType con la firma indicata nell'interfaccia Istats. La classe notesStats è la seguente:


// classi importate
// import notes;
// importa Istats;
// importa élève;

public class notesStats extends notes implements Istats {
     // attributi
    private double _moyenne;
    private double _écartType;

     // costruttore
    public notesStats (String MATIERE, élève[] ELEVES){
         // costruzione della classe padre
        super(MATIERE,ELEVES);
         // calcolo della media dei voti
        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;
         // deviazione standard
        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;
    }//generatore

    
     // ToString
    public String toString(){
        return super.toString()+",moyenne="+_moyenne+",écart-type="+_écartType;
    }//ToString

     // metodi dell'interfaccia Istats
    public double moyenne(){
         // calcola la media dei voti
        return _moyenne;
    }//media
    public double écartType(){
         // calcola la deviazione standard
        return _écartType;
    }//écartType
}//classe

La media _moyenne e la deviazione standard _ecartType vengono calcolate al momento della creazione dell'oggetto. Pertanto, i metodi moyenne e écartType devono semplicemente restituire il valore degli attributi _moyenne e _ecartType. Entrambi i metodi restituiscono -1 se l'array degli studenti è vuoto.

La seguente classe di test:


// classi importate
// importazione studenti;
// importa Istats;
// importa voti;
// importa notesStats;

// classe di prova
public class test{
    public static void main(String[] args){
        // alcuni studenti e voti
        élève[] ELEVES=new élève[] { new élève("paul",14),new élève("nicole",16), new élève("jacques",18)};
         // che vengono salvati in un oggetto "note"
        notes anglais=new notes("anglais",ELEVES);
         // e che vengono visualizzati
        System.out.println(""+anglais);
         // idem con media e deviazione standard
        anglais=new notesStats("anglais",ELEVES);
        System.out.println(""+anglais);
    }//main
}//classe

fornisce i seguenti 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 di questo esempio sono tutte contenute in file sorgente distinti:

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 moyenne e écartType per sé stessa senza specificare che implementava l'interfaccia Istats. Qual è quindi l’utilità delle interfacce? È la seguente: una funzione può accettare come parametro un dato del tipo di un’interfaccia I. Qualsiasi oggetto di una classe C che implementi l’interfaccia I potrà quindi essere un parametro di tale funzione. Consideriamo la seguente interfaccia:


// un'interfaccia Iesempio
public interface Iexemple{
    int ajouter(int i,int j);
    int soustraire(int i,int j);
}//interfaccia

L'interfaccia Iexemple definisce due metodi: ajouter e soustraire. Le seguenti classi, classe1 e classe2, implementano questa interfaccia.


// classi importate
// import Iesempio;

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;
    }
}//classe

// classi importate
// importa Iesempio;

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;
    }
}//classe

Per semplificare l'esempio, le classi non fanno altro che implementare l'interfaccia Iexemple. Consideriamo ora il seguente esempio:


// classi importate
// import classe1;
// importa classe2;

// classe di test
public class test{
    // una funzione statica
    private static void calculer(int i, int j, Iexemple inter){
        System.out.println(inter.ajouter(i,j));
        System.out.println(inter.soustraire(i,j));
    }//calcolare

     // la funzione main
    public static void main(String[] arg){
        // creazione di due oggetti classe1 e classe2
        classe1 c1=new classe1();
        classe2 c2=new classe2();
        // chiamate alla funzione statica «calcolare»
        calculer(4,3,c1);
        calculer(14,13,c2);
    }//main
}//classe test

La funzione statica calculer accetta come parametro un elemento di tipo Iexemple. Per questo parametro potrà quindi ricevere sia un oggetto di tipo classe1 che di tipo classe2. Questo è ciò che avviene nella funzione main con i seguenti risultati:

17
21
127
201

Si nota quindi che si tratta di una proprietà simile al polimorfismo visto per le classi. Se quindi un insieme di classi Ci non collegate tra loro tramite ereditarietà (e quindi non è possibile utilizzare il polimorfismo dell’ereditarietà) presenta un insieme di metodi con la stessa firma, può essere interessante raggruppare tali metodi in un’interfaccia I da cui erediterebbero tutte le classi interessate. Le istanze di queste classi Ci possono quindi essere utilizzate come parametri di funzioni che accettano un parametro di tipo I, c.a.d, ovvero funzioni che utilizzano solo i metodi degli oggetti Ci definiti nell’interfaccia I e non gli attributi e i metodi specifici delle diverse 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

Si noti infine che l'ereditarietà delle interfacce può essere multipla, c.a.d. Si può scrivere

public class classeDérivée extends classeDeBase implements i1,i2,..,in{
...
}

dove ij sono interfacce.

3.5. Classi anonime

Nell'esempio precedente, le classi classe1 e classe2 avrebbero potuto non essere definite esplicitamente. Consideriamo il seguente programma che fa sostanzialmente la stessa cosa del precedente, ma senza la definizione esplicita delle classi classe1 e classe2:


// classi importate
// import Iesempio;

// classe di test
public class test2{

  // una classe interna
  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;
        }
    };//definizione classe3

     // una funzione statica
    private static void calculer(int i, int j, Iexemple inter){
        System.out.println(inter.ajouter(i,j));
        System.out.println(inter.soustraire(i,j));
    }//calcolare

     // la funzione main
    public static void main(String[] arg){
        // creazione di due oggetti che implementano l'interfaccia Iesempio
        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;
        }
    };//definizione di 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;
        }
    };//definizione di i2
        // un altro oggetto Iesempio
    Iexemple i3=new classe3();

        // chiamate alla funzione statica «calcolare»
        calculer(4,3,i1);
        calculer(14,13,i2);
    calculer(24,23,i3);
    }//main
}//classe test

La particolarità sta nel codice:


         // creazione di due oggetti che implementano l'interfaccia Iesempio
        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;
        }
    };//definizione i1

Si crea un oggetto i1 il cui unico ruolo è quello di implementare l'interfaccia Iexemple. Questo oggetto è di tipo Iexemple. È quindi possibile creare oggetti di tipo interfaccia. Numerosissimi metodi delle classi Java restituiscono oggetti di tipo interfaccia c.a.d, ovvero oggetti il cui unico scopo è quello di 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ò esserlo. In questo caso, si definisce una classe di questo tipo «al volo» all’interno dello stesso corpo della definizione dell’oggetto i1:


        Iexemple i1=new Iexemple(){
        public int ajouter(int a, int b){
            // definizione di aggiungere
        }
        public int soustraire(int a, int b){
            // definizione di sottrarre
        }
    };//definizione di i1

Il significato di tale istruzione è analogo alla sequenza:

public class test2{
................
// una classe interna
private static class classe1 implements Iexemple{
        public int ajouter(int a, int b){
            // definizione di "aggiungere"
        }
        public int soustraire(int a, int b){
            // definizione di sottrarre
        }
};//definizione classe1
.................
    public static void main(String[] arg){
...........
        Iexemple i1=new classe1();
}//main
}//classe

Nell'esempio sopra riportato, viene istanziata una classe e non un'interfaccia. Una classe definita "al volo" è detta classe anonima. Si tratta di un metodo spesso utilizzato per istanziare oggetti il cui unico ruolo è quello di implementare un'interfaccia.

L'esecuzione del programma precedente fornisce i seguenti risultati:

17
21
127
201
1047
2001

L'esempio precedente utilizzava classi anonime per implementare un'interfaccia. Queste possono essere utilizzate anche per derivare classi prive di costruttori con parametri. Consideriamo il seguente esempio:

// classi importate
// import Iesempio;

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;
    }
};//definizione classe3

public class test4{

     // una funzione statica
    private static void calculer(int i, int j, Iexemple inter){
        System.out.println(inter.ajouter(i,j));
        System.out.println(inter.soustraire(i,j));
    }//calcolare

   // metodo main
    public static void main(String args[]){
       // definizione di una classe anonima derivata da classe3
     // per ridefinire la funzione sottrarre
    classe3 i1=new classe3(){
        public int ajouter(int a, int b){
          return a+b+10000;
      }//sottrarre
    };//i1
          // chiamate alla funzione statica «calcolare»
        calculer(4,3,i1);
  }//main
}//classe 

Qui troviamo una classe classe3 che implementa l'interfaccia Iexemple. Nella funzione main, definiamo una variabile i1 il cui tipo è una classe derivata da classe3. Questa classe derivata viene definita “al volo” in una classe anonima e ridefinisce il metodo ajouter della classe classe3. La sintassi è identica a quella della classe anonima che implementa un’interfaccia. Solo che in questo caso 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 che troverà nel corpo della classe anonima sostituiranno i metodi con lo stesso nome della classe base.

L'esecuzione del programma precedente fornisce i seguenti risultati:

E:\data\serge\JAVA\classes\anonyme>java test4
10007
2001

3.6. I pacchetti

3.6.1. Creare classi in un pacchetto

Per scrivere una riga sullo schermo, utilizziamo l'istruzione

System.out.println(...)

Se osserviamo la definizione della classe System, scopriamo che in realtà si chiama java.lang.System:

Image

Verifichiamolo con un esempio:


public class test1{
    public static void main(String[] args){
        java.lang.System.out.println("Coucou");
    }//main
}//classe

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

anziché


        java.lang.System.out.println("Coucou");

Poiché, implicitamente, per ogni programma Java viene importato sistematicamente il "pacchetto" java.lang. Pertanto, è come se all'inizio di ogni programma fosse presente l'istruzione:

import java.lang.*;

Cosa significa questa istruzione? Consente l’accesso a tutte le classi del pacchetto java.lang. Il compilatore vi troverà il file System.class che definisce la classe System. Non sappiamo ancora dove il compilatore troverà il pacchetto java.lang né che aspetto abbia un pacchetto. Ci torneremo più avanti. Per creare una classe in un pacchetto, si scrive:

package paquetage;
// definizione della classe
...

A titolo di esempio, creiamo all’interno di un pacchetto la nostra classe personne studiata in precedenza. Sceglieremo istia.st come nome del pacchetto. La classe personne diventa:


// nome del pacchetto in cui verrà creata la classe persona
package istia.st;

// classe persona
public class personne{
    // cognome, nome, età
    private String prenom;
    private String nom;
    private int age;
  
    // costruttore 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+")";
    }

}//classe

Questa classe viene compilata e poi inserita nella cartella 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 personne 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);
        }//mano
    }//classe di prova

Si noti che la classe personne è ora preceduta dal nome del proprio pacchetto, istia.st. Dove troverà il compilatore la classe istia.st.personne? Il compilatore cerca le classi di cui ha bisogno in un elenco predefinito di directory e in una struttura ad albero che parte dalla directory corrente. In questo caso, cercherà la classe istia.st.personne nel file istia\st\personne.class. Ecco perché abbiamo inserito il file personne.class nella directory istia\st. Compiliamo ed eseguiamo il programma di prova:

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 con una clausola import:


import istia.st.personne;

Possiamo quindi scrivere


            personne p1=new personne("Jean","Dupont",20);

e il compilatore lo tradurrà in


            istia.st.personne p1=new istia.st.personne("Jean","Dupont",20);

Il programma di test diventa quindi il seguente:


// spazi dei nomi importati
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);
    }//main
}//classe test2

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. Inseriamolo in una cartella denominata mesClasses, sempre nella directory corrente. Ricordiamo che le classi del pacchetto istia.st sono collocate in una cartella denominata istia\st. La struttura della directory corrente è la 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 da quando è stato spostato. Notiamo che lo cerca a causa dell'istruzione import. Per impostazione predefinita, lo cerca nella directory corrente all’interno di 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ò tornarci 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 avviene 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 è la volta della macchina virtuale Java, che non riesce a trovare la classe istia/st/personne. La cerca nella directory corrente, mentre ora si trova nella directory mesClasses. Diamo un'occhiata alle opzioni della macchina virtuale Java:

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

Si nota che anche JVM dispone di un'opzione classpath, proprio come il compilatore. Utilizziamola per indicare 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 è la classe test2 stessa a non essere trovata. Il motivo è il seguente: in assenza della parola chiave classpath, la directory corrente viene sistematicamente esplorata durante la ricerca delle classi, ma non quando è presente. Di conseguenza, la classe test2.class, che si trova nella directory corrente, non viene trovata. La soluzione? Aggiungere la directory corrente a classpath. La directory corrente è rappresentata dal simbolo .

E:\data\serge\JAVA\classes\paquetages\personne>java -classpath mesClasses;. test2
p1=personne(Jean,Dupont,20)

Perché tutte queste complicazioni? Lo scopo dei pacchetti è quello di evitare conflitti di nomi tra le classi. Consideriamo due aziende, E1 e E2, che distribuiscono classi raggruppate rispettivamente nei pacchetti com.e1 e com.e2. Supponiamo che un cliente C acquisti questi due insiemi di classi, in cui entrambe le aziende hanno definito una classe denominata personne. Il cliente C farà riferimento alla classe personne dell’azienda E1 con com.e1.personne e quella dell’azienda E2 con com.e2.personne, evitando così un conflitto di nomi.

3.6.2. Ricerca dei pacchetti

Quando scriviamo in un programma

import java.util.*;

per accedere a tutte le classi del pacchetto java.util, dove viene individuato quest’ultimo? Abbiamo detto che i pacchetti vengono cercati per impostazione predefinita nella directory corrente o nell’elenco delle directory dichiarate nell’opzione classpath del compilatore o della JVM, se tale opzione è presente. Vengono inoltre cercati nelle directory lib della directory di installazione di JDK. Consideriamo questa directory:

Image

In questo esempio, verranno esaminate le strutture ad albero jdk14\lib e jdk14\jre\lib alla ricerca di file .class, .jar o .zip che siano archivi di classi. Effettuiamo, ad esempio, una ricerca dei file .jar presenti nella directory precedente jdk14:

Image

Ce ne sono diverse decine. Un file .jar può essere aperto con l’utilità winzip. Apriamo il file rt.jar sopra indicato (rt=RunTime). Al suo interno si trovano diverse centinaia di file .class, tra cui quelli appartenenti al pacchetto java.util:

Image

Un metodo semplice per gestire i pacchetti consiste quindi nel collocarli nella directory <jdk>\jre\lib, dove <jdk> è la directory di installazione di JDK. In genere, un pacchetto contiene diverse classi ed è pratico raggrupparle in un unico file .jar (JAR = file Java ARchive). 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 assistenza sull’uso del programma jar richiamandolo senza parametri:

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''archives existant
    -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 pour les fichiers jar 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 doivent Ûtre spÚcifiÚs
dans l''ordre des indicateurs ''m'' et ''f''.

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 ''monmanifest'' pour archiver tous les fichiers du

           rÚpertoire foo/ dans ''classes.jar'':
       jar cvfm classes.jar monmanifest -C foo/ .

Torniamo alla classe personne.class creata in precedenza in un 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 istia.st.jar che archivi tutte le classi del pacchetto istia.st, ovvero tutte le classi dell'albero istia\st sopra riportato:

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 con winzip il contenuto del file istia.st.jar:

Image

Inseriamo 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 è bastato specificare il nome dell'archivio da esplorare senza dover indicare esplicitamente dove si trovava. Tutte le directory della struttura <jdk>\jre\lib vengono esplorate per trovare il file .jar richiesto.

3.7. L'esempio IMPÔTS

Riprendiamo il calcolo dell’imposta già esaminato nel capitolo precedente e lo elaboriamo utilizzando una classe. Ricordiamo il problema:

Consideriamo il caso semplificato di un contribuente che deve dichiarare solo il proprio stipendio:

  • si calcola il numero di quote del dipendente nbParts=nbEnfants/2 +1 se non è sposato, nbEnfants/2+2 se è sposato, dove nbEnfants è il numero dei suoi figli.
  • se ha almeno tre figli, ha diritto a una mezza quota in più
  • si calcola il suo reddito imponibile R = 0,72 * S, dove S è il suo stipendio annuale
  • si calcola il suo coefficiente familiare QF = R / nbParts
  • si calcola la sua imposta I. Consideriamo la seguente tabella:
12620,0
0
0
13190
0,05
631
15640
0,1
1290,5
24.740
0,15
2072,5
31810
0,2
3309,5
39.970
0,25
4900
48360
0,3
6898,5
55790
0,35
9316,5
92970
0,4
12106
127.860
0,45
16754,5
151250
0,50
23147,5
172.040
0,55
30710
195.000
0,60
39312
0
0,65
49062

Ogni riga ha 3 campi. Per calcolare l'imposta I, si cerca la prima riga in cui QF<=campo1. Ad esempio, se QF=23000, si troverà la riga

    24740        0.15        2072.5

L'imposta I è quindi pari a 0,15*R - 2072,5*nbParts. Se QF è tale che la relazione QF<=campo1 non è mai soddisfatta, allora vengono utilizzati i coefficienti dell’ultima riga. In questo caso:

    0                0.65        49062

il che dà l'imposta I=0,65*R - 49062*nbParts.

La classe «impots» sarà definita come segue:


// creazione di una classe imposti

public class impots{

    // i dati necessari per il calcolo dell'imposta
     // provengono da una fonte esterna

    private double[] limites, coeffR, coeffN;

     // produttore
    public impots(double[] LIMITES, double[] COEFFR, double[] COEFFN) throws Exception{
         // si verifica che i 3 array abbiano le stesse dimensioni
        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+")");
        // va bene
        this.limites=LIMITES;
        this.coeffR=COEFFR;
        this.coeffN=COEFFN;
    }//produttore

     // calcolo dell'imposta
    public long calculer(boolean marié, int nbEnfants, int salaire){
         // calcolo del numero di quote
        double nbParts;
        if (marié) nbParts=(double)nbEnfants/2+2;
        else nbParts=(double)nbEnfants/2+1;
        if (nbEnfants>=3) nbParts+=0.5;
         // calcolo del reddito imponibile e del quoziente familiare
        double revenu=0.72*salaire;
        double QF=revenu/nbParts;
         // calcolo dell'imposta
        limites[limites.length-1]=QF+1;
        int i=0;
        while(QF>limites[i]) i++;
        // restituzione del risultato
        return (long)(revenu*coeffR[i]-nbParts*coeffN[i]);
    }//calcolare
}//classe

Viene creato un oggetto "imposti" contenente i dati necessari per il calcolo dell'imposta di un contribuente. Questa è la parte stabile dell'oggetto. Una volta creato l'oggetto, è possibile richiamare ripetutamente il suo metodo "calcola", che calcola l'imposta del contribuente in base al suo stato civile (coniugato o meno), al numero di figli e al suo stipendio annuale.

Un programma di test potrebbe essere il seguente:


//classi importate
// importazione imposte;
import java.io.*;

    public class test
    {
        public static void main(String[] arg) throws IOException
        {
             // programma interattivo per il calcolo delle imposte
             // l'utente digita tre dati dalla tastiera: coniugato nbEnfants stipendio
             // il programma visualizza quindi l’imposta da pagare

            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";

             // tabelle di dati necessarie per il calcolo dell'imposta
            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};

       // creazione di un flusso di lettura
      BufferedReader IN=new BufferedReader(new InputStreamReader(System.in));
            // creazione di un oggetto imposta
            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

            // ciclo infinito
            while(true){
                 // vengono richiesti i parametri per il calcolo dell'imposta
                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();
                // C'è qualcosa da fare?
                if(paramètres==null || paramètres.equals("")) break;
                // verifica del numero di argomenti nella riga inserita
                String[] args=paramètres.split("\\s+");
                int nbParamètres=args.length;
                if (nbParamètres!=3){
                    System.err.println(syntaxe);
                    continue;
                }//if
                 // verifica della validità dei parametri
                // sposato
                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
                 // stipendio
                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
                 // i parametri sono corretti - si calcola l'imposta
                System.out.println("impôt="+objImpôt.calculer(marié.equals("o"),nbEnfants,salaire)+" F");
                 // contribuente successivo
            }//while
        }//main
    }//classe

Ecco un esempio di esecuzione del programma precedente:

E:\data\serge\MSNET\c#\imposti\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 :