Programação Orientada a

Objetos em Java

PIC

Frederico Borelli de Souza

Marcelo de Jesus Ferreira

Tiago Eugênio de Melo

2008

Conteúdo

1 Fundamentos da Linguagem Java
 1.1 Introdução
 1.2 Meu primeiro programa Java
  1.2.1 Características básicas da linguagem Java
  1.2.2 Executando um programa Java
 1.3 Palavras reservadas
 1.4 Tipos primitivos
 1.5 Declaração de variáveis e constantes
 1.6 Lendo valores do teclado
 1.7 Operadores
  1.7.1 Operadores Aritméticos
  1.7.2 Operadores Relacionais
  1.7.3 Operadores de Bits
  1.7.4 Operadores Lógicos
 1.8 Estruturas de controle
  1.8.1 Estruturas de seleção
  1.8.2 Estruturas de iteração
 1.9 Arrays
 1.10 Strings
 1.11 Atividades
2 Classes e objetos em Java
 2.1 Classes e objetos
  2.1.1 Acessibilidade
  2.1.2 Classes abstratas
  2.1.3 Classes internas
  2.1.4 Classes finais
  2.1.5 Classes anônimas
 2.2 Atributos
  2.2.1 Constantes
 2.3 Métodos
  2.3.1 Construtores
  2.3.2 Sobrecarga de métodos
  2.3.3 Métodos sobrescritos
  2.3.4 Métodos estáticos
  2.3.5 Método main
 2.4 Pacotes
 2.5 Interfaces
 2.6 Atividades
3 Encapsulamento
 3.1 Encapsulamento
 3.2 Abstração de dados e encapsulamento
 3.3 Encapsulamento em Java
  3.3.1 Classes que encapsulam valores nativos
  3.3.2 Modificadores de acesso em Java
  3.3.3 Sintaxe dos moderadores de acesso em Java
  3.3.4 Efeitos do uso dos modificadores em Java
  3.3.5 Uso dos métodos set() e get()
  3.3.6 Resumo dos modos de acesso em Java
 3.4 Atividades
4 Herança
 4.1 Introdução
 4.2 Composição
 4.3 Herança de classes
  4.3.1 Encapsulamento com herança de classes: a palavra-chave protected
  4.3.2 Lidando com construtores em subclasses
  4.3.3 Sobrescrita de Métodos
 4.4 Interfaces
  4.4.1 Herança múltipla
  4.4.2 Herança entre interfaces
 4.5 Classes Anônimas
 4.6 Evitando a Herança: a palavra-chave final
 4.7 Composição x Herança
 4.8 Atividades
5 Polimorfismo
 5.1 Introdução
 5.2 Trabalhando com superclasses e interfaces
  5.2.1 Resolução dinâmica de métodos
 5.3 O Conceito de Casting
  5.3.1 Upcasting
  5.3.2 Downcasting
 5.4 Identificação de tipos através de RTTI (Runtime Type Identification)
  5.4.1 A palavra-chave instanceof
  5.4.2 Utilizando reflexão
 5.5 Atividades
A Eclipse e Java
 A.1 Introdução
 A.2 Conceitos Básicos do Eclipse
  A.2.1 Welcome Page
  A.2.2 Workbench
  A.2.3 Perspectivas
  A.2.4 Views
  A.2.5 Editores
 A.3 Preparando o Eclipse para o ambiente Java
 A.4 Programação Java no IDE Eclipse
  A.4.1 Eclipse - Projeto Java
  A.4.2 Meu primeiro programa Java no IDE Eclipse
 A.5 Atividades
B Estrutura de dados básicas da API Java.util
 B.1 ArrayList
 B.2 HashSet
  B.2.1 Subconjunto, União, Intersecção e Complemento
 B.3 HashMap

Lista de Figuras

1.1 Diagrama do processo de geração de bytecodes e execução de programas Java
1.2 Compilação e execução do PrimeiroProgramaJava.java
1.3 Exemplo de execução do programa LendoAdicao.java
1.4 Execuções do programa Comparacao.java
2.1 Exemplo de hierarquia da classe Pessoa.
3.1 Encapsulamento: os métodos públicos podem ser empregados para proteger os dados privados.
3.2 Método correto de sobrescrever acesso. [HR04]
3.3 Mensagem do compilador ao se tentar acessar o atributo c diretamente.
4.1 Composição entre diferentes níveis
4.2 Qualquer classe sem superclasse explícita é derivada de java.lang.Object
4.3 Relação entre a superclasse Publicacao e suas subclasses.
4.4 Atributos privado e protegido.
4.5 Sequência de chamada de construtores em uma hierarquia
4.6 Implementação de diferentes interfaces
5.1 Cesta de compras utilizando polimorfismo
5.2 Upcasting e Downcasting
5.3 Hierarquia de Funcionários
A.1 Página de Boas-Vindas do Eclipse - Welcome Page
A.2 Exemplo de arquivos abertos lado a lado no Eclipse
A.3 Inicialização do Eclipse em um máquina com GNU/Linux UBUNTU
A.4 Preferências da JRE instalada
A.5 Preferências Gerais do Workspace
A.6 Preferências Java Build Path
A.7 Preferências Editor - Report Problem as you type
A.8 Exemplo de definição de um Projeto Java
A.9 Exemplo do conteúdo da view Navigator
A.10 Exemplo de nova classe Java
A.11 Tela inicial do PrimeiroProgramaJava.java
A.12 Código do PrimeiroProgramaJava.java
A.13 Execução do programa PrimeiroProgramaJava.java
A.14 Resultado da execução do PrimeiroProgramaJava.java
B.1 B é um subconjunto de A
B.2 B é um subconjunto de A
B.3 B é um subconjunto de A
B.4 B é um subconjunto de A

Lista de Tabelas

1.1 Conjunto de palavras-reservadas em Java.
1.2 Tipos primitivos da linguagem Java.
1.3 Operadores Aritméticos da linguagem Java.
1.4 Operadores Relacionais da linguagem Java.
1.5 Operadores de Bits da linguagem Java.
1.6 Operadores Lógicos da linguagem Java.
2.1 Comparação entre classes abstratas e interfaces.
3.1 Especificadores de acesso [Jun07].
5.1 Alguns dos movimentos permitidos no Xadrez.

Lista de Programas

