Ir para o conteúdo

Classes

As classes permitem definir o comportamento e as várias características dos nossos objectos. Servem de molde para as instâncias que usamos e possibilitam a criação de código onde desenvolvemos o nosso programa.

É nas classes que escrevemos o nosso código, e cada ficheiro de código Java contém uma, e só uma, classe pública. É possível que existam mais classes definidas no mesmo ficheiro, mas estas classes não podem conter o modificador public e são consideradas inner classes. O ficheiro de código tem, obrigatoriamente, o nome da classe pública que está definida no seu interior.

Uma classe em Java, é composta por 3 partes:

  • Atributos
  • Construtores
  • Métodos que definem comportamento

Embora tenhamos dividido a classe em 3 partes, esta divisão é apenas lógica, não afecta em nada a escrita do nosso código, e a ordem das secções pode ser completamente alterada. É comum ver a declaração das variáveis, os atributos, no fim do ficheiro.

Os exemplos de como escrever uma classe podem ser vistos na secção Primeira Classe, Uso Completo da Sintaxe Apresentada.

Características

De acordo com as convenções da linguagem todas as classes devem ter nomes começados por maiúsculas e os seus atributos e métodos devem ter nomes começados por minúsculas.

Como dito anteriormente, cada classe deve ter o nome do ficheiro em que está guardada. Se tentarmos criar uma classe com o nome diferente do ficheiro onde a estamos a guardar, o compilador irá mostrar um erro quando compilarmos a classe. Muitos IDEs actuais mostram como erro antes de compilarmos mas na verdade o erro é sempre de compilação.

Uma classe não possui memória associada, não possui dados, ou outro tipo de informação que exista em memória; uma classe é apenas o código que escrevemos e que compilamos, dando assim origem a ficheiros com extensão .class. Estes ficheiros possuem no seu interior, o código Java compilado para bytecode e são os ficheiros que executamos quando usamos o nosso programa.

Quando falamos em classes e no facto delas não terem dados, temos de salvaguardar o facto de que existem atributos aos quais chamamos atributos de classe. No entanto, atributos de classe são atributos que existem a partir de uma instância de um objecto fundamental da plataforma Java: a instância de Class.

Em Java, quando executamos o nosso código, para cada classe que escrevemos, vai ser instanciado um objecto do tipo Class que contém todas as características que definimos no nosso código. É essa instância que nos permite à plataforma Java executar a nossa aplicação.

Atributos de Classe

Todos os atributos que definirmos sem o modificador static são atributos de instância, isto significa que só estarão disponíveis depois de instanciarmos o nosso objecto1. Os atributos, e métodos, que forem definidos com o modificador static são chamados de atributos, ou métodos, de classe2 e estão acessíveis sem necessidade de se instanciar um objecto.

Este tipo de atributos é o que se pode chamar de atributos globais3, assim, enquanto que um atributo de instância pode conter valores diferentes para objectos diferentes, o conteúdo de um atributo de classe é único para todos os acessos ao atributo e quaisquer modificações são visíveis a todos os elementos que acederem ao atributo.

Para os leitores familiarizados com os problemas de variáveis globais, os atributos de classe oferecem, em grande parte, os mesmos problemas. Como qualquer modificação ao valor do atributo vai afectar todo o código que usa o atributo, as implicações de modificar um valor podem ser difíceis de prever.

Para acedermos a um atributo, ou método, de classe usamos simplesmente o nome da classe, um ponto, e o nome do atributo ou método que queremos invocar:

System.in = null;//Atributo de classe
String.getClass();//Método de classe

Classes e Polimorfismo: Hierarquia de Classes Imposta

A criação das nossas classes, em Java, é afectada por esta característica: uma hierarquia imposta, ou forçada. Em Java, todas as classes têm obrigatoriamente uma superclasse. Reforçando a ideia: em Java, não há classe nenhuma que não tenha uma superclasse.

