Skip to content

3. [TD]: Classes

Palavras-chave: classe, interface, herança, exceção, polimorfismo

Leitura recomendada:

  • secções 2.1, 2.2, 2.4 e 2.7 do Capítulo 2 de [ref1]: Classes e Interfaces
  • secções 3.3 (classe String), 3.5 (classe ArrayList), 3.6 (classe Arrays)

Na Parte 1 do exercício ELECTIONS, não foram utilizadas classes. Construímos uma solução tal como a teríamos construído na linguagem C. Vamos agora introduzir o conceito de classes Java.

3.1. Suporte

 

A pasta [support / chap-03] contém o projeto Eclipse para este capítulo.

Vamos agora trabalhar com o JDK 1.8, porque alguns dos próximos projetos exigem este JDK. Para determinar qual o JDK que está a ser utilizado, proceda da seguinte forma:

  • em [4], sendo utilizado o JRE (Java Runtime Environment). Este JRE é, na verdade, um JDK (Java Development Kit), mais concretamente o [jdk1.8.0_60]. Se não for um JDK ou se tiver uma versão inferior à 1.8, proceda da seguinte forma [5-21];
  • em [8], o JRE atualmente utilizado por predefinição pelo Eclipse;
  • em [11], os vários JDKs e JREs atualmente reconhecidos pelo Eclipse;
  • em [15], escolha um JDK em vez de um JRE. Este documento utiliza projetos Maven que requerem um JDK;
  • em [21], temos uma versão do JDK >=1.8;
  • em [22-23], aceda às facetas (diferentes vistas do mesmo projeto Eclipse) do projeto;
  • em [24], verifique se está a utilizar uma versão Java >=1.8;

3.2. A classe [ ListeElectorale]

Em C, provavelmente teríamos usado uma estrutura para representar uma lista de candidatos eleitorais. Poderia ter ficado mais ou menos assim:

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

O conceito de estrutura não existe na linguagem Java. Deve ser substituído pelo de classe. Decidimos, portanto, criar uma classe para armazenar informações sobre uma lista de candidatos. Esta classe teria a seguinte estrutura:


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() {
   ...
    }
}
  • linha 8: um identificador único para uma lista. Não é essencial aqui, mas está reservado para uso futuro.
  • linha 13: o nome da lista.
  • linha 17: o número de votos para a lista
  • linha 21: o número de lugares para a lista
  • linha 25: um valor booleano que indica se a lista foi eliminada (percentagem de votos obtida abaixo do limiar eleitoral) ou não.

Cada campo privado denominado [xyz] pode ser inicializado utilizando um método denominado [setXyz]. O método [getXyz] recupera o valor do campo privado [xyz]. No caso específico em que [xyz] é um campo booleano, o método [getXyz] pode ser substituído pelo método [isXyz]. A nomenclatura específica destes métodos segue uma norma de codificação denominada norma JavaBean. Assim, definimos os seguintes métodos públicos:

  • getId (linha 48), setId (linha 57)
  • getName (linha 65), setName (linha 74)
  • getVoice (linha 82), setVoice (linha 90)
  • getSeats (linha 98), setSeats (linha 106)
  • isEliminated (linha 114), setEliminated (linha 122)
  • linhas 30-31: definem um construtor sem parâmetros. Isto permite criar um objeto [VoterList] sem o inicializar. Pode então ser inicializado utilizando os métodos set.
  • Linhas 40–42: definem um construtor que cria um objeto [VoterList] enquanto inicializa os seus cinco campos privados.
  • Linhas 130–132: definem o método [toString], que devolve uma cadeia de caracteres contendo os valores dos cinco campos do objeto.

Um programa de teste para a classe [VoterList] poderia ter o seguinte aspeto:


package istia.st.elections.tests;
 
import istia.st.elections.ListeElectorale;
 
public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // creation of an electoral list
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // display identity list
        System.out.println("listeElectorale1=" + listeElectorale1);
        // change in number of seats
        listeElectorale1.setSieges(2);
        // display identity list 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // a new electoral roll
        ListeElectorale listeElectorale2 = listeElectorale1;
        // display identity list 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // change in number of seats
        listeElectorale2.setSieges(3);
        // display identity of the 2 lists
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
    }
}

