Programação Orientada a
Objetos em Java
Frederico Borelli de Souza
Marcelo de Jesus Ferreira
Tiago Eugênio de Melo
2008
Em 1995, a Sun anunciou formalmente a linguagem Java. Desde então, a linguagem Java vem despertando um enorme interesse da comunidade de desenvolvedores. É uma linguagem usada em projetos corporativos de grande porte, em desenvolvimento de serviços web, além de dispositivos de pequeno porte, como celulares, pagers e PDAs, entre outros.
A linguagem Java implementa os conceitos de programação orientada a objetos. Neste capítulo, entretanto, nosso enfoque não será na implementação dos conceitos de orientação a objetos, e sim em seus tipos primitivos, palavras reservadas, estruturas de controles e arrays. Um dos pontos fortes de Java é seu rico conjunto de classes pré-definidas que os programadores podem utilizar. Estas classes são agrupadas em coleções de classes, chamados de pacotes e formam a API (Application Programming Interface) da linguagem Java. A API Java distribuída pela SUN é conhecida como J2SE (Java Standard Editon. A versão atual da API Java é a 6.0 e utiliza a JVM (Java Virtual Machine) versão 1.6 [sun08].
Este capítulo esta organizado da seguinte maneira. Na seção 1.2 é apresentado um programa simples em Java para introduzir os conceitos iniciais da linguagem. A seção 1.3 apresenta as palavras reservadas da linguagem Java. As seções 1.4 e 1.5 apresentam, respectivamente, os tipos primitivos da linguagem e a sintaxe de declaração de variáveis. A seção 1.6 mostra como ler valores a partir do teclado. A seção 1.7 apresenta os operadores aritméticos, relacionais, lógicos e de bits da linguagem Java. A seção 1.8 apresenta as estruturas de seleção e de iteração. Nas seções 1.9 e 1.10 são apresentados os conceitos de arrays e de Strings, respectivamente.
A linguagem Java é interpretada através da Java Virtual Machine (JVM). Assim, inicialmente, um programa Java deve ser compilado para geração dos bytecodes, que representam as tarefas a serem executadas pela JVM. A Figura 1.1 apresenta um diagrama do processo de geração de bytecodes e da execução de programas em Java. Inicialmente o programador deve digitar o código fonte Java e salvá-lo em um arquivo com a extensão .java. Em seguida, o compilador Java é executado, através do comando javac <programa>.java. Caso nenhum erro de sintaxe da linguagem seja encontrado o compilador gera o arquivo <programa>.class com os bytecodes para JVM. Para execução do programa é necessário digitar o comando java <programa>.
O programa 1.1 é o mesmo utilizado como exemplo no Apêndice - Eclipse e Java. Tendo este programa exemplo por base serão explicadas a seguir algumas das características básicas da linguagem Java. As linhas 1, 2 e 5 deste programa são exemplos de comentários em Java. Portanto, estas linhas não são interpretadas pelo compilador. Estas linhas iniciam com o símbolo ∕∕, e são considerados comentários de fim de linha ( ou de única linha). É possível também a inserção de comentários no meio da linha, como ocorre nas linhas 8 e 9. A linguagem Java incorporou o estilo de comentários das linguagens C e C++. Assim também é válido o estilo de comentário se que inicia com ∕* e termina com *∕.
01. // Primeiro programa Java
02. // Data: 24 / 05 / 2008 03. 04. public class PrimeiroProgramaJava { 05. // O método main inicia a execução da aplicação Java 06. public static void main(String args[]) { 07. System.out.println("Bem vindo ao Curso de POO !!!"); 08. } // final do método main 09. } // final da class PrimeirPorogramaJava
|
Uma boa prática de programação é sempre manter comentários no início dos programas contendo informações do autor, do objetivo do programa e outras informações que julgar importantes.
A linguagem Java também fornece comentários no estilo Javadoc que são delimitados por ∕ ** e ∕*. Os comentários no estilo Javadoc podem ser utilizados posteriormente pelo utilitário Javadoc para geração de documentação automática dos programas em formato HTML. Para um detalhamento completo da utilização do padrão Javadoc consulte [jav08].
A linha 3 no programa é uma linha em branco. Esta linha não é interpretada pelo compilador Java e serve apenas para melhorar a legibilidade do código.
A linha 4
declara a classe PrimeiroProgramaJava. Cada programa Java deve conter pelo menos uma declaração de classe definida pelo programador. A palavra class é uma palavra reservada da linguagem Java, usada para criar classes. A linguagem Java é case sensitive, portanto há diferença entre letras maiúsculas e minúsculas. Assim, se ao invés de escrever class o programador escrevesse Class, o compilador indicaria um erro de sintaxe.
Neste capítulo inicial cada exemplo de classe definida iniciará com a palavra reservada public. Nos capítulos seguintes será explicado mais sobre classes public. Ao salvar a declaração da classe pública em uma arquivo, o nome do arquivo deve ser o mesmo da classe seguido da extensão .java. No Programa 1.1 o nome da classe pública é PrimeiroProgramaJava e o nome do arquivo PrimeiroProgramaJava.java.
Importante: O nome de um arquivo de programa fonte em Java deve ser o mesmo da classe pública seguido da extensão .java. Caso contrário o compilador indicará um erro. |
Uma chave esquerda, { , no final da linha 4, inicia o corpo da declaração da classe, e uma chave direita, }, na linha 9, finaliza a declaração da classe.
A linha 5, contém um comentário de final de linha, indicando o propósito das linhas a seguir:
A linha 6,
contém a definição do método main(). Declarações de classe Java contém normalmente um ou mais métodos. Todo aplicativo Java deve conter um método chamado main, caso contrário a JVM, não conseguirá executar o aplicativo. A palavra reservada void indica que o método main() irá executar e, ao final, não retornará nenhum valor. Outros detalhes sobre métodos serão explicados no capítulo 2. String args[], entre parênteses na linha 6, é uma parte requerida do método main.
Importante: Todo aplicativo Java deve conter um método chamado main, caso contrário a JVM, não conseguirá executar o aplicativo. |
A chave esquerda, {, ao final da linha 6 indica o corpo da definição do método. Uma chave direita, }, deve terminar o corpo da definição do método (linha 8).
A linha 7,
determina ao computador a impressão da string de caracteres delimitada por aspas duplas. O método System.out.println imprime uma linha de texto na janela de comando. A linha 7 é considerada uma instrução e toda instrução deve ser finalizada com um ponto-e-vírgula (;) .
Será mostrado a seguir como executar o Programa 1.1 a partir da linha de comando (de um shell GNU/Linux). No Apêndice Eclipse e Java, existem instruções para execução dentro do IDE Eclipse. A Figura 1.1 mostra um diagrama geraĺ dos passos que serão executados a seguir.
Inicialmente, é preciso compilar o programa através do seguinte comando digitado em um shell GNU/Linux:
Caso não haja nenhum erro de sintaxe, o comando acima irá gerar um outro arquivo chamado PrimeiroProgramaJava.class. Este arquivo possui os bytecodes gerados pelo compilador que deverão ser interpretados pela Java Virtual Machine (JVM).
Em seguida, para executar o programa, ou seja, invocar a JVM, digite o seguinte comando:
Observe o exemplo da Figura 1.2 que mostra os comandos javac e java, bem como o resultado da execução do programa que mostra a mensagem: Bem vindo ao Curso de POO !!!
Toda linguagem de programação possui um conjunto de palavras-chave que são reservadas para a construção dos programas. Dessa maneira, essas palavras não podem ser empregadas como nomes de variáveis, de classes ou de métodos. Apesar de não ser necessário memorizá-las, é importante ter uma referência para saber quais são os identificadores. A seguir, a Tabela 1.1 apresenta a relação dessas palavras reservadas da linguagem Java.
|
Os tipos primitivos são os tipos básicos iniciais que são definidos diretamente pela linguagem. A Tabela 1.2 apresenta os tipos primitivos da linguagem Java.
|
A variável é a unidade básica de um programa Java. Uma variável é definida pela combinação de um identificador, um tipo e uma inicialização opcional.
Em Java, todas as variáveis devem ser declaradas antes de poderem ser utilizadas. A sintaxe básica de declaração de uma variável é a seguinte:
O <tipo> é um dos tipos permitidos da linguagem Java, que pode ser um tipo primitivo (veja Tabela 1.2), uma classe ou interface ( veja Capítulos 2 e 4 para maiores detalhes sobre classes e interfaces). O <identificador> é o nome da variável. É possível inicializar a variável pela especificação do sinal de igual e um valor. Para declarar mais de uma variável de um tipo específico é possível utilizar a vírgula para separar lista de variáveis.
Veja alguns exemplos de declarações:
Os exemplos acima usaram apenas inicializações fixas. Entretanto, a linguagem Java permite o uso de inicialização dinâmica, usando uma expressão válida ao mesmo tempo que a variável é declarada.
O Programa 1.2 é o exemplo de um programa que inicializa dinamicamente uma variável.
01. // Demonstra inicialização dinâmica
02. 03. public class InicializacaoDinamica { 04. public static void main(String args[]) { 05. double a = 3.0, b = 4.0; 06. // A variável c é dinamicamente inicializada 07. double c = Math.sqrt(a * a + b * b); 08. System.out.println("O Valor da Hypotenusa é " + c); 09. 10. } 11. }
|
Neste programa, três variáveis locais - a, b e c - são declaradas. As duas primeiras, a e b, são inicializadas com valores fixos no início do programa. Contudo, a variável c é inicializada dinamicamente com o valor da hipotenusa. Neste programa, faz-se uso do método sqrt(), que é membro da classe Math (uma das muitas classes que a API Java fornece com facilidades para o programador), para calcular a raiz quadrada de um argumento. O ponto principal a ser lembrado é que na expressão de inicialização pode-se usar qualquer valor válido em tempo de inicialização, incluindo chamada para métodos, outras variáveis ou literais.
Uma variável pode ser declarada como final. Desta maneira o conteúdo da variável não poderá ser modificado. Isto significa que é obrigatório a inicialização de uma variável final quando ela é declarada. Assim, uma variável final é semelhante a const do C/C++. Por exemplo:
As partes subseqüentes deste programa podem usar FILE_NEW, etc, com se fossem constantes, sem preocupações que um valor possa ser modificado.
A palavra reservada final também pode ser aplicada a classes e métodos, mas seu significado é substancialmente diferente do que é aplicado a variáveis. Veja no capítulo 4, outras utilizações da palavra reservada final.
O Programa 1.3 apresenta um exemplo de como ler valores a partir do teclado.
01. //Programa de Adição - lê dois números do teclado
02. 03. import java.util.Scanner; // o programa utiliza a classe Scanner 04. 05. public class LendoAdicao { 06. 07. // método principal inicia a execução do aplicativo java 08. public static void main(String args[]) { 09. // cria Scanner para obter entrada a partir do teclado 10. Scanner entrada = new Scanner(System.in); 11. 12. int numero1; // primeiro número inteiro 13. int numero2; // segundo número inteiro 14. int soma; // soma do de numero1 e numero2 15. 16. System.out.print(" Digite o primeiro número: "); 17. numero1 = entrada.nextInt(); // lê o primeiro numero fornecido 18. System.out.print(" Digite o segundo número: "); 19. numero2 = entrada.nextInt(); // lê o segundo numero fornecido 20. 21. soma = numero1 + numero2; // soma os números the results 22. System.out.println("A Soma é " + soma); 23. 24. } // fim do método principal 25. } // fim da classe LendoAdicao
|
Observe que na linha 3 do Programa 1.3 é utilizado a diretiva import que indica ao compilador para localizar a classe utilizada neste programa.
A linha 10
especifica que a variável nomeada entrada é do tipo Scanner. Um objeto do tipo Scanner permite a um programa ler dados (por exemplo, números) para utilização em um programa. O sinal de igual ( = ) indica que a variável Scanner entrada deve ser inicializada na sua declaração com o resultado da expressão new Scanner( System.in ) à direita do sinal de igual.
O objeto de Saída padrão System.out permite que aplicativos Java exibam caracteres na janela de comando. De maneira semelhante, o objeto de entrada padrão, System.in, permite que aplicativos Java leiam as informações digitadas pelo usuário.
A linha 17
utiliza o método nextInt() do objeto entrada para obter um inteiro digitado pelo usuário. Neste momento o programa espera que o usuário digite o número e pressione a tecla Enter para submeter o número para o programa.
Observe na Figura 1.3 um exemplo de execução do programa 1.3.
A linguagem Java oferece um ambiente rico de operadores. A maioria dos operadores pode ser dividia no seguintes grupos: aritméticos, lógicos, relacionais e de bits.
Os operadores aritméticos são utilizados em expressões matemáticas do mesmo modo que são usados na álgebra. A Tabela 1.3 lista os operadores aritméticos:
|
O Programa 1.4 demonstra os operadores aritméticos com variáveis inteiras e double.
01. // Demonstras as operações aritméticas.
02. 03. class OperacoesAritmeticas { 04. public static void main(String args[]) { 05. // operadores aritméticos com variáveis inteiras 06. System.out.println("Aritmética com variáveis inteiras"); 07. int a = 1 + 1; 08. int b = a * 3; 09. int c = a % b; 10. int d = 9; 11. a += 2; 12. d /= 2; 13. System.out.println("a = " + a); 14. System.out.println("b = " + b); 15. System.out.println("c = " + c); 16. System.out.println("d = " + d); 17. 18. // Operadores aritméticos com variáveis double 19. System.out.println("\nAritméticas com variáveis double"); 20. double da = 1 + 1; 21. double db = da * 3; 22. double dc = db % da; 23. double dd = 9.0; 24. da += 2; 25. dd /= 2; 26. System.out.println("da = " + da); 27. System.out.println("db = " + db); 28. System.out.println("dc = " + dc); 29. System.out.println("dd = " + dd); 30. } 31. }
|
Quando o Programa 1.4 é executado a seguinte saída é gerada:
Aritmética com variáveis inteiras
a = 4 b = 6 c = 0 d = 4 Aritméticas com variáveis double da = 4.0 db = 6.0 dc = 2.0 dd = 4.5
|
Observe que nas linhas 7 a 12 do Programa 1.4 são efetuadas operações aritméticas sobre variáveis inteiras e nas linhas de 20 a 25 são efetuadas operações aritméticas sobre variáveis do tipo double. Os exemplos demonstrados no Programa 1.4 são simples, leia atentamente o código para criar maior familiaridade com a sintaxe da linguagem Java.
Veja que na linha 9 é calculado c = a%b, enquanto que na linha 22 é calculado dc = db%da (ocorre uma inversão na ordem dos operadores). Isto explica o fato do resultado da execução deste programa exibir c = 0 e dc = 2.0.
O Programa 1.5 demonstra as operações de incremento e decremento.
01. // Demonstra as operações de ++ e --
02. 03. public class IncDec { 04. public static void main(String args[]) { 05. int a; 06. int b = 2; 07. int c; 08. a = b++; 09. c = ++b; 10. System.out.println("a = " + a); 11. System.out.println("b = " + b); 12. System.out.println("c = " + c); 13. } 14. }
|
Observe que na linha 9 do Programa 1.5, a variável c recebe o valor da variável b (no caso 2) e em seguida a variável b e incrementada de 1.
Quando o Programa 1.5 é executado a seguinte saída é gerada:
a = 2
b = 4 c = 4
|
Os operadores relacionais determinam o relacionamento que um operando tem com outro. Especificamente, eles determinam igualdade e ordenação. Os operadores relacionais da linguagem Java são mostrados na tabela 1.4.
|
Veja o exemplo do Programa 1.6, que ilustra a utilização dos operadores relacionais. O Programa lê dois números do teclado é compara os números, imprimindo os resultados verdadeiros. O Programa faz uso da instrução if, que permite a um programa tomar uma decisão com base no valor de uma condição. As condições podem ser formadas pelos operadores relacionais. Mais detalhes sobre a instrução if serão vistos na seção 1.8
01. // Compara inteiros usando operadores relacionais
02. import java.util.Scanner; // o programa utiliza a classe Scanner 03. 04. public class Comparacao { 05. // método principal inicia a execução do aplicativo java 06. public static void main(String args[]) { 07. // cria Scanner para obter entrada apartir do teclado 08. Scanner entrada = new Scanner(System.in); 09. int numero1; // primeiro número inteiro 10. int numero2; // segundo número inteiro 11. System.out.print(" Digite o primeiro número: "); 12. numero1 = entrada.nextInt(); // lê o primeiro numero fornecido 13. System.out.print(" Digite o segundo número: "); 14. numero2 = entrada.nextInt(); // lê o segundo numero fornecido 15. if (numero1 == numero2) 16. System.out.printf("%d == %d\n", numero1, numero2); 17. if (numero1 != numero2) 18. System.out.printf("%d != %d\n", numero1, numero2); 19. if (numero1 <= numero2) 20. System.out.printf("%d <= %d\n", numero1, numero2); 21. if (numero1 >= numero2) 22. System.out.printf("%d >= %d\n", numero1, numero2); 23. } // fim do método principal 24. } // fim da classe Comparacao
|
A Figura 1.4 mostra um exemplo de execução do Programa 1.6.
A linguagem Java define vários operadores de bit que podem ser aplicados aos tipos inteiros (long, int, short, char e byte. Estes operadores atuam nos bits individuais dos seus operandos. A tabela 1.5 resume os operadores de bits da linguagem Java. Para maiores detalhes sobre operadores de bits consulte [NS01].
|
Os operadores lógicos mostrados na Tabela 1.6 operam somente em operadores booleanos. Todos os operadores lógicos binários combinam dois valores booleanos para forma um valor de resultado booleano.
|
O Programa 1.7 ilustra o exemplo de utilização dos operadores lógicos.
01. // Demonstra a utilização de operadores lógicos
02. public class OperadoresLogicos { 03. public static void main(String args[]) { 04. boolean a = true; 05. boolean b = false; 06. boolean c = a | b; 07. boolean d = a & b; 08. boolean e = a ^ b; 09. boolean f = (!a & b) | (a & !b); 10. boolean g = !a; 11. System.out.println(" a = " + a); 12. System.out.println(" b = " + b); 13. System.out.println(" a|b = " + c); 14. System.out.println(" a&b = " + d); 15. System.out.println(" a^b = " + e); 16. System.out.println("!a&b|a&!b = " + f); 17. System.out.println(" !a = " + g); 18. } 19. }
|
Quando o Programa 1.7 é executado a seguinte saída é gerada:
a = true
b = false a|b = true a&b = false a^b = true !a&b|a&!b = true !a = false
|
A linguagem Java inclui um operador ternário especial que pode substituir certos tipos de if-then-else. Este operador é o ”?”, e funciona em Java da mesma maneira que em C e C++. O operador ”?”tem a seguinte forma:
A expressao1 pode ser qualquer expressão que avalia um valor boolean. Se a expressao1 é verdadeira (true), a expressao2 é executada; de outra maneira a expressao3 é executada. O resultado da operação ”?”é o da expressão executada. Ambas expressões, expressao2 e expressao3, precisam retornar o mesmo tipo, que não pode ser void.
Veja um exemplo de emprego da operação ”?”:
Quando Java executa a atribuição da expressão, primeiro é observada a expressão à esquerda da ”?”(denom == 0). Se denom for igual zero, então a expressão entre a ”?”e os dois pontos (:) é executada. Se denom não for igual a zero, então a expressão depois dos dois pontos é executada e usada como resultado da expressão inteira. O resultado produzido pelo operador ”?”é então atribuído para a variável razao.
As estruturas de controle da linguagem Java podem ser divididas em duas categorias: estruturas de seleção e iteração. As estruturas de seleção permitem que o programa escolha diferentes caminhos de execução baseado no resultado de uma expressão ou do estado de uma variável. As estruturas de controle permitem que o programa repita a execução de uma operação uma ou mais vezes.
As estruturas de controle da linguagem Java são bem parecidas com C/C++. Entretanto, deve-se tomar cuidado especial com as diretivas break e continue.
Java suporta duas estruturas de seleção: if e switch. Estas estruturas permitem o controle de fluxo da execução do programa baseado em condições conhecidas somente durante a execução.
A declaração if é uma declaração de condição. Pode ser usada para desviar a execução de um programa em caminhos diferentes.
Forma geral do comando if :
if (condicao) execucao1;
else execucao2;
|
Veja que cada execução pode ser um comando simples ou um conjunto de comandos dentro de chaves formando um bloco. A condição é qualquer expressão que retorna um valor booleano. A cláusula else é opcional.
O comando if trabalha da seguinte maneira: se a condição é verdadeira, a execução1 é executada. De outro modo, a execução2 (caso exista) é executada.
Um if aninhado é uma declaração que é alvo de um outro if ou else. Ifs aninhados são muito comuns em programação. Importante lembrar que em ifs aninhados, o else sempre se refere ao if mais próximo que está dentro do mesmo bloco do else e que não está associado com nenhum outro else.
Veja um exemplo:
if(i == 10) {
if(j < 20) a = b; if(k > 100) c = d; // Esse if é else a = c; // associado com esse else } else a = d; // esse else refere-se ao if(i == 10)
|
Como os comentários indicam, o else final não está associado com o if(j < 20), porque ele não está no mesmo bloco (mesmo que seja o if mais próximo do if sem um else). O final else está associado com o if(i == 10), porque é o if mais próximo dentro do mesmo bloco.
Uma construção de programação comum, que é baseado em uma seqüência de ifs aninhados, é o if-else-if aninhado. Veja a forma geral:
Forma geral do comando if-else-if
if(condicao)
execucao; else if(condition) execucao; else if(condition) execucao; . . . else execucao;
|
As declarações if são executadas de cima para baixo. Tão logo uma das condições controladas pelo if seja verdadeira, a tarefa associada com esse if é executada, e o resto do aninhamento é ignorado. Se nenhuma das condições for verdadeira, o else final será executado. O else final atua como a condição default; isto é, se todos os outros testes condicionais falharem, então o último else é executado. O Programa 1.8 ilustra a utilização da estrutura if-else-if.
01. // Demonstra o aninhamento if-else-if
02. class IfElse { 03. public static void main(String args[]) { 04. int mes = 4; // Abril 05. String estacao; 06. if (mes == 12 || mes == 1 || mes == 2) 07. estacao = "Verão"; 08. else if (mes == 3 || mes == 4 || mes == 5) 09. estacao = "Outono"; 10. else if (mes == 6 || mes == 7 || mes == 8) 11. estacao = "Inverno"; 12. else if (mes == 9 || mes == 10 || mes == 11) 13. estacao = "Primavera"; 14. else 15. estacao = "Mês inválido"; 16. System.out.println("Abril está no " + estacao + "."); 17. } 18. }
|
O resultado produzido pelo Programa 1.8 seria:
Abril está no Outono.
|
A declaração switch é uma declaração de controle multi-caminhos. Ela fornece um modo fácil de despachar a execução para partes diferentes do código baseado no valor de uma expressão. É uma opção freqüentemente melhor do que a alternativa de uma série de comandos if-else-if aninhados.
Forma geral do comando switch:
switch (expressao) {
case valor1: // seqüência de comandos break; case valor2: // seqüência de comandos break; . . . case valorN: // seqüência de comandos break; default: // seqüência de comandos padrão }
|
A expressao deve ser do tipo byte, short, int ou char; cada um dos valores especificados na declaração do case deve ser de um tipo compatível com o da expressao. Cada case deve ser um literal único (isto é, deve ser uma constante, não uma variável). Valores duplicados de case não são permitidos.
O comando switch funciona da seguinte maneira: o valor da expressao é comparado com cada um dos valores literais da declaração case. Se uma comparação for positiva, a seqüência de código da declaração case é executada. Se nem uma comparação for positiva, então o comando default é executado. Contudo, a declaração default é opcional. Se nem uma comparação for positiva e o comando default não estiver presente, então nem um comando é executado. O comando break é usado dentro do switch para terminar uma seqüência de comandos. Quando um comando break é encontrado, a execução e desviada para a primeira linha de código seguinte a declaração completa do switch.
O comando break é opcional. Se o break for omitido, a execução irá continuar dentro do próximo case. Isto, em alguns casos, pode ser desejado. O Programa 1.9 apresenta uma versão do Programa 1.8 sobre estações da seção if-else-if re-escrito com o comando switch:
// Um versão melhorada do programa de estação.
class Switch { public static void main(String args[]) { int mes = 4; String estacao; switch (mes) { case 12: case 1: case 2: estacao = "Verão"; break; case 3: case 4: case 5: estacao = "Outono"; break; case 6: case 7: case 8: estacao = "Inverno"; break; case 9: case 10: case 11: estacao = "Primavera"; break; default: estacao = "Mes Inválido"; } System.out.println("Abril está no " + estacao + "."); } }
|
É possível usar um switch como parte de uma seqüência dentro de outro switch. Isto é chamado de switch aninhado. Uma vez que um switch tem seu próprio bloco definido, nenhum conflito surge entre os comandos case do switch mais interno e o switch externo. Veja o exemplo do fragmento de código válido mostrado a seguir:
Veja que o comando case 1: do switch mais interno (linha 7) não conflita com o comando do case 1: do switch mais externo (linha 2). A variável contador só é comparada com a lista de cases do nível externo. Se o contador é 1, então a variável destino é comparada com a lista de cases mais interna.
Os comandos Java para iteração são: do-while, while e for. Estes comandos criam o que normalmente se denomina de loops.
O loop while é o comando de loop mais básico. Ele repete um comando ou um bloco de comandos enquanto sua expressão de controle for verdadeira.
Forma geral do while:
while(condicao) {
// corpo do loop }
|
A condição pode usar qualquer expressão booleana. O corpo do loop será executado enquanto a expressao condicional seja verdadeira. Quando a condição se torna falsa, o controle passa para próxima linha de código imediatamente seguinte ao loop. As chaves não são necessárias se somente um comando for repetido. O Programa 1.10 implementa um loop usando while que decrementa uma variável n de 10 até 0:
// Demonstra a execução do loop while
public class While { public static void main(String args[]) { int n = 10; while (n > 0) { System.out.println("Contador " + n); n--; } } }
|
Resultado da execução do Programa 1.10:
Contador 10
Contador 9 Contador 8 Contador 7 Contador 6 Contador 5 Contador 4 Contador 3 Contador 2 Contador 1
|
Se a expressão de condicional de controle do loop while for inicialmente falsa, então o corpo do loop não será executado. Contudo, em algumas situações, é desejável executar o corpo de um loop while pelo menos uma vez, mesmo que a expressão condicional inicial seja falsa. Ou seja, algumas vezes existe a necessidade de testar a expressão condicional no final do loop e não no início. O loop do-while sempre executa seu corpo de comandos pelo menos uma vez, porque sua expressão condicional é no final do loop.
A sintaxe Java do comando do-while é:
do {
// corpo do loop } while (condicao);
|
Cada iteração do loop do-while primeiro executa o corpo do loop e então avalia a expressão condicional. Se a expressão é verdadeira, o loop será repetido. De outra forma, o loop terminará. Como todo loop Java, a condição deve ser uma expressão booleana. O Programa 1.11 é uma implementação do Programa 1.10 anterior re-escrito com o loop do-while e que gera o mesmo resultado de saída.
// Demonstra a execucao do loop do-while
public class DoWhile { public static void main(String args[]) { int n = 10; do { System.out.println("Contador " + n); n--; } while (n > 0); } }
|
A sintaxe do comando for de Java é similar à sintaxe em C/C++.
Sintaxe do comando for
for(inicializacao; condicao; iteracao) {
// corpo }
|
Tanto na opção de inicialização como na de iteração é possível usar mais de uma opção. Para isto deve-se usar vírgulas para colocar cada condição de inicialização e iteração. Se houver apenas um comando para ser repetido não há necessidade do uso das chaves.
O loop for funciona da seguinte forma: quando o loop inicia, a porção de inicialização do loop é executada. Geralmente, esta é uma expressão que inicializa o valor da variável de controle do loop, que atua como um contador que controla o loop. Entenda que a expressão de inicialização é executada somente uma vez. Em seguida, a condição é avaliada. Esta deve ser uma expressão booleana. Ela geralmente testa a variável de controle do loop contra um valor de destino. Se essa expressão é verdadeira, então o corpo do loop é executado. Se ela é falsa, o loop termina. Depois, a porção de iteração do loop é executada. Isso é geralmente uma expressão que incrementa ou decrementa a variável de controle do loop. O loop itera, primeiro avaliando a condição da expressão condicional, então executando o corpo do loop, e finalmente executando a expressão de iteração em cada passo. Este processo se repete até a expressão de controle ser falsa.
O Programa 1.12 implementa uma iteração que conta de 10 a 1 usando o comando for. Este programa é uma versão dos Programas 1.10 (while) e 1.11 (do-while).
// Exemplo de loop for
public class ForContador { public static void main(String args[]) { int n; for(n=10; n>0; n--) System.out.println("Contador " + n); } }
|
Geralmente, a variável de controle do loop for é necessária somente dentro do loop e não é usada é mais nenhum lugar. Quando este for o caso, é possível declarar a variável dentro da porção de inicialização do for.
Quando se declara a variável dentro do loop for, o escopo desta variável termina quando o comando for termina. Fora do loop for, a variável não irá mais existir. Veja o exemplo do Programa 1.13 abaixo que testa se o número é primo.
01. // Exemplo de declaracao de variável dentro do loop for
02. public class DescobrePrimo { 03. public static void main(String args[]) { 04. int num; 05. boolean primo = true; 06. num = 997; 07. for (int i = 2; i <= num / 2; i++) { // Declaração da variável i 08. if ((num % i) == 0) { 09. primo = false; 10. break; 11. } 12. } 13. if (primo) 14. System.out.println("Número Primo"); 15. else 16. System.out.println("Não é Primo"); 17. } 18. }
|
Observe que a variável i é declarada somente no loop for (linha 7) e portanto seu escopo de atuação só é válido dentro deste loop. Ainda na linha 7, observe que para verificar se o número é primo somente se faz necessário testar se o número é divisível pelos números menores que sua metade (i <= num∕2).
Java permite que os loops possam ser aninhados. Ou seja, um loop pode se inserido em outro. O Programa 1.14 implementa loops for aninhados.
01. //Exemplo de loops aninhados.
02. class ForAninhados { 03. public static void main(String args[]) { 04. int i, j; 05. for (i = 0; i < 10; i++) { 06. for (j = i; j < 10; j++) 07. System.out.print("*"); 08. System.out.println(); 09. } 10. } 11. }
|
Observe que o for da linha 6 do Programa 1.14 tem sua variável j inicializada pela variável i do loop for da linha 5. Desta forma, a cada iteração o for da linha 6 tem um número de iterações menor.
O Programa 1.14 produz a seguinte saída:
**********
********* ******** ******* ****** ***** **** *** ** *
|
Um array é um grupo de variáveis do mesmo tipo que podem ser referenciados por um nome comum. Os arrays de qualquer tipo podem ser criados e podem ter uma ou mais dimensões. Um elemento específico do array é acessado pelo seu índice. Os arrays oferecem meios convenientes de agrupamento de informação relacionada.
Um array de uma dimensão é, essencialmente, uma lista de variáveis do mesmo tipo. A forma geral para declaração de um array de uma dimensão é:
Por questões de conveniência, a linguagem Java permite também a seguinte declaração:
O type declara o tipo base do array. O tipo base determina o tipo de dado de cada elemento que faz parte do array. A seguinte declaração, por exemplo, declara um array nomeado diasdoMes do tipo int:
Apesar da declaração do array diasdoMes, nenhum array de fato existe. De fato, o valor de diasdoMes é setado para null, que representa um array sem nenhum valor. Para ligar diasdoMes com um array físico de inteiros, é necessário alocar um usando o operador new e atribuí-lo para diasdoMes. O operador new é um operador especial que aloca memória.
Nos próximos capítulos o operador new será mais detalhado. A forma geral para new para arrays de uma dimensão é da seguinte forma:
Neste comando, o tipo especifica o tipo de dados sendo alocado, tamanho especifica o número de elementos de um array, e nome-array é o nome da variável array que é ligada ao array. Ou seja, para usar o operador new para alocar um array, é necessário especificar o tipo e o número de elementos para alocar. No exemplo a seguir, são alocados doze (12) elementos inteiros e ligados para diasdoMes. Os elementos no array alocados pelo new serão automaticamente iniciados em zero (válido somente para arrays de tipos númericos, int, double, long e byte ).
Depois deste comando executado, diasdoMes irá referenciar a um array de 12 inteiros. E todos os elementos deste array serão inicializados com zero.
Portanto, para obter um array é necessário um processo de dois passos. Primeiro, é necessário declarar uma variável do tipo do array desejado. Segundo, é necessário alocar a memória que irá conter o array, usando o comando new, e atribuí-lo para a variável nome-array. É possível ainda combinar os dois passos anteriores. Assim, em nosso exemplo é possível a seguinte declaração:
ao invés dos dois passos:
Uma vez alocado um array, é possível acessar um elemento no array pela especificação de um índice dentro de colchetes. Em Java, todos os índices de um array iniciam em zero. Por exemplo, o seguinte comando atribui o valor 30 ao nono elemento do array diasdoMes.
E a próxima linha imprime o valor armazenado no índice 8.
Veja o exemplo do Programa 1.15 que ilustra a utilização de um array de uma dimensão:
// Demonstra um array de uma dimensão.
class Array { public static void main(String args[]) { int diasdoMes[]; diasdoMes = new int[12]; diasdoMes[0] = 31; diasdoMes[1] = 28; diasdoMes[2] = 31; diasdoMes[3] = 30; diasdoMes[4] = 31; diasdoMes[5] = 30; diasdoMes[6] = 31; diasdoMes[7] = 31; diasdoMes[8] = 30; diasdoMes[9] = 31; diasdoMes[10] = 30; diasdoMes[11] = 31; System.out.println("Abril tem " + diasdoMes[3] + " dias."); } }
|
Quando o Programa 1.15 é executado ele fornece a seguinte saída:
Abril tem 30 dias.
|
Os arrays podem ser inicializados quando eles são declarados. O processo é basicamente o mesmo usado para inicializar tipos simples. Um array é inicializado como uma lista de valores separados por vírgulas dentro de chaves. As vírgulas separam os valores dos elementos do array. O array será automaticamente criado com o tamanho para suportar o número de elementos especificados na inicialização. Não há necessidade do uso do new. O Programa 1.16 mostra o mesmo Programa 1.15 usando a inicialização durante a declaração:
// Um versão melhorada do programa anterior
class AutoArray { public static void main(String args[]) { int diasdoMes[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; System.out.println("Abril tem " + diasdoMes[3] + " dias."); } }
|
Em Java, um array multi-dimensional é de fato um array de arrays. Esses arrays parecem e funcionam como um array multi-dimensional normal. Contudo, existem algumas diferenças sutis. Para declarar um array multi-dimensional, é necessário especificar cada índice adicional usando outro conjunto de colchetes. A seguir, tem-se como exemplo uma declaração de um array multi-dimensional chamado DuasDimensoes.
Este exemplo aloca um array 4 por 5 e atribui a DuasDimensoes. Internamente esta matriz é implementada como uma array de array do tipo int.
O Programa 1.17 numera cada elemento no array da esquerda para direita, de cima para baixo, e imprime esses valores:
// Demonstra a two-dimensional array.
class DuasDimensoesArray { public static void main(String args[]) { int DuasDimensoes[][] = new int[4][5]; int i, j, k = 0; for (i = 0; i < 4; i++) for (j = 0; j < 5; j++) { DuasDimensoes[i][j] = k; k++; } for (i = 0; i < 4; i++) { for (j = 0; j < 5; j++) System.out.print(DuasDimensoes[i][j] + " "); System.out.println(); } } }
|
O resultado da execução do Programa 1.17 é a seguinte:
0 1 2 3 4
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
Quando se aloca memória para um array multi-dimensional, somente é necessário especificar o tamanho da primeira dimensão (mais a esquerda). As demais dimensões podem ser alocadas separadamente. Como foi dito anteriormente, em Java, os arrays multi-dimensionais são, na verdade, arrays de arrays. Assim é possível, por exemplo, que o tamanho do array em cada dimensão seja diferente.
O Programa 1.18 cria um array de duas dimensões em que os tamanhos da segunda dimensão são distintos.
A execução do Programa 1.18 gera a seguinte saída:
0
1 2 3 4 5 6 7 8 9
|
01. // Aloca manualmente diferentes tamanhos de segunda dimensão
02. class DuasDimDistintasArray { 03. public static void main(String args[]) { 04. int DuasDimensoes[][] = new int[4][]; 05. DuasDimensoes[0] = new int[1]; 06. DuasDimensoes[1] = new int[2]; 07. DuasDimensoes[2] = new int[3]; 08. DuasDimensoes[3] = new int[4]; 09. int i, j, k = 0; 10. for (i = 0; i < 4; i++) 11. for (j = 0; j < i + 1; j++) { 12. DuasDimensoes[i][j] = k; 13. k++; 14. } 15. for (i = 0; i < 4; i++) { 16. for (j = 0; j < i + 1; j++) 17. System.out.print(DuasDimensoes[i][j] + " "); 18. System.out.println(); 19. } 20. } 21. }
|
O J2SE 5.0 introduziu o conceito de for estendida, que itera pelos elementos do array sem utilizar um contador. A sintaxe de uma instrução for estendida é a seguinte:
for ( Parametro : NomedoArray )
instrucao
|
onde Parametro tem duas partes - um tipo e um identificador (por exemplo int numero) e NomedoArray é o array pelo qual iterar. Como ilustrado no Programa 1.19 o identificador representa valores sucessivos do array nas sucessivas iterações da instrução for estendida.
01. // Exemplo da instrução for estendida
02. public class InsForEstendida { 03. public static void main(String args[]) { 04. int DuasDimensoes[][] = new int[4][]; 05. DuasDimensoes[0] = new int[1]; 06. DuasDimensoes[1] = new int[2]; 07. DuasDimensoes[2] = new int[3]; 08. DuasDimensoes[3] = new int[4]; 09. int i, j, k = 0; 10. for (i = 0; i < 4; i++) 11. for (j = 0; j < i + 1; j++) { 12. DuasDimensoes[i][j] = k; 13. k++; 14. } 15. for (i = 0; i < 4; i++) { 16. for (int numero : DuasDimensoes[i]) 17. System.out.print(numero + " "); 18. System.out.println(); 19. } 20. } 21. }
|
O Programa 1.19 é a mesmo exemplo do Programa 1.18, só que desta vez implementado com a instrução for estendida. Observe que a linha 16 deste programa equivale a linha 16 do programa anterior, ou seja:
A instrução for estendida simplifica o código para iterar com um array. Entretanto, a instrução for estendida só pode ser utilizada para acessar elementos do array, e não para modificar.
Nas seções anteriores de tipos de dados e arrays não foi mencionado o tipo String. Isto porque o tipo String de Java, não é um tipo primitivo fornecido pela linguagem. Também não é um array de caracteres (como as strings do C/C++). Em Java, String define um objeto, e uma descrição completa irá requerer um entendimento de várias características de orientação a objeto. E esses conhecimentos só serão vistos nos próximos capítulos.
Para que o leitor possa usar strings simples em programas iniciais, será feita uma abordagem preliminar aqui. O tipo String é usado para declarar uma variável string. Também é possível declarar arrays de strings. Uma constante string entre aspas pode ser atribuída a uma variável String. Uma variável do tipo String pode ser atribuída a outra variável do tipo String. É possível usar um objeto do tipo String com um argumento para o método println( ). Veja o exemplo a seguir:
Neste exemplo, str é um objeto do tipo String. É atribuído a ele a string ”Isto é um teste”. Esta string é mostrada através do comando println( ). Objetos String têm muitas características e atributos especiais que fazem estes objetos serem muito poderosos e fáceis de usar.
Java define um operador para objetos String: +. Este operador é usado para concatenar duas strings. Por exemplo, este comando:
resulta na string teste contendo ”Bem-vindo ao Curso de POO”.
A classe String contém vários métodos que podem ser usados. É possível testar a igualdade de duas strings pelo uso do método equals( ). O tamanho de uma string pode ser obtido pelo uso do método lenght( ). Um caractere específico dentro de uma string pode ser obtido pelo uso do método charAt( ). O método trim( ) pode ser utilizado para retirar os espaços em branco anteriores e posteriores de uma string.
A sintaxe de uso destes métodos é a seguinte:
boolean equals(String objeto)
int length( ) char charAt(int indice) Strings trim( )
|
O Programa 1.20 utiliza os métodos equals( ), length( ), charAt( ), trim( ) e também o operador +. Observe que na linha 8 é feita a chamada do método length() do objeto strOb1 e o operador “+” é utilizado em seguida para concatenar o resultado deste método com a string ”Tamanho da strOb1: ”.
01. // Demonstra alguns métodos da classe String
02. public class StringMetodos { 03. public static void main(String args[]) { 04. String strOb1 = "Primeira String"; 05. String strOb2 = "Segunda String"; 06. String strOb3 = strOb1; 07. String strOb4 = " Alo Mundo "; 08. System.out.println("Tamanho da strOb1: " + strOb1.length()); 09. System.out.println("Caractere de indice 3 da string strOb1: " + strOb1.charAt(3)); 10. System.out.println(strOb4.trim()); 11. if (strOb1.equals(strOb2)) 12. System.out.println("strOb1 == strOb2"); 13. else 14. System.out.println("strOb1 != strOb2"); 15. if (strOb1.equals(strOb3)) 16. System.out.println("strOb1 == strOb3"); 17. else 18. System.out.println("strOb1 != strOb3"); 19. } 20. }
|
O resultado da execução do Programa 1.20 é a seguinte:
Tamanho da strOb1: 15
Caractere de indice 3 da string strOb1: m Alo Mundo strOb1 != strOb2 strOb1 == strOb3
|
Resposta: A linguagem Java suporta três tipos de comentários. O primeiro tipo usa os caracteres ∕∕, que podem ser utilizados no início da linha ou no final da linha. O Segundo tipo é o que utiliza os caracteres ∕* e *∕. E o terceiro tipo é o padrão Javadoc que utiliza os caracteres ∕ ** e *∕.
n! = n * (n - 1) * (n - 2) * ... * 1 para valores de n maiores ou iguais a 1
e
n! = 1 (para n = 0 ).
Por exemplo, 5! = 5 * 4 * 3 * 2 * 1 que é igual a 120.
*
** *** **** ***** ****** ******* ******** ********* **********
|
**********
********* ******** ******* ****** ***** **** *** ** *
|
*
** *** **** ***** ****** ******* ******** ********* **********
|
A Programação Orientada a Objetos (POO) é um modelo de programação baseado em conceitos tais como objetos, classes, tipos, ocultamento da informação, herança e polimorfismo. Este capítulo aborda os fundamentos da POO e está dividido nas seguintes seções: classes e objetos, atributos, métodos, pacotes, interfaces e, ao final, uma lista de atividades para fixação do aprendizado.
A Programação Orientada a Objetos (POO) é uma técnica de programação que se baseia na construção de classes e utilização de objetos. Os objetos são formados por dados e operações específicas, que delimitam um conjunto particular de atividades. [Jun07] define um sistema orientado a objetos como um conjunto de objetos diferentes que podem se inter-relacionar para produzir os resultados desejados.
As classes são modelos para os objetos. Um conjunto de objetos que possuem características (atributos) e comportamentos (métodos) comuns podem usar o mesmo modelo, ou seja, pertencem a mesma classe. Assim, uma classe é um modelo para um novo tipo de objeto que pode ser definido pelo programador. Já os objetos são instâncias de classes. Dessa forma, pode-se afirmar que as classes são utilizadas para definir novos tipos na POO.
A API Java é formada por uma vasta quantidade de classes, cada uma com características e funcionalidades próprias, que podem ser empregadas na construção de sistemas orientados a objetos. A sintaxe de uma classe em Java é descrita assim:
No corpo da classe estão codificados os atributos e os métodos. A criação de objetos é chamada de instanciação e envolve o uso do operador new de Java. A sintaxe da criação de um objeto é:
Na verdade, esta sentença realiza três ações:
Como regra geral, as classes são gravadas em arquivos com mesmo nome. Salvar uma classe num arquivo com nome diferente gera um erro na compilação. É possível que um arquivo tenha mais de uma classe, porém, para uma melhor obtenção de modularidade, recomenda-se que cada classe seja salva em um arquivo próprio. Se um arquivo possuir mais de uma classe, apenas uma dessas classes poderá ser pública, caso contrário, ocorrerá um erro na compilação do programa.
Acessibilidade, também conhecida como visibilidade, é um aspecto importante na POO, pois possibilita que o programador limite o uso de certos elementos das classes. É com a restrição de acesso que se implementa, em grande parte, o encapsulamento das classes. Java possui três moderadores de acesso explícitos (public, private e protected) e um especificador implícito. O próximo capítulo aprofundará mais sobre encapsulamento e o uso dos moderadores de acesso de Java.
Aqui vale ressaltar que o encapsulamento é um dos recursos importantes de Java, sendo implementado através do uso de moderadores. Por exemplo, em linguagens estruturadas, como C e Pascal, não é possível o programador impor limitações de acesso, pois essas linguagens de programação não possuem o conceito de encapsulamento. A seguir, serão apresentadas as principais espécies de classes em Java.
Uma classe em Java criada para funcionar como um tipo e que pode instanciar objetos é considerada concreta. Diferentemente disto, uma classe em Java será considerada como abstrata se for assim declarada ou nas situações em que os seus métodos declarados não estão implementados, mas são apenas especificados. Estes métodos que não possuem implementação também são chamados de abstratos. Classes que possuam métodos abstratos são consideradas também como abstratas e não podem ser instanciadas. Basta ter um único método abstrato para a classe ser considerada como abstrata.
Uma classe Exemplo terá métodos abstratos e, portanto, deverá também ser considerada como abstrata nas seguintes hipóteses:
A sintaxe da criação de uma classe abstrata é:
De forma semelhante, a sintaxe da criação de um método abstrato em Java é:
Uma classe somente deverá ser declarada como abstrata se a intenção do programador for torná-la, posteriormente, superclasse de outras classes que irão completar a sua implementação, pois uma classe abstrata nunca poderá ser instanciada.
Como exemplo, considere o domínio de uma universidade, em que as pessoas que participam do seu dia-a-dia poderiam ser alunos ou professores, conforme representa o diagrama de classes da Figura 2.1. Apesar de todas as pessoas terem características comuns como nome, endereço e telefone, somente os professores têm salário, assim como somente os alunos têm o atributo nota. Nesse caso, poder-se-ia representar a classe Pessoa como abstrata, pois, na verdade, todas as pessoas de uma universidade são alunos ou professores, não havendo necessidade de instanciar a classe Pessoa. O Programa 2.1 apresenta um exemplo de programa em que a classe Pessoa é abstrata e a classe Professor é descendente dessa classe.
A linha 04 do Programa 2.1 está comentada e faz a instanciação do objeto p, da classe Pessoa. Caso a linha não estivesse comentada, ocorreria um erro em tempo de compilação com o programa, pois não é possível instanciar a classe Pessoa, tendo em vista que a mesma é abstrata. A mensagem seria a seguinte:
01. public class TesteClasseAbstrata {
02. public static void main(String[] args) { 03. 04. //Pessoa p = new Pessoa("João"); 05. 06. Professor prof = new Professor(); 07. 08. prof.setNome("João"); 09. 10. System.out.println("O salário do professor " + prof.getNome() + " é " + prof.getSalario(40)); 11. } 12. 13. } 14. 15. abstract class Pessoa { 16. private String nome; 17. 18. public void setNome (String n) { 19. nome = n; 20. } 21. 22. public String getNome() { 23. return nome; 24. } 25. } 26. 27. class Professor extends Pessoa { 28. private double salario; 29. private double horaAula = 50.00; 30. 31. public double getSalario(double cargaHoraria) { 32. salario = horaAula * cargaHoraria; 33. return salario; 34. } 35. }
|
Na linha 06, o objeto prof é criado e instanciado a partir da classe Professor. Isto ocorre porque a classe Professor não é abstrata. Na linha 08, o objeto prof faz uso do método construtor Pessoa, da classe de igual nome. Isto é possível porque a classe Professor é filha da classe Pessoa.
Dica:: Uma classe abstrata pode ter métodos não abstratos. Porém, o contrário não é verdadeiro. Ou seja, uma classe concreta não pode ter métodos abstratos. |
Classes internas ou internas são classes declaradas dentro de outras classes, sendo consideradas como membros das classes externas. Considere o Programa 2.2, em que a classe Principal possui um campo privado do tipo inteiro (valor) e uma classe interna (Aninhada), ambos como seus membros.
1. public class Principal {
2. private int valor; 3. 4. public Principal(int valorPassado) { 5. valor = valorPassado; 6. } 7. 8. public class Aninhada { 9. public void imprimir() { 10 System.out.println("O valor é: " + valor); 11. } 12. } 13. 14. public static void main(String[] args) { 15. Principal objPrincipal = new Principal(100); 16. Principal.Aninhada objAninhada = objPrincipal.new Aninhada(); 17. objAninhada.imprimir(); 18. } 19. }
|
A classe interna Aninhada é declarada, nas linhas 8 a 12, como membro da classe Principal, e possui acesso irrestrito a todos os demais membros da classe Principal. Na classe interna é declarado um método imprimir(.) que exibe o conteúdo do atributo privado valor. Apesar do atributo valor ser privado e, como regra, não poder ser acessado por outras classes, a classe Aninhada consegue usá-lo, pelo fato de ser uma classe aninhada. Dessa maneira, é possível para o seu método imprimir(.) utilizar o valor do campo privado valor. A intenção deste exemplo é demonstrar o tipo de relacionamento que existe entre uma classe aninhada e a classe externa.
Para instanciar objetos de classes aninhadas, deve ser obtida uma instância da classe externa, reforçando a dependência da classe aninhada com sua classe externa. Por isso é necessário que, inicialmente, se crie um objeto da classe externa, conforme realizado na linha 15, em que se criou o objeto objPrincipal. Feito isto, é possível criar um objeto da classe aninhada, a partir do objeto da classe externa, conforme ilustrado na linha 16, em que se cria o objeto objAninhada. Caso contrário, não seria possível instanciar diretamente um objeto a partir da classe aninhada. Por exemplo, a declaração abaixo ocasionaria um erro de compilação:
Como a existência de instâncias da classe aninhada depende de uma instância da classe externa, esse tipo de classe aninhada recebe o nome de classe interna ou inner class, enquanto que a classe externa é conhecida como outter class. [NS01] ressalva o fato de que as classes internas somente são conhecidas dentro do escopo das classes externas, pois o compilador Java gera uma mensagem de erro se qualquer código, além da classe externa, tentar instanciar a classe interna.
As classes aninhadas são elementos auxiliares e somente devem ser criadas quando sua implementação se limita a dois ou três métodos simples. São muito utilizadas no processamento de eventos em aplicações dotadas de interfaces gráficas.
Uma classe pode ser declarada como final se a sua definição está completa e não se deseja que ela seja extendida. Um erro de compilação ocorrerá se uma classe herdar de outra classe, quando esta for declarada como final. Daí, pode-se afirmar que uma classe final não poderá ter subclasses.
Dica: Uma classe pode ser declarada como final e abstrata ao mesmo tempo? A resposta é não, porque uma classe abstrata nunca poderá ser completada, pois sendo final, não poderá ter subclasses. |
A sintaxe da criação de uma classe final é:
A classe java.lang.String é comumente utilizada como exemplo de classe final, pois as suas funcionalidades nunca podem ser modificadas. Por exemplo, o método public int length(.) pertence à classe String e deve retornar a quantidade de caracteres que um objeto desta classe contém. Pelo fato de pertencer a uma classe final, este método não pode ser modificado. Isto é importante pois garante que nenhum outro código irá alterar a semântica dessa classe.
Classe anônima é uma classe sem nome e definida como uma subclasse ou para a realização de uma interface, com o propósito de servir para a instanciação de um único objeto. Como as classes anônimas em Java comprometem a legibilidade do código, estas não devem ser usadas quando for necessário implementar mais do que dois ou três métodos de modo simples. Assim, este tipo de classe será examinada com maior profundidade no capítulo de herança.
As características dos objetos ou das suas partes são identificadas através dos seus atributos ou campos. A sintaxe da criação de atributos é:
Na criação do atributo, observa-se a presença de um moderador de acesso, o qual define a visibilidade externa deste campo, além do tipo do atributo, sendo que este poderá ser primitivo, se for provido pela própria linguagem Java, ou classe, que será construída ou importada pelo usuário, e também é definido o nome do atributo. Se desejar, o programador também poderá inicializar o atributo com um valor.
Importante: O tipo String, apesar de ser usado com bastante freqüência por programadores, não é um tipo primitivo. Deve-se observar o conjunto dos tipos primitivos apresentado no Capítulo 1. |
Um atributo pode se tornar uma constante em Java através do comando final. Opcionalmente, é possível adicionar um moderador de acesso à constante em Java, com o objetivo de determinar o escopo dessa constante. A sintaxe da declaração de constantes em Java é:
A inicialização de constantes deve, obrigatoriamente, ocorrer na declaração. Em adição, nenhum outro valor pode ser atribuído a esta constante, além deste valor inicial.
Importante: Observe que a mesma instrução de Java pode ter um efeito diverso, dependendo do contexto em que é aplicada. Por exemplo, o comando final quando aplicado a um atributo, torna-o uma constante, já quando este comando é aplicado a uma classe, torna-a imutável. |
Os métodos são blocos de rotinas associadas aos objetos, isto é, trechos de código que permitem realizar ações ou transformações sobre os valores dos atributos, modificando o estado dos objetos e proporcionando o comportamento desejado. A sintaxe de Java para a criação de métodos é:
Importante: Apesar de alguns autores usarem os termos método e operação indistintamente, isto não está correto, pois operação é a declaração de um comportamento de uma classe, já método é a implementação dessa operação. Dessa forma, não se pode falar operação quando houve código envolvido. |
O moderador de acesso determina a visibilidade do método. Em geral, os métodos são públicos para que as outras classes possam acessar os atributos privados. O tipo de retorno indica qual o tipo de valor devolvido como resultado do método. Se o método não retornar um valor, deve-se utilizar void para indicar, ao compilador, que não existirá retorno. O nome identifica o método dentro da classe. Os parênteses são obrigatórios e contêm uma lista de parâmetros. Essa lista pode ser vazia, ou seja, o método pode ser construído sem que tenha necessidade de receber parâmetros de entrada. O corpo do método é um bloco de código que define as ações realizadas pelo seu acionamento.
Se a lista de parâmetros não for vazia, cada elemento deverá ser declarado como uma variável, ou seja, deve-se declarar o nome e o tipo do argumento. Todas as declarações dos parâmetros devem ser separadas por vírgula. Mesmo que os parâmetros sejam de tipos iguais, cada um deve ser declarado separadamente. A seguir, são apresentados alguns exemplos de declaração de métodos:
Se um método possuir um tipo de retorno, diferente do void, deve-se empregar a diretiva return. Sendo que o valor retornado pelo método deve ser do mesmo tipo de retorno declarado para o método. Caso esta compatibilidade não seja atendida, haverá um erro na compilação do programa.
Após a instanciação de um objeto, este pode utilizar os seus métodos através do operador (.), também conhecido como seletor. A sintaxe de Java para uso de métodos pelos objetos é:
Dica: O Eclipse dispõe de um recurso interessante chamado auto-completar. Com este recurso, basta que o programador digite o nome do objeto e o seletor (.) para que a ferramenta apresente uma lista de métodos que estão disponíveis para este objeto. Além de melhorar a produtividade do programador, pois diminui a digitação de código, também permite que o programador visualize quais são os métodos que estão acessíveis aquele determinado objeto. |
O Programa 2.3 apresenta a criação de dois métodos: dobrar, que recebe um valor inteiro como parâmetro e retorna o dobro do valor de entrada, e imprimir, que recebe um valor inteiro como parâmetro e imprime o valor.
01. public class UsoMetodos {
02. public int dobrar(int valor) { 03. valor = valor * 2; 04. return valor; 05. } 06. 07. public void imprimir(int valor) { 08. System.out.println("O resultado é " + valor); 09. } 10. 11. public static void main(String[] args) { 12. int resultado; 13. UsoMetodos obj = new UsoMetodos(); 14. resultado = obj.dobrar(5); 15. obj.imprimir(resultado); 16. } 17. }
|
Antes de se utilizar dos métodos criados, é necessário criar um objeto para poder acessá-los. Isto é feito na linha 13, em que se cria o objeto obj. Na linha 14, o objeto obj utiliza o método dobrar(int valor), passando o valor 5 como parâmetro. O retorno deste método é atribuído na variável resultado. Em seguida, na linha 15, obj chama o método imprimir(int valor), passando o valor da variável resultado como parâmetro. O seguinte conteúdo é exibido na tela:
Deve-se observar que o método dobrar retorna um valor inteiro e, portanto, a variável valor deve ser do mesmo tipo. Caso contrário, ocorreria um erro de compilação. Já o método imprimir, como não precisa retornar um valor, utiliza o modificador void.
Dica: Se os métodos criados pelo programador fossem declarados como estáticos (static), não haveria necessidade de criar um objeto de classe, pois estes métodos poderiam ser utilizados diretamente. Maiores detalhes sobre métodos estáticos serão explanados mais adiante. |
Assim, pode-se afirmar que os métodos devem ser construídos para facilitar a utilização dos objetos de uma classe, como também podem ser empregados para garantir a consistência de valores internos da classe.
Construtores são métodos especiais utilizados para inicializar e preparar novos objetos durante a sua criação. Assim como acontece com os demais métodos, os construtores podem receber parâmetros, o que permite caracterizar um objeto já em sua criação. Porém, os métodos construtores só podem ser acionados através do operador new, que, como já foi visto, é o comando responsável pela criação de novos objetos.
Não existe um comando especial para diferenciar os construtores dos demais métodos. Porém, eles têm, obrigatoriamente, o mesmo nome que as suas classes. Além disso, os construtores não têm nenhum tipo de retorno, pois o resultado de sua chamada é sempre uma nova instância. Uma classe pode conter mais de um construtor, desde que não haja construtor com igual lista de parâmetros, ou seja, com igual assinatura do método.
Conceito: Assinatura de método é o nome dado à combinação do nome do método e dos tipos dos seus parâmetros. O tipo de retorno do método não faz parte da sua assinatura. |
Java não exige que o programador escreva construtores para as classes. Porém, quando o programdor não cria os construtores, o compilador automaticamente adiciona um construtor padrão durante a compilação, ou seja, um construtor sem parâmetros.
O uso mais comum dos construtores é para definir valores iniciais para os objetos de uma classe, como uma maneira de garantir um estado inicial consistente ou para simplificar o uso das instâncias.
Ainda sobre a inicialização dos objetos, deve-se salientar que todos os campos de uma classe são automaticamente inicializados antes do acionamento de qualquer construtor, mesmo o padrão, durante a instanciação de novos objetos. A inicialização padrão de Java segue as seguintes regras:
Normalmente, os construtores são criados como públicos, mas é possível especificar outros níveis de acesso, como protegido ou privado.
Assim como acontece com os métodos comuns, os construtores também podem ser sobrecarregados, possibilitando que novas instâncias de uma classe sejam obtidas de maneiras diferentes, flexibilizando o método de criação de objetos.
O Programa 2.4 apresenta um exemplo de uso de métodos construtores. Dentro da Classe Ponto, existem dois métodos construtores, declarados nas linas 04 e 08. O método construtor da linha 04 não recebe valores como parâmetros e sempre define o valor das coordenadas do ponto em 0.0,0.0. Já o método construtor da linha 08 recebe as coordenadas como parâmetros e então chama o método setPonto(a,b) para definir os valores das coordenadas do ponto criado.
01. public class Ponto {
02. protected double x, y; 03. 04. public Ponto() { 05. setPonto( 0, 0 ); 06. } 07. 08. public Ponto( double a, double b ) { 09. setPonto( a, b ); 10. } 11. 12. public void setPonto( double a, double b ) { 13. x = a; 14. y = b; 15. } 16. 17. public void imprimeCoordenadas() { 18. System.out.println("(" + x + " , "+ y + ")"); 19. } 20. 21. public static void main(String[] args) { 22. Ponto obj = new Ponto(); 23. obj.imprimeCoordenadas(); 24. } 25. }
|
Na linha 22, cria-se um objeto ponto de nome obj. Neste exemplo, foi utilizado o método construtor da linha 04, que não possui parâmetros. Na linha 23, o objeto obj utiliza o método imprimirCoordenadas(.) para exibir o valor das coordenadas do ponto obj e que, neste caso, será igual a (0.0,0.0).
Para se criar o objeto obj com o outro construtor bastaria, na linha 22, declarar a seguinte instrução:
Neste caso, o valor impresso pelo método imprimeCoordenadas(.), da linha 23, seria igual a (2.0,3.0).
O operador this é utilizado para que um objeto possa acessar uma referência a si próprio. Quando um método não estático é chamado por um objeto particular, o corpo do método utiliza implicitamente o operador this para referenciar as variáveis de instância do objeto e de outros métodos. [DD05] afirmar que é possível também utilizar o operador this explicitamente no corpo de um método não estático, porém não possível empregar este operador em um método estático. O Programa 2.5 apresenta um exemplo de uso do operador this.
Na linha 03, é criado um objeto obj do tipo Horário. Para tanto, é passado como parâmetros os valores 12, 25 e 30, que correspondem, respectivamente, a hora, minuto e segundo. O construtor da classe Horario, entre as linhas 13 e 17, recebe três variáveis como parâmetros e que possuem nomes iguais aos atributos da sua classe. Desse forma, para evitar a ambiguidade, é necessário usar o operador this para indicar que os valores empregados para inicializar o objeto obj serão os valores passados como parâmetros e não os atributos declarados nas linhas 09, 10 e 11.
01. public class TesteThis {
02. public static void main(String[] args) { 03. Horario obj = new Horario(12, 25, 30); 04. obj.imprimeHorario(); 05. } 06. } 07. 08. class Horario { 09. private int hora; 10. private int min; 11. private int seg; 12. 13. public Horario(int hora, int min, int seg) { 14. this.hora = hora; 15. this.min = min; 16. this.seg = seg; 17. } 18. 19. public void imprimeHorario() { 20. System.out.println(this.hora + ":" + this.min + ":" + this.seg); 21. } 22. }
|
E o que aconteceria se as linhas 14, 15 e 16 não utilizassem o operador this? Isto não seria um erro, mas os valores empregados seriam os valores dos atributos das linhas 09, 10 e 11. E qual seriam os valores, se esses atributos não foram inicializados? Neste caso, por padrão, o compilador Java iria atribuir valor igual a zero para os atributos hora, min e seg do objeto obj. O método imprimeHorario() também utiliza o operador this para imprimir os atributos do objeto obj. Porém, neste caso, mesmo que o operador this não fosse utilizado o resultado seria o mesmo, pois, implicitamente, o compilador iria acessar os atributos de obj.
Vale ressaltar que se o método Horario(.) fosse estático, não seria possível empregar o operador this. Conforme explica [DD05], um método estático não pode acessar membros de classe não estáticos, porque um método static pode ser chamado mesmo quando nenhum objeto da classe foi instanciado. Pela mesma razão, a referência this não pode ser utilizada em método static - a referência this deve referenciar um objeto específico da classe e, quando um método static é chamado, talvez não haja nenhum objeto da sua classe na memória. A referência this é exigida para permitir que um método de uma classe acesse outros membros não estáticos da mesma classe.
A sobrecarga de métodos (overload) é uma útil característica da programação orientada a objetos que permite a existência de dois ou mais métodos com o mesmo nome, desde que possuam listas de parâmetros diferentes. O compilador deve escolher qual método chamar. Vários métodos podem ser sobrecarregados numa mesma classe. O benefício deste recurso para o programador é que ele pode manter o mesmo nome para operações similares, mesmo com comportamentos distintos, conseguindo melhorar a semântica do código e simplificar o uso das classes.
Vale ressaltar que o programador ao realizar a sobrecarga de métodos, apenas se permite alterar o seu tipo de retorno se, e somente se, os métodos com os mesmos nomes tiverem listas de parâmetros distintas. Ou seja, a assinatura entre os métodos sobrecarregados devem ser distintas.
A presença de construtores e métodos sobrecarregados destaca a semelhança existente entre operações similares e simplifica o entendimento da interface da classe (i.e., dos elementos públicos disponíveis), pois reduz o número de operações diferentes. A API Java usa com freqüência a sobrecarga de métodos e construtores em suas classes. O Programa 2.6 apresenta um exemplo de sobrecarga de métodos.
01. public class ExemploSobrecargaMetodos {
02. public static void main(String[] args) { 03. ExemploSobrecargaMetodos obj = new ExemploSobrecargaMetodos(); 04. obj.imprimir(10); 05. obj.imprimir("TADS"); 06. // obj.imprimir(10.0); 07. } 08. void imprimir (String valor){ 09. System.out.println("O valor do parâmetro é do tipo String."); 10. } 11. void imprimir (int valor){ 12. System.out.println("O valor do parâmetro é do tipo int."); 13. } 14. }
|
Entre as linhas 08 e 13 são implementados dois métodos imprimir(.), de mesmo nome, mas com parâmetros diferentes. O método imprimir(.) declarado na linha 08 tem um parâmetro do tipo String, enquanto que o método declarado na linha 11 tem um parâmetro do tipo int. Cada método imprime uma frase informando o tipo de parâmetro que foi passado, como uma forma de identificar qual foi método chamado.
Na linha 03 do Programa 2.6, é criado um objeto obj para poder utilizar os dois métodos imprimir(.). Na linha 04, o método imprimir(.) recebe o valor 10 como parâmetro, e na linha 05, o método imprimir(.) recebe o valor TADS como parâmetro. A execução deste programa gera o seguinte resultado:
Assim, apesar de possuírem o mesmo nome, o compilador Java é capaz de distinguir qual método irá empregar, de acordo com o tipo de parâmetro passado. A linha 06 está comentada. Caso não estivesse, ocorreria um erro de compilação, pois o valor 10.0, passado como parâmetro, é do tipo double e não existe um método imprimir(.) no programa que receba este tipo de parâmetro. Mesmo existindo um método imprimir(.) do tipo numérico, pelo fato de ser de um tipo diferente, o compilador não consegue associar. A mensagem de erro do compilador, nesta situação, seria a seguinte:
Quando uma classe é herdada por outra, todos os seus métodos não privados são herdados. Porém, existem algumas situações que se deseja alterar o comportamento de alguns desses métodos herdados. Isto é realizado através da redefinição dos métodos da superclasse, a partir de sua reescrita, definindo-os com os mesmos nomes e com a mesma lista de parâmetros. Dessa forma, a chamada a um método sobrescrito, a partir de um objeto instância da subclasse executará o comportamento redefinido pela subclasse e não mais o definido pela superclasse. Se o objeto for instância da superclasse, o método executará o comportamento definido na própria superclasse. Como esses métodos estão diretamente relacionados com a noção de herança, os mesmo serão melhor explanados no capítulo sobre herança.
Existem algumas situações em que é desejável que as várias instâncias de uma classe compartilhem alguma informação, tal como o valor de uma constante, um número de identificação, contagem ou totalização. Esse efeito pode ser obtido com o emprego do modificador static. A sintaxe de Java para uso de métodos estáticos é:
[HC00] define métodos estáticos como métodos que não operam sobre objetos. Por exemplo, o método pow() da classe Math é um método estático. A expressão Math.pow(3, 2) calcula o quadrado de 3, resultando em 9. Isto é realizado sem a necessidade de criar um objeto da classe Math.
Devido ao fato de que os métodos estáticos não operam em objetos, o programador não pode acessar instâncias de campos de um método estático. Porém, os métodos estáticos podem acessar os campos estáticos da classe em que pertence. A seguir, apresenta-se o Programa 2.7, uma modificação do Programa 2.6.
01. public class ExemploMetodoEstatico {
02. public static void main(String[] args) { 03. imprimir(10); 04. imprimir("TADS"); 05. // imprimir(10.0); 06. } 07. static void imprimir(String valor) { 08. System.out.println("O valor do parâmetro é do tipo String."); 09. } 10. static void imprimir(int valor) { 11. System.out.println("O valor do parâmetro é do tipo int."); 12. } 13. }
|
Para transformar os dois métodos imprimir(.) em estáticos, basta colocar a palavra-chave static no início do método, conforme demonstrado nas linhas 07 e 10. Assim, como ambos os métodos são estáticos, não há necessidade de se criar o objeto obj, da linha 03, do Programa 2.6 e, por isso a chamada dos métodos imprimir(.) nas linhas 03 e 04 é realizada diretamente.
O método main(.) é o ponto de entrada de aplicativos Java. Para criar um aplicativo, é necessário que o programador escreva uma definição de classe que inclua o método main(.). A declaração padrão, em Java, deste método é:
Por convenção, o método main(.) é declarado como público. No entanto, é necessário que ele seja estático, para que possa ser executado sem a necessidade de construir uma cópia da classe correspondente. O array args, do tipo String, contém quais argumentos que o usuário possa ter fornecido na linha de comando. O nome do vetor é comumente chamado de args, mas poderia ter qualquer outro nome.
Pacotes são unidades de Java que agrupam classes relacionadas. Os pacotes definem restrições de acesso do seu conteúdo para clientes fora da unidade. A criação de pacotes é feita através da instrução package e que deve constar na primeira linha de um arquivo-fonte de Java. A seguir, um exemplo de criação de um pacote em Java.
Os nomes dos pacotes devem usar apenas letras minúsculas, com exceção dos prefixos java e javax, pois estes estão reservados para uso da Sun para indicar que pertencem à API Java. Por padrão, recomenda-se que os nomes dos pacotes comerciais utilizem o nome invertido do domínio de Internet das empresas. Por exemplo, num programa desenvolvido dentro da UEA, o pacote poderia ter o seguinte nome:
Dica: Para melhor organizar o código-fonte, deve-se criar um diretório geral com o nome do pacote onde existam os subdiretórios src, que irá guardar os arquivos-fonte, e bin, que irá guardar os arquivos da classe. |
Para fazer uso dos pacotes, é empregada a instrução import no início do arquivo-fonte de Java. A seguir, um exemplo de importação de pacotes em Java:
O asterisco indica que serão importadas todas as classes do pacote. Para importar uma classe específica, ao invés do uso do asterisco, o programador deveria escrever o nome da classe que deseja importar. A importação de todas as classes de um pacote não produz qualquer efeito negativo, como o aumento do tamanho do código das classe ou uma performance inferior, sendo apenas uma simplificação para o programador. Porém, recomenda-se a nomeação das classes que deverão ser importadas, pois isto evita problemas quando pacotes diferentes possuem classes com o mesmo nome.
Interface é o mecanismo pelo qual o programador pode definir um conjunto de operações sem se preocupar com sua implementação, indicando assim um modelo de comportamento para outras classes. As interfaces devem conter apenas métodos sem implementação. Conceitualmente, as interfaces equivalem aos métodos abstratos. Também é permitida a declaração de constantes. A sintaxe de uma interface em Java é:
Dica: Não se deve confundir o conceito de interface apresentado nesta seção, com o conceito de interface gráfica (GUI - Graphical User Interface). São conceitos distintos. |
Para a construção das interfaces em Java é necessário que o nome do arquivo tenha o mesmo nome da interface. Não deve existir implementação dos métodos das interfaces, mas apenas a declaração deste métodos. Além disso, as constantes (atributos possíveis em uma interface) são implicitamente públicas, estáticas e finais, sendo essa a forma usual de sua declaração. As constantes são consideradas como estáticas porque só pode existir uma cópia do dado disponível e final porque estes dados não devem ser modificados.
Dica: O especificador de acesso public e os modificadores abstract, static e final podem ser omitidos tanto na declaração de métodos como na declaração de constantes de interface por serem considerados redundantes. |
Apesar das semelhanças entre classes abstratas e interfaces, existem diferenças quanto às suas características. [Poo08] resume essas diferenças na Tabela 2.1:
|
As classes são responsáveis por implementarem os métodos abstratos declarados nas interfaces. Para tanto, é necessário o uso do comando implements de Java. Uma classe pode implementar ou realizar tantas interfaces quanto necessário. Daí a razão para alguns autores afirmarem que Java possui o recurso da herança múltipla através das suas interfaces. Maiores detalhes sobre este tópico serão abordados no Capítulo 4.
Resposta: a e b.
Resposta: c e d.
public class Teste {
public int soma (int a, int b) { return a + b; } }
|
Resposta: g.
01. public class ExercicioObjeto {
02. public static void main(String[] args){ 03. Object o = new Object() { 04. public boolean equals (Object obj){ 05. return true; 06. } 07. } 08. System.out.println(o.equals("Fred")); 09. } 10. }
|
01. class Ro {
02. public static void main(String[] args){ 03. Ro r = new Ro(); 04. Object o = r.teste(); 05. } 06. 07. Object teste(){ 08. 09. 10. } 11. }
|
Resposta: b e f.
class Exercicio {
int a, b; int maior () { if (a > b) return true; else return false; } void menor () { if (a < b) return a; else return b; } }
|
Este capítulo apresenta o conceito de encapsulamento e o seu uso na linguagem de programação Java. Ele está dividido em quatro partes. As duas primeiras partes apresentam os conceitos de encapsulamento, abstração de dados e tipos abstratos de dados. A seção seguinte apresenta o uso de encapsulamento em Java, através de exemplos da sintaxe e de códigos de programa. A última seção apresenta um conjunto de exercícios para revisão e fixação do conteúdo do capítulo.
Encapsulamento é um dos principais recursos da programação orientada a objetos e possui utilização bastante próxima ao uso de modularidade. Pode-se definir encapsulamento como o mecanismo que coloca juntos o código (métodos) e os dados (atributos), mantendo-os controlados em relação ao seu nível de acesso. Assim, pode-se afirmar que o encapsulamento é uma maneira de controlar o acesso de atributos e métodos de um objeto, através de uma interface bem definida.
Para relacionar este conceito com um exemplo do mundo real, considere o motor de um automóvel. O motor possui um vasto conjunto de funcionalidades, tais como a aceleração do carro, o controle de combustível, a geração de energia para as demais partes do carro, entre tantas outras. O motorista faz uso do motor, de forma indireta, através dos mecanismos que foram projetados para isto. Por exemplo, para acelerar o automóvel, o motorista usa o pedal do acelerador e do câmbio do carro. Para diminuir a sua velocidade, ele faz uso do pedal do freio. Neste exemplo, os pedais e o câmbio do carro seriam as interfaces disponíveis para o motorista poder manipular os dados do automóvel. Assim, o motorista não precisa ter conhecimento técnico de como funciona cada parte do motor, mas apenas saber qual é a sua finalidade e como usá-lo.
Como consequência do encapsulamento do motor, um fabricante pode fornecer o mesmo equipamento para diversas montadoras e cada uma delas pode empregá-lo em diversos carros. Para isto, precisam apenas escolher qual motor é o mais adequado às suas necessidades e como usá-lo nos seus carros.
Esta mesma idéia pode ser aplicada à programação. O poder de um código encapsulado é que ele fica disponível para ser usado por outros programadores, mesmo que estes não tenham conhecimento dos detalhes da sua implementação. Assim, pode-se afirmar que são vantagens do uso de encapsulamento:
As classes, normalmente, ocultam os detalhes de implementação dos seus usuários. Isso se chama ocultamento de informações. Por exemplo, o motorista de um veículo ao fazer uso do motor do carro está usando o motor para se locomover, porém não precisa saber dos seus detalhes de funcionamento. Neste caso, ele precisa apenas saber como interagir com o motor, através dos mecanismos de pedal e do câmbio, por exemplo. O cliente se preocupa com a funcionalidade que o motor oferece, não como essa funcionalidade é implementada. Esse conceito é conhecido como abstração de dados.
Embora os programadores possam conhecer os detalhes de implementação de uma classe, eles não devem escrever códigos que dependam desses detalhes. Isso permite que uma determinada classe particular possa ser substituída por outra versão sem afetar o restante do código. No caso da implementação de uma classe Motor, essa classe poderia ser substituída por outra classe Motor2 mais moderna, sem que isso afetasse o restante do carro. Evidentemente, se há uma troca por um motor mais moderno, é provável que o carro alcance uma maior velocidade, porém, basta fazer a substituição do motor do veículo para que ele funcione, sem a necessidade de se realizar mudanças no projeto do carro. Analogamente, contanto que os serviços públicos da classe não mudem, o restante do sistema não será afetado.
A ênfase da maioria das linguagens de programação está nas suas ações, tal qual acontece com as linguagens estruturadas, como C e Pascal. Nessas linguagens, os dados existem apenas para suportar as ações que os programas devem tomar. Os dados são menos interessantes que as ações. A linguagem Java e o estilo de programação orientado a objetos elevam a importância dos dados. As principais atividades da programação orientada a objetos em Java são, segundo[DD05], a criação de tipos e a expressão das interações entre objetos desses tipos. Essa atividade está diretamente associada à noção de tipo abstrato de dados (ADT - abstract data type), que melhora o processo de desenvolvimento de programas, pois permite mais flexibilidade ao programador na criação de novos tipos de dados.
Pense no tipo primitivo int, que a maioria das pessoas associaria com um inteiro na matemática. Antes, um int é uma representação abstrata de um inteiro. Diferentemente dos inteiros na matemática, ints de computador têm tamanho fixo. Por exemplo, o tipo int em Java está limitado ao intervalo -2.147.483.648 a +2.147.483.648. Se o resultado do cálculo estiver fora deste intervalo, ocorrerá um erro e o ambiente de execução responderá de alguma maneira dependente da implementação da máquina virtual de Java. Inteiros na matemática não têm esse problema, visto que possuem valores ilimitados. Portanto, a noção de um int de computador é somente uma aproximação da noção de um inteiro do mundo real. O mesmo é verdadeiro para os demais tipos primitivos.
Assim, pode-se afirmar que um ADT captura duas noções: representação de dados e operações que podem ser realizadas nesses dados. Programadores Java utilizam classes para implementar tipos abstratos de dados.
O encapsulamento em Java ocorre nas classes. Quando o programador cria uma classe, ele especifica o código e os dados que irão formar essa classe. Estes elementos serão chamados de membros da classe. Dessa forma, os dados definidos pela classe são chamados de variáveis membros, variáveis de instância ou atributos. Já o código que manipula esses dados constitui os métodos membros ou apenas métodos. Os programas em Java, costumeiramente, empregam os métodos para definir como as variáveis membros poderão ser usadas. Isto significa que o comportamento e a interface de uma classe são definidos pelos métodos que operam nas instâncias de dados.
O modificador private não foi criado para classes, mas apenas para membros de classes. Apesar disso, é possível empregar o modificador private nas classes. A dúvida comum que surge é: como uma classe pode acessar uma classe privada? A solução é declarar a classe privada como sendo interna, conforme apresentado no capítulo anterior. A seguir, o Programa 3.1 apresenta a forma de usar as classes privadas.
public class Desenho {
private class FerramentaDesenho { } }
|
Dica: O único modificador de acesso permitido em classes não internas é público; não é possível declarar uma classe de alto nível como protegida ou privada. |
Considerando que o objetivo de uma classe é encapsular a complexidade, existem mecanismos para ocultar a complexidade da implementação que está dentro da classe. Cada método ou variável em uma classe pode ser definida como pública, privada ou protegida. A interface de uma classe possibilita que todos os usuários externos possam acessar livremente os dados da classe que os métodos públicos permitem. Já os métodos privados estabelecem que os dados somente podem ser acessados pelos métodos que são membros da classe. Assim, nenhum outro código que não seja membro da classe poderá acessar os métodos ou variáveis privadas.
Considerando que os membros privados de uma classe só podem ser acessados por outras partes do programa através dos métodos públicos desta classe, o programador em Java pode fazer uso do encapsulamento para garantir que ações inapropriadas ou imprevistas não ocorram. Assim, o programador em Java deve ser bastante cuidadoso ao definir a interface pública de uma classe para não expor demasiadamente o funcionamento da classe. A Figura 3.1, retirada e adaptada de [NS01], apresenta, graficamente, a organização dos métodos e atributos, públicos e privados, de uma classe.
|
Os tipos primitivos em Java, a saber, byte, char, short, int, long, float, double e boolean, são oriundos de classes que possibilitam a representação de valores nativos como classes, o que é particularmente útil para uso em métodos que esperam um argumento que seja um herdeiro da classe Object. As classes, porém, não permitem a manipulação direta do conteúdo encapsulado: com estas classes não é possível efetuar as operações que se pode fazer com os tipos criados pelo programador.
Todas essas classes que correspondem aos tipos primitivos de Java fazem parte do pacote java.lang e, por isso, não é necessário nenhum comando import para utilizá-las.
Para exemplificar o que foi dito anteriormente, considere a classe Boolean, que possibilita o encapsulamento de valores do tipo primitivo boolean. Os construtores da classe permitem a inicialização do valor encapsulado através de um parâmetro do tipo boolean (true ou false) ou de uma string que contenha um valor do tipo boolean. Neste segundo caso, se a string for igual a true (independente de estar em maiúsculos, minúsculos ou misturados) o valor representado será igual a true, caso contrário, será igual a false. Não existem mecanismos nesta classe para conversão entre valores numéricos, já que o tipo boolean não é compatível com estes valores.
Alguns atributos e métodos úteis desta classe são:
O Programa 3.2 é um exemplo de código com diversas maneiras de criar e inicializar objetos do tipo Boolean. Como todos os atributos foram criados e inicializados corretamente, a resposta deste programa é Todos os atributos são verdadeiros. Vale observar que, conforme dito anteriormente, a classe Boolean possui métodos encapsulados que podem aceitar valores do tipo String, independentemente da maneira em que foram escritos, conforme as linhas 06 e 08.
01. public class UsoClasseBoolean {
02. public static void main(String[] args) { 03. Boolean a, b, c, d, e; 04. a = true; 05. b = new Boolean (true); 06. c = new Boolean ("TRUE"); 07. d = new Boolean (10 < 20); 08. e = Boolean.valueOf("True"); 09. 10. if (a && b && c && d && e) 11. System.out.println("Todos os atributos são verdadeiros."); 12. else 13. System.out.println("Nem todos os atributos são verdadeiros."); 14. } 15. }
|
Por outro lado, se ao invés de escrever o valor true, fosse colocado algo como t, os métodos da classe Boolean iriam converter esse valor para false. Assim como acontece com a classe Boolean, as demais classes de tipos primitivos possuem um conjunto próprio de métodos encapsulados. [San01] apresenta maiores detalhes sobre as classes que encapsulam valores nativos.
O encapsulamento relaciona os dados (atributos) com o código (métodos) que os manipula. O encapsulamento também fornece outro recurso importante que é o controle de acesso. Através dos modificadores de acesso, os programadores podem controlar o acesso aos membros de uma classe. É através desse controle que o programador garante que não haverá um uso indesejado dos dados de uma determinada classe. Por exemplo, permitir o acesso aos dados somente através de um conjunto definido de métodos (interface) pode prevenir esse uso indesejado dos dados. Assim, quando corretamente implementada, uma classe é criada como uma espécie de caixa preta, que pode ser usada, porém, somente através dos seus métodos públicos que foram colocados à disposição.
O modificador de acesso é uma instrução que define como um membro de uma classe poderá ser acessado. Java possui um rico conjunto destes modificadores. Alguns aspectos do controle de acesso estão relacionados à herança e ao conceito de pacotes. Esses aspectos serão detalhados no próximo capítulo, quando for abordado este tema.
Java possui os seguintes modificadores de acesso: public, private e protected. Java também define um nível de acesso padrão (default) e que se aplica somente quando há o uso de herança. O modo de acesso default também é conhecido como pacote (package).
Dica: Um membro em Java pode ter no máximo um modificador de acesso. |
Quando o membro de uma classe é definido com o modificador de acesso public, então este membro pode ser acessado por qualquer outro código no programa. Dessa forma, o modificador de acesso public é o mais liberal e que, portanto, exige maior responsabilidade do programador ao empregá-lo. O uso do moderador de acesso private determina que os membros de uma classe somente podem ser acessados por outros métodos da mesma classe. Assim, o modificador de acesso private é o mais restritivo e que deve ser empregado sempre que possível.
Dica: Quando uma classe implementa um método main(), deve torná-lo público, para que main() possa ser chamado a partir de qualquer ambiente de tempo de execução Java. |
O modificador de acesso protected, quanto ao nível de acesso, se situa entre os outros dois. Somente os atributos e métodos podem ser declarados como protected. Um membro protegido de uma classe está disponível a todas as classes do mesmo pacote, exatamente como um recurso padrão. Além do mais, um recurso protegido de uma classe está disponível a todas as subclasses da classe que possui o recurso protegido. Esse recurso é oferecido até a subclasses que ficam em um pacote diferente da classe que possui o recurso protegido, contanto que a superclasse seja importada pelo pacote da subclasse. Devido ao fato do seu uso estar diretamente relacionado com o conceito de herança, este modificador será mais detalhado no próximo capítulo.
Quando não é declarado o tipo de moderador, Java adotada como o padrão (default). Apesar de não existir a palavra-chave default em Java, ele é apenas um nome dado ao nível de acesso que resulta de não especificar um modificador de acesso. Dados e métodos de uma classe podem ser default, assim como a própria classe. Os recursos default de uma classe são acessíveis a qualquer classe no mesmo pacote que a classe em questão.
Pode parecer que o acesso padrão só interessa às pessoas que estão interessadas na construção de pacotes. Tecnicamente isto é correto, mas, na verdade, todos os programadores, ao escrevererem um código, estão definindo pacotes, mesmo que isso não aconteça de modo explícito. Quando um programador escreve um aplicativo que envolve várias classes diferentes, é possível que mantenha todos os seus códigos (arquivos .java) e todos os seus arquivos binários (arquivos .class) em um único diretório de trabalho. Ao executar o código, o programador o faz a partir daquele diretório. O ambiente de execução Java considera que todos os arquivos de classe no diretório atual de trabalho constituem um pacote.
Como consequência disto, considere o que pode acontecer quando o programador desenvolve várias classes da maneira anteriormente relatada e não se preocupa em fornecer modificadores de acesso às suas classes, dados ou métodos. Esses recursos não são públicos, privados ou protegidos. Eles resultam em um acesso padrão, o que significa que são acessíveis a quaisquer classes do pacote. Pelo fato de que Java considera todas as classes no diretório como, de fato, formando um pacote, todas as suas classes conseguem acessar os recursos umas das outras. Isto facilita desenvolver código rapidamente, mas a falta de preocupação quanto ao acesso pode incorrer em resultados indesejados.
A Figura 3.2 mostra os tipos de acesso legal para as subclasses. Um método com algum tipo especial de acesso pode ser sobrescrito por um método com um diferente tipo de acesso, desde que haja um caminho na figura do tipo original para o novo tipo.
|
A seguir, na Tabela 3.1, é apresentado um resumo dos modificadores de acesso em Java. Deve-se observar que são apresentados os três modificadores de acesso explícito (public, protected e private) e um modificador implícito.
|
O uso desses diferentes especificadores permite que o programador decida se atributos ou métodos sejam usados livremente (public) ou se devem ficar ocultos (private), evitando o seu uso por objetos de outras classes. Além disso, os modificadores de acesso possibilitam decidir quais elementos da classe poderão ser empregados na construção de novas subclasses (protected) por meio do mecanismo de herança. Este assunto será tratado no próximo capítulo. Quando não se indica explicitamente um especificador de acesso, é subentendido o nível de pacote (package ou default).
O uso dos modificadores em Java é feito através da palavra-chave (public, protected e private) antes do nome da classe, método ou atributo, com exceção do modificador padrão que é implícito. As declarações abaixo são exemplos do uso de modificadores:
Para entender os efeitos do uso dos moderadores de acesso public e private, considere o Programa 3.3. Acesso é a classe que guarda os atributos e os métodos que serão acessados e a classe TesteAcesso é a responsável por acessar os atributos e os métodos da classe Acesso.
01. /* Este programa demonstra a diferença no uso dos moderadores */
02. class Acesso { 03. int a; // acesso padrão 04. 05. public int b; // acesso público 06. 07. private int c; // acesso privado 08. 09. // métodos para acessar o atributo c 10. void setC(int i) { // atribui valor para c 11. c = i; 12. } 13. 14. int getC() { // recebe o valor de c 15. return c; 16. } 17. } 18. 19. public class TesteAcesso { 20. public static void main(String args[]) { 21. Acesso obj = new Acesso(); 22. 23. obj.a = 10; // o atributo a pode ser acessado diretamente 24. 25 obj.b = 20; // o atributo b pode ser acessado diretamente 26. 27. // obj.c = 100; o atributo c não pode ser acessado diretamente 28. 29. // O atributo c deve ser acessado através dos métodos disponíveis 30. 31. obj.setC(100); // setc é empregado para atribuir um valor para c 32. 33. System.out.println("Os valores de a, b, e c são: " + obj.a + " " + obj.b + " " + obj.getC()); 34. } 35. }
|
Na linha 03, é declarado o atributo a, sem o uso do modificador de acesso. Quando não se emprega moderador nos atributos, por padrão, estes se equivalem aos atributos com o nível de acesso de pacote. Na linha 05, é declarado o atributo b, com o modificador de acesso public. Neste caso, os atributos a e b terão o mesmo comportamento quanto ao nível de acesso, já que Acesso e TesteAcesso estão no mesmo pacote. Na linha 7, é declarado o atributo c, com o moderador private.
Entre as linhas 10 e 16 são declarados os métodos setC() e getC() da classe Acesso. O método setC() é utilizado para atribuir um valor para c. Já o método getC() é empregado para ler o valor de c. Isto é necessário porque o atributo c foi declarado como privado e, portanto, não pode ser acessado diretamente. Maiores detalhes sobre os métodos set e get serão abordados na próxima seção.
O corpo da classe TesteAcesso é implementado entre as linhas 19 e 35. Na linha 19 é criado um objeto obj, que é uma instância da classe Acesso. Na linha 23, o atributo a de obj recebe o valor 10. Na linha 25, o atributo b de obj recebe o valor 20. Em ambos os casos, é possível atribuir um valor diretamente a esses atributos, pois os mesmos são considerados públicos e, portanto, podem ser acessados diretamente.
Na linha 27, que está comentada, o atributo c está recebendo o valor igual a 100. Se fossem retirados os comentários, o programa não iria compilar. Qual seria o motivo? A razão é que o atributo c foi declarado como private e, portanto, não pode ser acessado diretamente, sendo necessário um método público para isto. A mensagem de erro que o compilador apresentaria seria algo como apresentado na Figura 3.3.
Como é possível observar, o compilador está informando ao programador que houve um erro, pois o atributo Acesso.c não está visível, ou seja, não pode ser acessado diretamente. Em seguida, o compilador informa a linha em que houve o erro e que, neste caso, foi na linha 27.
Assim, para poder acessar o atributo c, é necessário empregar o método setC() disponível na classe Acesso, conforme demonstrado na linha 31. Na linha 33 é realizada a impressão dos valores dos três atributos da classe Acesso. Vale ressaltar que, também na impressão do valor, é necessário usar um método para ler o valor de c. Caso contrário, se ao invés de usar o método getC(), o programador tentasse imprimir diretamente o valor de c, assim como foi feito com os atributos a e b, isto resultaria em um erro durante a compilação, semelhante ao apresentado na Figura 3.3.
Os atributos private de uma classe somente podem ser manipulados pelos métodos dessa classe. Em geral, esses atributos são utilizados internamente pelas classes em que foram criados. Porém, existem situações em que é necessário que objetos de outras classes também manipulem esses atributos privados. Para tanto, as classes costumam fornecer métodos públicos para permitir a clientes da classe modificarem ou receberem valores de variáveis de instância private e protected. O padrão adotado, pelos programadores em Java, para estes métodos é setNomeAtributo(.) e getNomeAtributo(.) para modificar e receber os valores dos atributos, respectivamente.
Numa avaliação superficial do que foi dito anteriormente, tem-se a impressão de que fornecer as capacidades get() e set() é essencialmente o mesmo que tornar públicas as variáveis de instância. Porém, deve-se ficar atento à essa sutileza do Java. Uma variável de instância public pode ser lida ou gravada por qualquer método que tem uma referência a um objeto que contém a variável de instância. Se uma variável de instância for declarada private, um método public get() certamente permitirá que outros métodos acessem essa variável, mas o método get() pode controlar como o cliente poderá acessar essa variável. Um método public set() pode, e deve, avaliar cuidadosamente das tentativas de modificar o valor da variável a fim de assegurar que o novo valor é apropriado para esse item de dados. Por exemplo, uma tentativa de modificar (set) o dia do mês com o valor igual a 32 também seria rejeitada, assim como uma tentativa de atribuir um peso a uma pessoa com valores negativos seria rejeitada, entre outros exemplos absurdos. Portanto, embora os métodos set() e get() possam fornecer acesso a dados private, o acesso é restrito pela maneira como os métodos foram implementados pelo programador. Isso ajuda a desenvolver programas mais seguros e confiáveis.
Apesar desse recurso de Java, o programador deve saber que os benefícios da integridade dos dados não são automáticos simplesmente porque as variáveis de instância são declaradas como private ou protected. É necessário que o programador forneça a verificação de validade. Java permite que programadores projetem programas melhores de uma maneira conveniente. Métodos set() de uma classe podem retornar valores indicando que foram feitas tentativas de atribuir dados inválidos a objetos da classe. Um cliente da classe pode testar o valor de retorno de um método set() para determinar se a tentativa do cliente de modificar o objeto foi bem-sucedida e tomar uma ação apropriada.
Dica: Os projetistas de classe não precisam fornecer métodos set() ou get() para cada atributo private. Essas capacidades devem ser fornecidas somente quando fizerem sentido. |
De forma resumida, os modos de acesso de Java são:
[h.1.] A classe Tempo segue os princípios do encapsulamento? Comente a respeito.
[h.2.] Como é possível estender o código para atender aos princípios do encapsulamento? Quais seriam as vantagens que isto traria? Faça as modificações necessárias no código.
class Tempo {
int hora; int minuto; Tempo() { }; public static void main(String arg[]) { Tempo t = new Tempo(); t.hora = 3; t.minuto = 25; System.out.println("A hora agora é " + t.hora + ":" + t.minuto); } }
|
Além do que já foi estudado até o momento, uma das grandes vantagens da Programação Orientada a Objetos (POO) é a possibilidade de reutilizar classes existentes, de forma a transformar pequenos blocos de código em estruturas maiores e mais complexas, integrando-os e adicionando funcionalidades específicas a eles. Ao alcance do programador, existem duas técnicas principais para efetivamente reutilizar código: herança e composição.
Em linhas gerais, a herança é a capacidade de especializar tipos de objetos (classes), de forma que os tipos especializados contenham, além de características estruturais e comportamentais já definidas pelos seus “ancestrais”, outras definidas para eles próprios. Isso significa que os tipos especializados, como o próprio nome diz, herdam certas características dos seus tipos ancestrais, com a vantagem de que podem redefinir o que for necessário, sem modificar o tipo ancestral. A relação de herança também pode ser vista como uma relação de derivação de uma classe ancestral para uma classe especializada. A segunda deriva da primeira [Hor05]. É possível ver a herança da seguinte forma : tipo especializado tipo ancestral.NestecapítuloseráadotadaanomenclaturacomumenteutilizadanalinguagemJava,queéadesuperclasse(tipoancestral)esubclasse(tipoderivado).
A composição, por sua vez, é a técnica de construir um tipo não pela derivação partindo de outra classe, mas pela junção de vários outros objetos de menor complexidade que fornecem ao objeto composto determinada funcionalidade quando em conjunto. A composição pode ser vista análogamente à construção de uma casa: vários tijolos, juntamente com portas, cimento, telhado, dentre outros, que sozinhos não teriam uma utilidade plena, ao serem unidos em um determinado arranjo (tijolos formam paredes fixadas através do cimento, e por sua vez sustentam várias telhas agrupadas no telhado) formam um conjunto harmônico, com funcionalidades bem definidas. Vale ressaltar que a composição é recursiva - objetos compostos podem fazer parte de uma composição maior, e assim por diante. É possível ver a composição da seguinte forma: tipo composto tipos fundamentais ou compostos.
Neste capítulo, serão estudadas as técnicas de reutilização supracitadas, incluindo tópicos tais como o uso da palavra-chave protected, o papel dos construtores na herança de classes, assim como o desenvolvimento de métodos sobrescritos. Em adição, o leitor ainda aprenderá os conceitos de interfaces e classes anônimas, além de formas de evitar a herança sobre métodos e classes, bem como os momentos mais adequados de se utilizar herança, composição, ou ambas em conjunto.
Não há nenhuma palavra-chave ou recurso especial para utilizar composição em Java, visto que esta técnica nada mais é do que um modo particular, para cada situação, de agrupar classes existentes de forma a criar novas classes com novas funcionalidades em determinado arranjo. A composição é essencialmente recursiva, ou seja, classes compostas podem também ser integrantes de outras classes compostas. Na Figura 4.1, pode-se observar uma relação de composição: Carro {Motor, Som, Pneu, Chassi}.
O Programa 4.1 apresenta um código para Carro, Motor, Chassi e Pneu. Perceba que existe de fato uma relação de composição entre elas, em consonância com a Figura 4.1. Vale ressaltar que as relações de composição podem envolver quaisquer nível de multiplicidade dos elementos, tais como arrays (observe a relação de carro com pneu).
01. // Composição entre classes
02. 03. class Carro { 04. private String modelo, fabricante; 05. private Chassi chassi; 06. private Motor motor; 07. private Pneu[] pneus; 08. } 09. class Chassi { 10. private String numero, dataFabricacao; 11. } 12. 13. class Motor { 14. private int potencia; 15. private String numero; 16. } 17. 18. class Pneu { 19. private int raio; 20. private String fabricante; 21. } 22. class Som { 23. private int potencia; 24. private String marca; 25. }
|
A herança, como explicado anteriormente, é um conceito que na prática se traduz em uma funcionalidade oferecida por linguagens de programação orientadas a objeto tais como Java, para que o programador possa, a partir de classes existentes, criar outras classes especializadas que se derivem destas. Vale ressaltar que tal especialização pode ser feita tanto a partir de classes já construídas pelo próprio programador, como por classes de terceiros ou classes-padrão da linguagem Java.
Quando se define um conjunto de classes relacionadas através de herança, este é denominado de hierarquia de classes. Em Java, a palavra-chave utilizada para criar uma subclasse é extends.
A sintaxe para uso de extends é a seguinte:
Uma característica da linguagem Java é que qualquer objeto que não herde explicitamente de outra classe, automaticamente será subclasse direta de java.lang.Object, que é a superclasse no nível mais alto da linguagem. Pensando nas classes Java hierarquicamente distribuídas em forma de árvore, a classe java.lang.Object seria o nó raiz desta árvore (Figura 4.2).
Desta forma, os exemplos abaixo são equivalentes:
Considere um sistema onde exista a classe Publicacao e algumas subclasses dela, que são Livro e Revista, conforme apresentado na Figura 4.3.
Um exemplo do uso de extends nesse contexto pode ser visto no Programa 4.2. A classe Publicacao define alguns atributos básicos comuns às suas descendentes, enquanto cada subclasse define atributos próprios ao seu tipo.
01. class Publicacao {
02. private String nome, dataPublicacao, editora; 03. } 04. 05. class Livro extends Publicacao { 06. private String ISBN; 07. } 08. 09. class Revista extends Publicacao { 10. private String periodicidade; 11. }
|
Conforme apresentado no Capítulo 3, a linguagem Java permite que certos dados sejam encapsulados de forma a estarem disponíveis apenas entre classes de uma mesma hierarquia, estando protegidos de acesso público. A palavra-chave que permite atribuir tal característica de encapsulamento a atributos e métodos de uma classe é protected. Como pode ser visto na Figura 4.4, a classe Veiculo possui dois atributos: Placa e Renavam, sendo eles, respectivamente, protegido e público. Isso significa que qualquer classe externa à hierarquia pode acessar o atributo Renavam, enquanto apenas as classes Carro e suas subclasses Passeio e Picape podem acessar o atributo Placa, já que este é protegido (considerando para isso que nenhum método set ou get exista para manipular o referido atributo indiretamente - relembre sobre tais métodos apresentados no Capítulo 3 - Encapsulamento).
OBSERVAÇÃO: Vale relembrar a semântica relacionada aos símbolos utilizados para representar o
nível de encapsulamento de um atributo ou método, de acordo com a terminologia da linguagem
UML[RBJ06].
|
Como exemplo de encapsulamento com herança, considere uma determinada hieraquia de classes projetada para um editor de textos, responsável pelos diferentes arquivos que o editor pode manipular (Ex: o OpenOffice Writer é capaz de abrir e salvar arquivos em vários formatos, além do seu próprio formato nativo). Neste contexto, definiu-se uma superclasse denominada ManipuladorArquivo, com algumas funcionalidades básicas, enquanto as classes derivadas (subclasses) iriam conter as especificidades de cada tipo de arquivo (por exemplo, ajustar o texto ao formato binário adequado, controle dos recursos gráficos disponíveis para cada padrão, definições de segurança para o documento, etc). Uma versão simples da classe ManipuladorArquivo pode ser vista no Programa 4.3, contendo um atributo protegido e dois métodos para definir a operação de salvar os dados em arquivo e ler dados do arquivo (salvarDados e lerDados, respectivamente). A razão para o atributo nomeArquivo ser protegido é que o mesmo não deve ser acessado fora da classe, mas apenas pelos seus descendentes (subclasses). Note que desta forma, as subclasses derivadas de ManipuladorArquivo não precisarão definir um construtor nem um atributo para conter o nome do arquivo, pois isto já foi feito na superclasse (apenas um exemplo, dentre várias outras funcionalidades que poderiam também ser incluídas na superclasse, afim de maior reutilização de código entre as classes da hierarquia).
Aproveitando o exemplo, pode surgir o questionamento sobre o porquê de se utilizar métodos abstratos neste contexto? A resposta é simples - considerando que a classe ManipuladorArquivo não tem funcionalidade plena, os métodos salvarDados e lerDados não têm de ser implementados senão em uma classe especializada.
01. // Superclasse para manipular arquivos em um editor de textos
02. public class ManipuladorArquivo { 03. protected String nomeArquivo; 04. public ManipuladorArquivo(String nomeArquivo) { 05. this.nomeArquivo = nomeArquivo; 06. } 07. // Os métodos salvarDados e lerDados vazios 08. // devem ser definido nas classes derivadas 09. public abstract boolean salvarDados(byte[] dados); 10. public abstract byte[] lerDados(); 11. } // final da classe ManipuladorArquivo
|
Considere que o programador deseja codificar inicialmente duas classes para abranger o formato nativo do OpenOffice e o formato RTF (Rich Text Format). Logo, ele construiria duas classes que herdam da classe ManipuladorArquivo: ManipuladorOpenOffice e ManipuladorRTF. Tais classes podem ser vistas nos Programas 4.4 e 4.5.
01. // Classe para manipular arquivos OpenOffice
02. public class ManipuladorOpenOffice extends ManipuladorArquivo { 03. // Os métodos salvarDados e lerDados definidos 04. // para o formato do OpenOffice 05. public boolean salvarDados(byte[] dados) { 06. // Escrevendo dados em um arquivo Openoffice 07. } 08. public byte[] lerDados() { 09. // Lendo a partir de um arquivo Openoffice 10. } 11. } // final da classe ManipuladoOpenOffice
|
01. // Classe para manipular arquivos RTF
02. public class ManipuladorRTF extends ManipuladorArquivo { 03. // Os métodos salvarDados e lerDados definidos 04. // para o formato Rich Text Format (RTF) 05. public boolean salvarDados(byte[] dados) { 06. // Escrevendo dados em um arquivo RTF 07. } 08. public byte[] lerDados() { 09. // Lendo a partir de um arquivo RTF 10. } 11. } // final da classe ManipuladoRTF
|
Desenvolver software orientado a objetos com a linguagem Java envolve alguns aspectos específicos que correspondem mais às características da linguagem do que à POO propriamente dita. Dentre estes, está a forma de lidar com os construtores em diferentes níveis da hierarquia. Considere a classe do Programa 4.6, que representa um avião em um jogo de guerra. Observe que a cor do avião é definida randomicamente (linha 07), no momento da criação do objeto, através do seu construtor, assim como suas coordenadas nos eixos X e Y (linhas 09 e 10).
O leitor deve perceber o uso de casting na linha 07, que é explicado pelo fato de que a variável cor é do tipo int, enquanto o resultado da multiplicação 255 * Math.random() retorna um número double. De forma a tornar compatível a atribuição de um valor de tipo diferente da variável (int ⇔ double) é necessário realizar uma conversão explícita (denominada de casting ou coerção). Nas linhas 09 e 10, os atributos que representam as coordenadas, sendo do tipo double, não necessitam desta conversão. Maiores detalhes sobre este aspecto serão discutidos no capítulo que trata sobre Polimorfismo.
DICA: O método Math.random() gera um número do tipo double entre 0.0 e 1.0, cujo resultado pode ser utilizado como fator de multiplicação na geração de números randômicos em Java. |
01. // Classe representando um Avião
02. public class Aviao { 03. protected int cor; 04. protected double coordenadaX, coordenadaY; 05. public Aviao() { // Construtor 06. // Número randômico entre 0 e 255 07. cor = (int) (255 * Math.random()); 08. // Números randômicos entre 0 e 65536 09. coordenadaX = 65536 * Math.random(); 10. coordenadaY = 65536 * Math.random(); 11. System.out.println("Inicializado com Cor = " + cor + 12. " e (x,y) = (" + coordenadaX + "," + coordenadaY + ")") 13. } 14. } // final da classe Aviao
|
Considerando diferentes tipos de avião previstos para o jogo, o próximo passo é criar uma subclasse denominada AviaoRadar. A classe pode ser vista no Programa 4.7. Observe o campo que adiciona um dado sobre o raio de abrangência do radar.
01. // Especialização de Avião com Radar
02. public class AviaoRadar extends Aviao { 03. private int raioRadar; 04. public AviaoRadar() { // Construtor 05. // Número randômico entre 0 e 10000 06. raioRadar = (int) (10000 * Math.random()); 07. System.out.println("Inicializado com Raio do Radar = " + raioRadar) 08. } 09. } // final da classe AviaoRadar
|
A classe AviaoRadar define um construtor próprio, com o intuito de inicializar seus próprios atributos. Quando há herança entre classes, ao se criar um objeto da classe derivada, este contém um sub-objeto de sua superclasse. Isto significa que este sub-objeto é equivalente a criar um objeto puro desta superclasse (no caso, AviaoRadar contém um sub-objeto de Aviao). Entretanto, apesar de existir internamente para a subclasse, quando considerado o “mundo externo”, apenas o objeto da classe derivada existe [Eck02]. A sequência de execução dos construtores sempre acontece no sentido top-down, ou seja, desde a superclasse, passando por todos os níveis da hierarquia, até chegar à subclasse em questão, como pode ser visto na Figura 4.5.
Observe o Programa 4.8, onde é apresentada uma simples rotina para criar uma instância de AviaoRadar, seguido da sua saída após a execução.
01. // Teste com Avião Radar
02. public class TesteAviaoRadar { 03. public static void main(String[] args) { 04. AviaoRadar av = new AviaoRadar(); // Cria uma instância de AviaoRadar 05. } 06. }
|
Devido à necessidade de customização de determinados atributos quando um objeto é inicializado, com frequência as classes Java possuem construtores sobrecarregados. Como já foi explicado em capítulos anteriores, a sobrecarga permite a possibilidade de construir métodos com o mesmo nome e em uma mesma classe, mas diferindo entre si na quantidade de argumentos. Quando se trata de construtores, não há nenhuma diferença em relação a isso, visto que eles são equivalentes a métodos convencionais. Na prática, projetar classes com construtores sobrecarregados permite ao programador inicializar objetos de diferentes formas, com diferentes parâmetros e opções.
Em se tratando de herança em Java, existe uma palavra-chave denominada super que permite às subclasses referenciar suas superclasses. Tal palavra-chave permite acessar a superclasse através de duas formas:
A sintaxe de super pode ser observada nos exemplos a seguir:
No funcionamento do mecanismo de herança com um construtor padrão, os construtores da superclasse e subclasses não possuem argumentos, o compilador descobre facilmente que deve chamar implicitamente o construtor da superclasse (sem necessidade do programador explicitar isso). . Por outro lado, quando há construtores parametrizados, o compilador não faz este trabalho implícito, impondo ao programador que realize uma chamada ao construtor correto através da palavra-chave super.
Observe as classe do Programa 4.9. É possível deduzir, a partir das explicações anteriores, que a chamada feita na linha 15 é desnecessária, visto que quando tratando de construtores padrão (sem parâmetros), o compilador realiza essa chamada automaticamente.
01. // Conta bancária normal
02. class ContaBancaria { 03. private double saldo; 04. private String titular; 05. public ContaBancaria() { 06. // ... Inicialização da conta ... 07. } 08. } 09. 10. // Conta bancária de clientes especiais 11. class ContaVIP extends ContaBancaria { 12. private double chequeEspecial; 13. private int diasSemJuros; 14. public ContaVIP() { 15. super() // Chamada ao construtor da superclasse ContaBancaria 16. // ... Inicialização da conta VIP ... 17. } 18. }
|
Considere agora uma pequena modificação aplicada de forma que o construtor da classe ContaBancaria necessite de um parâmetro na inicialização do construtor. O resultado pode ser visto no Programa 4.10. Neste caso, a chamada ao construtor da superclasse através da palavra-chave super é necessária, caso contrário não seria possível inicializar a subclasse da forma definida na superclasse, e haveria um erro em tempo de compilação.
01. // Conta bancária normal
02. class ContaBancaria { 03. private double saldo; 04. private String titular; 05. public ContaBancaria(double saldoInicial) { // novo construtor 06. saldo = saldoInicial; 07. // ... Inicialização da conta ... 08. } 09. } 10. 11. // Conta bancária de clientes especiais 12. class ContaVIP extends ContaBancaria { 13. private double chequeEspecial; 14. private int diasSemJuros; 15. public ContaVIP(double saldoInicial) { 16. super(saldoInicial) // Chamada ao novo construtor da superclasse 17. // ... Inicialização da conta VIP ... 18. } 19. }
|
Não há obrigação das subclasses em receber nos seus próprios construtores os mesmos parâmetros dos construtores da superclasse. Em relação ao exemplo do Programa 4.10, se o programador não desejasse utilizar o parâmetro novo no seu próprio construtor, poderia também ter utilizado um valor padrão para o atributo esperado. Considere que uma ContaVIP tivesse um valor padrão de saldo inicial, como por exemplo R$ 50.000. O código para um construtor padrão poderia ser reescrito da seguinte forma que se segue.
No exemplo contido no Programa 4.3, sobre o Manipulador de Arquivos, este não implementava os métodos para leitura e escrita de arquivos, que foram definidos de forma abstrata. Neste caso, esta abordagem foi suficiente, pois de fato a superclasse ManipuladorArquivo não necessitava da implementação de tais métodos. Entretanto, na herança de classes, existe a possibilidade de se redefinir métodos nas subclasses, mesmo quando estes estão implementados nas superclasses. Isto se chama sobrescrita de métodos1, cuja utilidade se dá no fato do programador poder criar um comportamento próprio a um método após sobrescrevê-lo, quando o respectivo método da superclasse não é suficiente.
O exemplo do Programa 4.11 demonstra a definição de sobrescrita de métodos em uma relação de herança. Neste caso, definiu-se uma classe Conexão que possui uma forma padrão para enviar dados. Por outro lado, para determinados tipos de transmissão, há necessidade de maior segurança na transmissão da informação. Posto isto, será criada então a classe ConexaoSegura, que redefine os métodos de transmissão e recepção de forma a prover criptografia na transmissão. Como pode ser visto no Programa 4.12, além da redefinição dos métodos de transmissão e recepção (linhas 09 e 12), perceba também que a classe ConexaoSegura definiu um construtor diferente da superclasse (linhas 04-07). Como foi dito anteriormente, utilizando super é possível chamar o construtor da classe Conexao passando o parâmetro correto.
01. // Conexão convencional
02. public class Conexao { 03. protected String endereco; 04. public Conexao(String enderecoRemoto) { // Construtor 05. endereco = enderecoRemoto; 06. } 07. public void transmitir(byte[] dados) { 08. // ... Código para transmissão convencional ... 09. } 10. public byte[] receber(int quantidade) { 11. // ... Código para recepção convencional ... 12. } 13. }
|
01. // Conexão segura
02. public class ConexaoSegura extends Conexao { 03. private String algoritmo; 04. public Conexao(String enderecoRemoto, String algoritmo) { // Construtor 05. super(enderecoRemoto); // Chamada ao construtor da superclasse 06. this.algoritmo = algoritmo; 07. } 08. // Redefinição dos método transmitir e receber para incluir criptografia 09. public void transmitir(byte[] dados) { 10. // ... Transmissão com criptografia ... 11. } 12. public byte[] receber(int quantidade) { 13. // ... Recepção com criptografia ... 14. } 15. }
|
Como já foi estudado anteriormente, Java possui o recurso de definição de classes abstratas. Uma classe abstrata serve como base para uma hierarquia de classes derivadas, podendo implementar parte do código comum a todas elas. Ao mesmo tempo que pode implementar parte do código, uma classe abstrata não pode ser instanciada. A instanciação deve ser feita sempre em uma classe concreta derivada.
Java possui, além das classes abstratas, um outro recurso relacionado denominado de interface. Uma interface é um contrato onde a classe que a implementa assume uma espécie de compromisso em implementar os métodos no formato que a interface define. Como define [HC00], uma interface é uma maneira de descrever o que a classe vai fazer, ao invés de como ela o faria.
Ao contrário das classes abstratas, as interfaces não podem ter uma implementação própria. Isso significa que elas não possuem atributos nem implementação de métodos, apenas as assinaturas dos mesmos. Quando uma classe Java implementa uma interface, ela deve criar o conjunto completo de métodos que esta interface define. Desta forma, assim como as classes abstratas, várias classes podem implementar a mesma interface com diferentes comportamentos. Grandes projetos em Java utilizam extensivamente o recurso de interfaces, uma vez que após a definição de interfaces no projeto de software, a implementação passa a ser apenas um detalhe subsequente (este contexto será melhor explorado no capítulo que trata sobre polimorfismo).
Uma característica importante das interfaces é que uma classe Java é capaz de implementar quantas interfaces forem necessárias, enquanto que ao utilizar classes abstratas, o programador está restrito ao fato de herdar de apenas uma classe ancestral.
O formato para uso de interface é descrito a seguir (observe que para mais de uma interface, o programador deve separar a declaração através de vírgula).
Observe a definição de uma interface relacionada a uma estrutura de dados conhecida, a Pilha. A fim de implementá-la, considere a criação de uma classe PilhaImplementacao. O código para a interface e sua implementação podem ser visto no Programa 4.13. Perceba a referência à constante Pilha.MAX_ELEM - os únicos membros que uma interface pode ter são constantes estáticas, ou seja, que podem ser referenciadas por qualquer classe sem modificação no seu valor e sem requerer a instanciação da interface, o que seria impossível de acordo com os preceitos da linguagem Java.
01. // Interface Pilha
02. interface Pilha { 03. // Constante estática - máximo de elementos 04. // Único tipo de membro que uma interface pode ter 05. public final static int MAX_ELEM = 100; 06. // Empilhar e desempilhar - Apenas a assinatura dos métodos 07. public void empilhar(Object dado); 08. public Object desempilhar(); 09. } 10. 11. // Implementação concreta da interface Pilha 12. class PilhaImplementacao implements Pilha { 13. // Pilha capaz de conter 100 objetos 14. Object[] dadosPilha = new Object[Pilha.MAX_ELEM]; 15. int ULTIMO = -1; // Índice do último elemento da pilha 16. // A classe deve implementar os dois métodos definidos 17. // Atenção: os códigos não estão fazendo checagem de erros 18. public void empilhar(Object dado) { 19. ULTIMO++; 20. dadosPilha[ULTIMO] = dado; 21. } 22. public Object desempilhar() { 23. Object obj = dadosPilha[ULTIMO]; 24. dadosPilha[ULTIMO] = null; 25. ULTIMO--; 26. return obj; 27. } 28. }
|
Com o intuito de mostrar a versatilidade das interfaces, será definida mais uma interface de uma estrutura de dados conhecida, a Fila. Para que sejam implementadas ambas as interfaces, será criada então uma classe MultiEstrutura que as implementa ao mesmo tempo (como foi dito anteriormente, não existe limitação para o número de interfaces implementadas, ao contrário do que acontece para classes abstratas), cujo diagrama pode ser visto na Figura 4.6.
Implementando ambas as interfaces é possível dizer com segurança que a classe MultiEstrutura é, ao mesmo tempo, um objeto do tipo Fila e do tipo Pilha, podendo ser utilizado como ambos em qualquer contexto. O código do Programa 4.14 contém as declarações e implementação das interfaces em questão (utilizando a interface pilha definida no Programa 4.13).
01. // Interface Fila
02. interface Fila { 03. public void enfileirar(Object dado); 04. public Object desenfileirar(); 05. } 06. 07. // Implementação concreta das interfaces Pilha e Fila 08. // Todos os métodos de ambas as interfaces são implementados 09. public class MultiEstrutura implements Pilha, Fila { 10. public void empilhar(Object dado) { 11. // Código para empilhar 12. } 13. public Object desempilhar() { 14. // Código para desempilhar 15. } 16. public void enfileirar(Object dado) { 17. // Código para enfileirar 18. } 19. public Object desenfileirar() { 20. // Código para desenfileirar 21. } 22. }
|
A herança múltipla é um conceito da teoria por trás da POO que significa uma classe possuir mais de uma superclasse. Em outros termos, significa construir uma classe que herde as características de mais de uma classe ao mesmo tempo. Linguagens como C++ permitem tal recurso, embora Java não o permita diretamente. Isso significa que Java não permite ao usuário utilizar a palavra-chave extends através de múltiplas classes ao mesmo tempo. Entretanto, a linguagem Java permite uma variante de herança múltipla2 que é conseguida através da implementação de múltiplas interfaces. Na prática, ao implementar mais de uma interface simultaneamente, como na classe MultiEstrutura do exemplo 4.14, o programador está propiciando à classe em questão a capacidade de assumir o tipo das referidas interfaces em outras classes que delas necessitem. Outrossim, no exemplo em questão, um objeto da classe MultiEstrutura pode ser utilizado como parâmetro em um método que exija, por exemplo, um objeto da classe Fila, como pode ser visto no programa 4.15.
01. public class Ordenador {
02. public Fila ordenarElementos(Fila f) { 03. // Código de ordenação 04. } 05. 06. public static void main(String[] args) { 07. Fila f = new MultiEstrutura(); 08. f.empilhar(10); 09. f.empilhar(20); 10. // MultiEstrutura também é uma fila e pode 11. // ser passada como parâmetro ao método ordenarElementos 12. Ordenador ord = new Ordenador(); 13. Fila filaOrdenada = ord.ordenarElementos(f); 14. } 15. }
|
Além da própria definição e uso de interfaces, Java também permite realizar a herança entre interfaces. O conceito de herdar uma interface pode parecer estranho à primeira vista, já que as interfaces não possuem implementação. Entretanto, justamente por isso, é um artifício de simples compreensão. Uma interface, ao herdar de outra, automaticamente assume os métodos desta de forma implícita. O Programa 4.16 demonstra um exemplo de herança entre interfaces, que é realizada de maneira semelhante à herança entre classes, através da palavra-chave extends.
01. // Interface Usuario
02. interface Janela { 03. public int getLargura(); 04. public int getAltura(); 05. } 06. 07. // Interface JanelaMensagem - herança de Janela 08. interface JanelaMensagem extends Janela { 09. public String getMensagem(); 10. } 11. 12. // A classe JanelaConfirmacao implementa apenas JanelaMensagem, 13. // mas também tem que implementar os métodos de Janela 14. // por causa da herança das interfaces 15. public class JanelaConfirmacao implements JanelaMensagem { 16. public int getLargura() { /* Largura */ } 17. public int getAltura() { /* Altura */ } 18. public String getMensagem() { 19. return "Por favor confirme!"; 20. } 21. }
|
A linguagem Java permite criar determinadas classes apenas para servir a um contexto muito específico, como argumento de um método por exemplo. Para este tipo de situação, Java oferece a opção de se criar as chamadas classes anônimas. Uma classe anônima, como o próprio nome diz, não tem um nome atribuído. Ela é criada necessariamente como subclasse de uma outra classe ou como implementação de uma interface, e tem a função de suprir uma necessidade pontual.
Um exemplo corriqueiro e bastante utilizado de classes anônimas é a programação com janelas visuais em Java3. A programação visual depende de vários aspectos e detalhes, embora algumas destas peculiaridades são bastante frequentes. Por exemplo, a imensa maioria dos programas visuais possui botões, e quando o programador deseja associar um determinado botão a uma respectiva ação, ele precisa definir como isto deve acontecer, ou seja, qual a ação a ser tomada pelo programa.
Normalmente, as ações associadas a um botão, como ser clicado (clique simples, clique duplo, etc.) são tratadas por um determinado método. Para associar este método àquela ação do botão, é necessário definir uma classe “administradora” dos eventos que esse botão gera. Este tipo de classe administradora é comumente chamada de listener. No Programa 4.17 existe um exemplo de classe criada a partir ActionListener e implementada como classe anônima, cujo objeto é criado somente para ser passado como parâmetro do método addActionListener de um botão. Nas linhas de 08 a 10 pode ser observada a criação de uma classe anônima. Na linha 10, perceba o final da classe anônima através do mesmo símbolo utilizado em classes convencionais, o “}”.
DICA: As classes anônimas não devem ser usadas indiscriminadamente em programas Java, pois acabam tornando os programas mais difíceis de interpretar por programadores diferentes. Desta forma, apenas em casos especiais como a programação em Swing elas devem ser utilizadas. |
01. // Janela visual com botão clicável
02. public class Janela extends JFrame { 03. private JButton botao = new JButton(); 04. public Janela() { 05. // Uma classe anônima é criada na passagem de parâmetros do botão 06. // Perceba que a declaração [ new ActionListener() { ] 07. // define uma classe sem nome! 08. botao.addActionListener(new ActionListener() { 09. // Código para fazer uma ação no clique do botão 10. }); // Final da classe e da chamada ao método addActionListener 11. } 12. }
|
A programação em Java envolve algumas situações peculiares onde além de não fazer uso de herança, o programador também necessita evitá-la por motivos diversos, como por exemplo para garantir que determinadas características da classe base não sejam modificadas por quaisquer subclasses geradas. A palavra-chave final possui basicamente três tipos de utilização na linguagem Java:
O enfoque deste capítulo compreende as duas últimas variantes de uso da palavra-chave final. Em relação à primeira variante, o Programa 4.18 demonstra a utilização de bloqueio de herança para um determinado método de uma classe, denominada GeradorDeSenhas. Considere que esta classe gera senhas aleatoriamente, a partir de um dado de entrada (por exemplo, o nome do usuário). Em adição, o seu método de geração de senhas tem que ser fixo para manter compatibilidade com o sistema de autenticação que a utiliza. Na linha 11, o método getSenha() é definido como final, e consequentemente nenhuma subclasse de GeradorDeSenhas pode sobreescrevê-lo.
01. // Uma classe base capaz de gerar senhas para um sistema a partir
02. // de um dado fornecido de forma a ser definida em subclasses. 03. public abstract class GeradorDeSenhas { 04. // Um determinado dado a ser lido (arquivo, banco de dados, rede, etc.) 05. protected Object dado; 06. // Método a ser implementado em subclasse que define 07. // como o dado é carregado 08. public abstract void carregarDado(); 09. // O código para gerar a senha é proprietário e deve ser 10. // mantido fixo independente das implementações 11. public final String getSenha() { 12. // Código para gerar a senha não pode ser sobreescrito pelas subclasses 13. } 14. }
|
A segunda variação do uso de final diz respeito ao bloqueio de herança não apenas para determinados métodos, mas para classes inteiras, que ao receberem esta marcação, indicam ao compilador que a herança delas para qualquer outra classe é impossível de ser realizada. O Programa 4.19 demonstra a utilização deste tipo de bloqueio para classes inteiras, onde uma classe responsável pela configuração de um sistema é marcada como final para que não haja tentativas de inserir uma versão adulterada da mesma, com comportamento diferente e que pudesse causar risco a este sistema configurado por ela.
DICA: Mesmo quando uma classe é declarada como final, seus atributos continuam mutáveis, a menos que declarados explicita e individualmente como finais também. |
01. // Classe de configuração de um sistema
02. public final class Configurador { // Esta classe não pode ser derivada 03. public void inicializarSistema() { 04. // Código para inicializar os módulos do sistema 05. } 06. public void finalizarSistema() { 07. // Código para finalizar o sistema 08. } 09. public void configurar(int modulo, String chave, Object valor) { 10. // Configura uma determinada variável do sistema 11. } 12. public int obterStatus() { 13. // Obtém o status do sistema 14. } 15. }
|
DICA: classes ou métodos abstratos não podem ser definidos como final, já que obrigatoriamente, são projetados para subclasses os implementarem. Isso significa que as palavras-chave abstract e final são mutuamente excludentes. |
Como foi apresentado no início deste capítulo, existem situações que levam o programador a ter de escolher entre projetar uma classe baseada em composição ou herança. Na maior parte das vezes, a solução mais comum é agrupar classes existentes em novas funcionalidades para criar novas classes, ou seja, utilizar composição. Em outras situações, uma análise levará à percepção de que o uso de herança será necessário. O programador deve ter em mente que a utilização de herança não deve ser excessiva, mas que possa atingir a demanda correta que o seu projeto exige. Uma discussão mais aprofundada sobre a utilização de composição em favor da herança pode ser vista em [Blo01].
De acordo com [Eck02], uma das maneiras mais diretas de se determinar a utilização de herança ou composição é se perguntar se a classe a ser criada nunca necessitará de um upcast4 para a suposta superclasse, ou seja, se ela nunca precisará “assumir” o tipo da superclasse em alguma situação. Lembre-se que quando uma classe herda de outra, ela também é daquele tipo. O upcast é justamente isso: em qualquer momento, um objeto da subclasse pode ser utilizado como se fosse um objeto da superclasse. A seguir, estão listados alguns exemplos desta afirmação:
O programador, desta forma, deve concluir que se a classe a ser criada nunca precisa assumir o referido tipo da superclasse, provavelmente esta situação será melhor modelada se esta superclasse for apenas um atributo, caracterizando assim uma situação de uso de composição.
01. class Usuario {
02. private String password; 03. private String login; 04. } 05. class Administrador extends Usuario { 06. }
|
01. class Pilha {
02. protected Object[] dadosPilha = new Object[100]; 03. protected int ULTIMO = -1; // Índice do último elemento da pilha 04. public void empilhar(Object dado) { 05. if (ULTIMO = 99) { 06. System.out.println("Pilha cheia"); 07. return; 08. } 09. ULTIMO++; 10. dadosPilha[ULTIMO] = dado; 11. } 12. public Object desempilhar() { 13. if (ULTIMO == -1) { 14. System.out.println("Pilha vazia"); 15. return; 16. } 17. Object obj = dadosPilha[ULTIMO]; 18. dadosPilha[ULTIMO] = null; 19. ULTIMO--; 20. return obj; 21. } 21. }
|
01. interface I {
02. void x(); 03. void y(); 04. } 05. class A implements I { 06. A() {} 07. public void w() { System.out.println("Em A.w"); } 08. public void x() { System.out.println("Em A.x"); } 09. public void y() { System.out.println("Em A.y"); } 10. } 11. public class B extends A { 12. B() {} 13. public void y() { 14. System.out.println("Em B.y"); 15. } 16. void z() { 17. w(); 18. x(); 19. } 20. public static void main(String args[]) { 21. A aa = new A(); 22. B bb = new B(); 23. bb.z(); 24. bb.y(); 25. } 26. }
|
Resposta: a.
01. final class Aaa {
02. int xxx; 03. 04. void yyy() { 05. xxx = 1; 06. } 07. } 08. 09. class Bbb extends Aaa { 10. final Aaa finalref = new Aaa(); 11. 12. final void yyy() { 13. System.out.println("No método yyy()"); 14. finalref.xxx = 12345; 15. } 16. }
|
Neste capítulo será apresentado o polimorfismo, uma interessante característica de programas orientados a objeto em Java. A palavra polimorfismo, proveniente do grego, significa muitas formas. Na prática, este termo denota uma característica que permite a uma interface (vale ressaltar o uso da palavra interface no sentido amplo: uma superclasse em uma hierarquia, ou de fato uma interface java) ser utilizada independentemente de suas implementações concretas.
Um exemplo de aplicação do polimorfismo é uma cesta de compras, encontrada em praticamente qualquer loja virtual que permita compras online. Uma hierarquia resumida para este programa pode ser observada na Figura 5.1. Observe que a função da cesta de compras basicamente é a mesma para qualquer tipo de produto: guardar os produtos e somar o valor total deles na conclusão da compra. Se a cesta de compras tivesse que ser feita para cada tipo de produto, existiriam centenas de tipos de cestas diferentes - livros, cd’s, celulares, tv’s, etc. Seria praticamente inviável manter o controle sobre tantas variações da cesta, além da própria dificuldade em integrá-las para gerar uma compra única. Entretanto, graças ao comportamento polimórfico aplicável na linguagem Java, o programador não precisa se preocupar com a manutenção de centenas de cestas de compras diferentes. Assim, é possivel, por exemplo, utilizar uma única cesta capaz de manipular uma única interface, denominada produto, de onde seriam implementados os diversos tipos de produtos concretos da loja virtual.
Com o polimorfismo em mente, o programador pode projetar as classes para interagirem entre si o máximo possível através apenas de interfaces, e a ação específica dependerá de cada situação concreta. Em [NS01], o polimorfismo é definido por uma única frase: “Uma interface, múltiplos métodos.” Isto significa desenvolver uma interface para que ela seja o ponto de contato entre diferentes classes relacionadas, expressando um único comportamento.
Ao longo do capítulo, serão apresentadas várias aplicações e conceitos do polimorfismo, além de programas contendo exemplos relacionados.
Como foi dito anteriormente, entender as vantagens do polimorfismo leva ao desenvolvimento de softwares com maior facilidade de manutenção e menor dependência de detalhes de implementação, já que as classes passam a ser projetadas de forma a diminuir a utilização de implementações concretas no código produzido.
Uma das primeiras coisas que o programador deve ter em mente para evitar a referência a classes concretas é que os atributos e parâmetros de métodos envolvidos na classe criada devem “esquecer” os tipos concretos na maior extensão possível. O exemplo a seguir demonstra uma forma de instanciação direta, utilizando apenas implementações concretas.
Alterando o referido exemplo, poderia ser utilizada a referência para a superclasse, ao invés de referenciar as classes concretas. Desta forma, haveria as seguintes alterações:
Percebam que no exemplo modificado as variáveis mouse e teclado são instâncias da classe Dispositivo, mas graças ao polimorfismo, o mecanismo de execução guarda informações internas que permitem identificar qual classe concreta é referenciada pela variável (no caso, Mouse e Teclado, respectivamente). Nunca é demais lembrar que mouse e teclado são, acima de tudo, dispositivos, o que valida a abordagem supracitada. A conversão entre tipos e subtipos será mais aprofundada na Seção 5.3.
Considere o seguinte exemplo: se o programador utiliza a hierarquia de dispositivos a partir da classe Dispositivo, aplicada ao projeto de um determinado sistema operacional, poderia desenvolver um gerenciador de drivers de forma a manipular apenas esta classe, o que tem por consequência direta a não necessidade de conhecer as subclasses derivadas de Dispositivos, como pode ser visto no exemplo do Programa 5.1.
01. // Classe gerenciadora dos drivers de um sistema operacional
02. public class GerenciadorDrivers { 03. // Apenas referências a dispositivos - facilidade de acrescentar novos tipos 04. private Dispositivo[] dispositivos; 05. public GerenciadorDrivers() { 06. // Código para ler a configuração atual do S.O. (dispositivos atuais, etc.) 07. } 08. public void inicializarDispositivos() { 09. for (int i = 0; i < dispositivos.length; i++) { 10. // Código para inicializar cada dispositivo 11. } 12. } 13. }
|
O leitor deve imaginar quais os benefícios imediatos da abordagem citada no exemplo do gerenciador de drivers. Se não fosse possível referenciar cada dispositivo através de sua superclasse Dispositivo, o gerenciador desenvolvido teria de ser capaz de lidar com várias classes de dispositivos, o que seria inviável do ponto de vista da manutenção do software. Da forma como foi apresentado acima, ele é capaz de lidar com cada tipo de dispositivo independentemente da sua implementação concreta, visto que ele enxerga apenas um array de objetos da classe Dispositivo. Neste caso, um método inicializar() em cada classe concreta de dispositivo poderia conter as particularidades envolvidas na inicialização de um mouse, teclado, câmera ou disco externo, por exemplo. Os detalhes sobre a resolução dinâmica de métodos serão vistos na Seção 5.2.1.
Vale ressaltar que os conceitos até aqui explorados são igualmente válidos para as interfaces e suas implementações. O exemplo a seguir demonstra a utilização de implementações referenciadas pelos tipos de suas interfaces. Observe que nas linhas 15 e 16 dois objetos Imposto são criados, através de implementações concretas (CPMF e IRPF).
Após a observação do exemplo anterior, é possível imaginar um sistema de compras onde haja a necessidade de se calcular o valor total da compra após a incidência das alíquotas de impostos associados a cada produto. Ao invés de ter que “saber” lidar com vários tipos de impostos, a utilização da hierarquia apresentada permite simplificar a manutenção e facilitar a adição de novos impostos ou remoção de impostos existentes. O código do Programa 5.2 demonstra tal contexto.
Analisando o referido exemplo, observe que foi utilizado na linha 03 um arraylist de produtos que correspondem a uma hierarquia semelhante à da Figura 5.1, tendo como base a classe Produto. Existem métodos para adicionar e remover produtos (linhas 07 e 10, respectivamente). Em seguida, o método getValorTotal() retorna o valor da compra já calculada a incidência dos impostos existentes (considere que para cada época do ano, ou também para o tipo de transação - regional/nacional/internacional, podem existir diferentes conjuntos de impostos a serem incididos sobre os produtos). Para cada produto da cesta, o valor sem impostos é obtido, e acrescido das alíquotas incidentes nos impostos existentes.
01. // Cesta de compras em um sistema online
02. public class CestaDeCompras { 03. private ArrayList produtos; 04. private Imposto[] impostos; 05. 06. // Adicionar e remover produtos do ArrayList 07. public void adicionarProduto(Produto p) { 08. produtos.add(p); 09. } 10. public void removerProduto(Produto p) { 11. produtos.remove(p); 12. } 13. 14. // Obtém o valor total da compra realizada 15. public float getValorTotal() { 16. float valorTotal = 0; 17. for (int i = 0; i < produtos.size(); i++) { // Varre a lista de produtos 18. Produto p = (Produto) produtos.get(i); // Obtém o produto atual 19. float valorTemp = p.getValor(); // Valor sem os impostos 20. for (int j = 0; j < impostos.length; j++) { // Varre o array de impostos 21. // Acrescenta os valores de incidência dos vários impostos 22. valorTemp += valorTemp * impostos[j].getAliquota(); 23. } 24. valorTotal += valorTemp; // Soma o valor atual ao valor total 25. } 26. return valorTotal; 27. } 28. }
|
Até agora, foi explicado ao leitor como o polimorfismo pode auxiliar na manutenção de software pela diminuição de referências a classes concretas, de forma que as classes interajam entre si através de interfaces ou de superclasses do mais alto nível possível. A seguir, será detalhada a característica que é responsável por permitir esta independência entre interface e implementação: a resolução dinâmica de métodos.
Em primeiro lugar, é necessário definir o que é resolução de métodos, que significa a conexão de uma chamada de método a um corpo de método. Existem dois tipos de resolução:
Conforme afirmação anterior, não é uma boa solução aquela que exige, quando há a criação de novos tipos a serem envolvidos no contexto, que vários métodos correspondentes sejam criados especificamente para estes novos tipos. Observe o exemplo do Programa 5.3, que representa um jogo de xadrez. As várias peças do xadrez são definidas tendo como base a classe Peca. Entretanto, a classe JogoDeXadrez não foi projetada com a característica de resolução dinâmica de métodos em mente, o que acarretou a construção de vários métodos para lidar com as diferentes peças.
01. // Classes para um Jogo de Xadrez
02. class Peca { // Classe raiz 03. public int x,y; // Valores atuais de posição 04. 05. public void mover(int x, int y) { 06. System.out.println("Peça desconhecida"); 07. } 08. } 09. class Peao extends Peca { 10. public void mover(int x, int y) { 11. System.out.println("Movendo peão para (" + x + "," + y + ")"); 12. } 13. } 14. class Bispo extends Peca { 15. public void mover(int x, int y) { 16. System.out.println("Movendo bispo para (" + x + "," + y + ")"); 17. } 18. } 19. class Rainha extends Peca { 20. public void mover(int x, int y) { 21. System.out.println("Movendo rainha para (" + x + "," + y + ")"); 22. } 23. } 24. class JogoDeXadrez { 25. // Definição de métodos mover para as diferentes peças 26. public void moverPeao(Peao p, int x, int y) { p.mover(x, y); } 27. public void moverBispo(Bispo b, int x, int y) { b.mover(x, y); } 28. public void moverRainha(Rainha r, int x, int y) { r.mover(x, y); } 29. }
|
De forma a permitir que o jogo de xadrez seja executável, um método main para a classe JogoDeXadrez pode possuir a seguinte forma:
Se agora o programador incluísse a peça Torre, a classe JogoDeXadrez teria que ser modificada, acrescentando o seguinte método:
Esta seria a solução adotada para qualquer nova inclusão, que implicaria em mudança nos métodos de JogoDeXadrez. Pensando nisso, o programador pode melhorar a classe simplesmente eliminando as referências às classes concretas - trabalhando desta forma, com referências à classe Peca. A máquina virtual Java garante que em tempo de execução o método do objeto correto será chamado, e apenas um método precisaria ser criado para mover peças, como pode ser visto a seguir.
Qualquer nova peça adicionada não implicaria na criação de um novo método, já que o método genérico comporta qualquer subclasse de Peca. O método main() anteriormente definido poderia tomar a seguinte forma:
O resultado da execução do método main() anterior seria:
No capítulo anterior, que tratava dos conceito relacionados à herança de classes, foram vistos vários exemplos de como uma classe pode herdar de outra, produzindo-se assim uma nova classe que pode agregar comportamentos distintos além dos já existentes. Foi observado também o fato de que uma classe, ao herdar de outra, “assume” aquele tipo onde quer que seja necessário. Neste capítulo, esta característica será bastante explorada, visto que é uma das bases do polimorfismo. Como pode ser visto na Figura 5.2, existem dois tipos de transição, quando uma classe assume o tipo de outra, o chamado casting ou coerção, em sentidos inversos de uma hierarquia, que são o upcasting e o downcasting, explicados a seguir.
Este tipo de coerção, que a linguagem realiza automaticamente, acontece de baixo para cima na hierarquia, ou seja, no sentido das subclasses para as superclasses. Não é necessário nenhum tipo de indicação explícita para que o compilador possa realizá-lo, pelos motivos apresentados a seguir.
Considere o diagrama UML na Figura 5.3, que engloba uma hierarquia relacionada à representação de funcionários em um determinado sistema. Em seguida, observe o Programa 5.4, que define as classes relacionadas à tal hierarquia de funcionários, assim como demonstra a utilização de uma subclasse no sentido de assumir o papel da superclasse quando assim for desejado. Veja que na linha 20 a referência a um objeto do tipo Funcionario é associada na verdade a um objeto do tipo Gerente. Esta associação, embora possa parecer estranha ao leitor, é completamente válida, visto que Gerente Funcionario.Quandosecriaumasubclasse,porconsequênciaocompiladordefinequeestano mínimotemasmesmascaracterísticaspúblicaseprotegidasdesuasuperclasse(alémdelogicamentepoderacrescentarnovasfuncionalidades).Emresumo,ocódigodalinha20nãoimplicaemnenhumerroouadvertência- otipoGerenteguardacaracterísticasprópriasdotipoFuncionario.
01. // Um exemplo de Upcasting
02. 03. class Funcionario { 04. protected String CPF, RG, telefone, nome; 05. } 06. class Gerente extends Funcionario { 07. private String departamento; 08. } 09. class Supervisor extends Funcionario { 10. private String setor; 11. } 12. class Auxiliar extends Funcionario { 13. } 14. 15. public class TesteUpcasting { 16. public static void main(String[] args) { 17. Gerente ger = new Gerente(); // Criação de instância de Gerente 18. Supervisor sup = new Supervisor(); // Criação de instância de Supervisor 19. // func é Funcionario, mas recebe uma instância de Gerente 20. Funcionario func = ger; 21. }
|
Existem situações onde o programador precisa realizar um outro tipo de casting, denominado downcasting. Este tipo de coerção acontece de cima para baixo na hierarquia, ou seja, no sentido das superclasses para as subclasses, e a linguagem não o faz de forma automática, sendo necessário explicitar a necessidade de fazê-lo através de parênteses antes do nome da variável que indiquem o tipo desejado a ser convertido. Esta conversão explícita (através dos parênteses) é necessária porquê nem sempre uma superclasse será capaz de assumir o tipo da subclasse - no exemplo anterior, todo auxiliar é um funcionário, enquanto nem todo funcionario é um auxiliar (pode ser um gerente ou supervisor). Caso a conversão com downcasting não seja possível, uma exceção do tipo java.lang.ClassCastException será lançada no programa em questão. Informações sobre exceções em Java podem ser consultadas em [DD05] e [Eck02].
Considerando as classes definidas no Programa 5.4, observe o Programa 5.5, em que há o processo de associação de uma superclasse para uma subclasse.
01. // Um exemplo de Downcasting
02. public class TesteDowncasting { 03. public static void main(String[] args) { 04. Gerente ger = new Gerente(); // Criação de instância de Gerente 05. Supervisor sup = new Supervisor(); // Criação de instância de Supervisor 06. Funcionario func = ger; 07. 08. // Um novo objeto Gerente recebe o valor de Funcionario 09. // O downcasting necessita de conversão explícita 10. Gerente ger_2 = (Gerente) func; 11. } 12. }
|
Na linha 10, uma referência a objeto do tipo Gerente recebe o valor de um objeto do tipo Funcionario, mas neste caso foi necessário um casting explícito.
Com uma pequena alteração no Programa 5.5, pode-se demonstrar uma situação de erro onde seria lançada uma exceção java.lang.ClassCastException, devido a uma conversão indevida. O objeto func (linha 06) recebe o valor de um objeto do tipo Gerente. No downcasting demontrado a seguir, o programador adiciona uma linha de código onde tenta converter func para o tipo Supervisor, o que é impossível, já que ele está associado, no momento da conversão, a um objeto do tipo Gerente.
A seguir, é possível ver a respectiva mensagem de erro em tempo de execução, que significa uma tentativa inválida de casting.
Até agora, foi explicado através de diferentes mecanismos que o principal benefício do polimorfismo é permitir ao programador que não se preocupe com as classes concretas, focando a interação entre interfaces e superclasses. Entretanto, há situações onde é necessário que o programador possa ter acesso ao tipo concreto que está sendo tratado por trás da referência genérica, através de RTTI (Runtime Type Identification - Identificação de Tipos em Tempo de Execução). A linguagem Java permite ter acesso a essa informação através de dois mecanismos:
De forma a demonstrar a necessidade eventual da identificação de tipos em tempo de execução, considere as peças de um jogo de xadrez definidas no programa 5.3. Com o intuito de esclarecer os movimentos básicos do jogo, a Tabela 5.1 resume movimentos previstos para algumas das peças do xadrez.
|
Uma implementação simplificada para o jogo de xadrez pode ser observada no Programa 5.6. A fim de não permitir que hajam jogadas com movimentos ilegais (por exemplo, a torre se movimentando em diagonal), o programa precisa de uma classe capaz de validar cada movimento. Tal classe será denominada Tabuleiro. Para cada movimento efetuado, a classe JogoDeXadrez solicita a validação do movimento da peça à classe Tabuleiro. O leitor pode perceber que para validar o movimento, uma das soluções possíveis é que a classe Tabuleiro tenha conhecimento de qual seria a classe concreta da peça em questão no ato de validação do movimento. De uma maneira simplificada, observe a classe Tabuleiro. Para que a classe possa realizar a tarefa desejada, o método analisarJogada() na linha 14 deve dispor de uma solução que envolva “decodificar” o tipo concreto a partir do parâmetro genérico do tipo Peca (primeiro argumento do método), retornando falso ou verdadeiro.
01. // O jogo de xadrez agora inclui um tabuleiro capaz de validar as jogadas
02. class JogoDeXadrez { 03. // Método mover genérico (para qualquer peça) 04. public void mover(Peca p, int x, int y) { 05. if (Tabuleiro.analisarJogada(p, x, y) { // Chama a validação do Tabuleiro 06. // Jogada válida - executar código para mover a peça no ambiente gráfico 07. } 08. else 09. System.out.println("Jogada inválida! Tente novamente."); 10. } 11. } 12. 13. class Tabuleiro { 14. public static boolean analisarJogada(Peca b, int x, int y) { 15. // Para analisar se a jogada foi válida, é necessário saber 16. // ** qual é ** o tipo da peça (torre, bispo, rainha, peão, rei, etc.) 17. } 18. }
|
A fim de resolver o problema do Programa 5.6, o programador dispõe das duas abordagens citadas anteriormente: uso do instanceof ou classes de reflexão. Ambas as abordagens serão detalhadas a seguir, com suas respectivas implementações para o problema do tabuleiro de xadrez em questão.
Java possui uma palavra-chave reservada denominada instanceof que permite a uma classe identificar qual o tipo concreto de uma referência genérica (interface ou superclasse.) A sintaxe desta palavra-chave é:
O retorno de uma expressão com instanceof é um booleano que retorna true caso a variável seja do tipo em questão, e false do contrário. Frequentemente, esta palavra-chave será incluída em expressões condicionais. Considerando a sintaxe apresentada, será proposta uma solução para o problema do Programa 5.6 utilizando instanceof, como pode ser visto no Programa 5.7. Neste exemplo, todas as jogadas estão sendo validadas. Em uma implementação completa, para cada if apresentado, o código para validação da jogada poderia ser inserido, retornando true ou false em cada caso concreto. Evidentemente que uma implementação de tal porte envolveria uma série de fatores, tais como a disposição atual das peças no tabuleiro, incluindo também o fato de se o movimento coloca em risco o rei.
01. // Implementação utilizando instanceof
02. class Tabuleiro { 03. public static boolean analisarJogada(Peca b, int x, int y) { 04. if (b instanceof Bispo) { 05. System.out.println(‘‘Bispo detectado’’); 06. return true; 07. } 08. else if (b instanceof Peao) { 09. System.out.println(‘‘Peão detectado’’); 10. return true; 11. } 12. else if (b instanceof Rainha) { 13. System.out.println(‘‘Rainha detectada’’); 14. return true; 15. } 16. else { 17. System.out.println("Erro! Peça desconhecida!"); 18. return false; 19. } 20. } 21. }
|
Um outro exemplo de utilização do operador instanceof é demonstrado no Programa 5.8, onde se faz uso das classes definidas no Programa 5.4. Perceba que nas linhas 03 e 04 são definidos dois objetos referenciados pelo tipo genérico Funcionario, mas que recebem referências de Auxiliar e Supervisor, respectivamente. Em seguida, são feitos testes através do operador instanceof (linhas 06-11), e o objeto func_1 passa a referenciar um objeto do tipo Supervisor (linha 14), o que pode ser verificado pelo teste que se segue (linhas 15-16).
01. public class TesteInstanceOf {
02. public static void main(String[] args) { 03. Funcionario func_1 = new Auxiliar(); 04. Funcionario func_2 = new Supervisor(); 05. Funcionario func_3 = new Gerente(); 06. 07. if (func_1 instanceof Auxiliar) 08. System.out.println(‘‘Func_1 -> Auxiliar’’); 09. if (func_2 instanceof Gerente) 10. System.out.println(‘‘Func_2 -> Gerente’’); 11. if (func_3 instanceof Gerente) 12. System.out.println(‘‘Func_3 -> Gerente’’); 13. 14. // func_1 agora é associado a um objeto do tipo Supervisor 15. func_1 = new Supervisor(); 16. if (func_1 instanceof Supervisor) 17. System.out.println(‘‘Func_1 -> Supervisor’’); 18. } 19. }
|
A saída do Programa 5.8 pode ser vista a seguir:
O termo reflexão é utilizado para descrever um conjunto de facilidades que as linguagens de programação orientadas a objeto oferecem aos programadores para que as classes possam obter informações também sobre classes, como por exemplo atributos, métodos e superclasses ou interfaces caracterizadores destas.
O arcabouço de classes que engloba as possibilidades de utilização da reflexão em Java é bastante abrangente, e foge do escopo deste livro - maiores detalhes podem ser encontrados em [Hor05]. Desta forma, a ênfase desta Subseção será o uso de reflexão apenas no que tange a identificação de tipos em tempo de execução.
A linguagem Java possui uma classe denominada Class, que como o próprio nome diz representa de fato uma classe Java, com informações tais como nome, atributos e métodos. Em adição, qualquer objeto em Java possui um método denominado getClass(), que retorna um objeto desta classe. Isto significa que a partir deste objeto da classe Class, é possível descobrir informações sobre um objeto desejado, dentre estas a identificação do tipo concreto do mesmo quando necessário.
Utilizando as classes definidas no Programa 5.4, o Programa 5.9 demonstra a utilização do método getClass().
public class TesteGetClass {
public static void main(String[] args) { Funcionario func_1 = new Auxiliar(); Funcionario func_2 = new Supervisor(); // Obtém o objeto Class Class classeFunc_1 = func_1.getClass(); Class classeFunc_2 = func_2.getClass(); // [Objeto Class].getName() obtém o nome da classe System.out.println("Classe de Func_1: " + classeFunc_1.getName()); System.out.println("Classe de Func_2: " + classeFunc_2.getName()); } }
|
Partindo da definição de que as classes do Programa 5.9 pertencem ao pacote br.teste, a saída deste trecho de código seria a seguinte:
IMPORTANTE: Observe dois conceitos de grafia bastante semelhante, mas com semânticas completamente distintas: a definição de uma classe, quando da utilização da palavra-chave class (com c minúsculo), e a criação de um objeto da classe Class (com C maiúsculo). |
Até este ponto, o leitor sabe que quando há um objeto do qual se deseja extrair a estrutura de sua classe, pode-se utilizar o método getClass(). Entretanto, para que possa haver uma comparação entre o tipo do objeto em questão e uma outra classe qualquer, é necessário saber que qualquer interface ou classe em Java possui um atributo estático que retorna uma referência ao objeto Class correspondente. Tal atributo é acessado da seguinte forma:
Observe que não foi necessário criar um objeto da classe Revista para conseguir obter uma representação desta classe através do objeto Class correspondente. Isto se dá porquê o atributo class é estático, e pertence à classe, e não a um objeto em particular.
Considerando as explicações anteriores, será apresentada uma versão da classe Tabuleiro que utiliza a reflexão ao invés da palavra-chave instanceof. A diferença é meramente sintática, não havendo pior ou melhor forma. É importante que o leitor perceba as diferenças apresentadas (linhas 04-06), onde foram utilizados tanto o método getClass(), quanto o atributo estático class para compor a expressão condicional. O código resultante é apresentado no Programa 5.10.
01. // Implementação utilizando reflexão
02. class Tabuleiro { 03. public static boolean analisarJogada(Peca b, int x, int y) { 04. if (b.getClass().equals(Bispo.class) { /* É válido para bispo ? */ } 05. else if (b.getClass().equals(Peao.class) { /* É válido para peão ? */ } 06. else if (b.getClass().equals(Rainha.class) { /* É válido para rainha ? */ } 07. else { 08. System.out.println("Erro! Peça desconhecida!"); 09. return false; 10. } 11. } 12. }
|
01. class Empresa {
02. ArrayList<Funcionario> funcionarios; 03. 04. public Empresa(ArrayList<Funcionario> listaFuncionarios) { 05. funcionarios = listaFuncionarios; 06. } 07. 08. public void exibirQuadroFuncional() { 09. } 10. 11. public static void main(String[] args) { 12. ArrayList<Funcionario> lista = new ArrayList<Funcionario>(); 13. for (int i = 0; i < 10; i++) { 14. double random = 9 * Math.random(); 15. if (random <= 3) 16. lista.add(new Auxiliar()); 17. else if (random > 3 && random <= 6) 18. lista.add(new Supervisor()); 19. else 20. lista.add(new Gerente()); 21. } 22. 23. Empresa es = new Empresa(lista); 24. es.exibirQuadroFuncional(); 25. } 26. }
|
Neste apêndice serão apresentados os conceitos básicos do IDE (Intregrated Development Enviroment - Ambiente Integrado de Desenvolvimento) Eclipse para o desenvolvimento de aplicações Java. Será utilizado o Eclipse versão 3.2 para o sistema operacional GNU/Linux.
O Eclipse é um software de código aberto mantido pela Eclipse Fundation e licenciado sobre os termos da Licença Pública Eclipse versão 1.0 (EPL - Eclipse Public License). Uma cópia da licença EPL pode ser obtida em [ecl08].
O Eclipse é uma ferramenta muito poderosa e flexível, podendo servir como um ambiente profissional de desenvolvimento para muitas linguagens. Os autores sugerem que o leitor domine inicialmente os conceitos básicos deste IDE, e à medida que se tornar mais proficiente no ambiente, procure conhecer recursos mais avançados.
A página Welcome é a primeira que é visualizada quando o Eclipse é inicializado. Sua finalidade é introduzir o Eclipse ao usuário iniciante. O conteúdo da página Welcome (Figura A.1) inclui tipicamente um panorama do produto e suas características principais, tutoriais para guiar o usuário através de suas tarefas básicas, exemplos para introduzir a ferramenta ao usuário, entre outras informações.
O termo Workbench refere-se ao ambiente de desenvolvimento fornecido pelo Eclipse. O Workbench tem o objetivo de ser uma ferramenta de integração e controle, fornecendo um paradigma comum para criação, gerenciamento e navegação de recursos do ambiente de trabalho.
Cada janela Workbench contém uma ou mais persceptivas. As perspectivas contêm views e editores que aparecem em certos menus e barras de ferramentas. Mais de uma janela de Workbench pode existir em um desktop do sistema operacional do usuário ao mesmo tempo.
A perspectiva define o conjunto inicial e o layout de views na janela Workbench. Dentro da janela, cada perspectiva compartilha o mesmo conjunto de editores. Cada perspectiva fornece um conjunto de funcionalidades com o objetivo de executar um tipo específico de tarefa ou trabalho com tipos específicos de recursos. Por exemplo, a perspectiva Java combina views em que o programador poderia usar enquanto edita um arquivo fonte Java, assim como a perspectiva Debug contém as views que poderiam ser usadas enquanto um programador depura um programa Java. À medida que o programdor trabalha com o Workbench, ele irá trocar freqüentemente de perspectivas, de acordo com a tarefa a ser executada.
As perspectivas controlam o que aparece em certos menus e barras de ferramentas. Elas definem os conjuntos de ações visíveis, que se podem ser modificados para adaptar a perspectiva. É possível salvar a perspectiva construída e desta maneira, fazer uma perspectiva adaptada que poderá ser aberta novamente mais tarde.
As views suportam editores e fornecem apresentações alternativas como também modos para navegar a informações dentro do Workbench. Por exemplo, a view Navegador (Navigator, em inglês) e outras views de navegação mostram projetos e outros recursos que estão sendo utilizados.
As views também têm seus próprios menus. Para abrir o menu para uma view, o programador deve clicar no ícone no final esquerdo da barra de título da view. Algumas views têm suas próprias barras de ferramentas. As ações representadas pelos botões na barra de ferramentas da view somente afetam os itens dentro da view.
Uma view pode aparecer por si mesma ou estar fixa com outras views em abas. É possível mudar o layout de uma perspectiva pela abertura e fechamento de views e pela colocação delas em diferentes posições dentro da janela Workbench.
A maioria das perspectivas em um Workbench está configurada com uma área de editor e uma ou mais views.
É possível associar editores diferentes com tipos diferentes de arquivos. Por exemplo, quando se abre uma arquivo para edição, pelo clique duplo no arquivo, dentro de uma das views de navegação, o editor associado é aberto dentro da Workbench. Se não existir um editor associado para um recurso, o Workbench irá tentar lançar um editor externo fora do Workbench.
Qualquer número de editores pode ser aberto de uma vez, mas somente um pode estar ativo por vez. A barra do menu principal e a barra de ferramentas para a janela do Workbench contêm operações que são aplicáveis para o editor ativo.
As abas dentro da área do editor indicam os nomes dos recursos que estão correntemente abertos para edição. Um asterisco, ”*”, indica que um editor tem mudanças não salvas.
Por padrão, editores são fixos dentro de uma área do editor, mas é possível escolher colocá-los lado a lado para facilitar a vista simultânea de arquivos. O exemplo da Figura A.2 apresenta um editor de texto com arquivos lado a lado no Workbench:
A borda cinzenta na margem esquerda do editor pode conter ícones que sinalizam erros, avisos, ou problemas detectados pelo sistema. Ícones também aparecem se o programador tiver criado bookmarks, adicionado breakpoints para depurar, ou gravando notas na view Task. É possível ver os detalhes de cada ícone na margem esquerda do editor movendo-se o cursor do mouse sobre os ícones.
Para as atividades a seguir, assume-se que:
Caso o leitor não esteja familiarizado com os conceitos iniciais sobre o Eclipse, por favor leia atentamente a Seção A.2. Para fixar os conceitos, os autores recomendam fortemente que seja resolvida a lista de exercícios disponível na seção A.5 no final deste apêndice.
Inicialmente é necessário inicializar o Eclipse. Caso o Eclipse esteja instalado na máquina, é possível encontrar uma entrada para inicializar o Eclipse no menu Aplicações - Programação do sistema operacional GNU/Linux Ubuntu, conforme demonstra a Figura A.3.
Com o software Eclipse inicializado deve-se seguir os seguintes passos:
Esta seção apresenta o conceito de projeto Java e como configurá-lo no IDE Eclipse. O conceito de projeto Java é muito útil quando se precisa gerenciar vários arquivos de código fonte Java, que compõem um único programa ou projeto.
Para criar e compilar programas Java dentro do IDE Eclipse é necessário primeiramente definir um projeto Java. Um projeto Java contém código fonte e arquivos relacionados para a construção de uma programa Java. Cada projeto tem um construtor Java associado que pode incrementalmente compilar arquivos fonte Java à medida que eles são modificados.
Um projeto Java também mantém um modelo de seus conteúdos. Este modelo inclui informação sobre o tipo de hierarquia, referências e declarações de elementos Java. Esta informação é constantemente atualizada à medida que o usuário modifica o código fonte Java. A atualização do modelo de projeto Java interno é independente do contrutor Java; em particular, quando modificações são realizadas no código, se a opção de auto-construir estiver desabilitada, o modelo irá refletir ainda o conteúdo do presente projeto.
É possível organizar projetos Java em dois modos diferentes:
É importante lembrar que o primeiro passo antes de codificar programas Java usando o Eclipse é definir um projeto Java.
Dentro do Eclipse o programdor deve selecionar o item do menu File - New - Project para abrir a janela New Project. Selecione Java Project e depois clique Next para iniciar a abertura da janela New Java Project. Veja o exemplo da Figura A.8.
Na opção Project Name:, o programador deve escrever o nome do projeto. Para este primeiro exemplo crie com o nome do exemplo da Figura A.8, ou seja, ProgramasIniciais.
Observe que na view Navigator aparecem dois arquivos: o .classpath e o .project. (caso a view navigator não esteja aparecendo, clique no item de menu Windows - Show View - Navigator). A Figura A.9 mostra o exemplo de como deve ficar o conteúdo da view Navigator.
Em seguida clique na opção do item do menu File - New - Class para abrir a janela New Java Class. Na opção Name: digite o seguinte: PrimeiroProgramaJava. Veja o exemplo da Figura A.10. Em seguida, clique no botão Finish.
Logo em seguida deverá ser apresentada a tela da Figura A.11. O editor abre automaticamente o programa PrimeiroProgramaJava.java e preenche a definição de um classe pública com o mesmo nome do arquivo. Na view Navigator aparecem dois novos arquivos, o arquivo PrimeiroProgramaJava.java e o arquivo PrimeiroProgramaJava.class. Observe ainda a view Outline, que mostra inicialmente um ícone verde com um ”c”ao lado de PrimeiroProgramaJava, indicando se tratar de uma classe pública Java.
Agora digite o programa Java conforme mostrado na Figura A.12. Observe que na Figura A.12 é utilizada a view Package Explorer ao invés da view Navigator. Os autores aconselham fortemente o uso da view Package Explorer por trazer mais informações utéis e necessárias ao desenvolvedor.
Para executar o programa, dentro da view do editor clique com o botão direito do mouse e selecione a opção Run as - Java Application, conforme mostra a Figura A.13.
Observe que a view console deverá mostrar a seguinte mensagem: ”Bem vindo ao Curso de POO !!!”, conforme a Figura A.14. Perceba também, na view Console, a indicação de que o programa finalizou sua execução através da palavra Terminated no canto superior esquerdo.
O pacote java.util implementa coleções. Uma coleção é um grupo de objetos. O framework Java para coleções padroniza o modo pelo qual grupos de objetos são manipulados pelos programas.
Antes da versão 1.5 de Java, uma classe só podia ser escrita com seus parâmetros e atributos identificados. Não era possível codificar o conceito de tipo independente dentro de uma classe. Com a introdução de Java Generics na versão 1.5, é possível definir uma classe sem especificar o tipo para certos atributos ou parâmetros. O tipo é especificado quando a classe é instanciada. O framework Java de coleções tem um conjunto pré-definido de classes generics e algoritmos que podem ser usados para armazenar e manipular uma coleção de objetos. Ao se instanciar uma classe do framework de coleções, caso não se especifique o tipo da coleção, assume-se, por padrão, o tipo Object.
O framework de coleções foi projetado para atingir algumas metas [Poo08]. Primeiro, o framework tinha que ter um bom desempenho. As implementações para as coleções fundamentais (arrays dinâmicos, listas ligadas, árvores e tabelas hash) são projetadas de forma a terem alta eficiência. Segundo, o framework tinha que permitir diferentes tipos de coleções para trabalhar de uma maneira similar e com alto grau de interoperabilidade. Terceiro, estender ou adaptar um coleção tinha que ser fácil. Para atingir estas metas, o framework inteiro de coleções é projetado através de um conjunto de interfaces padronizadas.
Nas próximas subseções serão detalhadas as principais implementações do framework de coleções em Java.
A classe ArrayList suporta arrays dinâmicos que podem crescer conforme a necessidade. Em Java, arrays são de tamanho fixo. Depois que um array é criado, ele não pode crescer ou diminuir, o que significa que o programador deve saber a priori quantos elementos um array irá conter. Porém, algumas vezes, o programador pode não saber o tamanho do array até que o programa seja executado. Para atender esta situação, existe a classe ArrayList. De maneira simples, um ArrayList é um array de tamanho variável de referências de objetos. Isto é, um ArrayList pode dinamicamente aumentar ou diminuir em tamanho. ArrayLists são criadas com um tamanho inicial. Quando este tamanho é ultrapassado, a coleção é automaticamente aumentada. Quando os objetos são removidos, o array pode ser encolhido.
A classe ArrayList tem os seguintes construtores:
O Programa B.1 apresenta um exemplo de uso da classe ArrayList. A linha 06
instancia um objeto al do tipo ArrayList. Como neste exemplo não foi especificado o tipo dos elementos do ArraList, por padrão os elementos do ArrayList serão do tipo Object (conforme mencionado no início deste capítulo).
01. // Demonstração de uso de um ArrayList
02. import java.util.*; 03. 04. public class SimplesArrayList { 05. public static void main(String args[]) { 06. ArrayList al = new ArrayList(); // cria um ArrayList 07. System.out.println("Tamanho inicial do al: " + al.size()); 08. al.add("Casa"); // Adiciona elementos a lista de array 09. al.add("Abelha"); 10. al.add("Elefante"); 11. al.add("Baleia"); 12. al.add("Dedo"); 13. al.add("Formiga"); 14. al.add(1, "Azeite"); 15. System.out.println("Tamanho do al após adições: " + al.size()); 16. // Mostra a lista do array 17. System.out.println("Conteúdo do al: " + al); 18. al.remove("Formiga"); // Remove elementos da lista de array 19. al.remove(2); 20. System.out.println("Tamanho do al após remoções: " + al.size()); 21. System.out.println("Conteúdo do al: " + al); 22. } 23. }
|
O resultado da execução do Programa B.1 é a seguinte: |
Para instanciar um ArrayList com elementos do tipo Integer deve se usar a seguinte sintaxe:
Observa-se no Programa B.1 que al inicia vazia e cresce conforme os elementos são adicionados. Quando os elementos são removidos, seu tamanho é reduzido. Os elementos são adicionados ao ArrayList através do método add() (linhas de 08 a 14). Na linha 14, o método add() é chamado com um índice da posição onde o elemento deve ser inserido. Os elementos são removidos através do método remove(), (linhas 18 e 19). Nas linhas 15 e 20 do Programa B.1, faz-se uso do método size() que retorna o tamanho do ArrayList.
Apesar da capacidade de um objeto ArrayList aumentar automaticamente conforme os objetos são armazenados, é possível aumentar a capacidade de um objeto ArrayList manualmente pela chamada do método ensureCapacity( ). Um programador pode aumentar a capacidade de um objeto ArrayList se souber antecipadamente que serão armazenados muito mais itens à coleção. Pelo aumento da capacidade no início, por exemplo, é possível previnir muitas realocações futuras. Uma vez que realocações são custosas em termos de tempo, a prevenção de realocações desnecessárias melhora o desempenho. De maneira semelhante, o programador pode reduzir o tamanho de um ArrayList, tal que seu tamanho seja precisamente o tamanho do número de itens que estão contidos no ArrayList. Para isto, basta usar o método trimToSize( ) .
Navegar através dos elementos de uma ArrayList, ou de muitas classes do framework de coleções, pode ser feito através do uso do for estendido1 ou usando um Iterator2.
O trecho de código abaixo mostra a iteração de um ArrayList com elementos do tipo String usando o for estendido:
Construtor for estendido:
O trecho de código abaixo mostra a iteração de uma ArrayList usando um Iterator:
Construtor Iterator:
É possível introduzir as seguinte linhas de código ao final do Programa B.1 para que seja impressa a lista dos elementos do ArrayList al:
Neste caso, o resultado impresso seria a lista de elementos do ArrayList, ou seja:
Veja que é necessário usar no for estendido o termo Object e não String, uma vez que na declaração do ArraList no Programa B.1 não foi especificado o tipo do ArrayList, assumindo-se portanto, por padrão, o tipo Object.
A classe HashSet implementa o conceito de conjunto. Assim os objetos podem ser armazenados, recuperados e manipulados sem haver objetos duplicados. O Programa B.2 mostra uma declaração típica, instanciando e usando um HashSet que armazena objetos do tipo String.
01. // Demonstra HashSet
02. import java.util.*; 03. 04. public class DemonstraHashSet { 05. public static void main(String[] args) { 06. HashSet<String> hashList = new HashSet<String>(); 07. String s1 = "John"; 08. String s2 = "Elliot"; 09. System.out.println("Adicionado = " + hashList.add(s1)); 10. System.out.println("Adicionado = " + hashList.add(s2)); 11. System.out.println("Adicionado = " + hashList.add(s2)); 12. } 13. }
|
O resultado da execução do Programa B.2 é a seguinte:
Adicionado = true
Adicionado = true Adicionado = false
|
A linha 06,
instancia um HashSet com elementos do tipo String.
O método add() é utilizado nas linhas de 09 a 11 para adicionar elementos ao HashSet hashList. Observe que o retorno da chamada a este método é um tipo boolean, indicando se a adição do elemento foi bem-sucedida ou não.
É possível detectar objetos duplicados pela verificação do valor de retorno do método add( ) como ilustrado no Programa B.3.
01. // Demonstra HashSet Duplicado
02. import java.util.*; 03. 04. public class DemonstraHashSetDuplicado { 05. public static void main(String[] args) { 06. HashSet<String> hashList = new HashSet<String>(); 07. String sArray[] = new String[6]; 08. sArray[0] = "Marcelo"; 09. sArray[1] = "Fred"; 10. sArray[2] = "Tiago"; 11. sArray[3] = "Fred"; 12. sArray[4] = "Maria"; 13. sArray[5] = "Marcelo"; 14. for (int i = 0; i < 6; i++) { 15. if (hashList.add(sArray[i])) 16. System.out.println("Nome Duplicado: " + sArray[i]); 17. else 18. System.out.println("Adicionado: " + sArray[i]); 19. } 20. } 21. }
|
O resultado da execução do Programa B.3 é a seguinte. Como se observar não é possível objetos
duplicados na HashSet
Adicionado: Marcelo
Adicionado: Fred Adicionado: Tiago Nome Duplicado: Fred Adicionado: Maria Nome Duplicado: Marcelo
|
Observe que na linha 15 do Programa B.3:
é testado o retorno boleano do método add, verificando desta forma se o item foi ou não adicionado ao HashSet. Observe no resultado da execução do Programa B.3 que os nomes duplicados do conjunto, Fred e Marcelo, são indicados e não adicionados.
Se todo objeto membro do conjunto B também é membro do conjunto A, então B é um subconjunto de A. A Figura B.1 ilustra um conjunto B contido em um conjunto A.
É possível determinar se um HashSet é um subconjunto de outro HashSet pelo uso do método containsAll() da seguinte maneira:
A união do conjunto A e do conjunto B é definida como o conjunto de todos os elementos que podem ser encontrados no conjunto A ou no conjunto B. A Figura B.2 ilustra a união do conjunto A com o conjunto B.
A união de duas HashSets pode ser derivada pelo uso do método addAll() da seguinte maneira:
A intersecção do conjunto A e do conjunto B é definida como o conjunto de todos elementos encontrados em ambos os conjuntos. A Figura B.3 ilustra a intersecção entre um conjunto A e um conjunto B.
A intersecção entre dois HashSets pode ser derivada pelo uso do método retainAll() da seguinte maneira:
O complemento do conjunto B em A é definido como o conjunto de todos os elementos no conjunto A que não são encontrados no conjunto B. A Figura B.4 ilustra o complemento do conjunto B em A.
O complemento de dois HashSets pode ser derivado pelo uso do método removeAll() da seguinte maneira:
A classe HashMap é usada para conter elementos com uma associação < chave,valor >. Uma chave é associada com um valor e armazenada em um HashMap. Valores podem então ser recuperados de uma dada chave. Por definição, HashMaps não podem conter chaves duplicadas. Esta classe não garante a ordem na qual os pares < chave,valor > são armazenados.
O Programa B.4 ilustra a declaração, instanciação e uso de um HashMap que contém objetos de um tipo Estudante. A chave de cada estudante é seu número de matrícula que é do tipo Integer.
01.import java.util.*; 02.import java.lang.Integer; 03. 04.class Estudante { 05. int numeroMatricula; 06. String nome; 07. 08. public Estudante(int numeroMatricula, String nome) { 09. this.numeroMatricula = numeroMatricula; 10. this.nome = nome; 11. } 12. int getnumeroMatricula() { 13. return numeroMatricula; 14. } 15. String getnome() { 16. return nome; 17. } 18.} 19. 20.public class DemoHashMap { 21. public static void main(String[] args) { 22. HashMap<Integer, Estudante> estudanteMap = 23. new HashMap<Integer, Estudante>(); 24. Estudante s1 = new Estudante(1, "Marcelo Ferreira"); 25. Estudante s2 = new Estudante(2, "Fred Borelli"); 26. Estudante s3 = new Estudante(3, "Tiago Melo"); 27. estudanteMap.put(new Integer(s1.getnumeroMatricula()), s1); 28. estudanteMap.put(new Integer(s2.getnumeroMatricula()), s2); 29. estudanteMap.put(new Integer(s3.getnumeroMatricula()), s3); 30. Estudante retestudante = null; 31. for (int i = 1; i < 4; i++) { 32. Integer chave = new Integer(i); 33. retestudante = estudanteMap.get(chave); 34. System.out.println("Estudante com chave = " 35. + chave + " se chama " + retestudante.getnome()); 36. } 37. } 38.}
|
O resultado da execução do Programa B.4 é a seguinte:
Estudante com chave = 1 se chama Marcelo Ferreira
Estudante com chave = 2 se chama Fred Borelli Estudante com chave = 3 se chama Tiago Melo
|
Um HashMap é declarado pela especificação de dois tipos de parâmetros, o primeiro é o tipo chave e o segundo sendo do tipo valor. No Programa B.4, o HashMap chamado de estudanteMap é declarado e instanciado como mostrado abaixo. O tipo chave é Integer (número de matrícula) e o valor do tipo é Estudante (um objeto do tipo Estudante).
Entradas podem ser adicionadas ao HashMap usando o método put() como mostrado abaixo:
Um objeto Integer é criado usando o número de matrícula (1) do estudante (s1). Objetos Estudantes podem ser recuperados pelo uso do método get() com o número de matrícula do estudante como mostrado abaixo:
[Blo01] Joshua Bloch. Effective Java Programming Language Guide. Addison-Wesley, 2001.
[DD05] H.M. Deitel and P.J. Deitel. Java - Como Programar. Pearson Education, 2005.
[Eck02] Bruce Eckel. Thinking in Java. Prentice Hall, 2002.
[ecl08] Site Oficial do Eclipse, http://www.eclipse.org/legal/epl-v10.html. Visitado em Junho, 2008.
[HC00] Cay S. Horstmann and Gary Cornnel. Core Java 2: Volume I - Fundamentals. Sun Microsystems, 2000.
[Hor05] Ivor Horton. Beginning Java 2, JDK 5 Edition. Wiley Publishing, 2005.
[HR04] Philip Heller and Simon Robert. Guia completo de estudos para certificação em Java 2. Ciência Moderna Ltda, 2004.
[jav08] Site Oficial da Sun, http://java.sun.com/j2se/javadoc. Visitado em Junho, 2008.
[Jun07] Jandl Peter Junior. Java: Guia do Programador. Novatec, 2007.
[NS01] Patrick Naughton and Herbert Schildt. The Complete Reference Java 2. Osborne, 4 edição edition, 2001.
[Poo08] Object-Oriented Programming Java. Springer, 2008.
[RBJ06] James Rumbaugh, Grady Booch, and Ivar Jacobson. UML - Guia do Usuário. Campus, 2006.
[San01] Rafael Santos. Introdução à Programação Orientada a Objetos usando Java. Campus, 2001.
[sun08] Site Oficial da Sun, http://java.sun.com. Visitado em Junho, 2008.