Hoje
nossa conversa se concentra nas minúcias inerentes e consequentes da
Herança na Orientação a Objetos e no Java. Aproveitaremos também para
aperfeiçoar o sentido pelo qual trabalhamos e projetamos sistemas
através do recurso da Herança. Ao final desse estudo terminaremos nosso
encontro com um simples caso onde o uso da Generalização em conjunto com
Classes Abstratas oferece poder ao projetista Java.

Herança Múltipla
Quando
pensamos em herança logo somos remetidos ao nosso dia-a-dia onde um
filho herda características do pai e mãe. Na modelagem UML podemos
também expressar esse tipo de comportamento através da Herança Múltipla.

A
Herança Múltipla é quando uma subclasse herda atributos e métodos de
duas ou mais superclasses ao mesmo tempo. Pensando um pouco na modelagem
UML ou num projeto real, onde poderíamos considerar essa alternativa
como solução, podemos visualizar a seguinte situação: uma classe Celular herda ao mesmo tempo as funcionalidades de uma CameraDigital, Relogio e (ah, sim... claro) de um Telefone.

Essa seria uma possível solução de modelagem.
Vamos codificá-la no Java?
Poisé... o Java não suporta Herança Múltipla!

Repetindo:
não podemos trabalhar com Herança Múltipla no Java. Isso é uma
limitação da linguagem. Pense em limitação no seguinte sentido: os
projetistas da linguagem Java optaram em não trabalhar com esse recurso,
por opção. Não é necessariamente uma função que ele não pode oferecer e sim uma função que optaram em não incluir na sua especificação.
Mas isso não é o fim do mundo! Existem
formas elegantes de resolver essa limitação da linguagem. Poderíamos
por exemplo usar o conceito de composição, onde o objeto do tipo Celular delegaria operações à objetos internos do tipo CameraDigital, Telefone e Relogio. Ou seja, Celular seria composto por objetos dos tipos CameraDigital, Telefone e Relogio.

Certamente,
caso o analista já tenha como requisito não funcional o fato de se
trabalhar com Java num projeto, em tempo de análise ele já deve
descartar o uso da herança múltipla e adotar soluções alternativas.
Durante a aula presencial surgiu uma dúvida interessante sobre esse assunto:
Suponha que uma classe B herde de uma classe A. Uma classe C não poderia herdar da classe B?

Pessoal,
Herança Múltipla não se aplica a esse caso. O diagrama acima expõe que a
classe C seria “neta” da classe A e não que a classe C herda, ao mesmo
tempo, de duas classes distintas. A hierarquia acima é suportada pelo
Java. Aproveitando o assunto, a modelagem abaixo também é suportada pelo
Java.

Aqui, duas classes são especializações da superclasse A. Isso não é herança múltipla.
Esses conceitos são fáceis, mas precisam ser compreendidos, senão...

Vamos ao próximo assunto!
Quem é o Object?
Vejam o código abaixo.
public class PessoaFisica extends Pessoa {
private String cpf;
public String getCpf() { return cpf; }
}
Observando o código acima, podemos notar facilmente que a classe PessoaFisica é subclasse da classe Pessoa. Já sabemos que isso permite à classe PessoaFisica herdar todos os atributos, relacionamentos e métodos da superclasse Pessoa. Observe então o código abaixo.
public class Pessoa {
private String nome;
public String getNome() { return nome; }
}
A pergunta é: quem é a superclasse da classe Pessoa?

Isso não é verdade Mano! Toda classe no Java herda da superclasse Object!