Esta imposição é conseguida porque, em última análise, todas as classes serão sub-classes de Object. Esta é a classe especial que oferece alguns métodos úteis que podem ser redefinidos pelas sub-classes e que é usada como super-classe de todos os objectos, sem que seja necessário o programador especificar esta relação.

O compilador é responsável por garantir que todas as classes estendem da classe Object sem que seja necessário incluir no nosso código a relação explicita, e na maioria dos casos esta imposição não afecta o desenvolvimento das nossas aplicações. No entanto é necessário que esta relação esteja presente para o programador.

Classes e Polimorfismo: Classes Abstractas

Embora as nossas classes representem objectos do mundo real, há situações onde os conceitos do mundo real não podem ser representados por classes que depois possam ser instanciadas, não faz sentido termos objectos que representem conceitos do mundo real quando, no mundo real, esses conceitos não se traduzem em objectos com os quais possamos interagir.

Se pensarmos na representação, através de classes, de formas geométricas, o próprio conceito de "forma geométrica" não nos permite concretizar em objectos específicos. Claro que podemos concretizar um quadrado, um cubo, uma esfera, uma linha, mas uma "forma geométrica" é um conceito que nos permite agrupar um conjunto de outros conceitos.

Assim, para classes que não fazem sentido ser instanciadas, podemos utilizar a palavra reservada abstract nas suas definições e essas classes passarão a ser consideradas classes abstractas pelo Java.

Uma classe abstracta:

  • Não pode ser instanciada. Se tentarmos criar uma instância da classe o compilador irá emitir um erro;
  • Pode conter métodos abstractos ou métodos com implementação;
  • Não pode ser a última classe da hierarquia se ainda existirem métodos abstractos por implementar. O compilador irá emitir erros caso existam métodos abstractos na hierarquia que não tenham sido implementados.

Classes e Polimorfismo: Construtores

Vimos numa secção anterior que o compilador força todas as nossas classes a serem sub-classes da classe Object, mas o compilador impõe muitas outras alterações ao nosso código, algumas sem que tenhamos de lhe dar qualquer indicação.

Uma dessas outras situações afecta os construtores das classes e a forma como estes são invocados.

Uma classe não precisa declarar explicitamente um construtor, se não o fizer o compilador adiciona ao nosso código um construtor sem argumentos, no entanto, se o programador definir explicitamente um construtor para a classe, o compilador não só não acrescenta qualquer outro construtor como só reconhece os que estiverem explicitamente implementados. Além desta característica, todas as sub-classes são obrigadas a invocar o construtor da super-classe nos seus construtores.

Mas, se o leitor já experimentou fazer uma classe em Java, e tendo em conta que todas as classes estendem de Object, poderá estar agora a pensar que nunca colocou qualquer invocação explicita ao construtor dessa classe. Afinal, se todas estendem de Object e somos obrigados a invocar o construtor da super-classe, porque é que, não tendo o leitor cumprido este último requisito o seu código compilou sem problemas?

A verdade é que, novamente, o compilador faz isso por nós, apenas e exclusivamente no caso da classe Object, em todas as restante situações, teremos de ser nós, programadores a cumprir com o requisito de invocação do construtor da super-classe.

Os nossos construtores são afectados da seguinte forma:

  • Se não existe um construtor na classe, o compilador adiciona um construtor sem argumentos. Se a classe não tiver super-classe explicita, então nada é alterado, por outro lado, se a classe tem uma super-classe explicita o compilar exige que exista uma chamada ao construtor da super-classe se esta tiver um construtor sem argumentos;
  • Se a super-classe da nossa classe não possuir construtor ou o construtor não tiver argumentos, o compilador aceita o nosso código e acrescenta automaticamente uma chamada ao construtor da super-classe;
  • Se a nossa classe definir um construtor explicitamente e não tiver uma super-classe explicita, o compilador nada faz;
  • Se a nossa classe definir um construtor e tiver uma super-classe explicita, o compilador exige que seja invocado o construtor da super-classe;

