Skip to content

3. [TD]: Classi

Parole chiave: classe, interfaccia, ereditarietà, eccezione, polimorfismo

Letture consigliate:

  • paragrafi 2.1, 2.2, 2.4 e 2.7 del capitolo 2 di [ref1]: Classi e interfacce
  • paragrafi 3.3 (classe String), 3.5 (classe ArrayList), 3.6 (classe Arrays)

Nella parte 1 dell'esercizio ELECTIONS non è stata utilizzata alcuna classe. Abbiamo costruito una soluzione come l'avremmo costruita in linguaggio C. Introduciamo ora il concetto di classe Java.

3.1. Support

 

La cartella [support / chap-03] contiene il progetto Eclipse di questo capitolo.

D'ora in poi lavoreremo con la versione 1.8 di JDK, poiché alcuni dei progetti che seguiranno richiedono questa versione di JDK. Per conoscere la versione di JDK utilizzata, procedere come segue:

  • in [4], il JRE (Java Runtime Environment) utilizzato. Questo JRE è in realtà un JDK (Java Development Kit), qui [jdk1.8.0_60]. Se non si tratta di un JDK o se si dispone di una versione inferiore alla 1.8, procedere come segue: [5-21];
  • in [8], il JRE attualmente utilizzato di default da Eclipse;
  • in [11], i vari JDK e JRE attualmente riconosciuti da Eclipse;
  • in [15], scegliete un JDK piuttosto che un JRE. Questo documento utilizza progetti Maven che richiedono un JDK;
  • in [21], si ha un JDK di versione >=1.8;
  • in [22-23], si accede alle faccette (diverse viste dello stesso progetto Eclipse) del progetto;
  • in [24], verificare di utilizzare una versione di Java >=1.8;

3.2. La classe [ListeElectorale]

In linguaggio C, probabilmente avremmo utilizzato una struttura per rappresentare una lista di candidati alle elezioni. Avrebbe potuto avere la seguente forma:

struct t_liste
   {
     char nom[15];
     long voix;
     int  elimine;
     int sieges;
   };

Il concetto di struttura non esiste nel linguaggio Java. È necessario sostituirlo con quello di classe. Si decide quindi di creare una classe per memorizzare le informazioni relative a una lista di candidati. Questa avrebbe la seguente struttura di base:


package istia.st.elections;

public class ListeElectorale {

    /**
     * identité de la liste
     */
    private int id;

    /**
     * nom de la liste
     */
    private String nom;
    /**
     * nombre de voix de la liste
     */
    private int voix;
    /**
     * nombre de sièges de la liste
     */
    private int sieges;
    /**
     * indique si la liste est éliminée ou non
     */
    private boolean elimine;

    /**
     * constructeur par défaut
     */
    public ListeElectorale() {
    }

    /**
     *
     * @param nom String : le nom de la liste
     * @param voix int : son nombre de voix
     * @param sieges int : son nombre de sieges
     * @param elimine boolean : son état éliminé ou non
     */
    public ListeElectorale(int id,String nom, int voix, int sieges, boolean elimine) {
...
    }

    /**
     *
     * @return int : l'identifiant de la liste
     */
    public int getId() {
...
    }

    /**
     * initialise l'identifiant de liste
     * @param id int : identifiant de la liste
     * @throws ElectionsException si id<1
     */
    public void setId(int id) {
...
    }

    /**
     *
     * @return String : le nom de la liste
     */
    public String getNom() {
...
    }

    /**
     * initialise le nom de la liste
     * @param nom String : nom de la liste
     *  @throws ElectionsException si le nom est vide ou blanc
     */
    public void setNom(String nom) {
...
    }

    /**
     *
     * @return int : le nombre de voix de la liste
     */
    public int getVoix() {
 ...
    }

    /**
     * initialise le nombre de voix de la liste
     * @param voix int : le nombre de voix de la liste
     */
    public void setVoix(int voix) {
 ...
    }

    /**
     *
     * @return int : le nombre de sièges de la liste
     */
    public int getSieges() {
 ...
    }

    /**
     * fixe le nombre de sièges de la liste
     * @param sieges int : le nombre de sièges de la liste
     */
    public void setSieges(int sieges) {
...
    }

    /**
     *
     * @return boolean : valeur du champ elimine
     */
    public boolean isElimine() {
  ...
    }