É isso mesmo. No Java, todas as classes do sistema, inclusive aquelas que nós programamos até agora, são subclasses da classe Object. No caso da classe Pessoa, mesmo não tendo colocado a sintaxe extends o Java inclui (implicitamente) o comando extends Object quando gera os byte-codes. Note que isso não impede de colocarmos explicitamente esse código na classe:
public class Pessoa extends Object {
private String nome;
public String getNome() { return nome; }
}
Acima, o resultado será o mesmo.
Sabendo dessa característica da linguagem, vamos agora aos benefícios.
A classe Object possui
alguns métodos que são úteis, de certa forma, a todos as outras classes
que precisamos trabalhar. Ela possui, por exemplo, os métodos equals() e toString() - além de outros.
O método equals() recebe como parâmetro um Object e retorna um booleano. Quando o objeto passado para o parâmetro for exatamente o mesmo objeto de que o método equals() foi invocado, o retorno será true. Caso contrário, o retorno será false. O entendimento é: esse método deve ser utilizado para comparar os objetos.
Ainda pensando na classe Pessoa, vejamos alguns exemplos abaixo.
public class Pessoa extends Object {
private String nome;
public String getNome() { return nome; }
public void setNome(String n) { nome = n; }
/*
* Método main de execução
*/
public static void main(String args[]) {
Pessoa p1 = new Pessoa();
p1.setNome( "Guilherme" );
Pessoa p2 = new Pessoa();
p2.setNome( "Guilherme" );
// Veja a comparacao entre os objetos
if ( p1.equals( p2 ) ) {
System.out.println( "Sao iguais!" );
} else {
System.out.println( "Sao diferentes!" );
}
}
}
Veja a execução dessa classe.

Observe que mesmo que as instâncias p1 e p2 tenham o mesmo nome (aliás, muito bonito o nome), eles são objetos diferentes. Daí o texto “Sao diferentes!” foi impresso. Veja agora o caso abaixo.
public class Pessoa extends Object {
private String nome;
public String getNome() { return nome; }
public void setNome(String n) { nome = n; }
/*
* Método main de execução
*/
public static void main(String args[]) {
Pessoa p1 = new Pessoa();
p1.setNome( "Guilherme" );
Pessoa p2 = p1;
// Veja a comparacao entre os objetos
if ( p1.equals( p2 ) ) {
System.out.println( "Sao iguais!" );
} else {
System.out.println( "Sao diferentes!" );
}
}
}
Veja que logo na declaração de p2 ele recebe a mesma instância de p1. Isso significa que as variáveis de instância p1 e p2 apontam para o mesmo objeto na Heap. Veja a execução.

Agora tivemos a reposta “Sao iguais!”. Para entendimento geral, é importante compreender que as variáveis locais p1 e p2 declaradas no método main() são ponteiros para os objetos que estão na Heap - e não os objetos em si. O que o método equals() compara é justamente se esses ponteiros apresentam valores iguais - ou seja, se apontam para o mesmo objeto.
O método toString(),
também muito utilizado na prática, informa justamente qual o valor
guardado nos ponteiros. Ou seja, se tentássemos chamar o método toString() desse mesmo código, teríamos os mesmos valores. Veja o exemplo abaixo:
public class Pessoa extends Object {
private String nome;
public String getNome() { return nome; }
public void setNome(String n) { nome = n; }
/*
* Método main de execução
*/
public static void main(String args[]) {
Pessoa p1 = new Pessoa();
p1.setNome( "Guilherme" );
Pessoa p2 = p1;
Pessoa p3 = new Pessoa();
System.out.println( "Valor da variavel p1: " + p1.toString() );
System.out.println( "Valor da variavel p2: " + p2.toString() );
System.out.println( "Valor da variavel p3: " + p3.toString() );
}
}
Existe expectativa que na execução, os valores de p1 e p2 sejam iguais. Já o p3 está apontando para um objeto diferente, daí seu valor será diferente. Vamos ver a execução.

Confirmando nossa estimativa, o valor Pessoa@3e25a5 está aguardado nas variáveis p1 e p2. Já a variável p3 mantém o valor Pessoa@19821f.
Você deve estar pensando: qual é a utilidade do toString()?
Por enquanto, nenhuma - confesso! Talvez na próxima aula (ou na seguinte), onde estaremos abordando o Polimorfismo, ele será de grande utilidade.
Nota: Além desses dois, existem ainda outros métodos úteis como getClass() e clone(). Trataremos deles quando for conveniente.