O ambiente Eclipse para este teste poderia ser o seguinte:

  • [1]: o projeto chama-se [elections-02A]
  • [2]: A aplicação será colocada num pacote, neste caso [istia.st.elections]
  • [3]: [VoterList.java] é o código-fonte da classe [VoterList]
  • [4]: As classes de teste serão colocadas num pacote, neste caso [istia.st.elections.tests]
  • [5]: a classe de teste [MainTest1VoterList]

A exibição no ecrã obtida após a execução do programa acima é a seguinte:

Image


Tarefa: Utilizando as informações acima, complete o código para a classe VoterList.


3.3. Criação de uma classe de exceção [ElectionsException]

Entre as várias classes de exceção na linguagem Java, existe uma chamada [RuntimeException]. Esta classe deriva da classe [Exception], a raiz de todas as classes de exceção. A característica distintiva das instâncias de [RuntimeException] ou das instâncias derivadas é que não é necessário declará-las ou tratá-las. São chamadas de exceções não capturadas.

Vejamos um primeiro exemplo. A classe [BufferedReader] é uma classe cujas instâncias permitem ler linhas de texto a partir de um fluxo de dados. Possui um método [readLine] com a seguinte assinatura:

public String readLine()throws IOException

Podemos ver que o método pode lançar uma [IOException]. A hierarquia de classes para esta classe é a seguinte:

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

A classe [IOException] deriva da classe [Exception] (linha 3). O compilador exige que tratemos e declaremos exceções do tipo [java.lang.Exception] ou tipos derivados (exceto para o ramo [RuntimeException], que discutiremos mais tarde). Assim, para ler uma linha de texto digitada no teclado, devemos escrever algo como:

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

Vejamos outro exemplo. Para converter uma string num inteiro, podemos usar o método estático [Integer.parseInt], cuja assinatura é a seguinte:

public static int parseInt(String s) throws NumberFormatException

O argumento [s] é a string a ser convertida num inteiro. Podemos ver que o método pode lançar uma [NumberFormatException]. A hierarquia de classes para esta classe é a seguinte:

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

A classe [NumberFormatException] estende a classe [RuntimeException] (linha 4). O compilador não exige que tratemos ou declaremos exceções do tipo [java.lang.RuntimeException] ou das suas subclasses. Assim, podemos escrever algo como:

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

Não é necessário incluir um bloco [try-catch] para tratar qualquer exceção gerada por [Integer.parseInt] (linha 9).

Existem vantagens e desvantagens em criar e utilizar classes de exceção derivadas de [RuntimeException]:

  • do lado positivo: o código é mais conciso
  • Do lado negativo: podemos acabar por recorrer a métodos ao estilo C, em que cada função devolve um código de erro — uma prática que poucas pessoas utilizam, precisamente para manter o código leve. Quando ocorre um erro não tratado deste tipo, o programa entra em falha, normalmente de uma forma pouco elegante.

Decidimos criar uma classe especial que agrupe todas as exceções que possam ocorrer na nossa aplicação ELECTIONS. Chamar-se-á [ElectionsException] e será derivada da classe [RuntimeException]. O seu código é o seguinte:


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);
    }
}
  • Linha 1: Colocamos a classe no pacote [istia.st.elections];
  • linha 3: a classe estende [RuntimeException]. Por isso, não é verificada;
  • linha 4: um identificador de serialização que podemos ignorar por enquanto;
  • Na nossa aplicação, utilizaremos dois tipos de construtores:
    • o clássico nas linhas 15–17, conforme mostrado abaixo:
throw new ElectionsException("Le nombre de sièges doit être >0")