1.1 Código do PrimeiroProgramaJava.java
1.2 Código de demonstração de inicialização dinâmica
1.3 Lê dois números do teclado e calcula a soma
1.4 Código de demonstração de operadores aritméticos
1.5 Código de demonstração de incrementos e decrementos
1.6 Compara dois inteiros
1.7 Demonstra operadores lógicos
1.8 Demonstra a estrutura de controle if-else-if
1.9 Demonstra a estrutura de controle switch
1.10 Demonstra a estrutura de iteração while
1.11 Demonstra a estrutura de iteração do-while
1.12 Demonstra a estrutura de iteração for
1.13 Demonstra o comando for com declaração de variável
1.14 Demonstra loops for aninhados
1.15 Exemplo de Array de uma dimensão
1.16 Exemplo de inicialização com declaração
1.17 Exemplo de programa com array de duas dimensões
1.18 Exemplo de Arrays de duas dimensões com tamanhos distintos
1.19 Instrução for estendida
1.20 Exemplo de métodos da classe String
2.1 Exemplo de uso de classes abstratas.
2.2 Exemplo de uso de classes aninhadas ou internas.
2.3 Exemplo de criação e uso de métodos.
2.4 Exemplo de uso de métodos construtores.
2.5 Exemplo de uso do operador this.
2.6 Exemplo de uso de métodos sobrecarregados.
2.7 Exemplo de uso de métodos estáticos.
2.8 Exercício.
2.9 Exercício.
2.10 Exercício.
2.11 Exercício.
3.1 Única maneira de usar classes privadas.
3.2 Exemplo de encapsulamento na classe Boolean.
3.3 Exemplo de programa Java que faz uso dos moderadores de acesso public e private.
3.4 Classe Tempo.
4.1 Composição entre classes
4.2 Diferentes classes herdam de Publicacao.
4.3 Código do ManipuladorArquivo.java
4.4 Código do ManipuladorOpenOffice.java
4.5 Código do ManipuladorRTF.java
4.6 Código do Aviao.java
4.7 Código do AviaoRadar.java
4.8 Teste de AviaoRadar
4.9 Classes de conta bancária - chamada desnecessária ao construtor da superclasse
4.10 Classes de conta bancária - chamada ao novo construtor
4.11 Código de Conexao.java
4.12 Código de ConexaoSegura.java
4.13 Código da interface e da implementação de uma pilha
4.14 Código da implementação de pilha e fila ao mesmo tempo
4.15 Interpretação de Herança Múltipla em Java
4.16 Herança de interfaces e implementação correspondente
4.17 Código de Janela.java
4.18 Classe com método bloqueado para herança
4.19 Classe bloqueada para herança
4.20 Exercício.
4.21 Exercício.
4.22 Exercício.
4.23 Exercício.
5.1 Referenciação através da superclasse
5.2 Implementação simplificada da cesta de compras
5.3 Jogo de xadrez sem utilização do polimorfismo
5.4 O processo de Upcasting
5.5 O processo de Downcasting
5.6 Implementação de Tabuleiro precisa conhecer as classes concretas de Peca
5.7 Implementação de Tabuleiro com instanceof
5.8 Exemplos de uso do instanceof
5.9 Exemplos de uso do instanceof
5.10 Implementação de Tabuleiro com reflexão
5.11 Exercício.
B.1 Um uso simples de ArrayList
B.2 Uma demonstração simples do uso HashSet
B.3 Uma demonstração de HashSet com detecção de itens duplicados
B.4 Uma demonstração de HashMap

Capítulo 1
Fundamentos da Linguagem Java

1.1 Introdução

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.

1.2 Meu primeiro programa Java

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>.


PIC

Figura 1.1: Diagrama do processo de geração de bytecodes e execução de programas Java


1.2.1 Características básicas da linguagem Java

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


Programa 1.1: Código do PrimeiroProgramaJava.java

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

 public class PrimeiroProgramaJava {

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:

         // O método main inicia a execução da aplicação Java

A linha 6,

     public static void main( String args[] )

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,

          System.out.println( "Bem vindo ao Curso de POO !!!" );

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 (;) .

1.2.2 Executando um programa Java

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:

 # javac PrimeiroProgramaJava.java

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:

 # java PrimeiroProgramaJava

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 !!!


PIC

Figura 1.2: Compilação e execução do PrimeiroProgramaJava.java


1.3 Palavras reservadas

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.







abstract assert boolean break byte





case catch char class continue





default do double else enum





extends final finally float for





if implements import instanceof int





interface long native new package





private protected public return short





static strictfp super switch synchronized





this throw throws transient try





void volatile while






Tabela 1.1: Conjunto de palavras-reservadas em Java.

1.4 Tipos primitivos

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.





Tipo Tamanho em bits Valores






boolean true ou false



char 16 Conj. de caracteres Unicode da ISO



Tipos Inteiros



byte 8 -128 a 127



short 16 –32.768 a 32.767



int 32 –2.147.483.648 a 2.147.483.647



long 64 –9.223.372.036.854.775.808 a 9.223.372.036.854.775.807



Tipos de ponto flutuante



float 32 -3.4e+038 to 3.4e+038



double 64 -1.7e+308 to 1.7e+308




Tabela 1.2: Tipos primitivos da linguagem Java.

1.5 Declaração de variáveis e constantes

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:

 <tipo> <identificador> [= valor][,<identificador> [=valor] ...];

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:

   int a, b, c;             // declara três inteiros a, b e c
   int d = 3, e, f = 5;     // declara mais três inteiros, inicializando d e f.
   byte z = 22;             // declara e inicializa uma variável byte
   double pi = 3.14159;     // declara uma variável pi com o valor aproximado de pi
   char x = ’x’;            // declara a variável caractere x com o valor ’x’

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


Programa 1.2: Código de demonstração de inicialização dinâmica

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:

  final     int  FILE_NEW = 1;
  final     int  FILE_OPEN = 2;
  final     int  FILE_SAVE = 3;
  final     int  FILE_SAVEAS = 4;
  final     int  FILE_QUIT = 5;

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.

1.6 Lendo valores do teclado

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


Programa 1.3: Lê dois números do teclado e calcula a soma

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

 Scanner entrada = new Scanner( System.in );

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

 numero1 = entrada.nextInt(); // lê o primeiro numero fornecido

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.


PIC

Figura 1.3: Exemplo de execução do programa LendoAdicao.java


1.7 Operadores

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.

1.7.1 Operadores Aritméticos

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:




Operação Operador Aritmético




Adição +


Subtração -


Multiplicação *


Divisão


Resto %


Incremento ++


Adicionar e atribuir +=


Subtrair e atribuir -=


Multiplicar e atribuir *=


Dividir e atribuir =


Tirar o resto e atribuir %=


Decremento --



Tabela 1.3: Operadores Aritméticos da linguagem Java.

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


Programa 1.4: Código de demonstração de operadores aritméticos


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


Programa 1.5: Código de demonstração de incrementos e decrementos

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


1.7.2 Operadores Relacionais

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.




Descrição Operador




Igual a ==


Não igual a !=


Maior que >


Menor que <


Maior ou igual a >=


Menor ou igual a <=



Tabela 1.4: Operadores Relacionais da linguagem Java.

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


Programa 1.6: Compara dois inteiros

A Figura 1.4 mostra um exemplo de execução do Programa 1.6.


PIC

Figura 1.4: Execuções do programa Comparacao.java


1.7.3 Operadores de Bits

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].