Usando os comandos THIS e SUPER
Fato: A palavra THIS é utilizada no Java para indicar a atual instância do objeto que está na memória.
Tudo bem, e daí?
Isso significa que, sempre que utilizarmos a referência this estaremos
pedindo ao Java que busque atributos ou métodos que foram declarados no
próprio objeto instanciado. A utilidade prática disso pode ser
entendida na seguinte situação:
public class Pessoa {
private String nome;
public String getNome() { return nome; }
public void setNome(String nome) {
nome = nome;
}
}
Veja os comandos em negrito! Note que existe um atributo chamado nome na classe Pessoa e que existe um parâmetro chamado nome na definição do método setNome(). Quando executamos o comando nome = nome;, qual é a variável do parâmetro e qual é o atributo do objeto?
Da forma como foi definido esse código, não temos como identificar!

Calma pessoal! Isso não é o fim do mundo. Para resolver esse impasse, temos duas alternativas:
1. modificar o parâmetro do método setNome() para que ele não gere conflito de nome com nenhum atributo da classe Pessoa, ou
2. de forma mais elegante, usar a palavra reservada this antes do atributo do objeto.
Como assim?
public void setNome(String nome) {
this.nome = nome;
}
Nesse código, quando chamamos simplesmente a palavra nome (de azul), estamos referenciando a variável nome do parâmetro do método setNome(). Quando chamamos this.nome (em vermelho), estamos referenciando o atributo nome da instância da classe Pessoa que está na memória. Dessa forma, estamos guardando o valor passado pelo parâmetro no valor do atributo - um típico método Setter, que já vimos em aulas passadas.
Essa
possibilidade é muito útil para facilitar a legibilidade do código.
Isso ficaria mais explícito se estivéssemos trabalhando com métodos
grandes.
public void setNome(String nome) {
if ( nome != null ) {
System.out.println( "Nome invalido!" );
}
if ( nome.equals("") ) {
System.out.println( "Nome invalido!" );
}
if ( nome.equals("Guilherme") ) {
this.salario = 15000; // por ano ou por mes?? rsrs
this.idade = 28;
}
if ( nome.equals("Jeanne Calment") ) {
this.idade = 122;
}
this.nome = nome;
}
Veja que quando usamos a sintaxe this fica
explícito (mesmo sem conhecermos a classe desse método) que estamos
fazendo referência aos atributos do objeto. Se adotarmos essa prática
sempre, facilita o trabalho de leitura do código.
Observação: a sintaxe this também pode (e deve) ser utilizada para indicarmos explicitamente a chamada de métodos definidos no próprio objeto.
Vejamos agora ao exemplo a seguir.
public class Pessoa {
public String nome;
public int idade;
}
class PessoaFisica extends Pessoa {
public String nome;
public void setNome(String nome) {
// 1- Como referenciar o parâmetro nome?
// 2- Como referenciar o atributo nome desse objeto?
// 3- Como referenciar o atributo nome definido na superclasse?
// 4- Como referenciar o atributo idade definido na superclasse?
}
}
Respondendo as perguntas (do código):
1. Como já vimos, basta apenas utilizar a palavra nome sozinha. Daí fazemos referência ao parâmetro nome.
2. Basta utilizarmos a sintaxe this.nome - como também já vimos.
3. Bom... ai entra um conceito novo: o uso da sintaxe super! O comando super é utilizado quando queremos fazer referência a um atributo da superclasse. Como nome também foi definido na superclasse, seremos obrigados a utilizar o comando super para referenciá-lo.
4. Note que nesse caso, o atributo idade só está definido na superclasse Pessoa. Isso significa dizer que podemos referenciá-lo como: idade, this.idade ou ainda super.idade. Mas como isso é possível? Veja que não há conflito entre um atributo idade nos parâmetros do método setNome(), daí se colocarmos somente o nome do atributo idade já
estamos referenciando o atributo do objeto. Nesse mesmo raciocínio veja
que não há conflito desse atributos entre as definições da superclasse Pessoa e da subclasse PessoaFisica. Daí podemos usar indiscriminadamente this.idade ou super.idade para apontar para o mesmo atributo definido na superclasse.
Após essas considerações, vejamos como fica o código com os exemplos.
public class Pessoa {
public String nome;
public int idade;
public static void main(String args[]) {
PessoaFisica p = new PessoaFisica();
p.setNome( "esse e o parametro" );
}
}
class PessoaFisica extends Pessoa {
public String nome;
public void setNome(String nome) {
// 1- Como referenciar o parâmetro nome?
System.out.println( nome );
// 2- Como referenciar o atributo nome desse objeto?
this.nome = "esse e o atributo do objeto PessoaFisica";
System.out.println( this.nome );
// 3- Como referenciar o atributo nome definido na superclasse?
super.nome = "esse e o atributo do objeto Pessoa";
System.out.println( super.nome );
// 4- Como referenciar o atributo idade definido na superclasse?
idade = 50;
this.idade = 50;
super.idade = 50;
System.out.println( "Idade: " + idade );
}
}
Veja que criei uma situação interessante no exemplo acima: A classe Pessoa foi definida como pública e mantém, dentro de seu próprio escopo, um método main(). Já a subclasse PessoaFisica é uma herança da Pessoa e coloca os exemplos necessários para o teste, porém, foi definida com a visibilidade package - ou seja, sem o public. Esse código ficou assim porque implementei o exemplo em um único arquivo de nome Cliente.java - que é justamente o nome da classe pública do arquivo. Na prática vamos manter nossa ideia de criar o método main() numa
classe fora do contexto das classes do sistema real (numa classe
Executora, por exemplo) e criar as classes em arquivos separados (fica
mais organizado).

