Mostrando postagens com marcador Curso Java. Mostrar todas as postagens
Mostrando postagens com marcador Curso Java. Mostrar todas as postagens

quarta-feira, 8 de agosto de 2012

Aula 13 - String, Construtores e Objetos Polimórficos


Senhoras e Senhores, nesse material entraremos no mundo misterioso do polimorfismo! Claro, falaremos também de outros assuntos tão quanto importantes: String e Constructor. Vamos lá.
A classe String
Já avançamos bastante nos conceitos básicos da orientação a objetos no contexto do Java e da UML. Eu tentei ao longo desse tempo esbarrar pouco em detalhes de sintaxe ou comandos da API da linguagem Java justamente para tentarmos fixar na quebra do paradigma da programação estruturada (para os que a conhecem) em relação à programação orientada a objetos. Todavia, faltam poucos passos para terminarmos esse material introdutório e já podemos nos dar o luxo de aproximar mais das ferramentas e componentes úteis no dia a dia de um programador. Veremos agora um pouco do que a classe String do Java tem para nos oferecer.
Antes de iniciarmos a apresentação de alguns detalhes dessa classe, você precisa ter em mente que a API (sigla de Application Programming Interface ou Interface de Programação de Aplicativos) do Java é imensa. Portanto lembre-se sempre dos três ensinamentos básicos do programador Java:
1- Não decore a API! Ela está disponível na Internet. Tudo bem, tudo bem!! Essa regra tem exceção se você for fazer concurso ou tirar uma certificação.
2- Não reinvente a roda! Se você precisar resolver algum algoritmo corriqueiro, busque-o na rede! A API provavelmente já oferece esse recurso para você.
3- Não tenha medo da API! Ela é simples de acessar, usar e entender (apesar de estar em inglês).
Hoje, na versão Java SE 7, o link de acesso da API é:
Veja abaixo um print screen com uma breve explicação.
A seta verde aponta para o frame que contém todos os pacotes do Java. Sei que ainda não falamos sobre pacotes, mas de forma breve imagine que são pastas onde as classes ficam guardadas (é isso mesmo, na prática). Caso você clique em um desses pacotes, os frames da seta vermelha e azul passarão a apresentar componentes presentes nesse pacote.
A seta vermelha mostra todas as classes acordando com o filtro da seta verde. Na apresentação default, todas as classes são exibidas. Caso você clique em uma das classes, o frame da seta azul passará a mostrar todos os detalhes dessa classe específica. Veja na imagem que foi clicada na classe String e, portanto, o frame da seta azul está exibindo os detalhes dela.
Por fim o frame da seta azul é capaz de exibir detalhes do pacote ou classe selecionados nos outros dois frames à esquerda da página. Caso esteja sendo exibida uma classe, mais abaixo desse frame (seta azul) você poderá visualizar todos os atributos, construtores e métodos (públicos) dela, com uma explicação abreviada e outra completa. Em alguns casos existem até exemplos.
Nota: na geração da API, o default é que apenas os elementos públicos de uma classes sejam exibidos na página. Outro detalhe muito interessante é que nós, meros mortais, também podemos gerar uma API das classes Java que programamos simplesmente acrescentando comentários de API no código. Ainda não vimos como fazer isso, mas já adianto que é extremamente simples e útil gerar a API no Java.
Bom, todo esse texto foi apenas para prepará-lo para a seguinte ideia:
A partir de agora, sempre que você precisar pesquisar quais métodos, atributos ou construtores, ou até mesmo quais classes estão disponíveis no Java, você recorrerá a API. Nunca se esqueça disso e torne essa prática um hábito (saudável, alias).
Ao que interessa...
A classe String no Java é utilizada para armazenar palavras. Qualquer palavra ou frase no Java poderá ser armazenada numa String. Note que String não é um tipo primitivo e sim uma classe!
Mas quantos caracteres cabem numa String? Não perdi muito tempo em entender que talvez seja mais fácil chegarmos no limite de memória do micro (ou da JVM) do que preencher toda a capacidade suportada pela classe String no Java. Se pensarmos de forma lógica, quando perguntamos o tamanho de uma String, o Java nos retorna um primitivo int. Como o int pode armazenar (na fração positiva do número) um valor máximo de 2.147.483.647, pode-se entender que esse seria o limite de caracteres que o String suporta. Ou seja, você poderá escrever muitos muitos muitos caracteres que ainda estará longe de alcançar esse limite. Abaixo postei um link de uma discussão sobre o assunto. Só recomendo a leitura para aqueles que querem dominar o mundo! Para os demais, bola pra frente!
Outro detalhe importante é que, diferente do char, uma String é delimitada por aspas duplas. Veja um trecho de código abaixo.
public class Principal {
  public static void main(String args[]) {
    String s1 = "minha casa é na lua...";
    String s2 = new String( "outra forma de criar uma String" );
    // Essa linha vai dar erro de compilação
    String erroDeCompilacao = 'outra String';
  }
}
Veja que podemos instanciar uma String apenas informando o seu valor diretamente entre as aspas duplas ou ainda através de uma inicialização corriqueira de objetos no Java (através da sintaxe new). Esse código não poderá ser compilado justamente por estarmos tentando criar uma String com aspas simples.
Vamos agora visualizar alguns métodos da classe String.
charAt(int index): Esse método retorna uma char (tipo primitivo) cujo índice é passado pelo parâmetro. O índice numa String funciona semelhante a um Array, onde o primeiro é zero e o último é igual ao tamanho da String menos 1.
equals(Object anObject): Esse método, assim como aquele que existe na classe Object, permite que comparemos os objetos. Há uma particularidade na comparação de objetos do tipo String: o equals() da String compara os valores dos caracteres presentes, de forma case sensitive, e retorna TRUE, caso estes sejam iguais. Vou mostrar um exemplo adiante.
equalsIgnoreCase(String anotherString): Para os casos onde devamos desprezar a diferença de maiúsculas e minúsculas para comparação, podemos utilizar esse método. Com ele, as frases “eu sou eu” e “Eu SOU eu” são iguais - ou seja, ele retornará TRUE.
getBytes(): Esse método retorna um Array de bytes (tipo primitivo) da String criada. Na prática ele não tem muita importância, todavia, caso você teve a curiosidade de estudar os elementos utilizados na classe Util.java que tem servido de apoio para nossos exercícios, verá que utilizei esse método. Com exceção das devidas apresentações da classe System, você já tem condições de tentar montar essa classe Util sozinho (talvez quase, mas o getBytes() você já conhece).
Nota: Na aula surgiu uma dúvida interessante sobre esse método: um byte é composto por 8 bits. Na String, todavia, os caracteres são guardados em unicode (16 bits). Como essa classe faz a conversão?
Ai ai ai!!! E agora, como vamos descobrir isso!?!
Simples: olhando na API! Veja abaixo um copy-paste do texto original.
Encodes this String into a sequence of bytes using the platform's default charset, storing the result into a new byte array.
The behavior of this method when this string cannot be encoded in the default charset is unspecified. The CharsetEncoder class should be used when more control over the encoding process is required.
Na prática significa que sempre seremos remetidos ao charset do sistema onde o programa está rodando. Caso não seja possível representar algum dos caracteres em tipo primitivo char, o comportamento desse método é desconhecido.
length(): Esse com certeza é um dos métodos mais importantes que a classe String nos oferece. Ele retorna um int com o tamanho da String. Em sistemas reais ele é muito utilizado na definição de regras do tipo “o nome do cliente deve ter no máximo 40 caracteres”. Veremos em breve um exemplo.
substring(int beginIndex, int endIndex): Esse outro método também é muito importante. Através dele podemos retornar uma substring de dentro de nossa String. O valor do primeiro parâmetro é o índice inicial da substring, inclusive. O segundo parâmetro define o índice final, exclusive. Isso significa dizer que o índice apontado pelo segundo parâmetro não participa da substring.
Veja por exemplo o código abaixo:
public class Principal {
  public static void main(String args[]) {
    //            01234567
    String str = "maracanã";
    /*
       Note que maracanã possui 8 letras, sendo que o último
       índice é igual a 7. Se tentarmos obter uma substring
       começando do índice 2 e terminando no índice 5, deveremos
       informar o segundo parâmetro como 6, pois a substring
       NÃO vai considerar o índice do segundo parâmetro (ou seja,
       é igual ao índice do segundo parâmetro menos um).
     */
     System.out.println( str.substring( 2 , 6 ) );
     // Esse comando vai exibir: raca
  }
}
Então, podemos por exemplo obter uma substring que contém exatamente os mesmos caracteres da String original através do comando:
str.substring( 0 , str.length() );
Veremos agora um exemplo de código que utiliza todos esses métodos.
public class Principal {
  public static void main(String args[]) {
    String str1 = "Eu sou uma String!";
    String str2 = "EU SOU UMA String!";
    // Esse comando vai mostrar o char E
    System.out.println( str1.charAt( 0 ) );
    /*
       As duas Strings apresentam as mesma frase,
       todavia, como o equals() é sensitivo ao
       tamanho do caracter (ou seja, a mesma letra
       maiúscula e minúscula são diferentes), as
       Strings são diferentes.
     */
     if ( str1.equals( str2 ) ) {
       System.out.println( "Sao iguais!" );
     } else {
       System.out.println( "Sao diferentes!" );
     }
    /*
       Nesse caso, elas são iguais!
     */
     if ( str1.equalsIgnoreCase( str2 ) ) {
       System.out.println( "Sao iguais!" );
     } else {
       System.out.println( "Sao diferentes!" );
     }
    /*
       Aqui serão exibidos os números (bytes) associados
       às letras da str1. Note que usei o comando print ao
       invés do println. A diferença é que o comando print
       não pula linha após a impressão no prompt.
     */
     byte[] meusBytes = str1.getBytes();
     for (int i = 0; i < meusBytes.length; i++)
       System.out.print( meusBytes[ i ] + " " );
     // Apenas para pular linha
     System.out.println();
     // Tamanho da str2
     System.out.println( "A str2 possui " + str2.length() + " caracteres!" );
     // Obtendo uma substring: sou
     System.out.println( str1.substring( 3 , 6 ) );
     /*
        Outros exemplos úteis.
        Veja que é possível concatenar duas Strings apenas com o sinal de +
        Veja que uma simples palavra dentro de aspas duplas já é considerada
        um objeto String para o Java, portanto, pode receber chamadas de métodos
      */
     String str3 = "Epa";
     System.out.println( str3 + " " + str3 );
     int tamanho = "Guilherme".length();
     System.out.println( tamanho );
  }
}
Pronto. Exercício! Encontre a definição desses métodos na API do Java. O link segue abaixo.
O que é um Constructor?
Ao longo desse tópico trataremos de algumas regras definitivas. Vamos à primeira:
Regra 1:
Um constructor (ou construtor, em português) é um método (por assim dizer) pelo qual um objeto que está sendo criado sempre passará. Ou seja, todo o objeto no Java que está sendo instanciado vai executar o código definido no seu construtor.
Regra 2:
A sintaxe do constructor é similar a dos métodos. Veja abaixo as regras para se criar um constructor. 
1. O constructor deve ter exatamente o mesmo nome da classe; 
2. Ele não deve conter nenhuma sintaxe para identificar o tipo de retorno - inclusive o void, também não é permitido. 
3. Ele pode ser criado com qualquer visibilidade, todavia, caso opte em colocar algo diferente de public, você estará limitando a criação de objetos dessa classe. Inclusive caso você opte em colocar como private, ninguém, exceto a própria classe, poderá criar um objeto desse.