Neste caso, o método que chama um método que lança tal exceção pode tratá-la da seguinte forma:


        // test exception
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }
  • (continuação)
    • ou o das linhas 14–20, que foi concebido para propagar uma exceção que já ocorreu, envolvendo-a numa [ElectionsException]:

    try {
        ...;
        } catch (SQLException ex) {
            // on encapsule l'exception
            throw new ElectionsException("erreur de fermeture de la connexion à la BD",ex);
        }

Este segundo método tem a vantagem de preservar a informação contida na primeira exceção. Neste caso, o método que chama um método que lança tal exceção pode tratá-la da seguinte forma:


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

Tarefa: Modifique o código da classe ListeElectorale para que os métodos set lancem uma [ElectionsException] se a inicialização solicitada estiver incorreta, como por exemplo, inicializar o nome com uma string vazia.


O projeto de teste do Eclipse para esta nova versão poderia ser o seguinte:

  • [1]: o projeto tem o nome [elections-02B]
  • [2]: a aplicação está colocada num pacote, neste caso [istia.st.elections]
  • [3]: as classes [VoterList] e [ElectionsException]
  • [4]: as classes de teste estão localizadas num pacote, neste caso [istia.st.elections.tests]
  • [5]: A classe de teste [MainTest1VoterList]

A classe de teste [MainTest1VoterList] discutida anteriormente foi ligeiramente modificada para testar casos de exceção:


package istia.st.elections.tests;
 
import istia.st.elections.ElectionsException;
import istia.st.elections.ListeElectorale;
 
public class MainTest1ListeElectorale {
    public static void main(String[] args) {
        // creation of an electoral list
        ListeElectorale listeElectorale1 = new ListeElectorale(1, "A", 32000,
                0, false);
        // display identity list
        System.out.println("listeElectorale1=" + listeElectorale1);
        // change in number of seats
        listeElectorale1.setSieges(2);
        // display identity list 1
        System.out.println("listeElectorale1=" + listeElectorale1);
        // a new electoral roll
        ListeElectorale listeElectorale2 = listeElectorale1;
        // display identity list 2
        System.out.println("listeElectorale2=" + listeElectorale2);
        // change in number of seats
        listeElectorale2.setSieges(3);
        // display identity of the 2 lists
        System.out.println("listeElectorale2=" + listeElectorale2);
        System.out.println("listeElectorale1=" + listeElectorale1);
        // test exception
        try {
            listeElectorale2.setSieges(-3);
        } catch (ElectionsException ex) {
            System.err.println("L'exception suivante s'est produite : ["
                    + ex.toString() + "]");
        }
 
    }
}
  • linha 28: tentamos inicializar o número de lugares com um valor inválido
  • linha 30: se ocorrer uma exceção, esta é apresentada

A execução do teste produz os seguintes resultados:

Image

Note-se que a classe [VoterList] lançou efetivamente uma exceção quando tentámos inicializar o número de lugares com um valor inválido (linha 28 do código).

3.4. Uma classe de teste unitário

O tipo de teste anterior depende da verificação visual. Verificamos se o que é apresentado no ecrã corresponde ao esperado. Este método não é recomendado num ambiente profissional. Os testes devem ser sempre automatizados tanto quanto possível e ter como objetivo não requerer intervenção humana. Os seres humanos são, de facto, propensos à fadiga, e a sua capacidade de verificar testes diminui ao longo do dia.

Uma aplicação evolui ao longo do tempo. A cada atualização, devemos verificar se a aplicação não sofre «regressão», ou seja, se continua a passar nos testes funcionais que foram realizados durante o seu desenvolvimento inicial. Estes testes são chamados de testes de «não regressão». Uma aplicação de dimensão moderada pode exigir centenas de testes. Na verdade, todos os métodos em todas as classes da aplicação são testados. Estes são chamados de testes unitários. Podem ocupar muitos programadores se não tiverem sido automatizados.

Foram desenvolvidas ferramentas para automatizar os testes. Uma delas chama-se [JUnit]. É uma biblioteca de classes concebida para gerir testes. Iremos utilizar esta ferramenta para testar a classe [VoterList].