    /**
     *
     * @param sieges int
     */
    public void setElimine(boolean elimine) {
 ...
    }

    /**
     *
     * @return String : identité de la liste électorale
     */
    public String toString() {
   ...
    }
}
  • riga 8: numero che identifica in modo univoco una lista. Non è indispensabile in questo caso, ma è previsto per un utilizzo futuro.
  • riga 13: il nome della lista.
  • riga 17: il numero di voti della lista
  • riga 21: il numero di seggi della lista
  • riga 25: valore booleano che indica se la lista è stata eliminata (percentuale di voti ottenuti al di sotto della soglia elettorale) o meno.

Ogni campo privato denominato [xyz] può essere inizializzato tramite un metodo denominato [setXyz]. Il metodo [getXyz] consente invece di ottenere il valore del campo privato [xyz]. Nel caso particolare in cui [xyz] sia un campo di tipo booleano, il metodo [getXyz] può essere sostituito dal metodo [isXyz]. La denominazione specifica di questi metodi segue uno standard di codifica denominato standard JavaBean. Definiamo quindi i seguenti metodi pubblici:

  • getId (riga 48), setId (riga 57)
  • getNom (riga 65), setNom (riga 74)
  • getVoix (riga 82), setVoix (riga 90)
  • getSieges (riga 98), setSieges (riga 106)
  • isElimine (riga 114), setElimine (riga 122)
  • righe 30-31: definiscono un costruttore senza parametri. Questo permette di creare un oggetto [ListeElectorale] senza inizializzarlo. L'oggetto può poi essere inizializzato tramite i metodi set.
  • righe 40-42: definiscono un costruttore che consente di creare un oggetto [ListeElectorale] inizializzandone i cinque campi privati.
  • righe 130-132: definiscono il metodo [toString] che restituisce una stringa contenente i valori dei cinque campi dell'oggetto.

Un programma di test della classe ListeElectorale potrebbe essere il seguente:


package istia.st.elections.tests;

import istia.st.elections.ListeElectorale;

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // creazione di una lista elettorale
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // visualizzazione dell'identità della lista
        System.out.println("listeElectorale1=" + listeElectorale1);
        // modifica del numero di seggi
        listeElectorale1.setSieges(2);
        // visualizzazione dei dati della lista 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // una nuova lista elettorale
        ListeElectorale listeElectorale2 = listeElectorale1;
        // visualizzazione identità lista 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // modifica del numero di seggi
        listeElectorale2.setSieges(3);
        // visualizzazione delle informazioni relative alle 2 liste
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
    }
}

L'ambiente Eclipse per questo test potrebbe essere il seguente:

  • [1]: il progetto si chiama [elections-02A]
  • [2]: l'applicazione verrà inserita in un pacchetto, in questo caso [istia.st.elections]
  • [3]: [ListeElectorale.java] è il codice sorgente della classe [ListeElectorale]
  • [4]: le classi di test saranno inserite in un pacchetto, in questo caso [istia.st.elections.tests]
  • [5]: la classe di test [MainTest1ListeElectorale]

La schermata ottenuta dopo l’esecuzione del programma sopra riportato è la seguente:

Image


Compito da svolgere: avvalendovi di quanto sopra, completate il codice della classe ListeElectorale.


3.3. Creazione di una classe di eccezione [ElectionsException]

Tra le diverse classi di eccezione del linguaggio Java, ce n'è una chiamata [RuntimeException]. Questa classe deriva dalla classe [Exception], radice di tutte le classi di eccezione. La particolarità delle istanze di [RuntimeException] o delle istanze da essa derivate è che non è necessario dichiararle né gestirle. Vengono definite eccezioni non controllate.

Prendiamo un primo esempio. La classe [BufferedReader] è una classe le cui istanze consentono di leggere righe di testo in un flusso di dati. Possiede un metodo [readLine] la cui firma è la seguente:

public String readLine()throws IOException

Si nota che il metodo può generare un'eccezione di tipo [IOException]. L'albero di questa classe è il seguente:

1
2
3
4
java.lang.Object
  java.lang.Throwable
      java.lang.Exception
          java.io.IOException