Esta característica é, por vezes, a fonte de muitas frustrações porque quem está a aprender não se lembra de invocar o construtor da super-classe, tipicamente isto resulta num erro de compilador que indica que o construtor que temos na nossa classe não existe na super-classe, o que pode causar alguma confusão.

Classes e Polimorfismo: Métodos Abstractos e Classes Finais

Na secção de classes abstactas indicamos que este tipo de classes não pode estar no fim da hierarquia. Esta situação está relacionada com métodos abstractos e com o facto de que uma classe abstracta não poder ser instanciada.

Consideremos uma hierarquia onde a primeira classe que fazemos é uma classe abstracta com o método public abstract void teste();. Enquanto o método não for implementado por alguma sub-classe, todas as sub-classes na hierarquia serão, obrigatoriamente, abstractas. Se até ao fim da hierarquia não existir uma classe que implemente este método e se todas as classes forem abstractas, então não é possível ao compilador aceitar a nossa hierarquia.

Se o compilador nos deixasse compilar o código então poderíamos correr o risco de tentarmos usar um método que, para todos os efeitos, não está implementado por classe alguma, colocando assim o programador numa situação impossível: por um lado ter uma declaração do método por outro não ter a sua implementação. Por essa razão, as classes que se situam no fim da hierarquia têm de ser classes não abstractas, o que significa que ou implementam todos os métodos abstractos que ainda possam existir ou já se encontram num lugar da hierarquia onde não há mais métodos abstractos a implementar.

Quando falamos em classes do fim da hierarquia surgem classes especiais designadas por classes finais.

Classes finais, definidas através do uso da palavra reservada final são classes que não podem ser usadas como super-classes de outras classes. Podemos considerar as classes finais como as folhas de um ramo, situadas no fim da hierarquia não admitem qualquer sub-classe.

Uma classe final:

  • Não aceita métodos abstractos;
  • Não permite sub-classes, efectivamente impedindo qualquer herança a partir do ponto onde são colocadas;
  • Permite melhorar a performance de aplicações dado que o compilador tem a garantia que a classe não contém sub-classes e pode efectuar várias opções de optimização, especialmente no que toca a polimorfismo, que de outro modo não poderia fazer.

Classes Aninhadas

O Java permite que sejam definidas classes dentro de outras classes, às quais damos o nome de classes aninhadas4, e que oferecem a possibilidade de agrupar de forma lógica classes. Esta forma de agrupar as classes não deve substituir o uso de packages mas apenas ser utilizada em situações específicas em que os packages não fazem sentido.

Tipicamente devem ser usadas quando precisamos de uma classe que não queremos expor para fora ou que, estando exposta, apenas faça sentido no âmbito da classe mãe em que está definida, por exemplo, ao definirmos uma lista ligada, podemos criar os nós da lista como classes aninhadas. Estes nós, úteis ao funcionamento interno da nossa lista, de nada servem para a utilização da lista enquanto estrutura de dados.

As classes aninhadas podem ser de 2 tipos: estáticas e não estáticas. Em que as estáticas são referidas como classes estáticas aninhadas5 e as não estáticas como classes internas6.

As vantagens de classes aninhadas são:

  • Agrupamento lógico de classes se as mesmas são apenas úties para uma outra classe, a classe que as contém;
  • Aumento do encapsulamento. As classes aninhadas podem ser escondidas do exterior e ajudar no funcionado da classe que as detém de forma privada;
  • Código mais simples de ler e de manter já que as classes de ajuda estão imediatamente acessíveis dentro das classes que estas ajudam.

Exemplo de sintaxe:

class ClasseExterior {
    //...
    class ClasseInterna {
        //...
    }
}
class ClasseExterior {
    //...
    static class ClasseEstaticaInterna {
        //...
    }

    class ClasseInterna {
        //...
    }
}

Classes Aninhadas: Classes Anónimas

