Programação Orientada A Objetos em Java t2
Programação Orientada A Objetos em Java t2
Programação Orientada A Objetos em Java t2
PROPÓSITO
Obter o conhecimento da linguagem Java, puramente orientada a objetos (OO) e que está entre
as mais utilizadas no mundo, é imprescindível para o profissional de Tecnologia da Informação. A
orientação a objetos (OO) é um paradigma
consagrado no desenvolvimento de software e
empregado independentemente da metodologia de projeto – ágil ou prescritiva.
PREPARAÇÃO
Para melhor absorção do conhecimento, recomenda-se o uso de computador com o Java
Development Kit – JDK e um IDE (Integrated Development Environment) instalados e acesso à
Internet para realização de pesquisa.
OBJETIVOS
MÓDULO 1
MÓDULO 2
MÓDULO 3
MÓDULO 4
Na verdade, muito do poder da Java vem das capacidades que a OO trouxe para o
desenvolvimento de softwares. Exemplos disso são, justamente, a herança e o polimorfismo.
Isso será feito com o apoio de atividades que irão reforçar o aprendizado e fornecer um feedback.
Ao fim, teremos compreendido apenas uma parte do desenvolvimento em Java, mas que é
indispensável para qualquer estudo
mais avançado.
MÓDULO 1
Uma classe define um tipo de dado, e cada objeto instanciado pertence ao conjunto formado pelos
objetos daquela classe.
Nesse conjunto, todos os objetos são do tipo da classe que lhes deu origem.
Todos os objetos do mesmo tipo terão os mesmos métodos e atributos, porém cada objeto terá
sua própria cópia destes.
1 //Pacotes
2 package com.mycompany.GereEscola;
4 //Classe
6 //Atributos
9 //Métodos
11 this.nome = nome;
12 this.nacionalidade = nacionalidade;
13 this.naturalidade = naturalidade;
14 }
16 this.nome = nome;
17 }
19 return this.nome;
20 }
22 return this.nacionalidade;
23 }
25 return this.naturalidade;
26 }
27 }
Qualquer objeto instanciado a partir dessa classe terá os atributos “nome”, “nacionalidade” e
“naturalidade”, além de uma cópia dos métodos mostrados. Na verdade, como “String”
é um
objeto de tamanho desconhecido em tempo de programação, as variáveis serão referências que
vão guardar a localização em memória dos objetos do tipo “String”, quando esses forem criados.
Quando objetos se relacionam por associação, por exemplo, essa relação se dá de maneira
horizontal. Normalmente são relações do tipo “contém” ou “possui”. A herança introduz um novo
tipo de relacionamento, inserindo a ideia de “é tipo”
ou “é um”. Esse relacionamento vertical origina
o que se chama de “hierarquia de classes”.
Uma hierarquia de classe é um conjunto de classes que guardam entre si uma relação de herança
(verticalizada), na qual as classes acima generalizam as classes abaixo ou, por simetria, as
classes abaixo especializam as classes acima.
2 package com.mycompany.GereEscola;
4 //Classe
9 }
10 }
A classe “Aluno” estende a classe “Pessoa”, ou seja, “Aluno” é uma subclasse de “Pessoa”,
formando assim uma relação hierárquica (“Aluno” deriva de “Pessoa”). Podemos aplicar a ideia
introduzida com
o conceito de herança para deixar mais claro o que estamos falando: “Aluno” é
um tipo de/é uma “Pessoa”. Um objeto do tipo “Aluno” também é do tipo “Pessoa”.
Nesse sentido, como vimos, a classe “Pessoa” lega seus métodos e atributos à classe “Aluno”.
Verificando o Código 2, notamos que a classe “Aluno” não possui métodos e atributos definidos
em sua declaração, exceto pelo seu construtor. Logo, ela terá apenas os componentes legados
por “Pessoa”. A instanciação de um objeto do
tipo “Aluno”, nesse caso, levará à reserva de um
espaço de memória para guardar apenas os atributos “nome”, “nacionalidade” e “naturalidade”, e
os métodos de “Pessoa” e o construtor de “Aluno”. No nosso exemplo, o objeto seria
virtualmente
igual a um objeto do tipo “Pessoa”.
Esse exemplo, porém, tem apenas um fim didático e busca mostrar na prática, ainda que
superficialmente, o mecanismo de herança. Como os tipos do exemplo são praticamente idênticos,
nenhuma utilidade prática se obtém - “Aluno” não especializa “Pessoa”.
Mas aí também atingimos outro fim: mostramos que a versatilidade da herança está na
possibilidade de realizarmos especializações e generalizações no modelo a ser implementado. A
especialização ocorre quando a classe derivada particulariza
comportamentos. Assim, são casos
em que as subclasses alteram métodos da superclasse.
No caso do Código 3, a classe “Aluno” definida possui um novo atributo – “matrícula” – que é
gerado automaticamente quando o objeto for instanciado. Apesar de esse objeto possuir os
métodos e atributos de “Pessoa”,
ele agora tem um comportamento particular: na instanciação,
além dos atributos definidos em “Pessoa”, ocorre também a definição de “matrícula”.
CÓDIGO 3: EXEMPLO DE ESPECIALIZAÇÃO
1 //Pacotes
2 package com.mycompany.GereEscola;
4 //Importações
5 import java.util.UUID;
7 //Classe
10 //Atributos
12
13 //Métodos
17 }
18 }
Já sabemos que o atributo “identificador” e os métodos que operam sobre ele (“atualizarID”
e “recuperarID”) são herdados pelas classes filhas.
Mas vale destacar que a relação de herança se propaga indefinidamente, isto é, um objeto
da classe “Aluno” também é um objeto das classes “Fisica” e “Pessoa”.
A classe “Pessoa” implementa o comportamento mais genérico de todos. Ela possui um atributo
de identificação definido como do tipo “String” (assim ele aceita letras, símbolos e números); e
métodos que operam sobre esse atributo
(veja o Código 4).
As classes “Fisica”, “Juridica” e “Aluno” especializam o método que define o atributo “Identificador”,
particularizando-o para suas necessidades específicas, como observamos no Código 5, no Código
6 e no Código 7, que mostram as respectivas
implementações.
1 //Classe
3 ...
5 ...
7 this.identificador = identificador;
8 }
10 return this.identificador;
11 }
12 ...
13 }
2 if ( validaCPF ( CPF ) )
3 this.identificador = CPF;
4 else
6 }
2 if ( validaCNPJ ( CNPJ ) )
3 this.identificador = CNPJ;
4 else
6 }
2 if ( this.identificador.isBlank() )
4 else
6 }
ATENÇÃO
Repare que cada código possui um comportamento específico. Apesar de não exibirmos a
implementação do método “validaCPF” e “validaCNPJ”, fica claro que os comportamentos são
distintos
entre si e entre as demais implementações de “atualizarID”, pois o Cadastro de Pessoas
Físicas (CPF) e o Cadastro Nacional de Pessoas Jurídicas (CNPJ) possuem regras de formação
diferentes.
Ao mesmo tempo, um objeto “Aluno” também é um tipo de “Fisica”, pois esta classe busca
modelar o conceito de “pessoa física” (é razoável esperar que um aluno possua CPF além de seu
código de matrícula). Será que essa situação
não seria melhor modelada por meio de herança
múltipla, com “Aluno” derivando diretamente de “Pessoa” e “Fisica”?
Já falamos que Java não permite herança múltipla e que é possível solucionar tais casos
modificando a modelagem. Agora, entenderemos o problema da herança múltipla que faz a Java
evitá-la. Para isso, vamos modificar o modelo
mostrado na Figura 1, criando a classe
“ProfessorJuridico”, que modela os professores contratados como pessoa jurídica.
Essa situação é vista na Figura 2. Nela, as classes “Fisica” e “Juridica” especializam o método
“atualizarID” de “Pessoa” e legam sua versão especializada à classe “ProfessorJuridico”.
A questão que surge agora é: quando “atualizarID” for invocado em um objeto do tipo
“ProfessorJuridico”, qual das duas versões especializadas será executada?
Essa é uma situação que não pode ser resolvida automaticamente pelo compilador. Por conta
dessa ambiguidade, a Figura 2 é também chamada de “Diamante da morte”.
Imagem: Marlos de Mendonça.
Figura 2: Diamante da morte.
Linguagens que admitem herança múltipla deixam para o desenvolvedor a solução desse
problema.
Em C++, por exemplo, a desambiguação é feita fazendo-se um cast explícito para o tipo que se
quer invocar.
Entretanto, deixar a cargo do programador é potencializar o risco de erro, o que Java tem como
meta evitar.
Portanto, em Java, tal situação é proibida, obrigando a se criar uma solução de modelagem
distinta.
ATENÇÃO
Se você estava atento, deve ter notado que o método “atualizarID” das classes “Pessoa”, “Fisica”
e “Juridica” possuem a mesma assinatura. Isso está alinhado ao princípio de Projeto por
Contrato (Design by Contract).
No Projeto por Contrato, os métodos de uma classe definem o contrato que se estabelece com a
classe ao consumir os serviços que esta disponibiliza. Assim, para o caso em questão, o contrato
“reza” que, para atualizar o campo “identificador”
de um objeto, deve-se invocar o método
“atualizarID”, passando-lhe um objeto do tipo “String” como parâmetro. E não se deve esperar
qualquer retorno do método.
O método “atualizarID” da classe “Aluno” é diferente. Sua assinatura não admite parâmetros, o
que altera o contrato estabelecido pela superclasse. Não há erro que seja conceitual ou formal,
isto é, trata-se de uma situação perfeitamente
válida na linguagem e dentro do paradigma OO, de
maneira que “Aluno” é subclasse de “Fisica”. Porém, “Aluno” não define um subtipo de “Pessoa”,
apesar de, corriqueiramente, essa distinção não ser considerada.
Um subtipo tem uma restrição adicional.
Para que uma subclasse seja um subtipo da superclasse, faz-se necessário que todas as
propriedades da superclasse sejam válidas na subclasse.
Isso não ocorre para o caso que acabamos de descrever. Nas classes “Pessoa”, “Fisica” e
“Juridica”, a propriedade de definição do identificador é a mesma: o campo “identificador” é
definido a partir de um objeto “String” fornecido.
Na classe “Aluno”, o campo “identificador” é
definido automaticamente.
Essa situação nos remete ao princípio da substituição de Liskov. Esse princípio foi primeiramente
apresentado por Barbara Liskov, em 1987, e faz parte dos chamados princípios SOLID (Single
responsibility, Open-closed,
Liskov substitution, Interface segregation and Dependency inversion).
Ele pode ser encontrado com diversos enunciados, sempre afirmando a substitutibilidade de um
objeto da classe base pela classe derivada, sem prejuízo para o funcionamento do software.
Se D for subtipo de B, então qualquer que seja o objeto b do tipo B, ele pode ser substituído por
um objeto d do tipo D sem prejuízo para P.
Assim, em todo ponto do programa no qual um objeto de “Pessoa” for usado, objetos de “Fisica” e
“Juridica” podem ser aplicados sem quebrar o programa, mas o mesmo não se dá com objetos do
tipo “Aluno”.
HIERARQUIA DE COLEÇÃO
Coleções ou containers são um conjunto de classes e interfaces Java chamados de Java
Collections Framework. Essas classes e interfaces implementam estruturas de dados comumente
utilizadas para agrupar múltiplos
elementos em uma única unidade. Sua finalidade é armazenar,
manipular e comunicar dados agregados.
Set
List
Queue
Deque
Curiosamente, apesar de a estrutura “Map” não ser, de fato, uma coleção, ela também é provida
pela Java Collections Framework. Isso significa que o framework é formado por duas árvores de
hierarquia, uma correspondente às entidades
derivadas de “Collection” (Figura 3) e a outra,
formada pela “Map” (Figura 4).
Como você pode observar pelas figuras que representam ambas as hierarquias de herança, essas
estruturas são implementadas aplicando-se os conceitos de herança vistos anteriormente.
EXEMPLO
A interface “Collection” possui os comportamentos mais genéricos (assim como “Map”, na sua
hierarquia). Por exemplo, o mecanismo para iterar pela coleção pode ser encontrado nela. Ela
possui, também, métodos que fornecem
comportamentos genéricos. É um total de 15 métodos,
mostrados no quadro a seguir.
Método Comportamento
removeAll(Collection
Remove todos os elementos da coleção.
c)
SET
Representa conjuntos e não admite elementos duplicados.
LIST
Implementa o conceito de listas e admite duplicidade de elementos. É uma coleção ordenada que
permite o acesso direto ao elemento armazenado, assim como a definição da posição onde
armazená-lo.
QUEUE
Trata-se de uma coleção que implementa algo mais genérico do que uma fila. Pode ser usada
para criar uma fila (FIFO), mas também pode implementar uma lista de prioridades, na qual os
elementos são ordenados e consumidos
segundo a prioridade e não na ordem de chegada. Essa
coleção admite a criação de outros tipos de filas com outras regras de ordenação.
DEQUE
Implementa a estrutura de dados conhecida como “Deque” (Double Ended Queue). Pode ser
usada como uma fila (FIFO) ou uma pilha (LIFO). Admite a inserção e a retirada em ambas as
extremidades.
Um tipo de dado é um conjunto fechado de elementos e propriedades que afeta os elementos. Por
exemplo, quando definimos uma variável como sendo do tipo inteiro, o compilador gera comandos
para alocação de memória suficiente para guardar
o maior valor possível para esse tipo.
Você se lembra de que o tipo inteiro em Java (e em qualquer linguagem) é uma abstração limitada
do conjunto dos números inteiros, que é infinito. Além disso, o compilador é capaz de operar com
os elementos do tipo “int”.
Ou seja, adições, subtrações e outras operações são realizadas sem a
necessidade de o comportamento ser implementado. O mesmo vale para os demais tipos
primitivos, como “String”, “float”
ou outros.
A tipagem dinâmica é determinada em tempo de execução.
Observe que não importa se o programador determinou o tipo. Se o compilador for capaz de inferir
esse tipo durante a compilação, então, trata-se de tipagem estática.
ATENÇÃO
A tipagem dinâmica ocorre quando o tipo só pode ser identificado durante a execução do
programa.
Um exemplo de linguagem que emprega tipagem dinâmica é a JavaScript. O Código 8 mostra um
trecho de programa em JavaScript que exemplifica a tipagem dinâmica.
1 var a;
3 a = 1;
5 a = "Palavra";
7 a = false;
9 a = [];
Podemos ver que a mesma variável (“a”) assumiu, ao longo da execução, diferentes tipos.
Observando a linha 1 do Código 8, podemos ver que, na declaração, o compilador não tinha
qualquer informação que lhe permitisse inferir o tipo
de dado de “a”. Esse tipo só pôde ser
determinado na execução, nas linhas 3, 5, 7 e 9. Todas essas linhas alteram sucessivamente o
tipo atribuído a “a”.
Você deve ter claro em sua mente que a linguagem de programação Java é estaticamente
tipada, o que significa que todas as variáveis devem ser primeiro declaradas (tipo e nome) e
depois utilizadas [2]. Uma das possíveis
fontes de confusão é a introdução da instrução “var” a
partir da versão 10 da Java [3]. Esse comando, aplicável apenas às variáveis locais, instrui o
compilador a inferir o tipo de variável. Todavia, o tipo é determinado ainda
em tempo de
compilação e, uma vez determinado, o mesmo não pode ser modificado. A Java exige que, no uso
de “var”, a variável seja inicializada, o que permite ao compilador determinar o seu tipo. O Código
9 mostra o emprego correto
de “var”, enquanto o Código 10 gera erro de compilação. Ou seja,
“var”, em Java, não possui a mesma semântica que “var” em JavaScript. São situações distintas e,
em Java, trata-se de tipagem estática.
1 ...
2 var a = 0;
3 a = a + 1;
4 ...
1 ...
2 var a = 0;
3 a = “Palavra”;
4 ...
Uma outra possível fonte de confusão diz respeito ao dynamic binding ou vinculação dinâmica de
método. Para ilustrar essa situação, vamos adicionar o método “retornaTipo” às classes “Pessoa”,
“Fisica” e “Juridica”, cujas respectivas
implementações são vistas no Código 11, no Código 12 e no
Código 13.
2 return null;
3 }
2 return “Fisica”;
3 }
2 return “Juridica”;
3 }
Se lembrarmos que as classes “Fisica” e “Juridica” especializam “Pessoa”, ficará óbvio que ambas
substituem o método “retornaTipo” de “Pessoa” por versões especializadas. O Código 14 mostra
um trecho de código da classe principal do
programa.
3 ...
5 grupo [0] = new Fisica ( "Marco Antônio" , data_nasc , null , null , "Brasil" , "
6 grupo [1] = new Juridica ("Escola Novo Mundo Ltda" , data_nasc , null , null ,
7 grupo [2] = new Pessoa ( null , null , null , null , "Brasil" , "Rio de Janeiro");
10 ...
11 }
1 Física
2 Jurídica
3 null
O motivo disso é a vinculação dinâmica de método. O vetor “grupo [ ]” é um arranjo de referências
para objetos do tipo “Pessoa”.
Como “Fisica” e “Juridica” são subclasses de “Pessoa”, os objetos dessas classes podem ser
referenciados por uma variável que guarda referência para a superclasse. Isso é o que ocorre nas
linhas 5 e 6 do Código 14.
A vinculação dinâmica ocorre na execução da linha 9. O compilador não sabe, a priori, qual objeto
será referenciado nas posições do vetor, mas, durante a execução, ele identifica o tipo do objeto e
vincula o método apropriado.
Essa situação, entretanto, não se configura como tipagem dinâmica,
pois o tipo do objeto é usado para se determinar o método a ser invocado.
Nesse caso, devemos lembrar, também, que Java oculta o mecanismo de ponteiros, e uma classe
definida pelo programador não é um dos tipos primitivos. Então “grupo []” é, como dito, um vetor
de referências para objetos do tipo “Pessoa”,
e isso se mantém mesmo nas linhas 5 e 6 (as
subclasses são um tipo da superclasse). Essa é uma diferença sutil, mas significativa, com relação
ao mostrado no Código 8.
Iremos explorar esse mecanismo em outra oportunidade. No entanto, é preciso ficar claro que
essa situação não é tipagem dinâmica, pois o vetor “grupo []” não alterou seu tipo, qual seja,
referência para objetos do tipo “Pessoa”.
VERIFICANDO O APRENDIZADO
A) I
B) II
C) III
D) I e III
E) I e II
A) Uma desvantagem das coleções é que os iteradores não são providos, precisando ser
desenvolvidos, pelo programador.
B) Uma desvantagem das coleções é que não podem ser usadas com tipos de dados criados pelo
programador (classes), já que sua estrutura é desconhecida.
GABARITO
1. Considere duas classes chamadas “Base” e “Derivada”, de tal forma que a primeira é
superclasse da última. A classe “derivada” atende ao princípio da substituição de Liskov.
II) Todos os métodos públicos e protegidos de “Derivada” devem ter a mesma assinatura
que os de “Base”.
O princípio da substituição de Liskov afirma que a classe “Base” deve poder ser substituída por
“Derivada” sem que o programa quebre. Assim, é necessário que os métodos de “Base”
redefinidos por “Derivada” tenham a mesma assinatura.
O Java Collections Framework permite que algumas estruturas de dados sejam implementadas de
forma simples e rápidas, um exemplo são as filas que podem ser implementadas por “Queue” e
“Deque”.
MÓDULO 2
O MÉTODO “TOSTRING”
Já vimos que em Java todas as classes descendem direta ou indiretamente da classe “Object”.
Essa classe provê um conjunto de métodos que, pela herança, estão disponíveis para as
subclasses. Tais métodos oferecem ao
programador a facilidade de contar com comportamentos
comuns fornecidos pela própria biblioteca da linguagem, além de poder especializar esses
métodos para atenderem às necessidades particulares de sua implementação.
Neste módulo, abordaremos alguns dos métodos da classe “Object” – “toString”, “equals” e
“hashCode” – e aproveitaremos
para discutir as nuances do acesso protegido e do operador
“Instanceof”.
O método “toString” permite identificar o objeto retornando uma representação “String” do próprio.
Ou seja, trata-se de uma representação textual que assume a forma:
1 com.mycompany.GereEscola.Juridica@72ea2f77
3 }
Essa, porém, é a implementação padrão fornecida pela biblioteca Java, que pode ser prontamente
usada pelo programador. Todavia, prover uma especialização é algo interessante, pois permite dar
um formato mais útil à saída do método.
Além de saber o nome completamente qualificado da classe, temos somente o seu código hash.
Assim, vamos modificar o método “toString” para que ele apresente outras informações, conforme
a linha 18 da implementação
da classe “Pessoa” parcialmente mostrada no Código 16.
2 //Atributos
8 //Métodos
11 this.data_inicio_existencia = data_inicio_existencia;
12 this.identificador = identificador;
13 this.endereco = endereco;
14 this.nacionalidade = nacionalidade;
15 this.naturalidade = naturalidade;
16 }
17 ...
19 return "Objeto:" + "\n\t- Classe: " + getClass().getName() + "\n\t- Hash: " + Intege
20 }
21 }
2 //Atributos
4 //Métodos
8 }
9 }
O Código 18 faz a invocação do método “toString” para dois objetos criados, um do tipo “Pessoa”
e outro do tipo “Fisica”.
9 }
1 grupo[0]: Objeto:
2 - Classe: com.mycompany.GereEscola.Fisica
3 - Hash: 77459877
5 - Identificador: 365.586.875-45
6 grupo[1]: Objeto:
7 - Classe: com.mycompany.GereEscola.Pessoa
8 - Hash: 378bf509
10 - Identificador: 43.186.666/0026-32
Veja que o método “toString” agora apresenta outra composição de saída, trazendo informações
como o nome e o identificador. Além disso, formatamos a saída para apresentar as informações
em múltiplas linhas identadas.
Se olharmos novamente o Código 18, veremos que, na linha 5, o objeto criado é do tipo “Fisica”.
Apesar de “Fisica” não reescrever o método “toString”, ela herda a versão especializada de sua
superclasse, que é invocada no
lugar da versão padrão. Olhando também para a saída,
percebemos que o objeto foi corretamente identificado como sendo da classe “Fisica”.
Ao modificar a implementação do método, tornamos seu retorno mais útil. Por esse motivo, ele é
frequentemente sobrescrito nas diversas classes providas pela biblioteca Java.
O método “equals” é utilizado para avaliar se outro objeto é igual ao objeto que invoca o método.
Se forem iguais, ele retorna “true”; caso contrário, ele retorna “false”. A sua assinatura é “boolean
equals (Object obj)”,
e sua implementação é mostrada no Código 19. Já que ele recebe como
parâmetro uma referência para um objeto da classe “Object”, da qual todas as classes descendem
em Java, ele aceita referência para qualquer objeto das
classes derivadas.
3 }
Reflexividade: qualquer que seja uma referência não nula R, R.equals(R) retorna sempre “true”.
Simetria: quaisquer que sejam as referências não nulas R e S, R.equals(S) retorna “true” se, e
somente se, S.equals(R) retorna “true”.
Transitividade: quaisquer que sejam as referências não nulas R, S e T, se R.equals(S) retorna
“true” e S.equals(T) retorna “true” então R.equals(T)
deve retornar “true”.
ATENÇÃO
Um caso excepcional do equals é a seguinte propriedade: qualquer que seja uma referência não
nula R, R.equals(null) retorna sempre “false”.
A seguir, veja uma aplicação simples de “equals”. Para isso, vamos trabalhar com 9 objetos, dos
quais 3 são do tipo “int” e 3, do tipo “String” – ambos tipos primitivos; e 3 são referências para
objetos das
classes “Pessoa” e “Fisica”, conforme consta no Código 20.
2 //Atributos
8 //Métodos
10 I1 = 1;
11 I2 = 2;
12 I3 = 1;
13 S1 = "a";
14 S2 = "b";
15 S3 = "a";
24 }
25
29 System.out.println("o1 == o2");
30 else
31 System.out.println("o1 != o2");
32 if ( o1.equals(o3) )
33 System.out.println("o1 == o3");
34 else
35 System.out.println("o1 != o3");
36 }
37 }
Nas linhas 21, 22 e 23 do Código 20, o que fazemos é comparar os diversos tipos de objeto
utilizando o método “equals”. O resultado é mostrado a seguir:
2 o1 != o2
3 o1 != o3
5 o1 != o2
6 o1 == o3
8 o1 != o2
9 o1 == o3
Podemos ver que o objeto “I1” foi considerado igual ao “I3”, assim como “S1” e “S3”. Todavia, “p1”
e “p2” foram considerados diferentes, embora sejam instâncias da mesma classe (“Fisica”), e seus
atributos, rigorosamente iguais. Por
quê?
A resposta dessa pergunta mobiliza vários conceitos, a começar pelo entendimento do que ocorre
nas linhas 5 e 6. As variáveis “p1”, “p2” e “p3” são referências para objetos das classes “Fisica” e
“Pessoa”.
Em contrapartida, as variáveis “I1”, “I2”, “I3”, “S1”, “S2” e “S3” são todas de tipos primitivos. O
operador de comparação “==” atua verificando o conteúdo das variáveis que, para os tipos
primitivos, são os valores neles acumulados.
Mesmo o tipo “String”, que é um objeto, tem seu
valor acumulado avaliado. O mesmo ocorre para os tipos de dados de usuário (classes), só que
nesse caso “p1”, “p2” e “p3” armazenam o endereço de memória (referência) onde
os objetos
estão. Como esses objetos ocupam diferentes endereços de memória, a comparação entre “p1” e
“p2” retorna “false” (são objetos iguais, mas não são o mesmo objeto). Uma comparação
“p1.equals(p1)” retornaria “true”
obviamente, pois, nesse caso, trata-se do mesmo objeto.
A resposta anterior também explica por que precisamos sobrescrever “equals” se quisermos
comparar objetos. No Código 21, mostramos a reimplementação desse método para a classe
“Pessoa”.
4 else
5 return false;
6 }
2 o1 == o2
3 o1 != o3
Contudo, da forma que implementamos “equals”, “p1” e “p2” serão considerados iguais mesmo
que os demais atributos sejam diferentes. Esse caso mostra a complexidade que mencionamos
anteriormente, em se estabelecer a relação
de equivalência entre objetos complexos. Cabe ao
programador determinar quais características devem ser consideradas na comparação.
Isso faz todo sentido, já que um código hash é uma impressão digital de uma entidade. Pense no
caso de arquivos. Quando baixamos um arquivo da Internet, como uma imagem de um sistema
operacional, usualmente calculamos o hash para verificar
se houve erro no download. Isto é,
mesmo sendo duas cópias distintas, esperamos que, se forem iguais, os arquivos tenham o
mesmo hash. Esse princípio é o mesmo por trás da necessidade de se reimplementar “hashCode”.
Nós já vimos o uso desse método quando estudamos “toString”, e a saída do Código 18 mostra o
resultado do seu emprego. A sua assinatura é “int hashCode()”, e ele tem como retorno o código
hash do objeto.
Invocações sucessivas sobre o mesmo objeto devem consistentemente retornar o mesmo valor,
dado que nenhuma informação do objeto usada pela comparação em “equals” tenha mudado.
Se dois objetos são iguais segundo “equals”, então a invocação de “hashCode” deve retornar o
mesmo valor para ambos.
Caso dois objetos sejam desiguais segundo “equals”, não é obrigatório que a invocação de
“hashCode” em cada um dos dois objetos produza resultados distintos. Mas produzir resultados
distintos para
objetos desiguais pode melhorar o desempenho das tabelas hash.
A invocação de “hashCode” nos objetos “p1” e “p2” do Código 20 nos dá a seguinte saída:
1 p1 = 2001049719
2 p2 = 250421012
Esse resultado contraria as propriedades de “hashCode”, uma vez que nossa nova implementação
de “equals” estabeleceu a igualdade entre “p1” e “p2” instanciados no Código 18. Para restaurar o
comportamento correto,
fornecemos a especialização de “hashCode” vista no Código 22.
3 return this.nome.hashCode();
4 else
5 return super.hashCode();
6 }
Veja que agora nossa implementação está consistente com a de “equals”. Na linha 2 do Código
22, asseguramo-nos de que se trata de um objeto da classe “Pessoa”. Se não for, chamamos a
implementação padrão de “hashCode”.
Caso seja, retornamos o valor de hash da “String” que
forma o atributo “nome”, o mesmo usado em “equals” para comparação. A saída é:
1 p1 = -456782095
2 p2 = -456782095
COMENTÁRIO
O OPERADOR “INSTANCEOF”
O operador “instanceof” é utilizado para comparar um objeto com um tipo específico. É o único
operador de comparação de tipo fornecido pela linguagem.
Sua sintaxe é simples: “op1 instanceof op2”, onde o operando “op2” é o tipo com o qual se deseja
comparar; e “op1” é o objeto ou a expressão a ser comparada. Um exemplo do uso desse
operador é mostrado na linha 2 do Código
22. O operador retorna verdadeiro, se “op1” for uma
instância do tipo “op2”. Então, devemos esperar que se “op1” for uma subclasse de “op2”, o
retorno será “true”. Vejamos se isso é válido executando o Código 23.
2 //Atributos
5 //Métodos
14 else
16 if ( p2 instanceof Pessoa )
18 else
20 if ( p3 instanceof Pessoa )
22 else
24 if ( p3 instanceof Fisica )
26 else
28 }
29 }
3
4 p3 é instância do tipo Pessoa.
Assim, o código valida nossa expectativa: “instanceof” retorna verdadeiro para uma instância da
subclasse quando comparada ao tipo da superclasse. Tal resultado independe se a variável é uma
referência para a superclasse (linha
9) ou a própria subclasse (linha 10).
Agora vamos explorar algumas situações particulares. Para isso, vamos criar o pacote
“Matemática" e, doravante, as classes que temos utilizado (“Principal”, “Pessoa”, “Fisica”,
“Juridica”, “Aluno” etc.) estarão inseridas no pacote
“GereEscola”.
No pacote “Matemática", vamos criar a classe “Nota”, que implementa alguns métodos de cálculos
relativos às notas dos alunos. Essa classe pode ser parcialmente vista no Código 24.
1 package com.mycompany.Matematica;
2 ...
4 ...
6 ...
7 }
9 ...
10 }
11 ...
12 }
No pacote “GereEscola”, vamos criar a classe “Desempenho”, conforme mostrado, parcialmente,
no Código 25.
1 //Pacote
2 package com.mycompany.GereEscola;
4 //Importações
5 import com.mycompany.Matematica.Nota;
7 //Classe
9 //Atributos
12
13 //Métodos
14 public Desempenho () {
16 media = calcularMedia();
17 CR = calcularCoeficienteRendimento();
18 //media = nota.calculaMedia();
19 //CR = nota.caulculaCoeficienteRendimento();
20 }
21 }
A classe “Desempenho” é filha da classe “Nota”. Por isso, ela tem acesso aos métodos
“calcularMedia” (público) e “calcularCoeficienteRendimento” (protegido) de “Nota” mesmo estando
em outro pacote.
Observando a linha 11, vemos que ela também possui um objeto do tipo “Nota”, instanciado na
linha 15.
PERGUNTA
Sendo assim, será que poderíamos comentar as linhas 15 e 16, substituindo-as pelas linhas 18 e
19? A resposta é não.
Se descomentarmos a linha 18, não haverá problema, mas a linha 19 irá causar erro de
compilação.
1 //Pacote
2 package com.mycompany.GereEscola;
3 ...
6 ...
9 CR = des. calcularCoeficienteRendimento();
10 ...
11 }
12 ...
13 }
VERIFICANDO O APRENDIZADO
1. SOBRE OS MÉTODOS “EQUALS” E “HASHCODE” DA CLASSE
“OBJECTS”, PODEMOS AFIRMAR QUE:
A) Mudanças em “equals” podem ensejar mudanças em “hashCode”, mas o inverso nunca ocorre.
B) Como “equals” é herdado por todos os objetos, não necessita ser modificado para estabelecer
a igualdade de objetos.
E) Objetos iguais terem código hash distintos viola o contrato geral de “hashCode”.
A) I.
B) II.
C) III.
D) I e II.
E) II e III.
GABARITO
Na linguagem Java, o contrato geral do método “hashCode” afirma que objetos iguais devem ter
códigos hash iguais”.
MÓDULO 3
O módulo contempla, também, o estudo do modificador “final” e seu impacto, assim como as
interações com as variáveis da super e subclasse.
Se você revir os métodos da classe “Pessoa”, notará que sempre buscamos dar um
comportamento genérico. Veja, como exemplo, o caso do método “atualizarID”, mostrado no
Código 4. Para a classe “Pessoa” nenhuma crítica é feita ao se atualizar
o atributo “identificador”,
ao contrário de suas subclasses “Fisica” e “Juridica”, que aplicam teste para verificar se o
identificador é válido.
Apesar de “atualizarID” ser uma generalização de comportamento, se você refletir bem, notará que
a implementação é provavelmente inútil. Qualquer caso de instanciação num caso real deverá
fazer uso das versões especializadas.
Mesmo no caso de um aluno que ainda não possua CPF, é mais razoável prever isso no
comportamento modelado pela versão especializada de “atualizarID” da subclasse “Pessoa”, pois
esse aluno é “pessoa física”. Podemos esperar que, em
algum momento, ele venha a ter um CPF
e, se usar essa abordagem, manteremos a coerência, evitando instanciar objetos da classe
“Pessoa”.
ATENÇÃO
Pensando em termos de flexibilidade, o que esperamos de “Pessoa” é que ela defina o contrato
comum a todas as classes derivadas e nos forneça o comportamento comum que não necessite
de especialização.
Além disso, há outra consideração importante. Se concebemos nosso modelo esperando que a
superclasse nunca seja instanciada, seria útil se houvesse uma maneira de se impedir que isso
aconteça, evitando assim um uso não previsto
de nosso código.
A solução para o problema apontado é a classe abstrata, ou seja, aquela que não admite a
instanciação de objetos. Em oposição, classes que podem ser instanciadas são chamadas
concretas.
O propósito de uma classe abstrata é, justamente, o que mencionamos antes: fornecer uma
interface e comportamentos (implementações) comuns para as subclasses. A linguagem Java
implementa esse conceito por meio da instrução “abstract”.
Em Java, uma classe é declarada abstrata pela aplicação do modificador “abstract” na declaração.
Isto é, podemos declarar a classe “Pessoa” como abstrata simplesmente fazendo “public abstract
class Pessoa {...}”.
Já se a instrução “abstract” for aplicada a um método, este passa a ser abstrato. Isso quer dizer
que ele não pode possuir implementação, pois faz parte do contrato (estrutura) fornecido para as
subclasses.
Deste modo, a classe deverá, obrigatoriamente, ser declarada abstrata também. No exemplo em
questão, transformamos “atualizarID” em abstrato, declarando-o “protected abstract void
atualizarID (String identificador)”.
Observe que, agora, o método não pode possuir corpo.
Uma nova versão da classe “Pessoa” é mostrada no Código 27. Nessa versão, o método
“atualizarID” é declarado abstrato, forçando a declaração da classe como abstrata. Entretanto, o
método “recuperarID” é concreto, uma vez que não há
necessidade para especializar o
comportamento que ele implementa.
1 //Classe
3 ...
5 ...
8 return this.identificador;
9 }
10 ...
11 }
Em Java, o efeito de declarar uma classe como abstrata é impedir sua instanciação. Quando um
método é declarado abstrato, sua implementação é postergada. Esse método permanecerá sendo
herdado como abstrato até que alguma subclasse
realize sua implementação. Isso quer dizer que
a abstração de um método se propaga pela hierarquia de classes. Por extensão, uma subclasse
de uma classe abstrata será também abstrata a menos que implemente o método abstrato da
superclasse.
Apesar de mencionarmos que uma classe abstrata fornece, também, o comportamento comum,
isso não é uma obrigação. Nada impede que uma classe abstrata apresente apenas a
interface ou apenas a implementação. Aliás, uma
classe abstrata pode ter dados de instância e
construtores.
1 //Pacotes
2 package com.mycompany.GereEscola;
3 //Importações
4 import java.util.Calendar;
6 //Atributos
8 //Método main
13 data_nasc.set(1980 , 10 , 23);
18 ref [ 1 ].atualizarID("43.186.666/0026-32");
19 }
20 }
A linha 7 do Código 28 cria um vetor de referências para objetos do tipo “Pessoa”. Nas linhas 14 e
16, são instanciados objetos do tipo “Fisica” e “Juridica”, respectivamente. Esses objetos são
referenciados pelo vetor “ref”. Nas linhas
17 e 18, é invocado o método “atualizarID”, que é
abstrato na superclasse, mas concreto nas subclasses.
Esse é um dos comportamentos comuns que mencionamos antes e, por isso, ele está na
superclasse. Mas se não há motivo para que esse método possa ser rescrito por uma subclasse, é
desejável que possamos impedir que isso ocorra inadvertidamente.
Felizmente, a linguagem Java
fornece um mecanismo para isso. Trata-se do modificador “final”.
Esse modificador pode ser aplicado à classe e aos membros da classe. Diferentemente de
“abstract”, declarar um método “final” não obriga que a classe seja declarada “final”. Porém, se
uma classe for declarada “final”, todos
os seus métodos são, implicitamente, “final” (isso não se
aplica aos seus atributos).
Métodos “final” não podem ser redefinidos nas subclasses. Dessa forma, se tornarmos
“recuperarID” “final”, impediremos que ele seja modificado, mesmo por futuras inclusões de
subclasses. Esse método permanecerá imutável ao longo de
toda a hierarquia de classes.
Métodos “static” e “private” são, implicitamente, “final”, pois não poderiam ser redefinidos de
qualquer forma.
COMENTÁRIO
Vimos que, quando aplicado à classe, todos os seus métodos se tornam “final”. Isso quer dizer
que nenhum deles poderá ser redefinido. Logo, não faz sentido permitir que essa classe seja
estendida, e a linguagem Java proíbe que uma
classe “final” possua subclasses. Contudo, ela
pode possuir uma superclasse.
Quando aplicada a uma variável, “final” irá impedir que essa variável seja modificada e exigirá sua
inicialização. Esta pode ser feita junto da declaração ou no construtor da classe. Quando
inicializada, qualquer tentativa de modificar
seu valor irá gerar erro de compilação.
Como podemos ver no Código 29, uma classe abstrata pode ter membros “final”. A variável “dias
letivos” foi declarada “final” (linha 9) e, por isso, precisa ser inicializada, o que é feito na linha 14,
no construtor da classe. A não
inicialização dessa variável assim como a tentativa de alterar seu
valor irão gerar erro em tempo de compilação.
2 package com.mycompany.GereEscola;
3 //Importações
5 //Atributos
9 //Métodos
11 this.dias_letivos = dias_letivos;
12 }
15 }
17 return freq;
18 }
20 }
O método “calcularFrequencia” (linha 13) foi declarado “final”, não podendo ser redefinido. Mas,
como dito, a classe não é “final” e pode, então, ter classes derivadas.
Vamos começar lembrando que, quando uma variável é declarada “private”, ela se torna
diretamente inacessível para as classes derivadas. Como vimos, nesse caso, elas são
implicitamente “final”.
Elas ainda podem ter seu valor alterado, mas isso só pode ocorrer por métodos públicos ou
privados providos pela superclasse. Pode soar contraditório, mas não é. As atualizações, feitas
pelos métodos da superclasse, ocorrem no contexto
desta, no qual a variável não foi declarada
“final” e nem é, implicitamente, tomada como tal.
O caso de variáveis privadas é óbvio. Vejamos o que ocorre com as variáveis públicas e
protegidas. Como, no escopo da herança, as duas se comportam da mesma maneira, quer dizer,
como ambas são plenamente acessíveis entre as classes
pai e filha, trataremos, apenas, de
variáveis protegidas. O mesmo raciocínio se aplicará às públicas.
3 public Base ( ){
4 var_base = -1;
5 }
7 this.var_base = valor;
8 }
11 }
13 ((Derivada)this).var_der = valor;
14 }
18 }
3 public Derivada ( ){
8 this.var_der = valor;
9 }
12 }
13 }
2 //Atributos
4 //Métodos
8 ref.imprimirVarBase();
12 ref.imprimirVarSub();
14 ref.imprimirVarDer();
15 }
16 }
3 O valor de var_base eh -2
Em primeiro lugar, o código mostra que “var_base” representa uma variável compartilhada por
ambas as classes da hierarquia. Então, a mudança através de uma referência para a subclasse
afeta o valor dessa variável na superclasse. Isso
é esperado, pois é o mesmo espaço de memória.
A linha 6 do Código 32 deixa isso claro. Ao instanciar o objeto do tipo “Derivada”, seu construtor é
chamado. A primeira ação é imprimir o valor da variável “var_base” da superclasse. Como a
classe pai é instanciada primeiro, “var_base”
assume valor -1. Em seguida, é sobrescrita na classe
derivada com o valor -2, e uma nova impressão do seu conteúdo confirma isso.
VERIFICANDO O APRENDIZADO
1. UMA CLASSE ABSTRATA EM JAVA É DECLARADA PELO USO DO
MODIFICADOR ABSTRACT. ANALISE AS OPÇÕES ABAIXO E MARQUE A
CORRETA SOBRE O ASSUNTO.
A) Se declararmos uma classe como abstrata, seus métodos também deverão ser declarados
abstratos.
C) Uma classe abstrata admite herança múltipla se as superclasses também forem abstratas.
D) Uma classe concreta não pode possuir referência para uma classe abstrata.
2. //ATRIBUTOS
4.
5. //MÉTODOS
9. }
10. }
D) A linha 3 irá gerar erro de compilação, pois está instanciando uma variável final.
GABARITO
1. Uma classe abstrata em Java é declarada pelo uso do modificador abstract. Analise as
opções abaixo e marque a correta sobre o assunto.
Uma classe abstrata não permite a instanciação, mas não altera as regras de herança. Assim, ela
pode tanto estender uma classe concreta quanto ser estendida por uma.
2. //Atributos
4.
5. //Métodos
9. }
10. }
A variável “ref” é uma referência para um objeto do tipo Escola. Ela é final, não o objeto. Assim,
ela precisa ser inicializada e não pode referenciar outra instância, por isso a tentativa feita em 8
gera erro de compilação.
MÓDULO 4
Uma interface é um elemento que permite a conexão entre dois sistemas de natureza distintos,
que não se conectam diretamente.
EXEMPLO
Um teclado fornece uma interface para conexão homem-máquina, bem como as telas gráficas de
um programa, chamadas Graphic User Interface (GUI) ou Interface Gráfica de Usuário, que
permitem a ligação entre o
usuário e o software.
O paradigma de programação orientada a objetos busca modelar o mundo real por meio de
entidades virtuais. Já discutimos as classes e os objetos como entidades que fazem tal coisa. A
interface tem o mesmo propósito.
EXEMPLO
Imagem: Shutterstock.com
Esta é toda a possibilidade de interação física com o mouse. A interface de interação com o
mouse é exatamente o que acabamos de descrever. Ela é uma especificação das interações
possíveis com o dispositivo, que, no entanto, não define
o comportamento. Sabemos que
podemos pressionar o botão direito para interagir, mas o comportamento que isso desencadeará
dependerá do programa em uso.
Trazendo nosso exemplo para o software, podemos criar um mouse virtual com o intuito de fazer
simulações por exemplo. Nesse caso, para que nosso mouse represente o modelo físico,
devemos impor que sua interface de interação
com o usuário seja a mesma.
Dessa forma, independentemente do tipo de mouse modelado (ótico, laser, mecânico), todos terão
de oferecer a mesma interface e versão ao usuário: detecção de movimento, detecção de
pressionamento dos botões 1, 2 e 3 e detecção de movimento
da roda. Ainda que cada mouse
simule sua própria versão do mecanismo.
Podemos, então, dizer que uma interface no paradigma OO é uma estrutura que permite garantir
certas propriedades de um objeto. Ela permite definir um contrato padrão para a interação, que
todos deverão seguir, isolando do mundo exterior
os detalhes de implementação.
Uma definição de interface pode ser dada pelo conjunto de métodos a seguir. Qualquer
programador que deseje estender nosso mouse deverá obedecer a esse contrato.
PARTICULARIDADES DA “INTERFACE”
Em C++, o conceito de interface é suportado por meio de classes virtuais puras, também
chamadas classes abstratas. A linguagem Java possui uma entidade específica chamada
“Interface”. A sintaxe para se declarar uma
interface em Java é muito parecida com a declaração
de classe: “public interface Nome { ...}”. No entanto, uma interface não admite atributos e não pode
ser instanciada diretamente.
Ela é um tipo de referência e somente pode conter constantes, assinaturas de métodos, tipos
aninhados, métodos estáticos e default. Apenas os métodos default e estático podem possuir
implementação. Tipicamente,
uma interface declara um ou mais métodos abstratos que são
implementados pelas classes.
7 }
3 String recuperarNome ( );
4 String recuperarNacionalidade ( );
5 String recuperarNaturalidade ( );
7 int recuperarIdade ( );
8 int retornaTipo ( );
10 }
2 //Atributos
3 int idade;
7 if ( !nome.isBlank() )
8 this.nome = nome;
9 else
11 }
13 return this.nome;
14 }
16 this.identificador = identificador;
17 }
19 return this.identificador;
20 }
23 }
25 if ( id.isBlank() || id.isEmpty() )
26 return false;
27 else
28 return true;
29 }
31 return this.nacionalidade;
32 }
34 return this.naturalidade;
35 }
38 }
40 return this.idade;
41 }
43 return 0;
44 }
46 int lapso;
51 return lapso;
52 }
53 }
Veja que na linha 1 do Código 35 foi declarado que a classe “Pessoa” implementará as interfaces
“iPessoa” e “Identificador”.
Quando uma classe implementa uma ou mais interfaces, ela deve implementar todos os métodos
abstratos das interfaces.
E de fato é o que ocorre. A classe, contudo, não pode alterar a visibilidade dos métodos da
interface. Assim, métodos públicos não podem ser tornados protegidos por ocasião da
implementação.
Lembre-se de que uma interface define um contrato. Sua finalidade é proporcionar a interação
com o mundo exterior. Por isso, não faz sentido a restrição de acesso aos métodos da interface.
Ou seja, numa interface, todos os métodos são públicos, mesmo se omitirmos o modificador
“public”, como no Código 33 e Código 34. Mais ainda, em uma interface, todas as declarações de
membro são implicitamente estáticas e
públicas.
A declaração de um membro em uma interface oculta toda e qualquer declaração desse membro
nas superinterfaces.
EXEMPLO
Podemos fazer declarações aninhadas, isto é, declarar uma interface no corpo de outra. Nesse
caso, temos uma interface aninhada. Quando uma interface não é declarada no corpo de outra,
ela é uma interface do nível mais alto (top level interface). Uma interface também pode conter uma
classe declarada no corpo.
Observando o Código 33, vemos, na linha 2, que a interface possui um atributo estático
(“tamanho_max”). Esse é um caso de atributo permitido em uma interface. Também não é possível
declarar os métodos com o corpo vazio em uma interface.
Imediatamente após a assinatura, a
cláusula deve ser fechada com “;”. Java também exige que sejam utilizados identificadores nos
parâmetros dos métodos, não sendo suficiente informar apenas o tipo.
Além do tipo normal de interface, existe um especial, o Annotation, que permite a criação de tipos
de anotações pelo programador. Ele é declarado precedendo-se o identificador “interface” com o
símbolo “@”,
por exemplo: “@interface Preambulo {...}”. Uma vez definido, esse novo tipo de
anotação torna-se disponível para uso juntamente com os tipos bult-in.
Fica claro, pelo que estudamos até aqui, que as interfaces oferecem um mecanismo para ocultar
as implementações. São, portanto, mecanismos que nos permitem construir as API (Application
Programming Interface)
de softwares e bibliotecas.
VOCÊ SABIA
As API possibilitam que o código desenvolvido seja disponibilizado para uso por terceiros,
mantendo-se oculta a implementação, tendo muita utilidade quando não se deseja compartilhar o
código que realmente implementa
as funcionalidades, seja por motivos comerciais, de segurança,
de proteção de conhecimento ou outros.
Essa é uma ótima pergunta. Uma classe abstrata define um tipo abstrato de dado. Define-se um
padrão de comportamento segundo o qual todas as classes que se valham dos métodos da classe
abstrata herdarão da classe que os implementar.
Se você se recordar, uma classe abstrata pode possuir estado (atributos) e membros privados,
protegidos e públicos. Isso é consistente com o que acabamos de dizer e muito mais do que uma
interface pode possuir, significa que as classes
abstratas podem representar ou realizar coisas
que as interfaces não podem.
É claro que uma classe puramente abstrata e sem atributos terminará assumindo o
comportamento de uma interface. Mas elas não serão equivalentes, já que uma interface admite
herança múltipla, e as classes, não.
Uma interface é uma maneira de se definir um contrato. Se uma classe abstrata define um tipo
(especifica o que um objeto é), uma interface especifica uma ou mais capacidades. Veja o caso do
Código 33. Essa interface define as capacidades
necessárias para se manipular um identificador.
Não se trata propriamente de um tipo abstrato, pois tudo que ela oferece é o contrato.
Não há estado e nem mesmo comportamentos ocultos. Então, uma classe que implementar essa
interface proverá o comportamento especificado pela interface.
ATENÇÃO
Cuidado para não confundir a linha 2 com um estado da instância, pois não é. Além de essa
variável ser declarada como final, não há métodos que atuam sobre ela para modificar seu estado.
Esclarecidas as diferenças entre classes abstratas e interfaces, resta saber quando usá-las. Essa
discussão é aberta, e não há uma regra. Isso dependerá muito de cada caso, da experiência do
programador e da familiaridade com o uso
de ambas. Mas, com base nas diferenças apontadas,
podemos estabelecer uma regra geral.
Quando seu objetivo for especificar as capacidades que devem ser disponibilizadas, a interface é
a escolha mais indicada.
Quando estiver buscando generalização de comportamento e compartilhamento de código e
atributos comuns (um tipo abstrato de dado), classes abstratas surgem como a opção mais
adequada.
Veja a Figura 3 e a Figura 4. Ambas mesclam o uso de interfaces e classes abstratas. Na Figura 3,
as interfaces “Set”, “List” e “Queue”, para citar apenas alguns, definem as capacidades que estão
ligadas
ao contrato que deverá ser estabelecido para a estrutura de dados com que cada uma se
relaciona.
USO DE INTERFACES
Vimos a declaração das interfaces “Identificador” e “iPessoa” e da classe “Pessoa”, que as
implementa, respectivamente no Código 33, no Código 34 e no Código 35. Nota-se que todos os
métodos abstratos de ambas as interfaces foram implementados
em “Pessoa”. O método “main”
do programa, mostrado no Código 36, permite explorar seu uso.
2 //Atributos
5 //Métodos
8 refIdt.atualizarID("M-1020/001");
9 System.out.println ( refIdt.recuperarID() );
12 refiPessoa.atualizarNome("João Batista");
13 System.out.println(refiPessoa.recuperarNome());
14 }
15 }
Nas linhas 3 e 4 do Código 36, declaramos duas variáveis, uma que referencia “Identificador” e
outra “iPessoa”. Recorde-se de que dissemos que uma interface não admite instanciação direta,
mas nenhuma restrição fizemos quanto a se
ter uma variável declarada como referência para a
interface. E, de fato, na linha 8, não estamos instanciando a interface, mas a classe “Pessoa”. O
objeto instanciado, contudo, será referenciado pela variável “refIdt”.
ATENÇÃO
Podemos usar uma variável que guarda referências do tipo da interface para referenciar objetos
da classe que a implementa. Entretanto, não é suficiente que a classe forneça o comportamento
dos métodos abstratos, ela
precisa explicitamente declarar implementar a interface.
Uma vez instanciado o objeto, podemos invocar o comportamento dos métodos especificados por
“Identificador”. Mas apenas esses métodos e os que a interface “Identificador” herda estarão
disponíveis nesse caso.
Não é possível fazermos a atribuição de “refIdt” para “refiPessoa”, pois os tipos não são
compatíveis. Todavia, se fizermos um typecasting, como o mostrado na linha 12, a atribuição se
tornará possível. Agora, podemos usar
a variável “refiPessoa” e acessar os métodos dessa
interface que foram implementados em “Pessoa”.
É claro que, se tivéssemos usado uma variável do tipo “Pessoa” para referenciar o objeto
instanciado, todos os métodos estariam prontamente acessíveis.
A saída do Código 36 é:
1 M-1020/001
2 João Batista
CLASSES E OBJETOS EM JAVA
No vídeo a seguir, serão apresentados os conceitos de interfaces e exemplos do emprego de
herança em Java.
VERIFICANDO O APRENDIZADO
1. SOBRE INTERFACES EM JAVA, É CORRETO AFIRMAR QUE:
A AFIRMATIVA CORRETA É:
A) Somente I.
B) Somente II.
C) Somente III.
D) I e II.
E) II e III.
GABARITO
A finalidade de uma interface é prover métodos abstratos que formam um contrato a ser seguido e
cuja implementação é oculta. Ela é formada por métodos implicitamente abstratos.
I) Podemos usar uma variável do tipo “iContabil” para referenciar um objeto do tipo
“Pessoa” e, nesse caso, teremos acesso aos métodos de “iContabil” e “iBalanco”.
II) Não é possível usar uma variável do tipo da superinterface para referenciar um objeto da
classe que implementa “iBalanco”.
A afirmativa correta é:
Uma classe que implementa uma interface deve implementar todos os seus métodos abstratos.
Os métodos abstratos da superinterface que não são ocultados pela subinterface são herdados
por ela e, por isso, também devem ser implementados.
CONCLUSÃO
CONSIDERAÇÕES FINAIS
Neste conteúdo, tivemos a oportunidade de aprofundar o estudo da linguagem Java. Fizemos isso
investigando, em maiores detalhes, a hierarquia de herança. Compreendemos como ocorre a
subtipagem e sua diferença para a subclasse,
assim como vimos a vinculação dinâmica de
métodos. Esses tópicos são essenciais para a melhor compreensão do polimorfismo e da
programação orientada a objetos.
Esses conceitos não são apenas uma base importante, eles são indispensáveis para qualquer
programador que deseje tirar proveito de tudo o que a linguagem Java tem a oferecer.
AVALIAÇÃO DO TEMA:
REFERÊNCIAS
ORACLE AMERICA INC. Java 10 Local Variable Type Inference. Oracle Developer Resource
Center, 2020.
ORACLE AMERICA INC. Lesson: Introduction to Collections (The JavaTM Tutorials >
Collections). Java Documentation, 2020.
ORACLE AMERICA INC. Object (Java SE 15 & JDK 15). Java Documentation, 2020.
ORACLE AMERICA INC. Primitive Data Types (The JavaTM Tutorials > Learning the Java
Language > Language Basics). The Java Tutorial, 2020.
EXPLORE+
Os princípios Solid formam um importante conjunto que todo bom programador deve dominar.
Nesse módulo vimos apenas um, o princípio da substituição. Sugerimos que você procure e
domine, também, os demais. Para isso, uma busca
como os termos princípios Solid ou com cada
um deles deve lhe trazer bons resultados.
As coleções, em Java, são outras ferramentas que merecem ser dominadas por quem quer
melhorar suas habilidades. Uma boa fonte de pesquisa sobre cada tipo de coleção é a
documentação Java disponibilizada pela Oracle.
Por fim, estude os códigos da API das coleções para aumentar o entendimento de como interfaces
funcionam e se integram com as classes concretas e abstratas. A documentação de Java da
Oracle é um bom ponto de partida!
CONTEUDISTA
Marlos de Mendonça Corrêa
CURRÍCULO LATTES