Descrição Operador




Operador de bit unário NOT ~


Operador de bit AND &


Operador de bit OR


Operador de bit Exclusive OR


Deslocamento a direita >>


Deslocamento a direita com preenchimento de zero >>>


Deslocamento a esquerda <<


Atribuição e operação de bit AND &=


Atribuição e operação de OR |=


Atribuição e operação de bit Exclusive OR =


Atribuição e Deslocamento a direita >>=


Atribuição e Deslocamento a direita com preenchimento de zero >>>=


Atribuição e Deslocamento a esquerda <<=



Tabela 1.5: Operadores de Bits da linguagem Java.

1.7.4 Operadores Lógicos

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.




Descrição Operador




Lógico AND &


Lógico OR


Lógico XOR


Lógico Resumido &&


Lógico unário NOT !


Atribui e Lógico AND &=


Atribui e Lógico OR =


Atribui e Lógico XOR OR =


Igual a ==


Não igual a !=


Ternário if-then-else ?:



Tabela 1.6: Operadores Lógicos da linguagem Java.

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


Programa 1.7: Demonstra operadores lógicos


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:

 expressao1 ? expressao2 : expressao3

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 ”?”:

 razao = denom == 0 ? 0 : num / denom;

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.

1.8 Estruturas de controle

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.

1.8.1 Estruturas de seleção

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.

Estruturas de seleção - if

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.

Estruturas de seleção - if s aninhados

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.

Estruturas de seleção - if-else-if aninhado

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


Programa 1.8: Demonstra a estrutura de controle if-else-if


O resultado produzido pelo Programa 1.8 seria:
 Abril está no Outono.


Estruturas de seleção - switch

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 + ".");  
    }  
}


Programa 1.9: Demonstra a estrutura de controle switch

Estruturas de seleção - switch aninhados

É 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:

   01.switch(contador) {
   02. case 1:
   03.    switch(destino) { // switch aninhado
   04.       case 0:
   05.         System.out.println("O destino é zero");
   06.         break;
   07.      case 1: // Nenhum conflito com o switch externo
   08.         System.out.println("O destino é um");
   09.         break;
   10.     }
   11.     break;
   12.  case 2: // ...

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.

1.8.2 Estruturas de iteração

Os comandos Java para iteração são: do-while, while e for. Estes comandos criam o que normalmente se denomina de loops.

Estruturas de iteração - while

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


Programa 1.10: Demonstra a estrutura de iteração while


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


Estruturas de iteração - do-while

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


Programa 1.11: Demonstra a estrutura de iteração do-while

Estruturas de iteração - for

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


Programa 1.12: Demonstra a estrutura de iteração for

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


Programa 1.13: Demonstra o comando for com declaração de variável

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).

Estruturas de iteração - Loops Aninhados

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


Programa 1.14: Demonstra loops for aninhados

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:
**********  
*********  
********  
*******  
******  
*****  
****  
***  
**  
*


1.9 Arrays

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.

Arrays de uma dimensão

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 é:

 type var-nome[];

Por questões de conveniência, a linguagem Java permite também a seguinte declaração:

 type[] var-nome;

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:

 int diasdoMes[];  
   // ou  
 int[] diasdoMes;

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:

 nome-array = new tipo[tamanho];

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 ).

 diasdoMes = new int[12];

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:

 int diasdoMes[] = new int[12];

ao invés dos dois passos:

 int diasdoMes[];  // Declaração  
 diasdoMes = new int[12];  // Alocação

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.

 diasdoMes[8] = 30;

E a próxima linha imprime o valor armazenado no índice 8.

 System.out.println(diasdoMes[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.");  
  }  
}


Programa 1.15: Exemplo de Array de uma dimensão


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


Programa 1.16: Exemplo de inicialização com declaração

Arrays de múltiplas dimensões

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.

 int DuasDimensoes[][] = new int[4][5];

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


Programa 1.17: Exemplo de programa com array de duas dimensões


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


Programa 1.18: Exemplo de Arrays de duas dimensões com tamanhos distintos

A instrução for estendida

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


Programa 1.19: Instrução for estendida

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:

                         for (j = 0; j < i + 1; j++)

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.

1.10 Strings

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:

 String str = "Isto é um teste";  
 System.out.println(str);

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:

 String teste = "Bem-vindo" + " ao " + "Curso de POO.";

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: ”.

                System.out.println("Tamanho da strOb1: " + strOb1.length());


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


Programa 1.20: Exemplo de métodos da classe String


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


1.11 Atividades

  1. Quais os tipos de comentários suportados pela linguagem Java ?

    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 *.

  2. Qual a versão atual da API Java ?
  3. Explique o que significa uma variável ser declarada como final.
  4. Explique o que faz a palavra reservada import em um programa Java.
  5. Explique para que serve os comandos javac e java.
  6. Determine se as frase abaixo são verdadeiras ou falsas
    1. (  ) A linguagem Java é uma linguagem fundamentalmente procedimental, não implementando os conceitos de programação orientada a objetos.
    2. (  ) Um array pode armazenar muitos tipos de valores diferentes.
    3. (  ) A linguagem Java é interpretada através da Java Virtual Machine (JVM)
    4. (  ) O nome do arquivo de um programa Java não precisa ser o mesmo classe pública definida seguido da extensão .java
    5. (  ) O método main não é um método obrigatório em todo programa Java.
    6. (  ) Ao declarar um array de inteiros sem iniciá-lo por padrão na linguagem Java seus valores inicias serão zero.
    7. (  ) Em Java, um array multi-dimensional são de fato arrays de arrays.
    8. (  ) Em Java, o tipo String nada mais é do que um array de caracteres.
    9. (  ) A instrução for estendida basicamente itera pelo array sem necessidade de utilizar um contador.
    10. (  ) A opção default do comando switch é obrigatória.
  7. Exercícios de Programação em Java:
    1. Modifique o Programa 1.8, atribuindo outros valores para variável mes.
    2. Tendo por base o Programa 1.13, escreva um programa para imprimir os números primos contidos no intervalo de 0 a 10.000.
    3. Escreva um programa que leia 10 números do teclado e então calcule e imprima:
      1. A média aritmética destes números
      2. O maior e o menor número
    4. O fatorial de um número inteiro não negativo n é escrito como n! e é definido como segue:

      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.

      1. Escreva um programa em Java que leia um inteiro não negativo e calcule e imprime seu fatorial
      2. Escreva um programa que calcule o valor da constante matemática e. (Defina n com o valor de 50.)
               1   1   1        1