Classes Abstratas
Falaremos agora de um assunto muito importante: as classes abstratas!

Pessoal...
isso não tem nada a ver! O conceito de Abstração que vimos nas
primeiras apostilas foi sobre um dos pilares da Orientação a Objetos. O
assunto que vamos tratar aqui é outro!
Vamos então à incrível explicação sobre o que é uma classe abstrata:
Uma classe abstrata é aquela que não é concreta!

Incrível
né?! Poisé, mas é isso mesmo. Uma classe concreta é aquela cuja
instanciação é possível de ser realizada. Ou seja, poderíamos criar uma
classe concreta através do comando new.
public class ClasseConcreta {
}
public class Executora {
public static void main(String args[]) {
ClasseConcreta aaa = new ClasseConcreta();
}
}
Sobre
esse entendimento podemos facilmente entender que uma classe abstrata
não oferece a possibilidade de criarmos objetos! Ou seja, se definirmos
uma classe como abstrata, não poderemos criá-la via o comando new.
public abstract class ClasseAbstrata {
}
public class Executora {
public static void main(String args[]) {
ClasseAbstrata bbb = new ClasseAbstrata();
}
}
Veja que a sintaxe abstract foi colocada no início da definição da classe ClasseAbstrata. Esse é justamente o comando pelo qual podemos definir que uma classe é abstrata. No código da Executora tentamos instanciar um objeto a partir dessa classe. Ao compilarmos esses códigos, teremos a seguinte mensagem:

A mensagem de erro de compilação mostra claramente (em inglês, é claro) que não podemos instanciar um objeto da classe ClasseAbstrata pelo fato dela ser... poisé.... abstrata. Papo de maluco né?!

Em
alguns momentos da modelagem podemos entender que não seria coerente
que uma determinada classe fosse instanciada no sistema. Veja por
exemplo o caso abaixo.

Suponha que a modelagem acima tenha sido elaborada para compor um sistema de cadastro de clientes de uma empresa qualquer. Faria sentido existir um cliente que não fosse nem pessoa física, nem pessoa jurídica? Para
essa modelagem, não! Então para evitar equívocos quanto a criação dos
objetos desse sistema, o projetista pode (e deve) optar em definir a
superclasse Pessoa como
abstrata. Na UML, a definição de classes ou métodos abstratados é
representada pelo itálico. Veja a redefinição do modelo abaixo:

