Usamos as classes para construir objetos, o que é chamado de instanciação. E os objetos consistem a essência da programação orientada a objetos (ou OOP, do inglês Object-Oriented Programming). Falando intuitivamente, as classes consistem de uma maneira de organizar um conjunto de dados, e designar todos os métodos necessários para usar ou alterar esses dados.
O conjunto de todos os dados contidos em uma classe definem o estado de um objeto. Por exemplo, se tivéssemos uma classe Semaforo contendo uma única variável chamada VermelhoVerdeAmarelo, então o estado de Semaforo é determinado pelo valor da de VermelhoVerdeAmarelo.
public class Semaforo { int VermelhoVerdeAmarelo = 0; // 0=vermelho,1=verde,2=amarelo void Alternar() { VermelhoVerdeAmarelo = ++VermelhoVerdeAmarelo % 3; } }Os métodos de uma classse, por sua vez, determinam a utilidade que uma classe terá. No caso da classe Semaforo, seu único método Alternar tem como função provocar a mudança da luz de vermelho a verde, de verde a amarelo e de amarelo a vermelho, respectivamente, em cada nova chamada. Assim, se o método Alternar for chamado em intervalos de tempo regulares, poderemos utilizar o estado da classe Semaforo para controlar um semáforo com luzes reais.
Para distinguir entre variáveis declaradas em classes daquelas declaradas localmente dentro de métodos, comumente nos referimos àquelas como campos. Assim, dizemos que VermelhoVerdeAmarelo é um campo da classe Semaforo.
class Vértice { public double x, y; Vértice( double x, double y ) { this.x = x; this.y = y; } } class Polígono { int numVértices; // Quantidade de vértices do polígono Vértice v[]; // Coordenadas de seus vértices }
class Gráfico { // Desenha o polígono no dispositivo gráfico void Desenhar(Polígono p) { SaídaGráfica g; // saída gráfica com método DesenhaLinha. Vértice anterior; anterior = p.v[0]; // Une os vértices do polígono desenhando uma linha entre eles for(int i=1; i<numVértices; i++) { g.DesenhaLinha( anterior, p.v[i] ); anterior = p.v[i]; } } } class Retângulo extends Polígono { Retângulo(Vértice v[]) { this.v = new Vértice[4]; numVértices = 4; for(int i=0; i<4; i++) this.v[i] = v[i]; } } class Triângulo extends Polígono { Triângulo(Vértice v[]) { this.v = new Vértice[3]; numVértices=3; for(int i=0; i<3; i++) this.v[i] = v[i]; } } public class polimorfismo { // Coordenadas dos vértices de um retângulo static Vértice v[] = { new Vértice(0.0,0.0), new Vértice(2.0,0.0), new Vértice(2.0,1.0), new Vértice(0.0,1.0) }; // Coordenadas dos vértices de um triângulo static Vértice w[] = { new Vértice(-1.0,0.0), new Vértice(1.0,0.0), new Vértice(0.0,1.0) }; public static void main(String args[]) { Polígono r, t; Gráfico g = new Gráfico(); r = new Retângulo(v); // Isto é válido, pois Retângulo é um Polígono t = new Triângulo(w); // Isto é válido, pois Triângulo é um Polígono // Desenha o retângulo g.Desenhar( r ); // Desenha o triângulo g.Desenhar( t ); } }Algumas partes deste código são novidade, como por exemplo os métodos declarados nas classes Vértice, Retângulo e Polígono, que parecem não obedecer às regras estabelecidas no capítulo sobre métodos.
[modificadores] class [nome classe] extends [nome super] implements [nome interface]onde as partes que aparecem em itálico são opcionais. Como podemos notar, há quatro diferentes propriedades de uma classe definidas por essa declaração:
Ao declarar uma nova classe, é possivel especificar um dos seguintes modificadores: public, final, abstract.
Ao derivar uma classe, estamos primeiramente fazendo uma cópia da classe parente. É exatamente isto que obtemos se deixarmos vazio o corpo da subclasse. Tal classe se comportaria exatamente como sua superclasse. Entretanto, podemos acrescentar novos campos e métodos à subclasse, além de sobrepor métodos existentes na superclasse, declarando-os exatamente como na superclasse, exceto por dar um corpo diferente.
Não existe herança múltipla em Java. E contraste com a linguagem C++, em Java somente é possível derivar uma classe a partir de uma outra, e não de várias.
Vértice( double x, double y ) { this.x = x; this.y = y; }Sua única finalidade é inicializar o objeto com um par de coordenadas fornecidas no momento da instanciação. Aliás, esta é a principal finalidade dos construtores: atribuir a um objeto um estado inicial, apropriado ao processamento subseqüente.
Os contrutores são métodos facilmente identificáveis pois têm o mesmo nome da classe. Além disso, os construtores não especificam nenhum valor de retorno, mesmo que este seja void, uma vez que não são chamados como os outros métodos. Os construtores somente podem ser chamados no momento da instanciação. Por exemplo:
Vértice v = new Vértice(1.0, 2.0);Temos neste trecho de código a instanciação da classe Vértice, que ocorre no momento em que reservamos espaço para conter um novo objeto dessa classe. Nesse momento o construtor Vértice é chamado com os argumentos 1.0 e 2.0.
É usual declarar os contrutores como públicos. Isto porque, se eles tiverem um nível de acesso inferior ao da classe propriamente dita, outra classe será capaz de declarar uma instância dessa classe, mas não será capaz de realizar ela mesma a instanciação, isto é, não poderá usar o operador new para essa classe. Há situações, porém, em que essa característica é desejável. Deixando seus construtores como privativos, permite a outras classes usar métodos estáticos, sem permitir que elas criem instâncias dessa classe.
Uma classe pode múltiplos construtores declarados, desde que cada um tenha lista de argumentos distinta dos demais. Isto é muito útil, pois em determinados contextos do programa um objeto deve ser inicializado de uma maneira particular em relação a outros contextos possíveis.
Quando nenhum construtor é declarado explicitamente, um construtor vazio é provido implicitamente. Por exemplo, se não tivéssemos especificado um construtor na classe Vértice, este sería o construtor default:
Vértice() { }Os construtores não podem ser declarados com os modificadores: native, abstract, static, synchronized ou final.
Entretanto, uma das finalidades de permitir a derivação de classes é atribuir a elas novas funcionalidades. Isto é possível acrescentando-se novos métodos às subclasses. Mas também é possível subrepor qualquer dos métodos existentes na superclasse, declarando o novo método na subclasse exatamente com o mesmo nome e lista de argumentos, como consta na superclasse. Por exemplo, considere a classe Computador abaixo:
class Computador { private boolean ligado = true; public void Desligar() { ligado = false; } }Esta classe permite que o computador seja desligado, através da chamada do método Desligar. Porém, isto pode não ser muito seguro, pois poderíamos desligar o computador mesmo quando ele estiver executando algum programa. Nesse caso, podemos evitar uma catástrofe derivando a classe computador do seguinte modo:
class ComputadorSeguro extends Computador { private boolean executando = true; public void Desligar() { if ( executando ) System.out.println("Há programas rodando. Não desligue!"); else ligado = false; } }Agora, um objeto da classe ComputadorSeguro somente será desligado quando não tiver programas rodando (exceto quando alguém acidentalmente chutar o fio da tomada!).
A sobreposição somente acontece quando o novo método é declarado com exatamente o mesmo nome e lista de argumentos que o método existente na superclasse. Além disso, a sobreposição não permite que o novo método tenha mais proteções do que o método original. No exemplo acima, como o método Desligar foi declarado como public na superclasse, este não pode ser declarado private na subclasse.
public class Geometria { Vértice v = new Vértice(1.2, 3.5); ... }A diferença mais evidente entre a declaração de um objeto de uma classe e a declaração de um dado primitivo reside na necessidade de reservar memória para o objeto através do uso do operador new. Na verdade, esse operador realiza uma série de tarefas:
Entretanto, considere a seguinte classe chamada CPD a qual contém várias instâncias da classe Computador:
public class CPD { Computador Gauss = new Computador(), Davinci = new Computador(), Fermat = new Computador(); ... public void Fechar() { Gauss.Desligar(); Davinci.Desligar(); Fermat.Desligar(); } ...O método Fechar realiza o desligamento de cada particular instância da classe Computador chamando seu método Desligar. Para indicar a qual objeto o método se refere, devemos precedê-lo do nome do objeto seguido de um operador ponto '.'. A notação geral é
[nome da instância].[nome do método ou variável]Uma excessão a essa regra aplica-se à referência de campos ou métodos declarados como static. Tais declarações são compartilhadas por todas as instâncias de uma classe, desse modo não fornecemos o nome de uma particular instância, mas o nome da própria classe ao referenciá-los.
Há basicamente duas situações em que devemos empregar a palavra this:
class ComputadorSeguro extends Computador { private boolean executando = true; public void Desligar() { if ( executando ) System.out.println("Há programas rodando. Não desligue!"); else super.Desligar(); } }Note a chamada super.Desligar(). Esta corresponde a chamada do método Desligar declarado na superclasse Compudador, o qual vai efetivamente ajustar o campo ligado para o valor false. Imaginando que o método Desligar fosse muito mais complicado, não precisaríamos recodificá-lo completamente na subclasse para acrescentar a funcionalidade que permite o desligamento apenas quando o computador estiver desocupado. Basta chamá-lo da maneira prescrita.
E se o método que desejamos chamar é um construtor? Bem, nesse caso a chamada usando a palavra super bem particular. Examinemos o seguinte exemplo de uma classe chamada VérticeNumerado que estende a classe Vértice, acrescentando às coordenadas do vértice um rótulo numérico que o identifica visualmente:
class VérticeNumerado extends Vértice { int numero; VérticeNumerado( int numero, int x, int y ) { this.numero = numero; super(x, y); } }Note que a chamada super(x, y) se traduz na chamada do construtor Vértice(x,y) da superclasse. Com isto, evitamos de ter que recodificar no novo construtor as tarefas contidas no construtor da superclasse: basta chamá-lo. Vale observar que esse tipo de chamada também só é permitida de dentro de um construtor.
A esta altura, já devemos ter feito a seguinte indagação: se os campos são acessíveis por qualquer dos métodos declarados em uma classse e eventualmente por métodos de classes derivadas, por que não declararmos todos os dados empregados por uma classe como campos? Uma resposta imediata a essa pergunta seria: isso provocaria um significativo desperdício de memória, pois os campos existem durante todo período de existência de um objeto. Entretanto, os dados declarados localmente por um método existem somente enquanto esse método estiver sendo executado, de modo que o espaço de memória previamente ocupado por eles é reaproveitado quando o método termina sua execução.
A capacidade de acessar uma variável de uma classe depende fundamentalmente de duas coisas: moderadores de acesso e localização da variável dentro da classe. As variáveis locais somente são acessíveis pelo método que as declara, enquanto que os campos dependem dos moderadores. Apesar de ser possível deixar todos os campos de uma classe publicamente acessíveis, isto não é recomendável. Do contrário estaríamos desperdiçando o sofisticado mecanismo de proteção de dados fornecido pela OOP, o qual permite escrever programas mais robustos e livres de erros (vulgarmente chamados bugs).
Os possíveis moderadores empregados na declaração de campos são os seguintes:
final int MaxDimen = 10;O uso de constantes dentro de um programa torna-o mais facilmente legível e fácil de seguir. Para economizar memória, é recomendável também declarar constantes como static.
Um outro método interessante da classe Object é o método getClass, que retorna uma referência a um objeto contendo informações sobre a classe a que é aplicado. Isto será visto logo abaixo.
Vértice v = new Vértice(1.0, 2.0); Class cv = v.getClass();
Class cv = Class.forName("Vértice");De posse de uma instância da classe Class, podemos obter informações interesantes sobre a classe da qual ela provém. Por exemplo:
Polígono p; ... p = new Retângulo( ... ); ... System.out.println("O polígono é um " + p.getClass().getName() );deverá exibir na tela a mensagem:
O poligono é um Retângulo
Retângulo r; ... System.out.println("A classe parente do objeto é " + r.getClass().getSuperClass().getName() );deverá exibir na tela a mensagem:
A classe parente do objeto é PolígonoOutra possibilididade interessante do uso da classe Class está na instanciação dinâmica de objetos:
Polígono p; String nome; System.out.print("Qual o poligono que deseja criar?"); System.out.flush(); nome = System.in.read(); p = (Polígono) Class.forName(nome).newInstance();[anterior, índice, seguinte]