Veja abaixo um exemplo de código.
public class Recipiente {
  int capMax;
  public Recipiente() {
    // Esse é o constructor
  }
}
Lembra da aula 5, onde fizemos um exercício cujo foi necessário a classe Recipiente (http://linubr.blogspot.com.br/2012/08/aula-5-lapidando-o-aprendizado-da.html)? Poisé, estou utilizando ela de propósito para uma explicação posterior. Veja que esse constructor foi definido com a visibilidade public. Tenha em mente que sempre que um objeto da classe Recipiente for criado, o código que colocarmos dentro do constructor será executado. Veja o próximo exemplo.
public class Recipiente {
  int capMax;
  public Recipiente() {
    System.out.println( "Ebaaa!!! Eu sou um novo recipiente!" );
  }
}
class Executora {
  public static void main(String args[]) {
    Recipiente r = new Recipiente();
  }
}
Note que coloquei uma impressão no prompt dentro dos comandos do constructor da classe Recipiente. A classe Executora simplesmente está criando um novo objeto Recipiente no método main(). Olhe o que ocorre na execução da classe Executora.
Exatamente! A mensagem foi exibida no prompt.
Mas espera ai!? Nós criamos vários objetos nos outros exercícios e em nenhum deles eu defini um constructor!
Regra 3:
Caso você não defina um constructor manualmente no código, o Java sempre definirá um para você. Isso significa dizer que em todas as classes que criamos até aqui o Java teve que criar um constructor padrão para nós.
Mas o que é um constructor padrão?
É um constructor público, sem parâmetros e sem código nenhum definido. Algo parecido com o exemplo abaixo.
  public Recipiente() {
  }
Uma questão importante é que caso você crie seu próprio constructor, o Java não criará um para você! Lembre-se sempre disso.
Você deve estar se perguntando sobre a utilidade de um constructor. Por que criar um constructor se o Java é capaz de criá-lo para mim?
Regra 4:
Pense na classe Recipiente. Existe algum recipiente, seja em qualquer sistema, que não possua a capacidade máxima definida? Não!
Sempre que um objeto precisar da definição de algum atributo ou execução de algum comportamento para que sua existência não fique inconsistente, devemos fazer isso no constructor. Veja o caso abaixo.
public class Recipiente {
  private int capMax;
  public void definirCapMax(int capMax) {
    if ( capMax < 1 ) {
      System.out.println( "Capacidade Maxima invalida!" );
    } else {
      this.capMax = capMax;
    }
  }
}
class Executora {
  public static void main(String args[]) {
    // Aqui estou criando o objeto
    Recipiente r = new Recipiente();
    // ATENÇÃO - vou comentar sobre esse ponto!!
    // Aqui estou definindo sua capacidade
    r.definirCapMax( 50 );
  }
}
Veja que no main() da classe Executora o objeto Recipiente foi criado em momento separado da definição da sua capacidade máxima. Note que após o chamada do definirCapMax() o objeto passou a existir num estado consistente, mas antes disso (no trecho comentado com a palavra ATENÇÃO), esse sistema coexistia com um Recipiente misterioso, sem definição de capacidade máxima. Volto a perguntar: existe agum recipiente sem capacidade máxima?
Nesse caso, a melhor (e única solução) seria permitir que a capacidade máxima do recipiente fosse definida tão logo o objeto fosse instanciado. Qual é o método que é chamado na criação do objeto? Isso mesmo!! O constructor vai nos ajudar agora.
public class Recipiente {
  private int capMax;
  public Recipiente(int capMax) {
    this.definirCapMax( capMax );
  }
  public void definirCapMax(int capMax) {
    if ( capMax < 1 ) {
      System.out.println( "Capacidade Maxima invalida!" );
    } else {
      this.capMax = capMax;
    }
  }
}
class Executora {
  public static void main(String args[]) {
    // Aqui estou criando o objeto
    Recipiente r = new Recipiente( 50 );
    // Legal!! O recipiente já está consistente!
  }
}
Incrível né?! Da forma como o código foi construído, nunca será possível criar um recipiente sem que seja informada a capacidade máxima. Note que dentro do constructor eu optei em chamar o próprio método da definição da capacidade máxima definirCapMax(). Essa é uma boa prática, pois evita replicação de código de controle em lugares diferentes da classe, melhorando a coesão.
Regra 5:
Por fim, só crie seu constructor se realmente for necessário. Estou dizendo isso para que vocês não saiam criando constructor em todas as classes a partir de agora. Só criem quando realmente for necessário. Lembre-se que classes bem definidas não são aquelas em que não temos como incluir mais nada e sim aquelas em que não podemos retirar mais nada, caso contrário o código ficará errado. Simplicidade é tudo!
Então, resumindo as regras do constructor:
Regra 1: Quando um objeto está sendo instanciado o constructor sempre será executado.
Regra 2: Um constructor deve ter o mesmo nome da classe e não deve possuir valor de retorno.
Regra 3: Se você não definir um constructor, o Java criará um para você. Todavia, caso você crie um, o seu passará a ser utilizado na criação do objeto (ou seja, o Java não vai criar um outro).
Regra 4: Sempre que a consistência de um objeto depender da execução de algum código (seja métodos ou definição de atributos), utilize um constructor.
Regra 5: Só crie um constructor se este for imprescindível.
Senhores, a partir de agora, devemos modelar nossas classes contemplando os construtores, sempre que necessário.
Objetos Polimórficos
É chegada a hora! Entraremos agora no último pilar da Orientação a Objetos.
Desde a primeira aula vínhamos conversando que a Orientação a Objetos e a programação da linguagem Java é sustentada por quatro pilares: Abstração, Encapsulamento, Herança e Polimorfismo. A partir de agora introduziremos o último, mas não menos importante, dos pilares: o Polimorfismo.
A essência desse conceito é simples, mas confesso que seu entendimento na prática é, digamos assim, de difícil digestão. Entretanto, não tema, vamos caminhando aos poucos.
Afinal, o que são objetos polimórficos?
O real significado é mais complexo do que o tema que trataremos nessa aula. Ao longo das próximas aulas iremos aperfeiçoando esse conceito. Por enquanto, podemos resumir em apenas uma assertiva:
Através do polimorfismo é possível instanciar um subtipo em uma classe mais genérica.
Certamente ninguém espera entender o que é polimorfismo através dessa frase! Vamos contextualizá-la. Lembra-se do exercício da Aula 12 (http://linubr.blogspot.com.br/2012/08/aula-12-lapidando-o-aprendizado-da.html)? Aquele da hierarquia de mamíferos e aves? Vou replicar o diagrama de classes aqui.
Pense agora na seguinte situação: imagine um sistema de simulação de uma fazenda, onde um usuário (o fazendeiro) poderia se aproximar de determinado animal e alimentá-lo. Neste contexto, sempre que o fazendeiro for alimentar, o animal emitirá um som. Isso deve ser verdadeiro para todos os animais! Vamos então modelar esse cenário. Atenção: para simplificar o exemplo eu retirei as classes abstratas Mamifero e Ave. Ahh sim, coloquei também dois constructors nas subclasses e incluí um método voar() para a classe Galinha.
Note que o método alimentar() de Fazendeiro recebe como parâmetro um Animal.
A grande sacada é justamente essa!
Veja que o Fazendeiro deve ser capaz de alimentar qualquer objeto cujo supertipo é um Animal. Nunca acontecerá dele alimentar uma instância de Animal (pois essa classe é Abstrata - veja que ela está em itálico). Nós somos capazes de programar a classe Animal e Fazendeiro independentemente das classes Cachorro e Galinha. Vamos fazer um teste!!! Veja as classes abaixo.
Arquivo 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();
}
Arquivo Fazendeiro.java
public class Fazendeiro {
  public void alimentar(Animal animal) {
    // ...
    // código para alimentar o animal
    // ...
    // Depois de alimentado, o animal fala!
    animal.falar();
  }
}
Veja que nós criamos o método alimentar() ainda antes de conhecer quais animais efetivamente serão alimentados! Mas se o método recebe como parâmetro um Animal, como passaremos para ele um Cachorro (ou uma Galinha)?
Essa é a pergunta primordial! Você acha que o código acima compila? Veja.
Não há erro nenhum de sintaxe que poderia causar problemas na compilação, mas por enquanto não temos como executar nosso sistema. Isso se deve ao fato de não existir nenhuma subclasse concreta de Animal e, é claro, não termos criado uma classe Executora.
Vamos continuar com o código.
Arquivo Cachorro.java
public class Cachorro extends Animal {
  public Cachorro(String nome) {
    super.setNome( nome );
  }
  public void falar() {
    System.out.println( "O cachorro " + super.getNome() + " falou: Au Au!" );
  }
}
Arquivo Galinha.java
public class Galinha extends Animal {
  public Galinha(String nome) {
    super.setNome( nome );
  }
  public void falar() {
    System.out.println( "A galinha " + super.getNome() + " falou: Co co ri co!" );
  }
  public void voar() {
    System.out.println( "A galinha " + super.getNome() + " voou!" );
  }
}
Por enquanto não há novidade alguma. Vamos agora ao gran finale!
Arquivo Executora.java
public class Executora {
  public static void main(String args[]) {
    Fazendeiro joaoDoCaminhao = new Fazendeiro();
    Animal a1 = new Cachorro( "Bidu" );
    Animal a2 = new Cachorro( "Nemu" );
    Animal a3 = new Galinha( "Chica" );
    joaoDoCaminhao.alimentar( a1 );
    joaoDoCaminhao.alimentar( a2 );
    joaoDoCaminhao.alimentar( a3 );
  }
}
Observe, com muita atenção, que ambos os cachorros e a galinha foram instanciados e guardados numa variável de instância do supertipo Animal. E essa é justamente a classe aguardada pelo método alimentar() do objeto joaoDoCaminhao (que é do tipo Fazendeiro). Você deve estar se perguntando: isso compila?
Não só compila, como roda! Veja que conforme conversamos, o método alimentar() da classe Fazendeiro foi capaz de executar os métodos falar() de cada um dos animais instanciados sem sequer saber qual animal estava instanciado no momento. Isso só foi possível porque o polimorfismo nos permite instanciar um subtipo e guardá-lo numa classe do supertipo. Foi exatamente isso que fizemos quando criamos uma Galinha e colocamos em um Animal. Veja o código abaixo.
Animal a3 = new Galinha( "Chica" );
Como o supertipo Animal possui o método falar(), o comportamento alimentar() de Fazendeiro pôde chamá-lo. Agora pense no seguinte: Eu também poderia chamar o método voar() da Galinha que está guardada no Animal?
Veja uma modificação na classe Executora onde tentamos fazer isso.
Arquivo Executora.java
public class Executora {
  public static void main(String args[]) {
    Fazendeiro joaoDoCaminhao = new Fazendeiro();
    Animal a = new Galinha( "Chica" );
    joaoDoCaminhao.alimentar( a );
    // Aqui vai dar erro!!
    a.voar();
  }
}
Porém quando tentamos compilar:
Veja que o Java não encontrou o método voar() em Animal. Mas isso ocorre justamente porque nossa classe de trabalho Animal (a variável de instância) não contém esse método. Apesar da instância guardada em a ser do tipo Galinha, nós não poderemos acessar seus métodos ou atributos particulares pelo fato de estarmos usando um supertipo mais genérico.
Agora que fizemos esse exemplo, vou repetir a frase que explica o polimorfismo:
Vejam que foi exatamente isso que fizemos: criamos um Cachorro e uma Galinha (subtipos) dentro da classe mais genérica, ou supertipo, Animal.
Sei que vocês devem estar com aquela sensação de que a ficha não caiu! Não se preocupem!! Isso é absolutamente normal quando falamos em polimorfismo. Faremos a partir de agora um intensivão de casos onde essa característica estará presente.
Exercício das Datas (difícil)
Esse é mais um exercício que não é by Guilherme. O copiei de uma boa listagem de exercícios sobre Java que pode ser encontrada no link abaixo:
Nosso foco estará no exercício cujo texto repeti abaixo (com algumas adaptações):
Implemente as classes especificadas abaixo:
Data - Classe abstracta
Atributos: ano, dia, mes.
Métodos:
        boolean bissexto() //verifica se a data pertence a um ano bissexto
        boolean valida() // valida se a data está correta
        void mostra() // método abstrato
        void defineData(String data) // método abstrato
DataEuropeia - Sub-Classe de Data
Métodos:
        void mostra() // mostra a data no formato ano/mes/dia
        void defineData(String data) // lê os dados na forma ano/mes/dia
DataAmericana - Sub-Classe de Data
Métodos:
        void mostra() // mostra a data no formato mes/dia/ano
        void defineData(String data) // lê os dados na forma mes/dia/ano
DataBrasileira - Sub-Classe de Data
Métodos:
        void mostra() // mostra a data no formato dia/mes/ano
        void defineData(String data) // lê os dados na forma dia/mes/ano
O sistema basicamente deverá permitir que o usuário trabalhe com uma data. Podemos definir os passos da classe Executora da seguite forma: 
1. O usuário deverá escolher o tipo da data trabalhado; 
2. O usuário deve entrar com uma String no formato adequado com a data escolhida (utilize o método lerTexto() da classe Util); 
3. O sistema deve validar a data; 
4. Se a data for válida, então: 
      a. Verificar se a data pertence a um ano bissexto e 
      b. Exibir a data no formato padrão.
Observações: note que o usuário deverá informar uma String que seja compatível com a data escolhida. Por exemplo, caso ele opte em trabalhar com a DataAmericana, ele deverá digitar algo parecido com 04/13/2011 (ou seja, mês/dia/ano). Caso ele não digite nesse formato o sistema deve considerar que a data está inválida.
Pessoal, boa sorte!!!! Tente resolver esse exercício. Vamos resolvê-lo juntos na próxima aula.
Ahh sim... isso foi só o início do polimorfismo! Ainda existem outros conceitos sobre ele...
Abs, Guilherme Pontes

Aula 12 - Lapidando o Aprendizado da Herança


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