Note que a identificação da classe Pessoa e o método validarDocumento() ficaram dispostos numa fonte em itálico.
Ahhh sim!! Método abstratos são aqueles que não podem ser executados!
De
forma semelhante, talvez seja interessante que o projetista impeça a
utilização de um método em uma determinada classe. Para isso ele torna o
método abstrato. Nesse mesmo exemplo podemos enxergar uma utilidade
prática para isso: faz sentido, na classe Pessoa, validar o documento de identificação se ainda não sabemos qual documento é esse (CPF ou CNPJ)? Não! Só teremos como validar se um documento é válido ou não a partir do momento que as subclasses PessoaFisica e PessoaJuridica implementarem corretamente os atributos dos documentos e criarem o comportamento do método validarDocumento().
Confuso?! Vamos traduzir isso para o código Java. Veja primeiro a classe abstrata Pessoa.
public abstract class Pessoa {
private String nome;
private int idade;
// aqui entrariam outros métodos Getters e Setters necessários
public abstract void validarDocumento();
}
Mantive em negrito a sintaxe que definiu a classe Pessoa e o método validarDocumento() como abstratos. Sobre a sintaxe da classe abstrata, já havíamos discutido. O método, porém, além de manter o comando abstract, deve ser definido sem o código (e terminado com um ponto-e-vírgula). Repetindo: não podemos nem executar o método validarDocumento() (é claro, nem código ele tem!!), nem instanciar um objeto do tipo Pessoa. Vejamos agora a implementação da classe PessoaFisica.
public class PessoaFisica extends Pessoa {
private String cpf;
public void validarDocumento() {
// código que valida o valor do CPF
System.out.println( "CPF válido!" );
}
}
Como a classe PessoaFisica é uma subclasse de Pessoa e é concreta (não é abstrata), ela é obrigada a
implementar (criar o código) dos métodos abstratos da superclasse. Caso
contrário teríamos um erro de compilação. É claro: nesse caso, como se
trata de uma pessoa física, o método validarDocumento() deve apresentar o código de validação do CPF. Em contrapartida veja o código de PessoaJuridica.
public class PessoaJuridica extends Pessoa {
private String cnpj;
public void validarDocumento() {
// código que valida o valor do CNPJ
System.out.println( "CNPJ válido!" );
}
}
Como precisamos validar o CNPJ, espera-se que o código do método validarDocumento() seja diferente. Percebem a intenção de se criar métodos ou classes abstratas?
Quando precisamos definir uma classe pela qual não seja possível criar objetos, a definimos como abstrata!
Quando
precisamos postergar a implementação de um método para suas subclasses
(pois ainda não temos como implementá-lo na superclasse), o definimos
como abstrato.

Parece
bobeira, mas ao longo das próximas aulas perceberemos que as classes
abstratas e as interfaces (conceito ainda não aprendido... mas em breve o
veremos) são um dos recursos mais importantes da orientação a objetos.
Muitos padrões de projeto e boas práticas na programação Java são
possíveis somente pelo fato desses recursos existirem. Confesso que não é
simples esse entendimento, mas faremos um exercício de fixação para
iniciar essa caminhada - ainda nesse material!
Nota muito importante (decore isso e não esqueça mais): Sempre que uma classe possuir métodos abstratos ela deve também ser definida como abstrata! Isso é uma regra geral! Pronto!
Cuidado (não se confunda): O
contrário não é verdadeiro. Uma classe pode ser abstrata mesmo que não
possua métodos abstratos! Escreva isso em mármore, ok?!
Vamos fazer agora um exercício de fixação.
Exercício da Família de Animais
Antes de discutirmos sobre o diagrama, perdoe-me pela má modelagem do exemplo. Eu acho que eu faltei essa aula de biologia...
Ahh sim... Esse exercício não é by Guilherme!!
Eu busquei na web alguns exemplos legais do uso de classes abstratas e
encontrei esse ai. O original é mais bem detalhado. Recomendo como
leitura complementar:
Aqui trabalharemos nesse exemplo mais sucinto.