La classe [IOException] deriva dalla classe [Exception] (riga 3). Il compilatore ci obbliga a gestire e dichiarare le eccezioni di tipo [java.lang.Exception] o derivate (ad eccezione del ramo [RuntimeException] che presenteremo più avanti). Pertanto, per leggere una riga di testo digitata dalla tastiera, saremo costretti a scrivere qualcosa del tipo:

1
2
3
4
5
6
7
8
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // gestire l'eccezione
    ....
}

Prendiamo un altro esempio. Per convertire una stringa in un numero intero è possibile utilizzare il metodo statico [Integer.parseInt], la cui firma è la seguente:

public static int parseInt(String s) throws NumberFormatException

L’argomento [s] è la stringa di caratteri da convertire in numero intero. Si nota che il metodo può generare un’eccezione di tipo [NumberFormatException]. L’albero di questa classe è il seguente:

1
2
3
4
5
6
java.lang.Object
  java.lang.Throwable
      java.lang.Exception
          java.lang.RuntimeException
              java.lang.IllegalArgumentException
                  java.lang.NumberFormatException

La classe [NumberFormatException] deriva dalla classe [RuntimeException] (riga 4). Il compilatore non ci obbliga a gestire e dichiarare le eccezioni di tipo [java.lang.RuntimeException] o derivate. Pertanto, potremo scrivere qualcosa del tipo:

1
2
3
4
5
6
7
8
9
BufferedReader clavier=....;
String ligne=null;
try{
    ligne=clavier.readLine();
}catch (IOException ex){
     // gestire l'eccezione
    ....
}
int age=Integer.parseInt(ligne);

Non siamo obbligati a inserire una clausola [try - catch] per gestire l'eventuale eccezione generata da [Integer.parseInt] (riga 9).

La creazione e l’utilizzo di classi di eccezione derivate da [RuntimeException] presentano vantaggi e svantaggi:

  • tra i vantaggi: il codice è più snello
  • tra gli svantaggi: si rischia di ricadere nei metodi del C, dove ogni funzione restituisce un codice di errore che pochi utilizzano, proprio per avere un codice più snello. Quando si verifica un errore non gestito di questo tipo, il programma va in crash, solitamente in modo poco elegante.

Decidiamo di creare una classe speciale che raggruppi tutte le eccezioni che potrebbero verificarsi nella nostra applicazione ELECTIONS. Si chiamerà [ElectionsException] e deriverà dalla classe [RuntimeException]. Il suo codice è il seguente:


package istia.st.elections;

public class ElectionsException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public ElectionsException() {
        super();
    }

    public ElectionsException(String message) {
        super(message);
    }

    public ElectionsException(Throwable cause) {
        super(cause);
    }

    public ElectionsException(String message, Throwable cause) {
        super(message, cause);
    }
}
  • riga 1: inseriamo la classe nel pacchetto [istia.st.elections];
  • riga 3: la classe deriva da [RuntimeException]. È quindi non controllata;
  • riga 4: un identificatore di serializzazione che per il momento possiamo ignorare;
  • nella nostra applicazione useremo due tipi di costruttori:
    • quello classico delle righe 15-17 come riportato di seguito:
throw new ElectionsException("Le nombre de sièges doit être >0")

In questo caso, il metodo che chiama un metodo che genera tale eccezione può gestirla come segue:


        // test dell'eccezione
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }
  • (continua)
    • oppure quello delle righe 14-20, destinato a segnalare un'eccezione già verificatasi, incapsulandola in un'eccezione di tipo [ElectionsException]:

    try {
        ...;
        } catch (SQLException ex) {
            // incapsulamento dell'eccezione
            throw new ElectionsException("erreur de fermeture de la connexion à la BD",ex);
        }

Questo secondo metodo ha il vantaggio di conservare le informazioni contenute nella prima eccezione. In questo caso, il metodo che chiama un metodo che genera tale eccezione può gestirla come segue:


        try {
            ...;
        } catch (ElectionsException ex) {
            System.out.println(ex.getMessage() + ", Cause : "+ ex.getCause().getMessage());
            System.exit(1);
        }

Compito da svolgere: modificare il codice della classe ListeElectorale in modo che i metodi set generino un'eccezione di tipo [ElectionsException] se l'inizializzazione richiesta non è corretta, come ad esempio l'inizializzazione del nome con una stringa vuota.