Um programa de teste JUnit (versão 4.x) tem o seguinte formato:


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);
    }
 
}
  • linha 1: a classe foi colocada no pacote [istia.st.elections.tests];
  • linha 11: o método anotado com a anotação [@Before] é executado antes de cada teste unitário;
  • linha 16: o método anotado com a anotação [@After] é executado após cada teste unitário;
  • linha 21: um método anotado com a anotação [@Test] é um método testado pelo teste unitário. Os métodos anotados com [@Test] serão executados um após o outro, a menos que especificado de outra forma pelo testador, que pode selecionar os métodos a serem testados. Antes de cada execução de um método [@Test], o método [@Before] é executado. Após cada execução de um método [@Test], o método [@After] é executado;
  • linhas 22–25: definem um método de teste [t1];
  • Linha 18: Um dos métodos [Assert.assert*] utilizados para verificar asserções. Estão disponíveis os seguintes métodos [assert]:
    • assertEquals(expressão1, expressão2): verifica se os valores das duas expressões são iguais. São aceites muitos tipos de expressões (int, String, float, double, boolean, char, short). Se as duas expressões não forem iguais, é lançada uma exceção [AssertionFailedError],
    • assertEquals(real1, real2, delta): verifica se dois números reais são iguais com uma diferença inferior a delta, ou seja, abs(real1-real2) <= delta. Por exemplo, pode-se escrever assertEquals(real1, real2, 1E-6) para verificar se dois valores são iguais com uma diferença inferior a 10⁻⁶,
    • assertEquals(message, expression1, expression2) e assertEquals(message, real1, real2, delta) são variantes que permitem especificar a mensagem de erro a ser associada à exceção [AssertionFailedError] lançada quando o método [assertEquals] falha,
    • assertNotNull(Object) e assertNotNull(message, Object): verifica se Object não é igual a null,
    • assertNull(Object) e assertNull(message, Object): verifica se Object é igual a null,
    • assertSame(Object1, Object2) e assertSame(message, Object1, Object2): verifica se as referências Object1 e Object2 apontam para o mesmo objeto,
    • assertNotSame(Object1, Object2) e assertNotSame(message, Object1, Object2): verifica se as referências Object1 e Object2 não apontam para o mesmo objeto;
  • linha 24: esta asserção deve ser bem-sucedida;
  • linha 30: esta asserção deve falhar;

No ambiente Eclipse, uma classe de teste JUnit pode ser criada da seguinte forma:

  • [1]: clique com o botão direito do rato no pacote onde pretende adicionar a classe de teste e, em seguida, selecione [JUnit / Novo / Caso de Teste JUnit]
  • [1]: Selecione uma versão do JUnit;
  • [2]: Selecione a pasta onde a classe de teste deve ser criada;
  • [3]: Selecione o pacote no qual a classe de teste deve ser criada;
  • [4]: Introduza o nome da classe de teste;
  • [5]: Selecione os métodos a incluir na classe que será gerada;
  • [6]: A classe JUnitEssai foi gerada

O assistente anterior gera uma classe praticamente vazia:


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 {
    }
}

Vamos completar e modificar o código anterior da seguinte forma:


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

No Eclipse, clique com o botão direito do rato na classe de teste e, em seguida, selecione [Executar como / Teste JUnit] para a executar:

Image

Os resultados obtidos ao executar este teste são os seguintes:

Image

Acima, o método [test2] falhou. Sempre que um teste falha, é associada uma mensagem de erro. Para [test2], é a mensagem apresentada acima. A mensagem indica o número da linha onde ocorreu o erro (linha 30). Na linha 30, a chamada que falhou foi:


    Assert.assertEquals(1, 2);

O primeiro parâmetro é chamado de valor esperado, o segundo de valor real. A mensagem de erro para [test2] acima indica que o valor esperado era 2, mas o valor real era 3.

Por fim, as mensagens gravadas na consola pelos vários métodos de teste foram as seguintes:

Image

Estas mensagens mostram que os métodos [@Before] e [@After] foram efetivamente chamados, respetivamente, antes e depois de cada método de teste.