e = 1+ --+ --+ --+ ...+ --
       1!  2!  3!      n!
      3. Escreva um programa que calcule o valor de ex. (Defina n com o valor de 50.)
         x      x    x   x       x
e  = 1+ 1! + 2! + 3! + ...+ n!
    5. A série de Fibonaci
      0,1,1,2,3,5,8,13,21,...
      inicia com os termo 0 e 1 e tem a propriedade de que cada termo sucessivo é calculado pela soma dos dois termos anteriores. Escreva um programa em Java que leia do teclado uma variável n e calcule o n-ésimo termo da série de Fibonaci.
    6. Tendo por base o Programa 1.14, escreva outros três programas que produzam o padrão de saída abaixo:

      1. *  
        **  
        ***  
        ****  
        *****  
        ******  
        *******  
        ********  
        *********  
        **********



      2. **********  
         *********  
          ********  
           *******  
            ******  
             *****  
              ****  
               ***  
                **  
                 *



      3.          *  
                **  
               ***  
              ****  
             *****  
            ******  
           *******  
          ********  
         *********  
        **********


Capítulo 2
Classes e objetos em Java

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.

2.1 Classes e objetos

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:

class <nome-da-classe> {  
    // corpo da classe  
}

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 é:

<nome-da-classe> <nome-do-objeto> = new <nome-do-construtor> ( );

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.

2.1.1 Acessibilidade

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.

2.1.2 Classes abstratas

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 é:

abstract class <nome-da-classe> {  
// declarações da classe  
}

De forma semelhante, a sintaxe da criação de um método abstrato em Java é:

abstract <tipo-retorno> <nome-do-método> {  
// corpo do método  
}

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.


PIC

Figura 2.1: Exemplo de hierarquia da classe Pessoa.


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:

Exception in thread "main" java.lang.Error: Unresolved compilation problem:  
        Cannot instantiate the type Pessoa  
        at TesteClasseAbstrata.main(TesteClasseAbstrata.java:4)


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


Programa 2.1: Exemplo de uso de classes abstratas.

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.

2.1.3 Classes internas

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


Programa 2.2: Exemplo de uso de classes aninhadas ou internas.

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:

Principal.Aninhada objAninhada = objAninhada.new Aninhada();

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.

2.1.4 Classes finais

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 é:

final class <nome-da-classe> {  
// corpo da classe  
}

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.

2.1.5 Classes anônimas

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.

2.2 Atributos

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 é:

<moderador-de-acesso> <tipo> <nome-do-atributo> [= expressãoInicialização];

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.

2.2.1 Constantes

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 é:

<moderador-de-acesso> final <tipo> <nome-do-atributo> = <valor-literal>;

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.

2.3 Métodos

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 é:

<moderador-de-acesso> <tipoRetorno> <nome-do-método> ([lista de parâmetros]) {  
// corpo do método  
}


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:

boolean aprovado (int matricula) { ... }  
 
double equacao (int a, int b, int c) { ... }  
 
void imprimir (int matricula, String nome) { ... }  

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 é:

<nome-objeto>.<método>


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


Programa 2.3: Exemplo de criação e uso de métodos.

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:

O resultado é 10.

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.

2.3.1 Construtores

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


Programa 2.4: Exemplo de uso de métodos construtores.

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:

Ponto obj = new Ponto(2,3);

Neste caso, o valor impresso pelo método imprimeCoordenadas(.), da linha 23, seria igual a (2.0,3.0).

Operador this

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


Programa 2.5: Exemplo de uso do operador this.

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.

2.3.2 Sobrecarga de métodos

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


Programa 2.6: Exemplo de uso de métodos sobrecarregados.

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:

O valor do parâmetro é do tipo int.  
O valor do parâmetro é do tipo String.

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:

Exception in thread "main" java.lang.Error: Unresolved compilation problem:  
        The method imprimir(String) in the type ExemploSobrecargaMetodos is  
        not applicable for the arguments (double)  
 
        at ExemploSobrecargaMetodos.main(ExemploSobrecargaMetodos.java:6)

2.3.3 Métodos sobrescritos

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.

2.3.4 Métodos estáticos

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 é:

<moderador> static <tipo> <nome-método> {  
\\ corpo do método  
}

[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. }


Programa 2.7: Exemplo de uso de métodos estáticos.

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.

2.3.5 Método main

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 é:

public static void main(String[] args)

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.

2.4 Pacotes

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.

package exemplo;  
 
public class Exercicio {  
 
\\ corpo do programa  
 
}

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:

package br.edu.uea.tads;


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:

import br.edu.uea.tads.*;

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.

2.5 Interfaces

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 é:

interface <nome-da-interface> {  
    // declarações da interface  
}


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:




Classe Abstrata Interface


Pode ter alguns métodos abstratos. Somente pode ter métodos abstratos.


Pode ter propriedades protegidas Somente pode ter métodos públicos
e métodos estáticos. abstratos.


Pode ter atributos finais e não finais.É limitado somente a constantes.



Tabela 2.1: Comparação entre classes abstratas e interfaces.

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.

2.6 Atividades

  1. Como uma classe em Java é estruturada?
  2. Qual é a diferença, quanto às restrições, dos modificadores final e static?
  3. Por que uma classe abstrata não pode ser instanciada?
  4. Por que uma classe concreta não pode ter métodos abstratos?
  5. Qual é a diferença entre uma interface e uma classe abstrata?
  6. Qual é a diferença entre um construtor e um método?
  7. Como é possível reconhecer um construtor em Java?
  8. Um método estático pode acessar membros de classe não estáticos? Justifique a sua resposta.
  9. Implemente uma classe em Java contendo apenas métodos estáticos capazes de realizar todas as conversões de temperatura possíveis entre as unidades Celsius, Farenheit e Kelvin. As relações de conversão são: C