Il progetto Eclipse di prova di questa nuova versione potrebbe essere il seguente:

  • [1]: il progetto si chiama [elections-02B]
  • [2]: l’applicazione è inserita in un pacchetto, in questo caso [istia.st.elections]
  • [3]: le classi [ListeElectorale] e [ElectionsException]
  • [4]: le classi di test sono inserite in un pacchetto, in questo caso [istia.st.elections.tests]
  • [5]: la classe di test [MainTest1ListeElectorale]

La classe di test [MainTest1ListeElectorale] già esaminata viene leggermente modificata per testare i casi di eccezione:


package istia.st.elections.tests;

import istia.st.elections.ElectionsException;
import istia.st.elections.ListeElectorale;

public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // creazione di una lista elettorale
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // visualizzazione identità lista
        System.out.println("listeElectorale1=" + listeElectorale1);
        // modifica del numero di seggi
        listeElectorale1.setSieges(2);
        // visualizzazione dei dati della lista 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // una nuova lista elettorale
        ListeElectorale listeElectorale2 = listeElectorale1;
        // visualizzazione identità lista 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // modifica del numero di seggi
        listeElectorale2.setSieges(3);
        // visualizzazione delle informazioni relative alle 2 liste
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
        // test di eccezione
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }

    }
}
  • riga 28: si tenta di inizializzare il numero di posti a sedere con un valore non consentito
  • riga 30: se si verifica un'eccezione, questa viene visualizzata

L'esecuzione del test fornisce i seguenti risultati:

Image

Si nota che la classe [ListeElectorale] ha effettivamente generato un'eccezione quando si è cercato di inizializzare il numero di posti con un valore non valido (riga 28 del codice).

3.4. Una classe di test unitario

Il tipo di test precedente si basa su una verifica visiva. Si controlla che sullo schermo appaia ciò che ci si aspetta. Si tratta di un metodo sconsigliabile in ambito professionale. I test devono sempre essere automatizzati il più possibile e mirare a non richiedere alcun intervento umano. L’essere umano è infatti soggetto alla stanchezza e la sua capacità di verificare i test si affievolisce nel corso della giornata.

Un'applicazione si evolve nel tempo. Ad ogni evoluzione, è necessario verificare che l'applicazione non subisca una "regressione", c.a.d, e che continui a superare i test di corretto funzionamento effettuati al momento della sua scrittura iniziale. Questi test sono chiamati test di "non regressione". Un'applicazione di una certa importanza può richiedere centinaia di test. Si testano infatti tutti i metodi di ogni classe dell’applicazione. Si tratta dei cosiddetti test unitari. Questi possono richiedere l’impiego di molti sviluppatori se non sono stati automatizzati.

Sono stati sviluppati strumenti per automatizzare i test. Uno di questi si chiama [JUnit]. Si tratta di una libreria di classi destinata alla gestione dei test. Utilizzeremo questo strumento per testare la classe [ListeElectorale].

Un programma di test JUnit (versioni 4.x) ha la seguente struttura:


package istia.st.elections.tests;