As classes de teste não são necessariamente escritas pelos próprios programadores. Podem ser escritas pelas pessoas que redigiram as especificações da aplicação. Alguns métodos de desenvolvimento conhecidos como TDD (Test-Driven Development) defendem a escrita de classes de teste mesmo antes de escrever as classes a serem testadas. Isto ajuda, por vezes, a esclarecer especificações que, de outra forma, poderiam ser interpretadas de várias maneiras.

Vamos criar um teste JUnit 4, denominado [JUnitTest1VoterList], para a classe [VoterList]. No Eclipse, procederemos conforme descrito anteriormente:

Completamos o código gerado pelo assistente da seguinte forma:


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() {
        // electoral list creation
        ListeElectorale liste = new ListeElectorale(1, "a", 32000, 0, false);
        // checks
        Assert.assertEquals("a", liste.getNom());
        Assert.assertEquals(32000, liste.getVoix());
        Assert.assertEquals(false, liste.isElimine());
        Assert.assertEquals(0, liste.getSieges());
        // validity check id
        boolean erreur = false;
        try {
            liste.setId(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // name validity check
        erreur = false;
        try {
            liste.setNom("");
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // voice validity check
        erreur = false;
        try {
            liste.setVoix(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
        // seat validity check
        erreur = false;
        try {
            liste.setSieges(-4);
        } catch (ElectionsException e) {
            erreur = true;
        }
        Assert.assertEquals(true, erreur);
    }
 
}

A execução do teste produz o seguinte resultado:

Image

Os testes foram aprovados. Consideraremos agora a classe [VoterList] como operacional.

3.5. MainElections: versão 2

Leitura recomendada:

  • Secções 2.1, 2.2, 2.4 e 2.7 do Capítulo 2 em [1]: Classes e Interfaces
  • secções 3.3 (classe String), 3.5 (classe ArrayList), 3.6 (classe Arrays)

Queremos reescrever a aplicação [Eleições] adicionando as seguintes novas restrições:

  • Iremos utilizar a classe [VoterList] para representar uma lista de candidatos
  • a aplicação solicitará ao utilizador as seguintes informações:
  • o número de lugares a preencher
  • os nomes e votos para as listas. Não sabemos antecipadamente quantas listas existem. A última lista será indicada por um nome igual à cadeia de caracteres "*".
  • Como não sabemos antecipadamente o número de listas, estas serão primeiro armazenadas num objeto [ArrayList]. Depois, assim que todas as listas tiverem sido introduzidas, serão transferidas para uma matriz de listas.
  • Os resultados serão apresentados por ordem decrescente do número de lugares obtidos.

Para ordenar uma matriz T, podemos utilizar vários métodos estáticos da classe [Arrays]:

  • Arrays.sort(T): ordena a matriz T numa ordem natural, caso exista (ascendente para números, datas, alfabética para cadeias de caracteres, etc.)
  • Arrays.sort(T,comparator): para ordenar matrizes T que não têm uma ordem natural. É o caso aqui com a matriz de listas, que deve ser ordenada de acordo com um campo específico da lista: o número de lugares obtidos.

No método Arrays.sort(T,comparator), o parâmetro comparator é um objeto que implementa a seguinte interface Comparator:

Image

  • O método compare permite comparar dois elementos da matriz T
  • O método equals determina se dois objetos são iguais

Ambos os métodos comparam os tipos de objeto obj1 e obj2. Determinar se obj1<obj2, obj1=obj2 ou obj1>obj2 depende da relação de ordenação que pretendemos estabelecer entre os dois objetos. Cabe ao programador que implementa esta interface especificar como sabemos que:

  • obj1 é menor que obj2
  • obj1 é maior que obj2
  • obj1 é igual a obj2

A classe Object, da qual todas as classes Java derivam, já possui um método [equals]. Para ordenar uma matriz T de objetos do tipo O, o método [equals] da classe O não é útil. Podemos, portanto, manter a implementação padrão fornecida pela classe Object. Apenas o método [compare] deve então ser implementado. Este método é chamado repetidamente pelo método [Arrays.sort]. Cada vez, [Arrays.sort] passa obj1 e obj2 — dois elementos da matriz T a ser ordenada — como parâmetros para o método compare. No nosso caso, estes elementos serão do tipo [VoterList]. Repare no polimorfismo em ação aqui. O método [compare] está definido para aceitar parâmetros do tipo [Object]. Isto significa que pode aceitar parâmetros do tipo [Object] ou de qualquer tipo derivado (polimorfismo). Uma vez que [Object] é a classe pai de todas as classes Java, os parâmetros reais podem ser do tipo [VoterList].

Para ordenar em ordem crescente, o método [compare] deve devolver:

  • -1 se obj1 for menor que obj2
  • +1 se obj1 for maior que obj2
  • 0 se obj1 for igual a obj2

Para ordenar por ordem decrescente, os valores +1 e -1 são invertidos. Os termos «é menor que», «é maior que» e «é igual a» expressam uma relação de ordem. Para objetos do tipo [VoterList], a relação list1 «é menor que» list2 verifica-se se list1 tiver menos votos do que list2.

No mesmo ficheiro fonte da classe [MainElections], podemos adicionar uma segunda classe:

// classe de comparaison de listes électorales
class CompareListesElectorales implements Comparator {

    // comparaison de deux listes électorales selon le nombre de voix
    public int compare(Object obj1, Object obj2) {
        // on récupère les listes électorales
        ListeElectorale listeElectorale1 = (ListeElectorale) obj1;
        ListeElectorale listeElectorale2 = (ListeElectorale) obj2;
        // on compare les voix de ces deux listes
....        
    }
}
  • Linha 2: A classe não está declarada como pública. Num ficheiro fonte Java, pode haver várias classes, mas apenas uma pode ter o atributo público — aquela com o mesmo nome que o ficheiro fonte.

No método compare anterior, os parâmetros são do tipo Object, o que requer que as linhas 7 e 8 convertam os parâmetros do método do tipo Object para o tipo VoterList. A assinatura do método compare é imposta pela interface Comparator , que foi escrita para comparar objetos arbitrários. Desde o JDK 1.5, existe uma interface Comparator genérica: Comparator<T>, onde T é qualquer tipo Java. O método compare da interface Comparator<T> compara objetos do tipo T em vez de do tipo Object, o que evita as conversões de tipo anteriores. A classe de comparação para objetos do tipo VoterList pode ter o seguinte aspeto:


// classe de comparaison de listes électorales
class CompareListesElectorales implements Comparator<ListeElectorale> {
 
    // comparaison de deux listes électorales selon le nombre de sièges
    public int compare(ListeElectorale listeElectorale1,
            ListeElectorale listeElectorale2) {
...
    }
}
  • linha 2: a classe implementa a interface Comparator<VoterList>
  • linhas 5-6: os parâmetros do método compare são do tipo VoterList. A conversão de tipos já não é necessária.

O JDK 1.5 introduziu o conceito de classes/interfaces genéricas para várias classes/interfaces do JDK 1.4 que, inicialmente, lidavam apenas com objetos do tipo Object. É o caso das listas, dos dicionários, ...

Mencionámos anteriormente que, como não sabíamos o número de listas, não podíamos armazená-las numa matriz. Podem ser armazenadas num objeto ArrayList, que implementa o conceito de uma «lista de objetos». Esta classe armazena objetos do tipo Object. Desde o JDK 1.5 que existem listas de objetos tipadas. Assim, utilizaremos um objeto ArrayList<VoterList> para armazenar as listas antes de as transferirmos para uma matriz. Se a matriz se chamar tLists, será ordenada utilizando a seguinte instrução:


// tri des listes
Arrays.sort(tListes, new CompareListesElectorales());

onde CompareVoterLists é a classe que implementa a interface Comparator<VoterList>.


Tarefa: Reescreva a aplicação [Elections] tendo em conta estas novas especificações.


O projeto Eclipse poderia ter o seguinte aspeto:

Um exemplo da execução de [1] é o seguinte:

Image