5- = F-32
--9- e K = C + 273.
  10. Para implementar jogos com cartas, são necessárias classes para representar uma carta individual e também um baralho. Sugira implementações para essas classes, considerando:
  11. Utilizando as classes construídas no exercício anterior, implemente um jogo de 21. As regras resumidamente são: (a) existem dois jogadores, apostador e banca; (b) o objetivo é fazer o maior número de pontos; (c) pontuação das cartas: ás equivale a 1, 2, a 10 equivalem seus próprios valores, e as demais cartas (valete, dama e rei) valem 10 pontos. (d) o apostador sempre inicia e pede cartas, deixadas abertas, até que esteja satisfeito com o valor ou estoure (pontuação > 21), situação em que perde; (e) banca joga até igualar ou superar o valor obtido pelo apostador, situação em que ganha (a banca só perde se estourar).
  12. Qual(is) das seguintes alternativa pode(m) sobreescrever um método cuja assinatura seja void xyz(float f)?

    Resposta: a e b.

  13. Qual(is) das seguintes alternativas pode(m) aparecer dentro do Programa 2.8, com o objetivo de sobrecarregar o método soma( )?

    Resposta: c e d.


    public class Teste {  
        public int soma (int a, int b) {  
            return a + b;  
        }  
    }


    Programa 2.8: Exercício.

  14. De acordo com o Programa 2.10, qual será o resultado?

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


    Programa 2.9: Exercício.

  15. De acordo com o Programa 2.10, dois dos trechos de código abaixo inseridos nas linhas 08 e 09 não serão compilados. Quais? Justifique a sua resposta.


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


    Programa 2.10: Exercício.

    Resposta: b e f.

  16. Identifique e explique os erros do Programa 2.11.


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


    Programa 2.11: Exercício.

Capítulo 3
Encapsulamento

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.

3.1 Encapsulamento

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:

3.2 Abstração de dados e 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.

3.3 Encapsulamento em Java

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


Programa 3.1: Única maneira de usar classes privadas.


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.


PIC

Figura 3.1: Encapsulamento: os métodos públicos podem ser empregados para proteger os dados privados.


3.3.1 Classes que encapsulam valores nativos

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


Programa 3.2: Exemplo de encapsulamento na classe Boolean.

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.

3.3.2 Modificadores de acesso em Java

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.


PIC

Figura 3.2: Método correto de sobrescrever acesso. [HR04]


3.3.3 Sintaxe dos moderadores de acesso em Java

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.





Especificador Nível Indica que o campo ou método:



public público Pode ser usado livremente pelas instâncias da classe.



protected protegido Só pode ser usado na implementação de subclasses.



pacote Só pode ser usado por instâncias dentro do mesmo pacote



private privado Não pode ser usado fora da implementação da própria classe.




Tabela 3.1: Especificadores de acesso [Jun07].

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:

class Motor { ... }  
public class static void main(String[] args) { ... }  
private int i;  
protected double calcular_saldo () { ... }

3.3.4 Efeitos do uso dos modificadores em Java

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


Programa 3.3: Exemplo de programa Java que faz uso dos moderadores de acesso public e private.

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.


Exception in thread "main" java.lang.Error: Unresolved compilation problem:  
The field Acesso.c is not visible  
 
at TesteAcesso.main(TesteAcesso.java:27)


Figura 3.3: Mensagem do compilador ao se tentar acessar o atributo c diretamente.


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.

3.3.5 Uso dos métodos set() e get()

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.

3.3.6 Resumo dos modos de acesso em Java

De forma resumida, os modos de acesso de Java são:

3.4 Atividades

Capítulo 4
Herança

4.1 Introdução

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 ´e um tipo de
-- ------→ 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 ´e formado por
--- ------→ 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.

4.2 Composição

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 -´e- c-om-p-o-s-to--p-or→{Motor, Som, Pneu, Chassi}.


PIC

Figura 4.1: Composição entre diferentes níveis


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. }
Programa 4.1: Composição entre classes

4.3 Herança de classes

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:

   [Subclasse a ser criada] extends [Superclasse existente]

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).


PIC

Figura 4.2: Qualquer classe sem superclasse explícita é derivada de java.lang.Object


Desta forma, os exemplos abaixo são equivalentes:

   public class NovaClasse
      e
   public class NovaClasse extends java.lang.Object

Considere um sistema onde exista a classe Publicacao e algumas subclasses dela, que são Livro e Revista, conforme apresentado na Figura 4.3.


PIC

Figura 4.3: Relação entre a superclasse Publicacao e suas subclasses.


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. }
Programa 4.2: Diferentes classes herdam de Publicacao.

4.3.1 Encapsulamento com herança de classes: a palavra-chave protected

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].



- privado


#protegido


+ público




PIC

Figura 4.4: Atributos privado e protegido.


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
Programa 4.3: Código do ManipuladorArquivo.java

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
Programa 4.4: Código do ManipuladorOpenOffice.java


  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
Programa 4.5: Código do ManipuladorRTF.java

4.3.2 Lidando com construtores em subclasses

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
Programa 4.6: Código do Aviao.java

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
Programa 4.7: Código do AviaoRadar.java

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.


PIC

Figura 4.5: Sequência de chamada de construtores em uma hierarquia


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. }
Programa 4.8: Teste de AviaoRadar

  # java TesteAviaoRadar
  Inicializado com Cor = 58 e (x,y) = (10350, 400154)
  Inicializado com Raio do Radar = 4452
Construtores parametrizados

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:

   // Referencia o construtor padrão da superclasse
   super()
   // Referencia um construtor com dois parâmetros (se a superclasse possuir!)
   super(parametro1, parametro2)
   // Referencia um atributo da superclasse (que seja público ou protegido)
   super.algumAtributo;
   // Referencia um método da superclasse (que seja público ou protegido)
   super.algumMetodo();

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. }
Programa 4.9: Classes de conta bancária - chamada desnecessária ao construtor da superclasse

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. }
Programa 4.10: Classes de conta bancária - chamada ao novo construtor

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.

  private double SALDO_INICIAL = 50000.0;
  public ContaVIP() {
      super(SALDO_INICIAl);
  }

4.3.3 Sobrescrita de Métodos

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. }
Programa 4.11: Código de Conexao.java


  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. }
Programa 4.12: Código de ConexaoSegura.java

4.4 Interfaces

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).

   [Classe implementadora] implements [Interface 1], [Interface 2], ... , [Interface N]

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. }
Programa 4.13: Código da interface e da implementação de uma pilha

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.


PIC

Figura 4.6: Implementação de diferentes interfaces


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. }
Programa 4.14: Código da implementação de pilha e fila ao mesmo tempo

4.4.1 Herança múltipla

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. }
Programa 4.15: Interpretação de Herança Múltipla em Java

4.4.2 Herança entre interfaces

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. }
Programa 4.16: Herança de interfaces e implementação correspondente

4.5 Classes Anônimas

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. }
Programa 4.17: Código de Janela.java