A ideia é simples: Veja que a superclasse Animal é abstrata. Além do nome do Animal e seus métodos Getter e Setter, existe um método abstrato chamado falar().
Nesse método deveríamos programar uma reprodução sonora (via
onomatopéia) equivalente ao tipo de animal, todavia, ainda não temos
como fazer isso nessa superclasse. Também não faz sentido definir um
animal genérico. Por esse motivo definimos essa classe e método como
abstratos!
Descendo na hierarquia chegamos às classes Mamifero e Ave. Ambas já especializam mais a classe genérica Animal, mas ainda não o suficiente para criarmos objetos concretos. Por isso optei em definí-las como abstratas.
É muito importante entender que como essas classes não são concretas, não somos obrigados a implementar o método abstrato falar() da superclasse Animal.
Poderíamos tê-lo implementado, mas do ponto de vista dessa modelagem,
não faria sentido ainda - por exemplo, tanto a vaca quanto o lobo são
mamíferos, mas ambos emitem sons diferentes.
Por fim, nos limites inferiores da nossa definição chegamos a duas classes elegíveis para se tornarem concretas. Tanto em Cachorro quanto em Galinha já somos capazes de codificar o método falar().
Nesse exemplo também podemos considerar que instâncias de cachorros e
galinhas são possíveis e viáveis. Dessa forma optamos em colocá-los como
classes concretas e implementaremos seus métodos falar(). Lembre-se: a partir do momento que essas classes são concretas, somos obrigados a implementar os métodos abstratos.
Vocês já devem estar pensando no próximo passo!
Isso mesmo, preciso que vocês tentem codificar essas classes no Java!
Codificando o Exercício da Família de Animais
Vocês
já devem estar acostumados com essa imagem. Mas se por acaso você não
leu as aulas passadas, vou explicar: tente codificar esse modelo antes
de prosseguir com esse material, ok!?

Vamos primeiro codificar a classe Animal.
Aquivo Animal.java
public abstract class Animal {
private String nome;
public void setNome(String nome) {
this.nome = nome;
}
public String getNome() { return this.nome; }
public abstract void falar();
}
|
Vou agora mostrar, em conjunto, as classes Mamifero e Ave. Programe-as em arquivos separados!
Aquivo Mamifero.java
public abstract class Mamifero extends Animal {
}
|
Aquivo Ave.java
public abstract class Ave extends Animal {
}
|
Não se assuste! Essas classes são vazias mesmos. Só foram criadas para atender a modelagem da hierarquia.
Veja agora as classes concretas Cachorro e Galinha (arquivos separados, por favor).
Aquivo Cachorro.java
public class Cachorro extends Mamifero {
public void falar() {
System.out.println( "Au Au!" );
}
}
|
Aquivo Galinha.java
public class Galinha extends Ave {
public void falar() {
System.out.println( "Co co ri co!" );
}
}
|
Muito bem! Veja que somente nas classes concretas foi possível definir o comportamento (código) do método falar().
O que você precisa visualizar nesse exemplo?
Primeiramente
atente-se na necessidade de postergar a definição do comportamentos de
métodos abstratos para as subclasses da hierarquia.
Essa
frase parece complicada, mas foi isso que codificamos nesse exercício. É
muito importante que para a próxima aula você compreenda essa
possibilidade.
Vou
deixar um gatilho de conhecimento sobre o assunto que trataremos no
futuro (muito próximo). Veja a classe executora que criei para rodar
esse exemplo da família de animas:
Aquivo ExemploPolimorfismo.java
public class ExemploPolimorfismo {
public static void main(String args[]) {
Animal a1 = new Cachorro();
Animal a2 = new Galinha();
a1.falar();
a2.falar();
}
}
|
Veja a execução do código:

Pense sobre o assunto!
Eu gostaria que vocês lessem esse código da classe ExemploPolimorfismo e identificassem algum ponto estranho, digamos assim. Guarde essa dúvida que vamos falar sobre isso na próxima aula.
Foi um prazer tê-los como companhia.
Um abraço, Guilherme Pontes
Nenhum comentário:
Postar um comentário