import org.junit.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class JUnitEssai {

    @Before
    public void avant() throws Exception {
        System.out.println("tearUp");
    }

    @After
    public void après() throws Exception {
        System.out.println("tearDown");
    }

    @Test
    public void t1() {
        System.out.println("test1");
        Assert.assertEquals(1, 1);
    }

    @Test
    public void t2() {
        System.out.println("test2");
        Assert.assertEquals(1, 2);
    }

}
  • riga 1: la classe è stata inserita nel pacchetto [istia.st.elections.tests];
  • riga 11: il metodo annotato con l'annotazione [@Before] viene eseguito prima di ogni test unitario;
  • riga 16: il metodo annotato con l'annotazione [@After] viene eseguito dopo ogni test unitario;
  • riga 21: un metodo annotato con l'annotazione [@Test] è un metodo testato dal test unitario. I metodi annotati con [@Test] saranno eseguiti uno dopo l'altro, salvo diversa indicazione da parte del tester, che può selezionare autonomamente i metodi da testare. Prima di ogni esecuzione di un metodo [@Test], viene eseguito il metodo [@Before]. Dopo ogni esecuzione di un metodo [@Test], viene eseguito il metodo [@After];
  • righe 22-25: definiscono un metodo di test [t1];
  • riga 18: uno dei metodi [Assert.assert*] che consente di verificare le asserzioni. Sono disponibili i seguenti metodi [assert]:
    • assertEquals(espressione1, espressione2): verifica che i valori delle due espressioni siano uguali. Sono accettati numerosi tipi di espressione (int, String, float, double, boolean, char, short). Se le due espressioni non sono uguali, viene generata un'eccezione di tipo [AssertionFailedError ],
    • assertEquals(reale1, reale2, delta): verifica che due numeri reali siano uguali con un margine di delta, c.a.d abs(reale1-reale2)<=delta. Si potrà scrivere, ad esempio, assertEquals(reale1, reale2, 1E-6) per verificare che due valori siano uguali con una tolleranza di 10⁻⁶,
    • assertEquals(messaggio, espressione1, espressione2) e assertEquals(messaggio, reale1, reale2, delta) sono varianti che consentono di specificare il messaggio di errore da associare all'eccezione di tipo [AssertionFailedError] generata quando il metodo [assertEquals] fallisce,
    • assertNotNull(Object) e assertNotNull(message, Object): verifica che Object non sia uguale a null,
    • assertNull(Object) e assertNull(messaggio, Object): verificano che Object sia uguale a null,
    • assertSame(Object1, Object2) e assertSame(message, Object1, Object2): verifica che i riferimenti Object1 e Object2 puntino allo stesso oggetto,
    • assertNotSame(Object1, Object2) e assertNotSame(message, Object1, Object2): verifica che i riferimenti Object1 e Object2 non puntino allo stesso oggetto;
  • riga 24: questa asserzione deve avere esito positivo;
  • riga 30: questa asserzione deve fallire;

Nell’ambiente Eclipse, la creazione di una classe di test JUnit può avvenire nel modo seguente:

  • [1]: clicca con il tasto destro del mouse sul pacchetto in cui desideri aggiungere la classe di test, quindi seleziona l’opzione [JUnit / New / JUnit Test Case]
  • [1]: selezione di una versione JUnit;
  • [2]: selezione della cartella in cui deve essere creata la classe di test;
  • [3]: selezione del pacchetto in cui deve essere creata la classe di test;
  • [4]: nome della classe di test;
  • [5]: selezione dei metodi da includere nella classe che verrà generata;
  • [6]: la classe JUnitEssai è stata generata

La procedura guidata precedente genera una classe quasi vuota:


package istia.st.elections.tests;

import org.junit.Assert;
import org.junit.After;
import org.junit.Before;

public class JUnitEssai {

    @Before
    public void setUp() throws Exception {        
    }

    @After
    public void tearDown() throws Exception {
    }
}

Completiamo e modifichiamo il codice precedente nel modo seguente:


package istia.st.elections.tests;

import org.junit.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class JUnitEssai2 {

    @Before
    public void avant() throws Exception {
        System.out.println("tearUp");
    }

    @After
    public void après() throws Exception {
        System.out.println("tearDown");
    }

    @Test
    public void t1() {
        System.out.println("test1");
        Assert.assertEquals(1, 1);
    }

    @Test
    public void t2() {
        System.out.println("test2");
        Assert.assertEquals(1, 2);
    }

}

In Eclipse, facendo clic con il tasto destro sulla classe di test e selezionando l’opzione [Run as / JUnit test], è possibile eseguirla:

Image

I risultati ottenuti dall'esecuzione di questo test sono i seguenti:

Image

Come si vede sopra, il metodo [test2] ha dato esito negativo. Ogni volta che un test fallisce, gli viene associato un messaggio di errore. Per [test2], è quello visualizzato sopra. Il messaggio indica il numero della riga in cui si è verificato l’errore (riga 30). Alla riga 30, la chiamata che ha dato esito negativo era:


    Assert.assertEquals(1, 2);

Il primo parametro è denominato «valore atteso», il secondo «valore effettivo». Il messaggio di errore di [test2] riportato sopra indica che il valore atteso era 2, mentre il valore effettivo è stato 3.

Infine, i messaggi visualizzati sulla console dai vari metodi di test sono stati i seguenti:

Image

Questi messaggi dimostrano che i metodi [@Before] e [@After] sono stati effettivamente chiamati, rispettivamente, prima e dopo ciascun metodo di test.