4.6 Evitando a Herança: a palavra-chave final

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:

  1. Criação de atributos constantes em classes, de forma que ao ser declarado como final, o atributo tem de receber um valor no ato da declaração, que não vai poder ser modificado ao longo do programa (esta é a forma que a linguagem permite definir constantes).
  2. Marcação de métodos de tal forma que eles que não possam ser sobrescritos em subclasses, ou seja, o comportamento do método permanece o mesmo em qualquer subclasse derivada da classe que o definiu.
  3. Marcação de classes para que as mesmas não possam servir como base de uma herança. Isso significa que uma classe final não pode ser superclasse de nenhuma outra.

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. }
Programa 4.18: Classe com método bloqueado para herança

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. }
Programa 4.19: Classe bloqueada para herança


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.

4.7 Composição x Herança

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:

  1. Uma revista TAMBÉM É uma publicação
  2. Um bombardeiro TAMBÉM É um avião.
  3. Um caminhão TAMBÉM É um veículo.
  4. Uma conexão segura TAMBÉM É uma conexã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.

4.8 Atividades

  1. Explique de que maneira o uso da herança promove a reutilização de código.
  2. Qual a utilidade de se definir métodos e atributos com o modificador de acesso protected ?
  3. Qual as principais diferenças entre herdar interfaces e herdar classes?
  4. Explique, com suas palavras, porque construtores parametrizados em superclasses têm de ser explicitamente invocados pelas subclasses.
  5. Qual é a diferença entre herança simples e herança múltipla? Como Java trata a herança múltipla?
  6. Explique, com suas palavras, as principais diferenças entre composição e herança.
  7. Qual a principal diferença entre uma classe final e uma classe convencional?
  8. Responda V (verdadeiro) ou F (falso) às afirmações abaixo:
  9. Considere o Programa 4.20. Que mudanças seriam aplicadas à classe Usuario para que a classe Administrador pudesse implementar um método mostrarDados() de forma a imprimir a senha e o password ? Realize também a implementação de um método main() para a classe Administrador que a faça a chamada ao método mostrarDados().


    01. class Usuario {  
    02.     private String password;  
    03.     private String login;  
    04. }  
    05. class Administrador extends Usuario {  
    06. }


    Programa 4.20: Exercício.

  10. Levando em consideração a necessidade de reaproveitamento de código em várias situações cotidianas do programador Java, a partir do Programa 4.21, implemente uma classe PilhaMelhorada que, utilizando a implementação existente de Pilha sobrescreva os métodos empilhar() e desempilhar() de forma que eles imprimam, a cada chamada, o número de elementos existentes na pilha. (Dica: lembre-se da palavra-chave super).


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


    Programa 4.21: Exercício.

  11. Qual é o resultado do Programa 4.22?


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


    Programa 4.22: Exercício.

  12. Não é possível compilar o código o Programa 4.23. Qual é a moficação que fará este código compilar corretamente?

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


    Programa 4.23: Exercício.

Capítulo 5
Polimorfismo

5.1 Introdução

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.


PIC

Figura 5.1: Cesta de compras utilizando polimorfismo


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.

5.2 Trabalhando com superclasses e interfaces

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.

  public class Dispositivo { ... }
  public class Mouse extends Dispositivo { ... }
  public class Teclado extends Dispositivo { ... }
  
  Mouse mouse = new Mouse(); // Criação de instância de Mouse
  Teclado teclado = new Teclado();  // Criação de instância de Teclado

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:

  // Mouse e Teclado, referenciadas através do tipo Dispositivo
  Dispositivo mouse = new Mouse();
  Dispositivo teclado = new Teclado();

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. }
Programa 5.1: Referenciação através da superclasse

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).

  01. public interface Imposto {
  02.     public float getAliquota();
  03. }
  04. public class CPMF implements Imposto {
  05.       public float getAliquota() {
  06.           return 0.38;
  07.       }
  08. }
  09. public class IRPF implements Imposto {
  10.       public float getAliquota() {
  11.           return 27.5;
  12.       }
  13. }
  14.
  15. Imposto cpmf = new CPMF(); // CPMF associada à referência de Imposto
  16. Imposto irpf = new IRPF(); // IRPF associada à referência de Imposto

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. }
Programa 5.2: Implementação simplificada da cesta de compras

5.2.1 Resolução dinâmica de métodos

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:

Antecipada ou estática:
quando a resolução é feita antes do programa ser executado, ou seja, estaticamente, e que normalmente é realizada pelo compilador. A linguagem C, por exemplo, só possui este tipo de resolução.
Tardia ou dinâmica:
quando a resolução é deixada a cargo do ambiente de execução do programa, que dinamicamente vai permitir a resolução adequada a uma referência concreta que não é necessariamente conhecida em tempo de compilação, ou seja, o compilador, neste caso, não sabe qual o método correto a ser chamado, já que ele enxerga apenas uma chamada de método a um tipo de alto nível, que pode ser uma interface ou uma determinada superclasse. Este tipo de resolução é adotada na linguagem Java por padrão, e a única exceção são os métodos declarados como final (ver capítulo sobre Herança), que são resolvido desde a compilação. Nestes termos, Java provê um ambiente capaz de descobrir o tipo correto do objeto e chamar o método correspondente (isto é feito através da associação de informações internas que a máquina virtual realiza para cada objeto, cujos detalhes fogem ao escopo deste livro).

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. }
Programa 5.3: Jogo de xadrez sem utilização do polimorfismo

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:

  public static void main(String[] args) {
      JogoDeXadrez jx = new JogoDeXadrez();
  
      Peao p = new Peao();
      Bispo b = new Bispo();
      Rainha r = new Rainha();
  
      // Executando três métodos diferentes
      jx.moverPeao(p, 6, 3);
      jx.moverBispo(b, 2, 8);
      jx.moverRainha(r, 4, 7);
  }

Se agora o programador incluísse a peça Torre, a classe JogoDeXadrez teria que ser modificada, acrescentando o seguinte método:

  public void moverTorre(Torre t, int x, int y)   { t.mover(x, y); }

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.

  public void moverPeca(Peca p, int x, int y)   { p.mover(x, y); }

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:

  public static void main(String[] args) {
      JogoDeXadrez jx = new JogoDeXadrez();
  
      Peao p = new Peao();
      Bispo b = new Bispo();
      Rainha r = new Rainha();
  
      // Executando três métodos diferentes
      jx.moverPeca(p, 6, 3);
      jx.moverPeca(b, 2, 8);
      jx.moverPeca(r, 4, 7);
  }

O resultado da execução do método main() anterior seria:

  # java JogoDeXadrez
  Movendo peão para (6,3)
  Movendo bispo para (2,8)
  Movendo rainha para (4,7)

5.3 O Conceito de Casting

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.


PIC

Figura 5.2: Upcasting e Downcasting