Classes anónimas são classes definidas de forma especial, muito úteis quando se trabalha com interfaces gráficas, e que permitem fazer código que noutras linguagens é feito com funções anónimas.

De forma simples, uma classe anónima é uma classe que, no código, não tem nome, e é definida dentro da invocação de outros métodos. Este tipo de classes deve ser pequena, e ser usada apenas quando não faz sentido implementar a classe com interna ou estática interna ou num ficheiro próprio. Devem também ser usadas, apenas quando são sub-classes de outras classes ou implementações de interfaces.

Quando o nosso código é compilado, estas classes anónimas são, na verdade, extraídas para um ficheiro próprio, com o nome da classe onde foram criadas seguido de $<numero da classe>. Se existirem várias classes internas, anónimas ou não, o número é atribuído de forma sequencial dependente da ordem com que aparecem no código fonte.

Exemplos:

new Thread(new Runnable() {

    public void run() {
        while(true) {
            System.out.println("Thread a executar infinitamente...");
        }
    }
}).start();

Se decomposermos o código acima, vamos ver que estamos a usar a classe Thread7, esta classe tem um método, run(), onde temos de implementar o código que a thread vai executar. Tem também um método, start() que indica que a thread deve começar a execução.

Uma forma comum de se implementar uma thread é estender a classe Thread, e na sub-classe redefinir o método run() como pretendermos. Mas se pretendermos apenas implementar o método run() uma outra solução é usar o construtor especial da classe Thread que aceita um objecto que implemente a interface Runnable, esta interface define apenas um método, o run(), que é depois usado pela thread para executar.

Assim, o que fazemos é criar um objecto que implemente Runnable, implementar o método run() desse objecto, criar um objecto do tipo Thread e passar par ao construtor o objecto do tipo Runnable anterior. Depois disto ao iniciarmos a thread com o método start(), o nosso código no método run() do objecto Runnable vai ser executado.

Embora na descrição possa parecer que estamos a ter mais trabalho, lembrem-se que pretendemos usar classes anónimas.

//numa execução normal, este código iria iniciar uma thread.
//Atenção que este código não tem qualquer efeito como está.
new Thread().start();
//Podemos usar o construtor que recebe um Runnable para
//passarmos o código que queremos executar.
//Ser tentarem executar o código seguinte vão receber um bom
//erro de compilação :), reparem que Runnable é uma interface, não 
//pode ser instanciada.
new Thread(new Runnable()).start();
//Reparem nas chavetas depois da classe Runnable... o que estamos a 
//dizer é: new Thread(new <sem nome> implements Runnable { ... }).start();
//mas estamos a omitir várias palavras reservadas, nomeadamente "implements"
//e o nome da classe que estamos a construir
new Thread(new Runnable() {

    public void run() {
        while(true) {
            System.out.println("Thread a executar infinitamente...");
        }
    }
}).start();

Esta técnica funciona para classes que estendam outras classes ou que implementem interfaces e seguem sempre o mesmo formato: o nome da classe e a palavra extends/implements são omitidos, apenas a super-classe o interface é mantida. De seguida são colocados os parêntesis e as chavetas, dentro das quais implementamos o corpo da classe anónima.

Notas finais:

  • Classes anónimas não são reutilizáveis e são conhecidas apenas dentro do método onde foram criadas.
  • Se quisermos passar parâmetros para métodos de classes anónimas que estejam fora da classe, por exemplo no método onde a classe é criada ou na classe principal do ficheiro, as variáveis terão de ser atributos de instância da classe principal ou serem métodos definidos como finais.

  1. Exemplos de instanciação poderão ser vistos na secção seguinte onde falamos sobre objectos. 

  2. Devido ao uso do modificador static estes atributos e métodos são também chamados de atributos estáticos ou métodos estáticos. 

  3. O nome não está correcto, não existe qualquer noção de atributo ou variável global em Java, mas o comportamento é aproximado. 

  4. tradução de nested classes 

  5. static nested classes 

  6. inner classes 

  7. Ver capítulo sobre threads