Le classi di test non sono necessariamente scritte dagli sviluppatori stessi. Possono essere redatte da chi ha redatto le specifiche dell’applicazione. Alcuni metodi di sviluppo, noti come TDD (Test Driven Development), raccomandano di scrivere le classi di test prima ancora di scrivere le classi da testare. Ciò consente talvolta di chiarire specifiche che altrimenti potrebbero essere interpretate in diversi modi.

Creiamo un test JUnit 4, denominato [JUnitTest1ListeElectorale], per la classe [ListeElectorale]. In Eclipse, si procederà come descritto in precedenza:

Completiamo il codice generato dall’assistente nel modo seguente:


package istia.st.elections.tests;

import org.junit.Assert;
import istia.st.elections.ElectionsException;
import istia.st.elections.ListeElectorale;

import org.junit.Test;

public class JUnitTest1ListeElectorale {

    @Test
    public void t1() {
        // creazione lista elettorale
        ListeElectorale liste = new ListeElectorale(1, "a", 32000, 0, false);
        // verifiche
        Assert.assertEquals("a", liste.getNom());
        Assert.assertEquals(32000, liste.getVoix());
        Assert.assertEquals(false, liste.isElimine());
        Assert.assertEquals(0, liste.getSieges());
        // verifica validità ID
        boolean erreur = false;
        try {
            liste.setId(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // verifica della validità del nome
        erreur = false;
        try {
            liste.setNom("");
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // verifica validità voti
        erreur = false;
        try {
            liste.setVoix(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // verifica della validità dei seggi
        erreur = false;
        try {
            liste.setSieges(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
    }

}

L'esecuzione del test fornisce il seguente risultato:

Image

I test hanno avuto esito positivo. Considereremo quindi che disponiamo di una classe [ListeElectorale] operativa.

3.5. MainElections: versione 2

Letture consigliate:

  • paragrafi 2.1, 2.2, 2.4 e 2.7 del capitolo 2 di [1]: Classi e interfacce
  • paragrafi 3.3 (classe String), 3.5 (classe ArrayList), 3.6 (classe Arrays)

Si desidera riscrivere l’applicazione [Elections] aggiungendo i seguenti nuovi vincoli:

  • si utilizzerà la classe [ListeElectorale] per rappresentare un elenco di candidati
  • l’applicazione richiederà dalla tastiera le seguenti informazioni:
  • il numero di seggi da assegnare
  • i nomi e le voci delle liste. Non si sa a priori quante liste ci siano. L'ultima lista sarà contrassegnata da un nome pari alla stringa "*".
  • Poiché non si conosce a priori il numero di liste, queste verranno innanzitutto memorizzate in un oggetto di tipo [ArrayList]. Successivamente, una volta inserite tutte le liste, queste verranno trasferite in un array di liste.
  • I risultati saranno visualizzati in ordine decrescente in base al numero di seggi ottenuti.

Per ordinare un array T, sono disponibili diversi metodi statici della classe [Arrays]:

  • Arrays.sort(T): ordina l’array T secondo un ordine naturale, se presente (crescente per i numeri, le date, alfabetico per le stringhe, ...)
  • Arrays.sort(T,comparatore): per ordinare tabelle T che non hanno un ordine naturale. È il caso, in questo contesto, della tabella delle liste che deve essere ordinata in base a un campo specifico della lista: il numero di seggi ottenuti.

Nel metodo Arrays.sort(T,comparatore), il parametro comparatore è un oggetto che implementa la seguente interfaccia Comparator:

Image

  • il metodo compare consente di confrontare due elementi dell’array T
  • il metodo equals consente di stabilire se due oggetti sono uguali

Entrambi i metodi confrontano oggetti di tipo Object obj1 e obj2. Stabilire se obj1<obj2, obj1=obj2 o obj1>obj2 è maggiore dipende dalla relazione di ordine che si desidera creare tra i due oggetti. Spetta allo sviluppatore che implementa questa interfaccia indicare come si determina che:

  • obj1 sia più piccolo di obj2
  • obj1 è maggiore di obj2
  • obj1 è uguale a obj2

La classe Object, da cui deriva ogni classe Java, dispone già di un metodo [equals]. Per ordinare un array T di oggetti di tipo O, il metodo [equals] della classe O non è necessario. È quindi possibile mantenere l’implementazione predefinita fornita dalla classe Object. A questo punto occorre implementare solo il metodo [compare]. Questo metodo viene chiamato ripetutamente dal metodo [Arrays.sort]. Quest’ultimo passerà ogni volta come parametri obj1 e obj2 del metodo compare, due elementi dell’array T da ordinare. Nel nostro caso, questi elementi saranno di tipo [ListeElectorale]. Si noti qui il polimorfismo in azione. Il metodo [compare] è definito in modo da ricevere parametri di tipo [Object]. Ciò significa che può ricevere parametri di tipo [Object] o derivati (polimorfismo). Poiché [Object] è la classe padre di tutte le classi Java, i parametri effettivi possono essere di tipo [ListeElectorale].

Per un ordinamento in ordine crescente, il metodo [compare] deve restituire:

  • -1 se obj1 è minore di obj2
  • +1 se obj1 è maggiore di obj2
  • 0 se obj1 è uguale a obj2

Per un ordinamento in ordine decrescente, i valori +1 e -1 vengono invertiti. I termini «è minore di», «è maggiore di», «è uguale a» esprimono una relazione di ordine. Per gli oggetti di tipo [ListeElectorale], si avrà la relazione lista1 «è minore di» lista2 se lista1 ha meno voti di lista2.

Nello stesso file sorgente della classe [MainElections], è possibile aggiungere una seconda classe:

// classe di confronto delle liste elettorali
class CompareListesElectorales implements Comparator {

     // confronto tra due liste elettorali in base al numero di voti
    public int compare(Object obj1, Object obj2) {
         // si recuperano le liste elettorali
        ListeElectorale listeElectorale1 = (ListeElectorale) obj1;
        ListeElectorale listeElectorale2 = (ListeElectorale) obj2;
         // si confrontano i voti di queste due liste
....        
    }
}
  • riga 2: la classe non è dichiarata pubblica. In un file sorgente Java possono esserci più classi, ma solo una può avere l'attributo public, ovvero quella che porta il nome del file sorgente.

Nel metodo compare precedente, i parametri sono di tipo Object,, il che richiede che le righe 7 e 8 effettuino una conversione dei parametri del metodo dal tipo Object al tipo ListeElectorale. La firma del metodo compare è imposta dall’interfaccia Comparator, che è stata scritta per confrontare oggetti generici. A partire dalla versione 1.5 di JDK, esiste un’interfaccia generica Comparator: Comparator<T>, dove T è un tipo Java qualsiasi. Il metodo compare dell’interfaccia Comparator<T> confronta oggetti di tipo T e non di tipo Object, il che evita le precedenti conversioni di tipo. La classe di confronto degli oggetti di tipo ListeElectorale potrebbe essere simile alla seguente:


// classe di confronto delle liste elettorali
class CompareListesElectorales implements Comparator<ListeElectorale> {

    // confronto tra due liste elettorali in base al numero di seggi
    public int compare(ListeElectorale listeElectorale1,
            ListeElectorale listeElectorale2) {
...
    }
}
  • riga 2: la classe implementa l’interfaccia Comparator<ListeElectorale>
  • righe 5-6: i parametri del metodo compare sono di tipo ListeElectorale. La conversione di tipo non è più necessaria.

La versione 1.5 di JDK ha introdotto il concetto di classe/interfaccia generica per diverse classi/interfacce della versione 1.4 di JDK che inizialmente gestivano solo oggetti di tipo Object. È il caso delle liste, dei dizionari, ...

Abbiamo detto poco sopra che, poiché non si conosceva il numero di liste, non era possibile memorizzarle in un array. Possono essere memorizzate in un oggetto ArrayList che implementa il concetto di «lista di oggetti». Questa classe memorizza oggetti di tipo Object. A partire dalla versione 1.5 di JDK, esistono liste di oggetti tipizzati. Pertanto, si utilizzerà un oggetto ArrayList<ListeElectorale> per memorizzare gli elenchi prima di trasferirli in un array. Se quest’ultimo si chiama tListes, l’ordinamento verrà ottenuto tramite l’istruzione:


// ordinamento delle liste
Arrays.sort(tListes, new CompareListesElectorales());

dove CompareListesElectorales è la classe che implementa l'interfaccia Comparator<ListeElectorale>.


Compito da svolgere: riscrivere l’applicazione [Elections] tenendo conto di queste nuove specifiche.


Il progetto Eclipse potrebbe essere il seguente:

Un esempio di esecuzione di [1] è il seguente:

Image