5.3.1 Upcasting

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  tamb´em ´e
------→ Funcionario.Quandosecriaumasubclasse,porconsequênciaocompiladordefinequeestano mínimotemasmesmascaracterísticaspúblicaseprotegidasdesuasuperclasse(alémdelogicamentepoderacrescentarnovasfuncionalidades).Emresumo,ocódigodalinha20nãoimplicaemnenhumerroouadvertência- otipoGerenteguardacaracterísticasprópriasdotipoFuncionario.


PIC

Figura 5.3: Hierarquia de Funcionários



  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. }
Programa 5.4: O processo de Upcasting

5.3.2 Downcasting

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. }
Programa 5.5: O processo de Downcasting

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.

  Supervisor sup_2 = (Supervisor) func;

A seguir, é possível ver a respectiva mensagem de erro em tempo de execução, que significa uma tentativa inválida de casting.

  # java br.teste.TesteDowncasting
  Exception in thread "main" java.lang.ClassCastException:
          br.teste.Gerente cannot be cast to br.teste.Supervisor
   at br.teste.TesteDowncasting.main(TesteDowncasting.java:11)

5.4 Identificação de tipos através de RTTI (Runtime Type Identification)

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:

  1. Através da palavra-chave instanceof, disponibilizada pela linguagem Java especificamente para a identificação de tipos em tempo de execução.
  2. Através de reflexão, que significa uma forma de objetos poderem ter acesso às suas próprias estruturas internas (seus métodos, atributos, classe, superclasse, etc.) e que também pode ser utilizada para identificar tipos em tempo de execução.

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.




Peça

Movimento



Peão

- Uma casa na direção vertical (apenas quando não há peça adversária no caminho)
- Uma casa na direção diagonal (apenas quando há peça adversária, que é consumida)



Torre

- Número ilimitado de casas
- Apenas nas direções horizontal e vertical (não é permitida a diagonal)



Bispo

- Número ilimitado de casas
- Apenas na diagonal (não são permitidas as direções horizontal e vertical)



Rainha

- Número ilimitado de casas
- Movimentos horizontal, vertical e diagonal



Rei

- Apenas uma casa
- Movimentos horizontal, vertical e diagonal




Tabela 5.1: Alguns dos movimentos permitidos no 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. }
Programa 5.6: Implementação de Tabuleiro precisa conhecer as classes concretas de Peca

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.

5.4.1 A palavra-chave instanceof

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 é:

  [Nome do Objeto] instanceof [Nome da Classe]

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. }
Programa 5.7: Implementação de Tabuleiro com instanceof

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. }
Programa 5.8: Exemplos de uso do instanceof

A saída do Programa 5.8 pode ser vista a seguir:

  # java br.teste.TesteInstanceOf
  Func_1 -> Auxiliar
  Func_3 -> Gerente
  Func_1 -> Supervisor

5.4.2 Utilizando reflexão

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.

O método getClass()

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());
      }
  }
Programa 5.9: Exemplos de uso do instanceof

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:

  # java br.teste.TesteGetClass
  Classe de Func_1: br.teste.Auxiliar
  Classe de Func_2: br.teste.Supervisor


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).

O atributo estático class

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:

  // Instanciação de um objeto Class da classe Revista
  Class classeRevista = Revista.class;

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.

Resolvendo o Tabuleiro

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. }
Programa 5.10: Implementação de Tabuleiro com reflexão

5.5 Atividades

  1. De que maneira o polimorfismo contribui para manutenção de software?
  2. Qual é a diferença entre redefinição de método e sobrecarga de operação?
  3. Discuta os problemas de programação com a lógica switch. Explique por que o polimorfismo é uma alternativa efetiva à utilização da lógica switch.
  4. Quais as principais diferenças entre upcasting e downcasting? Por que o downcasting necessita de conversão explícita?
  5. Apresente, com suas palavras, os tipos de resolução de métodos e suas definições.
  6. Qual a utilidade dos mecanismos para identificação de tipos em tempo de execução (RTTI)?
  7. Em Java, qual a diferença entre os termos class e Class?
  8. Descreva uma situação que leva ao lançamento da exceção java.lang.ClassCastException.
  9. Responda V (verdadeiro) ou F (falso) às afirmações abaixo:
  10. Considere as classes Funcionario, Supervisor e Gerente definidas no Programa 5.4. A classe Empresa definida no Programa 5.11 utiliza tais classes para definir um quadro funcional. O método main() das linhas 11-25 adiciona diferentes tipos de funcionário randomicamente, de forma a preencher um ArrayList de funcionários (caso necessite, consulte o apêndice sobre as classes do pacote java.util). Implemente o método exibirQuadroFuncional() de forma que a classe Empresa possa identificar quantos Supervisores, Gerentes e Auxiliares existem em seus quadros, exibindo um relatório com a contagem de cada tipo de funcionário.


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


    Programa 5.11: Exercício.

  11. Considere o Programa 5.7. Insira um trecho de código, nos mesmos moldes do código para as peças existentes, para validação dos movimentos de uma peça da classe Torre.

Apêndice A
Eclipse e Java

A.1 Introdução

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.2 Conceitos Básicos do Eclipse

A.2.1 Welcome Page

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.


PIC

Figura A.1: Página de Boas-Vindas do Eclipse - Welcome Page


A.2.2 Workbench

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.2.3 Perspectivas

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.

A.2.4 Views

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.2.5 Editores

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:


PIC

Figura A.2: Exemplo de arquivos abertos lado a lado no Eclipse


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.

A.3 Preparando o Eclipse para o ambiente Java

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.


PIC

Figura A.3: Inicialização do Eclipse em um máquina com GNU/Linux UBUNTU


Com o software Eclipse inicializado deve-se seguir os seguintes passos:

  1. Caso a janela Eclipse Welcome page (veja o exemplo da Figura A.1) esteja aberta, o programador deve clicar no ícone da seta para começar a utilizar o Eclipse.
  2. Selecione o item de menu Window - Preferences para abrir as preferências do Workbench.
  3. Selecione a opção Java - Installed JREs para mostrar o JRE (Java Runtime Enviroment) instalado. Por padrão, o JRE usado no Workbench é utilizado para construir e executar os programas Java. Outras opções de ambientes Java instalados na máquina podem ser configurados. Execute a opção search para que seja procurado no computador máquinas Java instaladas a partir de uma determinada pasta indicada. Veja a Figura A.4.


    PIC
    Figura A.4: Preferências da JRE instalada


  4. Selecione a opção General - Workspace da página de preferências. Confirme se a opção Build automatically está selecionada. Veja a Figura A.5.


    PIC
    Figura A.5: Preferências Gerais do Workspace


  5. Selecione a opção Java - Build Path da página de preferências. Confirme se Source and output folder está selecionado para Project. Veja a Figura A.6.


    PIC
    Figura A.6: Preferências Java Build Path


  6. Selecione a opção Java - Editor da página de preferências. Confirme se a opção Report problems as you type está marcada. Veja a Figura A.7.


    PIC
    Figura A.7: Preferências Editor - Report Problem as you type


  7. Ao final, clique no botão OK para salvar as preferências.

A.4 Programação Java no IDE Eclipse

A.4.1 Eclipse - Projeto Java

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:

A.4.2 Meu primeiro programa Java no IDE Eclipse

É 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.


PIC

Figura A.8: Exemplo de definição de um Projeto Java


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.


PIC

Figura A.9: Exemplo do 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.


PIC

Figura A.10: Exemplo de nova classe Java


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.


PIC

Figura A.11: Tela inicial do PrimeiroProgramaJava.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.


PIC

Figura A.12: Código do PrimeiroProgramaJava.java


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.


PIC

Figura A.13: Execução do programa PrimeiroProgramaJava.java


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.


PIC

Figura A.14: Resultado da execução do PrimeiroProgramaJava.java


A.5 Atividades

  1. Leia e execute as atividades do Help do Eclipse no menu Help - Help Contens - Workbench User Guide - Getting Started - Basic Tutorial.
  2. Como se faz para restaurar uma perspectiva ao seu padrão original ?
  3. Configure uma perspectiva de trabalho que você achar mais adequada, salve esta perspectiva com seu nome para que você possa utilizá-la mais tarde.
  4. Explique para que serve as views Navigator e Outline do Eclipse.
  5. Qual a finalidade de se definir um projeto Java dentro do Eclipse ?
  6. Modifique o programa apresentado na Seção 1.4 para imprimir seu nome completo.
  7. Uma janela Workbench pode conter mais de uma perspectiva ?
  8. Qual a finalidade da página Welcome do Eclipse ?
  9. Quais as principais diferenças entre as views Navigator e Package Explorer ?
  10. Julgue cada um dos itens a seguir como certo (C) ou Errado (E):
    1. (  ) O Eclipse é um software proprietário da IBM, muito poderoso e flexível.
    2. (  ) As perspectivas controlam o que aparece em certos menus e barras de ferramentas.
    3. (  ) Para criar e compilar programas Java dentro do Eclipse é necessário primeiramente definir um novo Workbench.
    4. (  ) Qualquer número de editores pode ser aberto de uma vez, mas somente um pode estar ativo por vez.
    5. (  ) Para executar um programa Java, dentro da view do editor é necessário clicar com o botão esquerdo do mouse e selecionar a opção Run as - Java Application.

Apêndice B
Estrutura de dados básicas da API Java.util

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.

B.1 ArrayList

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:

ArrayList( ).
Gera uma lista vazia.
ArrayList(Colletion c).
Gera um ArrayList que é inicializado com os elementos de um colletion c.
ArrayList(int capacidade).
Gera um ArrayList que tem uma capacidade inicial especificada. A capacidade cresce automaticamente conforme os elementos são adicionados ao ArrayList.

O Programa B.1 apresenta um exemplo de uso da classe ArrayList. A linha 06

                ArrayList al = new ArrayList(); // cria um ArrayList

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


Programa B.1: Um uso simples de ArrayList


O resultado da execução do Programa B.1 é a seguinte:
  Tamanho inicial do al: 0
  Tamanho do al após adições: 7
  Conteúdo do al: [Casa, Azeite, Abelha, Elefante, Baleia, Dedo, Formiga]
  Tamanho do al após remoções: 5
  Conteúdo do al: [Casa, Azeite, Elefante, Baleia, Dedo]

Para instanciar um ArrayList com elementos do tipo Integer deve se usar a seguinte sintaxe:

                ArrayList<Integer> al = new ArrayList<Integer>();

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:

     for (String s : strList)
                     System.out.println(s);

O trecho de código abaixo mostra a iteração de uma ArrayList usando um Iterator:

Construtor Iterator:

                    Iterator<String> iter = strList.iterator();
                    while( iter.hasNext())
                     System.out.println(iter.next());

É 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:

                for (Object elemento: al)
                     System.out.println(elemento);

Neste caso, o resultado impresso seria a lista de elementos do ArrayList, ou seja:

 Casa, Azeite, Elefante, Baleia, Dedo

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.

B.2 HashSet

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


Programa B.2: Uma demonstração simples do uso HashSet


O resultado da execução do Programa B.2 é a seguinte:
Adicionado  = true  
Adicionado  = true  
Adicionado  = false


A linha 06,

                 HashSet<String> hashList = new HashSet<String>();

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


Programa B.3: Uma demonstração de HashSet com detecção de itens duplicados


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:

    if (hashList.add(sArray[i]))

é 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.

B.2.1 Subconjunto, União, Intersecção e Complemento

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.


PIC

Figura B.1: B é um subconjunto de A


É possível determinar se um HashSet é um subconjunto de outro HashSet pelo uso do método containsAll() da seguinte maneira:

     if (primeiroSet.containsAll(segundoSet))  
       System.out.println("segundoSet é um subconjunto do primeiroSet");  
     else  
       System.out.println("segundoSet não é um subconjunto do primeiroSet");

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.


PIC

Figura B.2: B é um subconjunto de A


A união de duas HashSets pode ser derivada pelo uso do método addAll() da seguinte maneira:

      Set<String> uniaoSet = new HashSet<String>(primeiroSet);  
      unionSet.addAll(segundoSet);

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.


PIC

Figura B.3: B é um subconjunto de A


A intersecção entre dois HashSets pode ser derivada pelo uso do método retainAll() da seguinte maneira:

    Set<String> intersectionSet = new HashSet<String>(primeiroSet);  
    intersectionSet.retainAll(segundoSet);

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.


PIC

Figura B.4: B é um subconjunto de A


O complemento de dois HashSets pode ser derivado pelo uso do método removeAll() da seguinte maneira:

    Set<String> differenceSet = new HashSet<String>(primeiroSet);  
    differenceSet.removeAll(segundoSet);

B.3 HashMap

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


Programa B.4: Uma demonstração de HashMap


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).

 HashMap<Integer, Estudante> estudanteMap = new HashMap<Integer, Estudante>();

Entradas podem ser adicionadas ao HashMap usando o método put() como mostrado abaixo:

 Estudante s1 = new Estudante (1,"Maria das Graças Soares");  
 estudanteMap.put(new Integer(s1.getnumeroMatricula()), s1);

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:

           Integer key = new Integer(i);  
           retestudante = estudanteMap.get(key);

Bibliografia

[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.