MindView Inc.

Pensando em Java, 3ª ed. Revisão 4.0


[ Viewing Hints ] [ Book Home Page ] [ Free Newsletter ]
[ Seminars ] [ Seminars on CD ROM ] [ Consulting ]

Anterior Próximo Página Inicial Índice Conteúdo

7: Polimorfismo



Polimorfismo é a terceira característica essencial de uma linguagem de programação orientada a objetos, depois de abstração de dados e herança.



Ele provê uma outra dimensão de separação entre interface e implementação, para descasar o quê do como. Polimorfismo permite uma melhor organização do código e uma leitura mais fácil, assim como a criação de programas extensíveis, que podem “crescer” não apenas durante a criação original do projeto, mas também quando novas características são desejadas.Comentários (em inglês)



Encapsulamento cria um novo tipo de dado pela combinação de características e comportamentos. Implementação oculta separa a interface da implementação tornando os detalhes private. Este tipo de organização mecânica faz total sentido para alguém com experiência em programação procedural. Mas polimorfismo negocia com a separação em termos de tipos. No último capítulo, você viu como herança permite o tratamento de um objeto como de seu próprio tipo ou de seu tipo básico. Esta habilidade é crítica porque permite que muitos tipos (derivados de um mesmo tipo básico) sejam tratados como se eles fossem um tipo, e uma simples peça de código pode funcionar em todos estes diferentes tipos igualmente. A chamada de um método polimórfico permite que um tipo expresse sua diferença do outro, tipo similar, pois ambos são derivados do mesmo tipo básico. Esta distinção é expressa através de diferenças no comportamento dos métodos que você pode chamar através da classe básica.Comentários (em inglês)



Nesta capítulo, você aprenderá sobre polimorfismo (também chamado de ligação dinâmica ou ligação atrasada ou ligação em tempo de execução) partindo do básico, com exemplos simples que desvendarão tudo sobre o comportamento polimórfico do programa. Comentários (em inglês)

Revisão de upcasting



No Capítulo 6 você viu como um objeto pode ser usado como seu próprio tipo ou como um objeto de seu tipo básico. Tomar a referência de um objeto e tratá-la como uma referência a seu tipo básico é chamado de upcasting porque o caminho das árvores de herança são desenhados com a classe básica no topo. Comentários (em inglês)



Você também viu um problema levantado, o qual está encorpado no exemplo seguinte que é sobre instrumentos musicais. Como diversos exemplos tocam Notes, nós deveriamos criar a classe Note separadamente, em uma package:



//: c07:music:Note.java
// Notas para tocar em instrumentos musicaises to play on musical instruments.
package c07.music;
import com.bruceeckel.simpletest.*;

public class Note {
  private String noteName;
  private Note(String noteName) {
    this.noteName = noteName;
  }
  public String toString() { return noteName; }
  public static final Note
    MIDDLE_C = new Note("Middle C"),
    C_SHARP  = new Note("C Sharp"),
    B_FLAT   = new Note("B Flat");
    // Etc.
} ///:~




Esta é uma classe “enumeration”, que tem um número fixo de objetos constante para escolher. Você não pode fazer objetos adicionais porque o construtor é private.



No exemplo seguinte, Wind é um tipo de Instrument, portanto Wind é herdado de Instrument:



//: c07:music:Music.java
// Herança e upcasting.
package c07.music;
import com.bruceeckel.simpletest.*;

public class Music {
  private static Test monitor = new Test();
  public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  public static void main(String[] args) {
    Wind flute = new Wind();
    tune(flute); // Upcasting
    monitor.expect(new String[] {
      "Wind.play() Middle C"
    });
  }
} ///:~




//: c07:music:Wind.java
package c07.music;

// Objetos Wind são instrumetnos
// porque eles tem a mesma interface:
public class Wind extends Instrument {
  // Redefine o método da interface:
  public void play(Note n) {
    System.out.println("Wind.play() " + n);
  }
} ///:~




//: c07:music:Music.java
// Herança e upcasting.
package c07.music;
import com.bruceeckel.simpletest.*;

public class Music {
  private static Test monitor = new Test();
  public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  public static void main(String[] args) {
    Wind flute = new Wind();
    tune(flute); // Upcasting
    monitor.expect(new String[] {
      "Wind.play() Middle C"
    });
  }
} ///:~




O método Music.tune( ) aceita uma referência a Instrument , mas também qualquer derivada de Instrument. Em main( ), você pode ver isto acontecendo pois uma referência Wind é passada para tune( ), sem a necessidade de conversão. Isto é aceitável—a interface em Instrument deve existir em Wind, porque Wind é herdado de Instrument. Fazer upcast de Wind para Instrument pode “estreitar” aquela interface, mas não pode torná-la em nada menor que a interface completo de Instrument. Comentários (em inglês)

Esquecendo o tipo do objeto



Music.java pode parecer estranho para você. Por que alguém quereria intencionalmente esquecer o tipo de um objeto? Isto é o que acontece quando você faz um upcast, e parece como se pudesse ser muito mais direto se tune( ) simplesmente tomasse a referência a Wind como seu argumento. Isto traz a tona um ponto essencial: Se você fêz aquilo, você não precisa escrever um novo tune( ) para cada tipo de Instrument em seu sistema. Suponha que nós seguíssemos este raciocínio e adicionassemos instrumentos Stringed e Brass : Comentários (em inglês)



//: c07:music:Music2.java
// Sobrecarregando ao invés de fazer um upcasting.
package c07.music;
import com.bruceeckel.simpletest.*;

class Stringed extends Instrument {
  public void play(Note n) {
    System.out.println("Stringed.play() " + n);
  }
}

class Brass extends Instrument {
  public void play(Note n) {
    System.out.println("Brass.play() " + n);
  }
}

public class Music2 {
  private static Test monitor = new Test();
  public static void tune(Wind i) {
    i.play(Note.MIDDLE_C);
  }
  public static void tune(Stringed i) {
    i.play(Note.MIDDLE_C);
  }
  public static void tune(Brass i) {
    i.play(Note.MIDDLE_C);
  }
  public static void main(String[] args) {
    Wind flute = new Wind();
    Stringed violin = new Stringed();
    Brass frenchHorn = new Brass();
    tune(flute); // Não upcasting
    tune(violin);
    tune(frenchHorn);
    monitor.expect(new String[] {
      "Wind.play() Middle C",
      "Stringed.play() Middle C",
      "Brass.play() Middle C"
    });
  }
} ///:~




Isto funciona, mas há desvantagens maiores: você deve escrever métodos de tipo específico para cada classe nova de Instrument que você adicionar. Isto significa mais programação em primeiro lugar, mas também significa que se você quer adicionar um novo método como tune( ) ou um novo tipo de Instrument, você terá um bocado de trabalho a fazer. Adicione o fato de que o compilador não dará a você qualquer mensagem de erro se você esquecer de sobrecarregar um de seus métodos e o processo inteiro de trabalho se tornará ingerenciável. Comentários (em inglês)



Não seria ótimo se você pudesse só escrever um método único que tomasse a classe básica como seu argumento, e não qualquer das classes derivadas específicas? Isto é, não seria ótimo se você esquecesse que há classes derivadas, e escrevesse seu código para falar somente com a classe básica? Comentários (em inglês)



Isto é exatamente o que polimorfismo deixa você fazer. Contudo, muitos programadores que vêm de experiências com linguagens procedurais tem um pouco de problemas com a forma de trabalho do polimorfismo. Comentários (em inglês)

O giro



A dificuldade com Music.java pode ser vista ao rodar o programa. A saída é Wind.play( ). Esta é claramente a saída desejada, mas não parece fazer sentido que trabalharia desta maneira. Veja no método tune( ) :



  public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }




Ela recebe uma referência Instrument . Assim como pode o compilador possivelmente saber que esta referência a Instrument aponta para um Wind neste caso e não a um Brass ou Stringed? O compilador não pode. Para obter uma compreensão profunda do assunto, é útil examinar o assunto ligação. Comentários (em inglês)

Ligação de chamada de método



Conectar a chamada de um método ao corpo de um método é chamado binding [ligação]. Quando o binding é executado antes do programa estar rodando (pelo compilador e linkador, se houver um), ele é chamado early binding. Você pode não ter ouvido o termo antes porque ele nunca teve esta opção com linguagens procedurais. Compiladores C tem somente um tipo de chamada de método, e esta é a early binding. Comentários (em inglês)



A parte confusa do programa precedente gira em torno do early binding, porque o compilador não pode saber o método correto a chamar quando ele tem somente uma referência a Instrument . Comentários (em inglês)



A solução é chamada de late[atrasado] binding , o qual significa que o binding ocorre em tempo de execução, baseado no tipo de objeto. O Late binding é também chamado de dynamic[dinâmico] binding ou run-time[em tempo de execução] binding. Quando uma linguagem implementa o late binding, deve haver algum mecanismo para determinar o tipo de objeto em tempo de execução para chamar o método apropriado. Isto é, o compilador ainda não sabe o tipo do objeto, mas o mecanismo de chamada de método procura e chama o corpo do método correto. O mecanismo do late-binding varia de linguagem a linguagem, mas você pode imaginar que alguma ordem de informação de tipo deve estar instalada nos objetos. Comentários (em inglês)



Todos os métodos de binding em Java usam late binding a menos que o método seja static ou final (métodos private são implicitamente final). Isto significa que ordinariamente você não precisa tomar qualquer decisão a respeito de se o late binding irá ocorrer—acontece automaticamente. Comentários (em inglês)



Por que você declararia um método como final? Como notado no último capítulo, previne qualquer um de fazer um overriding daquele método. Talvez mais importante, é efetivamente “desviar” do dynamic binding, ou de preferência contar ao compilador que o dynamic binding não é necessário. Isto permite ao compilador gerar código levemente mais eficiente para chamadas de métodos final . Contudo, em muitos casos não fará qualquer diferença completa de performance em seu programa, assim o melhor é somente usar final como uma decisão de projeto, e não como uma tentativa de melhorar a performance. Comentários (em inglês)

Produzindo o comportamento correto



Uma vez que você sabe que toda ligação de método em Java acontece polimorficamente via late binding, você pode escrever seu código para avisar para a classe básica e saber que em todos os casos a classe derivada trabalhará corretamente usando o mesmo código. Ou para colocar de outra maneira, você “envia uma mensagem para um objeto e deixa a estrutura do objeto determinar a coisa certa a fazer.” Comentários (em inglês)



O exemplo clássico em POO é o exemplo “shape”. Este é comumente usado porque é fácil de visualizar, mas desafortunadamente pode confundir programadores iniciantes na idéia de que POO é só para programadores gráficos, o que sem dúvida não é o caso. Comentários (em inglês)



O exemplo shape tem um classe básica chamada Shape e vários tipos derivados: Circle, Square, Triangle, etc. A razão do exemplo funcionar tão bem é que é fácil dizer “um circle é um tipo de shape” e ser compreendidoand be understood. O diagrama de herança mostra os relacionamentos : Comentários (em inglês)

PEJ314.png



O upcast poderia ocorrer em uma declaração tão simples quanto:



Shape s = new Circle();




Aqui, um objeto Circle é criado, e a referência resultante é imediatamente atribuida a um Shape, o que poderia parecer ser um erro (atribuição de um tipo a outro); e mesmo assim está certo porque um Circle é um Shape por herança. Assim o compilador concorda com a declaração e não emite uma mensagem de erro. Comentários (em inglês)



Suponha que você chame um dos métodos da classe básica (que tenha sofrido um override nas classes derivadas):



s.draw();




Novamente, você pode esperar que draw( ) de Shape seja chamado porque este é , depois de todas, uma referência a Shape —assim como poderia o compilador saber fazer algo mais? E ainda assim o apropriado Circle.draw( ) é chamado por causa do late binding (polimorfismo). Comentários (em inglês)



O exemplo seguinte coloca de uma forma levemente diferente:



//: c07:Shapes.java
// Polimorfismo em Java.
import com.bruceeckel.simpletest.*;
import java.util.*;

class Shape {
  void draw() {}
  void erase() {}
}

class Circle extends Shape {
  void draw() {
    System.out.println("Circle.draw()");
  }
  void erase() {
    System.out.println("Circle.erase()");
  }
}

class Square extends Shape {
  void draw() {
    System.out.println("Square.draw()");
  }
  void erase() {
    System.out.println("Square.erase()");
  }
}

class Triangle extends Shape {
  void draw() {
    System.out.println("Triangle.draw()");
  }
  void erase() {
    System.out.println("Triangle.erase()");
  }
}

// Uma "fábrica" que cria shapes randomicamente:
class RandomShapeGenerator {
  private Random rand = new Random();
  public Shape next() {
    switch(rand.nextInt(3)) {
      default:
      case 0: return new Circle();
      case 1: return new Square();
      case 2: return new Triangle();
    }
  }
}

public class Shapes {
  private static Test monitor = new Test();
  private static RandomShapeGenerator gen =
    new RandomShapeGenerator();
  public static void main(String[] args) {
    Shape[] s = new Shape[9];
    // Preenche o array com shapes:
    for(int i = 0; i < s.length; i++)
      s[i] = gen.next();
    // Faz chamadas a métodos polimórficos:
    for(int i = 0; i < s.length; i++)
      s[i].draw();
    monitor.expect(new Object[] {
      new TestExpression("%% (Circle¦Square¦Triangle)"
        + "\\\\.draw\\\\(\\\\)", s.length)
    });
  }
} ///:~




A classe básica Shape estabelece a interface comum para qualquer herdado de Shape—isto é, todos os shapes podem ser desenhados e apagados. A classe derivada faz um override destas definições para providenciar um comportamento único para cada tipo específico de shape. Comentários (em inglês)



RandomShapeGenerator é um tipo de “fábrica” que produz uma referência para um objeto Shape randomicamente selecionado cada vez que você chama seu método next( ). Note que o upcast acontece na declaração return, cada uma das quais toma uma referência a um Circle, Square, ou Triangle e a envia para fora de next( ) como o tipo de retorno, Shape. Assim quando você chamar next( ), você nunca terá a sorte de ver qual o tipo específico dele, pois você sempre receberá de volta uma simples referência a Shape. Comentários (em inglês)



main( ) contêm um array de referências Shape preenchido através de chamadas a RandomShapeGenerator.next( ). Neste ponto você sabe que você tem Shapes, mas você não sabe nada mais específico que isto (e nem também o compilador). Contudo, quando você anda através deste array e chama draw( ) para cada um, o comportamento mágico do tipo específico ocorre, como você pode ver nas saídas quando você roda o programa. Comentários (em inglês)



O ponto de escolha randômica dos shapes é para direcionar a compreensão de que o compilador não pode ter conhecimento especial que permita a ele fazer a chamada correta em tempo de compilação. Todas as chamadas a draw( ) devem ser feitas através de ligação dinâmica. Comentários (em inglês)

Extensibilidade



Agora deixe-me retornar ao exemplo de instrumentos musicais. Por causa do polimorfismo, você pode adicionar tantos tipos novos quantos quiser ao sistema sem alterar o método tune( ). Em um programa POO bem projetado, muitos ou todos os seus métodos seguirão o modelo de tune( ) e comunicam-se somente com a interface da classe básica. Um programa é extensível porque você pode adicionar novas funcionalidades herdando novos tipos de dados da classe básica comum. Os métodos que manipulam a interface da classe básica não precisarão se alterados no todo para acomodar as novas classes. Comentários (em inglês)



Considere oque acontece se você tomar o exemplo instrument e adicionar mais métodos na classe básica e um número de novas classes. Aqui está o diagrama:

PEJ315.png



Todas estas classes novas funcionam corretamente com o velho, inalterado método tune( ) . Mesmo se tune( ) está em um arquivo separado e novos métodos forem adicionados a interface de Instrument, tune( ) ainda trabalhará corretamente, mesmo sem recompilá-lo. Aqui está uma implementação do diagrama: Comentários (em inglês)



//: c07:music3:Music3.java
// Um programa extensível.
package c07.music3;
import com.bruceeckel.simpletest.*;
import c07.music.Note;

class Instrument {
  void play(Note n) {
    System.out.println("Instrument.play() " + n);
  }
  String what() { return "Instrument"; }
  void adjust() {}
}

class Wind extends Instrument {
  void play(Note n) {
    System.out.println("Wind.play() " + n);
  }
  String what() { return "Wind"; }
  void adjust() {}
}

class Percussion extends Instrument {
  void play(Note n) {
    System.out.println("Percussion.play() " + n);
  }
  String what() { return "Percussion"; }
  void adjust() {}
}

class Stringed extends Instrument {
  void play(Note n) {
    System.out.println("Stringed.play() " + n);
  }
  String what() { return "Stringed"; }
  void adjust() {}
}

class Brass extends Wind {
  void play(Note n) {
    System.out.println("Brass.play() " + n);
  }
  void adjust() {
    System.out.println("Brass.adjust()");
  }
}

class Woodwind extends Wind {
  void play(Note n) {
    System.out.println("Woodwind.play() " + n);
  }
  String what() { return "Woodwind"; }
}

public class Music3 {
  private static Test monitor = new Test();
  // Não se preocupe com os tipos, assim novos tipos
  // adicionados ao sistema ainda funcionarão corretamente:
  public static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  public static void tuneAll(Instrument[] e) {
    for(int i = 0; i < e.length; i++)
      tune(e[i]);
  }
  public static void main(String[] args) {
    // Fazendo upcast durante a adição ao array:
    Instrument[] orchestra = {
      new Wind(),
      new Percussion(),
      new Stringed(),
      new Brass(),
      new Woodwind()
    };
    tuneAll(orchestra);
    monitor.expect(new String[] {
      "Wind.play() Middle C",
      "Percussion.play() Middle C",
      "Stringed.play() Middle C",
      "Brass.play() Middle C",
      "Woodwind.play() Middle C"
    });
  }
} ///:~




Os novos métodos são what( ), o qual retorna uma referência String com a descrição da classe, e adjust( ), o qual provê alguma forma de ajustar cada instrumento. Comentários (em inglês)



Em main( ), quando você coloca algo dentro do array orchestra , você automaticamente faz um upcast de Instrument. Comentários (em inglês)



Você pode ver que o método tune( ) é felizmente ignorante de todas as alterações de código que ocorrem em sua volta, e ainda funciona corretamente. Isto é exatamente o que se supões que o polimorfismo deve prover. Alterações em seu código não devem causar danos a partes do programa que não seriam afetadas. Colocando de outra forma, polimorfismo é uma importante técnica para o programador “separar as coisas que alteram das coisas que ficam as mesmas.” Comentários (em inglês)

Cilada: Fazendo “override” de métodos private



Aqui esta algo que você poderá inocentemente tentar fazer:



//: c07:PrivateOverride.java
// Classes e métodos abstratos.
import com.bruceeckel.simpletest.*;

public class PrivateOverride {
  private static Test monitor = new Test();
  private void f() {
    System.out.println("private f()");
  }
  public static void main(String[] args) {
    PrivateOverride po = new Derived();
    po.f();
    monitor.expect(new String[] {
      "private f()"
    });
  }
}

class Derived extends PrivateOverride {
  public void f() {
    System.out.println("public f()");
  }
} ///:~




Você pode aguardar racionalmente que a saída seja “public f( )”, mas um método private é automaticamente final, e é também oculto da classe derivada. Assim o f( ) de Derived neste caso é um novo método estigmatizado; ele nunca será sobrecarregado, pois a versão da classe básica de f( ) não é visível em Derived. Comentários (em inglês)



O resultado disto é que somente métodos não-private podem ser overridde, mas você deve ficar alerta para o aspecto de métodos private que sofreram overridde, que não geram alertas do compilador, mas não fazem o que se deve esperar. Para ficar claro, você deveria usar um nome diferente de um método private de classe básica em sua classe derivada. Comentários (em inglês)

Classes e
e métodos abstratos



Em todos os exemplos instrument, os métodos na classe básica Instrument foram sempre métodos “de fachada”. Se este métodos fossem chamados alguma vez, você teria feito algo errado. Isto é porque o intento de Instrument é criar uma interface comum para todas as classes derivadas dela. Comentários (em inglês)



A única razão para estabelecer esta interface comum é que ela pode ser expressa diferentemente para cada subtipo diferente. Ela estabelece uma forma básica, assim você pode dizer oque há em comum com todas as classes derivadas. Outra maneira de dizer isto é chamar Instrument uma classe básica abstrata (ou simplesmente uma classe abstrata). Você cria uma classe abstrata quando você quer manipular um conjunto de classes através desta interface comum. Todos os métodos da classe derivada com igual assinatura de declaração da classe básica serão chamados usando o mecanismo da ligação dinâmica. (Contudo, como visto na última seção, se o nome do método é o mesmo que o da classe básica mas os argumentos são diferentes, você obteve uma sobrecarga, o que provavelmente não é o que você queria.) Comentários (em inglês)



Se você tem uma classe abstrata como Instrument, objetos desta classe quase sempre não tem significado. Isto é, Instrument é significativo para expressar somente a interface, e não uma implementação particular, assim criar um objeto Instrument não faz sentido, e você provavelmente vai querer prevenir o usuário de fazê-lo. Isto pode ser conseguido fazendo todos os métodos em Instrument imprimir mensagens de erro, mas isto atrasa a informação até a execução e requer testes confiáveis exaustivos por parte do usuário. É melhor capturar problemas em tempo de compilação. Comentários (em inglês)



Java providencia uma mecanismo para fazer os chamados métodos abstratos.[32] Este é um método que é incompleto; tem somente uma declaração e não tem o corpo do método. Aqui está a sintaxe para a declaração de um método abstrato:



abstract void f();




Uma classe contendo métodos abstratos é chamada de classe abstrata. Se uma classe contem um ou mais métodos abstratos, a classe deve ser auto-qualificada como abstract. (Caso contrário, o compilador lhe dará uma mensagem de erro.) Comentários (em inglês)



Se uma classe abstrata está incompleta, oque você supõe que o compilador vai fazer quando alguém tenta fazer um objeto daquela classe? Não pode ser seguro criar um objeto de uma classe abstrata, assim você recebe uma mensagem de erro do compilador. Desta forma, o compilador assegura a pureza da classe abstrata, e você não precisa se preocupar com o mal uso dela. Comentários (em inglês)



Se você herda de uma classe abstrata e você quer fazer objetos do novo tipo, você deve providenciar definições de métodos para todos os métodos abstratos na classe básica. Se você não fizer (e você pode escolher não fazer), então a classe derivada é também abstrata, e o compilador forçará você a qualificar aquela classe com a palavra chave abstract. Comentários (em inglês)



É possível criar uma classe abstract sem incluir qualquer método abstract . Isto é util quando você quer obter uma classe na qual não faz sentido ter qualquer método abstract , e ainda você quer prevenir qualquer instância desta classe. Comentários (em inglês)



A classe Instrument pode ser facilmente alterada em uma classe abstract . Somente alguns dos métodos serão abstract, pois tornar uma classe abstrata não exige que você torne todos os seu métodos abstract. Aqui está como isto parece:

PEJ316.png



Aqui está um exemplo orchestra modificado para usar classes e métodos abstract :



//: c07:music4:Music4.java
// Classes e métodos abstratos.
package c07.music4;
import com.bruceeckel.simpletest.*;
import java.util.*;
import c07.music.Note;

abstract class Instrument {
  private int i; // Memória alocada para cada 
  public abstract void play(Note n);
  public String what() {
    return "Instrument";
  }
  public abstract void adjust();
}

class Wind extends Instrument {
  public void play(Note n) {
    System.out.println("Wind.play() " + n);
  }
  public String what() { return "Wind"; }
  public void adjust() {}
}

class Percussion extends Instrument {
  public void play(Note n) {
    System.out.println("Percussion.play() " + n);
  }
  public String what() { return "Percussion"; }
  public void adjust() {}
}

class Stringed extends Instrument {
  public void play(Note n) {
    System.out.println("Stringed.play() " + n);
  }
  public String what() { return "Stringed"; }
  public void adjust() {}
}

class Brass extends Wind {
  public void play(Note n) {
    System.out.println("Brass.play() " + n);
  }
  public void adjust() {
    System.out.println("Brass.adjust()");
  }
}

class Woodwind extends Wind {
  public void play(Note n) {
    System.out.println("Woodwind.play() " + n);
  }
  public String what() { return "Woodwind"; }
}

public class Music4 {
  private static Test monitor = new Test();
  // Não cuide do tipo, assim tipos novos 
  // adicionador ao sistema funcionarão corretamente:
  static void tune(Instrument i) {
    // ...
    i.play(Note.MIDDLE_C);
  }
  static void tuneAll(Instrument[] e) {
    for(int i = 0; i < e.length; i++)
      tune(e[i]);
  }
  public static void main(String[] args) {
    // Fazendo upcast durante a adição ao array:
    Instrument[] orchestra = {
      new Wind(),
      new Percussion(),
      new Stringed(),
      new Brass(),
      new Woodwind()
    };
    tuneAll(orchestra);
    monitor.expect(new String[] {
      "Wind.play() Middle C",
      "Percussion.play() Middle C",
      "Stringed.play() Middle C",
      "Brass.play() Middle C",
      "Woodwind.play() Middle C"
    });
  }
} ///:~




Você pode ver que não há realmente alterações exceto na classe básica. Comentários (em inglês)



É muito útil criar classes e métodos abstract porque elas tornam explícita a abstração da classe, e avisam tanto para o usuário quanto para o compilador como ela foi entendida para ser usada. Comentários (em inglês)

Construtores e polimorfismo



Como usual, construtores são diferentes de outros tipos de métodos. Isto é verdade também quando polimorfismo é envolvido. De qualquer forma construtores nunca são polimórficos (eles são atualmente métodos static , mas a declaração static é implícita), é importante compreender a maneira como construtores trabalham em complexas hierarquias e com polimorfismo. Esta compreensão ajudará você a evitar complicações desprazerosas. Comentários (em inglês)

Ordem de chamada de construtores



A ordem de chamadas do construtor foi brevemente discutida no Capítulo 4 e novamente no Capítulo 6, mas foi antes do polimorfismo ser introduzido. Comentários (em inglês)



Um construtor de uma classe básica é sempre chamado durante o processo de construção de uma classe derivada, canalizando pela hierarquia da herança para que um construtor para cada classe básica seja chamado. Isto faz sentido porque o construtor tem um trabalho especial: ver se o objeto é construido apropriadamente. Uma classe derivada tem acesso a seus próprios membros somente, e não aqueles da classe básica (aqueles membros são normalmente private). Somente o construtor da classe básica tem o conhecimento apropriado para inicializar seus próprios elementos. Entretanto, é essencial que todos os construtores sejam chamados, caso contrário o objeto inteiro não seria construido. É por isto que o construtor força a chamada de um construtor para todas as porções de uma classe derivada. Ele silenciosamente chamará o construtor padrão se você não chamar explicitamente o construtor da classe básica no corpo do construtor da classe derivada. Se não há construtor padrão, o compilador reclamará. (No caso de a classe não ter construtores, o compilador sintetizará automaticamente um construtor padrão.) Comentários (em inglês)



Veja um exemplo que mostra os efeitos da composição, herança, e polimorfismo na ordem de construção:



//: c07:Sandwich.java
// Ordem de chamadas do construtor.
package c07;
import com.bruceeckel.simpletest.*;

class Meal {
  Meal() { System.out.println("Meal()"); }
}

class Bread {
  Bread() { System.out.println("Bread()"); }
}

class Cheese {
  Cheese() { System.out.println("Cheese()"); }
}

class Lettuce {
  Lettuce() { System.out.println("Lettuce()"); }
}

class Lunch extends Meal {
  Lunch() { System.out.println("Lunch()"); }
}

class PortableLunch extends Lunch {
  PortableLunch() { System.out.println("PortableLunch()");}
}

public class Sandwich extends PortableLunch {
  private static Test monitor = new Test();
  private Bread b = new Bread();
  private Cheese c = new Cheese();
  private Lettuce l = new Lettuce();
  public Sandwich() {
    System.out.println("Sandwich()");
  }
  public static void main(String[] args) {
    new Sandwich();
    monitor.expect(new String[] {
      "Meal()",
      "Lunch()",
      "PortableLunch()",
      "Bread()",
      "Cheese()",
      "Lettuce()",
      "Sandwich()"
    });
  }
} ///:~




Este exemplo cria uma classe complexa fora das outras classes, e cada classe tem um construtor que anuncia a si mesmo. A classe importante é Sandwich, a qual reflete três níveis de herança (quatro, se você contar a herança implícita de Object) e três objetos membro. Você pode ver a saída quando um objeto Sandwich é criado em main( ). Isto significa que a ordem de chamadas do construtor por uma objeto complexo é a seguinte: Comentários (em inglês)

  1. O construtor da classe básica é chamado. Este passo é repetido recursivamente tal que a raiz da hierarquia é construida primeiro, seguida pela próxima classe derivada , etc., até que a maior classe derivada seja encontrada. Análise
  2. Inicializadores de membros são chamados pela ordem de declaração. Análise
  3. O corpo do construtor da classe derivada é chamado. Análise


A ordem de chamadas do construtor é importante. Quando você herda, você sabe tudo sobre a classe básica e pode acessar qualquer membro public e protected da classe básica. Isto significa que você deve estar habilitado a assumir que todos os membros da classe básica estão válidos quando você está na classe derivada. Em um método normal, construção já aconteceu, assim todos os membros de todas as partes do objeto foram construídos. Dentro do construtor, contudo, você deve estar habilitado a assumir que todos os membros que você usa foram construídos. A única maneira de garantir isto é que o construtor da classe básica seja chamado primeiro. Então quando você está no construtor da classe derivada, todos os membros que você pode acessar na classe básica foram inicializados. Saber que todos os membros estão válidos dentro do construtor é também a razão por que, sempre que possível, você deveria inicializar todos os objetos membro (Isto é, objetos colocados na classe usando composição) em seus pontos de definição na classe (e.g., b, c, e l no exemplo anterior). Se você seguir esta prática, você ajudará a assegurar que todos os membros da classe básica e objetos membros da objeto corrente foram inicializados. Infelizmente, isto não cobre todos os casos, como você verá na próxima seção. Comentários (em inglês)

Herança e limpeza



Quando usando composição e herança para criar uma nova classe, a maior parte do tempo que você não terá de se preocupar é sobre a limpeza; sub-objetos podem ser usualmente deixados para o coletor de lixo. Se você tem artifícios de limpeza, você deve ser diligente e criar um método dispose( ) (o nome que eu escolhi para usar aqui; você pode vir com outro melhor) para sua nova classe. E como herança, você deve override dispose( ) na classe derivada se você tem qualquer limpeza especial que deva acontecer como parte da coleta de lixo. Quando você faz o override de dispose( ) em uma classe herdada, é importante lembrar de chamar a versão da classe básica de dispose( ), caso contrário a limpeza da classe básica não acontecerá. O exemplo seguinte demonstra isso:



//: c07:Frog.java
// Limpeza e herança.
import com.bruceeckel.simpletest.*;

class Characteristic {
  private String s;
  Characteristic(String s) {
    this.s = s;
    System.out.println("Creating Characteristic " + s);
  }
  protected void dispose() {
    System.out.println("finalizing Characteristic " + s);
  }
}

class Description {
  private String s;
  Description(String s) {
    this.s = s;
    System.out.println("Creating Description " + s);
  }
  protected void dispose() {
    System.out.println("finalizing Description " + s);
  }
}

class LivingCreature {
  private Characteristic p = new Characteristic("is alive");
  private Description t =
    new Description("Basic Living Creature");
  LivingCreature() {
    System.out.println("LivingCreature()");
  }
  protected void dispose() {
    System.out.println("LivingCreature dispose");
    t.dispose();
    p.dispose();
  }
}

class Animal extends LivingCreature {
  private Characteristic p= new Characteristic("has heart");
  private Description t =
    new Description("Animal not Vegetable");
  Animal() {
    System.out.println("Animal()");
  }
  protected void dispose() {
    System.out.println("Animal dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
}

class Amphibian extends Animal {
  private Characteristic p =
    new Characteristic("can live in water");
  private Description t =
    new Description("Both water and land");
  Amphibian() {
    System.out.println("Amphibian()");
  }
  protected void dispose() {
    System.out.println("Amphibian dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
}

public class Frog extends Amphibian {
  private static Test monitor = new Test();
  private Characteristic p = new Characteristic("Croaks");
  private Description t = new Description("Eats Bugs");
  public Frog() {
    System.out.println("Frog()");
  }
  protected void dispose() {
    System.out.println("Frog dispose");
    t.dispose();
    p.dispose();
    super.dispose();
  }
  public static void main(String[] args) {
    Frog frog = new Frog();
    System.out.println("Bye!");
    frog.dispose();
    monitor.expect(new String[] {
      "Creating Characteristic is alive",
      "Creating Description Basic Living Creature",
      "LivingCreature()",
      "Creating Characteristic has heart",
      "Creating Description Animal not Vegetable",
      "Animal()",
      "Creating Characteristic can live in water",
      "Creating Description Both water and land",
      "Amphibian()",
      "Creating Characteristic Croaks",
      "Creating Description Eats Bugs",
      "Frog()",
      "Bye!",
      "Frog dispose",
      "finalizing Description Eats Bugs",
      "finalizing Characteristic Croaks",
      "Amphibian dispose",
      "finalizing Description Both water and land",
      "finalizing Characteristic can live in water",
      "Animal dispose",
      "finalizing Description Animal not Vegetable",
      "finalizing Characteristic has heart",
      "LivingCreature dispose",
      "finalizing Description Basic Living Creature",
      "finalizing Characteristic is alive"
    });
  }
} ///:~




Cada classe na hierarquia também contêm objetos membro dos tipos Characteristic e Description, os quais devem também ser descartados. A ordem do descarte deveria ser a inversa da ordem de inicialização, no caso de um sub-objeto ser depentente de outro. Para campos, isto significa o inverso da ordem de declaração (pois campos são inicializados em ordem de declaração). Para classes básicas (seguindo a forma que é usada em C++ para destrutores), você deveria executar primeiro a limpeza da classe básica. Isto é porque a limpeza da classe derivada poderia chamar alguns métodos na classe básica que precisam de componentes da classe básica para viver, assim você não deve destruí-los prematuramente. Da saída você pode ver que todas as partes do objeto Frog são descartas na ordem inversa da criação. Comentários (em inglês)



Neste exemplo, você pode ver que embora você nem sempre precise executar a limpeza, quando você faz, o processo requer cuidado e consciência. Comentários (em inglês)

Comportamento de métodos polimórficos
dentro de construtores



A hierarquia das chamadas de construtores nos traz um interessante dilema. Oque acontece se você está dentro de um construtor e você chama um método ligado dinamicamente ao objeto que está sendo construido? Dentro de um método ordinário, você pode imaginar oque acontecerá: A chamada dinamicamente ligada é resolvida em tempo de execução, porque o objeto não pode saber se ele pertence a classe que o método está dentro ou alguma classe derivada dele. Por consistência, você pode achar que isto é o que deveria acontecer dentro de construtores. Comentários (em inglês)



Este não é exatamente o caso. Se você chamar um método dinamicamente ligado dentro de um construtor, a definição que sofreu override para aquele método é usada. Contudo, o efeito pode ser particularmente inesperado e pode ocultar alguns bugs difíceis de achar. Comentários (em inglês)



Conceitualmente, o trabalho do construtor é trazer o objeto a existência (o que é uma difícil e normal tarefa). Dentro de qualquer construtor, o objeto inteiro pode ser só parcialmente formado—você pode saber apenas que os objetos da classe básica foram inicializados, mas você não pode saber quais classes estão herdadas dele. Uma chamada a um método ligado dinamicamente, contudo, alcança “externamente” na hierarquia de herança. Ele chama um método na classe derivada. Se você fizer isto dentro de um construtor, você chama um método que pode manipular membros que não foram inicializados ainda—um depósito seguro para desastres. Comentários (em inglês)



Você pode ver o problema no seguinte exemplo:



//: c07:PolyConstructors.java
// Construtores e polimorfismo
// não produzem o que você pode esperar.
import com.bruceeckel.simpletest.*;

abstract class Glyph {
  abstract void draw();
  Glyph() {
    System.out.println("Glyph() before draw()");
    draw();
    System.out.println("Glyph() after draw()");
  }
}

class RoundGlyph extends Glyph {
  private int radius = 1;
  RoundGlyph(int r) {
    radius = r;
    System.out.println(
      "RoundGlyph.RoundGlyph(), radius = " + radius);
  }
  void draw() {
    System.out.println(
      "RoundGlyph.draw(), radius = " + radius);
  }
}

public class PolyConstructors {
  private static Test monitor = new Test();
  public static void main(String[] args) {
    new RoundGlyph(5);
    monitor.expect(new String[] {
      "Glyph() before draw()",
      "RoundGlyph.draw(), radius = 0",
      "Glyph() after draw()",
      "RoundGlyph.RoundGlyph(), radius = 5"
    });
  }
} ///:~




Em Glyph, o método draw( ) é abstract, então ele é projetado para sofrer um overridde. Realmente, você é forçado a fazer um override dele em RoundGlyph. Mas o construtor de Glyph chama este método, e a chamada termina em RoundGlyph.draw( ), o que pareceria ser o objetivo. Mas se você olhar nas saídas, você pode ver que quando construtor de Glyph chama draw( ), o valor de radius não é sempre o valor inicial padrão 1. Ele é 0. Isto provavelmente resultaria ou em um ponto ou absolutamente nada sendo desenhado na tela, e você estaria atônito, tentando entender por que o programa não quer funcionar. Comentários (em inglês)



A ordem de inicialização descrita na seção anterior não é suficientemente completa, e esta é a chave para resolver o mistério. O processo atual de inicialização é:

  1. A memória alocada para o objeto é inicializada com zeros binários antes que algo mais aconteça. Análise
  2. Os construtores da classe básica são chamados como descrito anteriormente. Neste ponto, o método draw( ) que sofreu um override é chamado (sim, antes do construtor de RoundGlyph ser chamado), o qual descobre o valor zero de radius , devido ao Passo 1. Análise
  3. Inicializadores de membros são chamados pela ordem de declaração. Análise
  4. O corpo do construtor da classe derivada é chamado. Análise


Há uma motivo maior para isto, o qual é que tudo está ao menos inicializado com zero (ou seja o que for que zero signifique para aquele tipo particular de dado) e não só deixado como lixo. Isto inclue referências a objetos que estão embutidas dentro da classe via composição, as quais tornam-se nulas. Assim se você esquecer de inicializar aquela referência, você receberá uma exceção em tempo de execução. Tudo mais recebe zero, o que é normalmente um valor intrigante quando visto na saída. Comentários (em inglês)



Na outra mão, você estaria lindamente horrorizado com o resultado deste programa. Você fez uma coisa perfeitamente lógica, e ainda o comportamento está misteriosamente errado, sem nenhuma queixa do compilador. (C++ produz comportamento mais racional nesta situação.) Bugs como estes poderiam facilmente ser enterrados e tomam um longo tempo para descobrir. Comentários (em inglês)



Como um resultado, uma boa regra para construtores é, “Faça menos atribuições possíveis para deixar o objeto em um bom estado, e se você puder possivelmente evitá-lo, não chame quaisquer métodos.” Os únicos métodos seguros para chamar dentro de um construtor são aqueles que são final na classe básica. (Isto também se aplica a métodos private, que são automaticamente final.) Estes não podem sofrer override e então não podem produzir este tipo de surpresa. Comentários (em inglês)

Projetando com herança



Uma vez que você aprende sobre polimorfismo, pode parecer que tudo deve ser herdado, porque polimorfismo é uma ferramenta esperta. Isto pode pesar em seus projetos; De fato, se você escolher herança primeiro quando você está usando uma classe existente para fazer uma nova classe, coisas pode se tornar complicadas desnecessariamente. Comentários (em inglês)



A melhor abordagem é escolher primeiro a composição, especialmente quando não é óbvio qual delas você deveria usar. Composição não força um projeto na hierarquia da herança. Mas composição é também mais flexível pois é possível escolher dinamicamente um tipo (e seu comportamento) quando usando composição, enquanto que herança requer um tipo exato que seja conhecido em tempo de compilação. O exemplo seguinte ilustra isto:



//: c07:Transmogrify.java
// Alterando dinamicamente o comportamento de um objeto 
// via composição (O modelo de projeto "State").
import com.bruceeckel.simpletest.*;

abstract class Actor {
  public abstract void act();
}

class HappyActor extends Actor {
  public void act() {
    System.out.println("HappyActor");
  }
}

class SadActor extends Actor {
  public void act() {
    System.out.println("SadActor");
  }
}

class Stage {
  private Actor actor = new HappyActor();
  public void change() { actor = new SadActor(); }
  public void performPlay() { actor.act(); }
}

public class Transmogrify {
  private static Test monitor = new Test();
  public static void main(String[] args) {
    Stage stage = new Stage();
    stage.performPlay();
    stage.change();
    stage.performPlay();
    monitor.expect(new String[] {
      "HappyActor",
      "SadActor"
    });
  }
} ///:~




Um objeto Stage contêm uma referência a um Actor, o qual é inicializado com um objeto HappyActor. Isto significa que performPlay( ) produz um comportamento particular. Mas como uma referência pode ser ligada a um objeto diferente em tempo de execução, uma referência para um objeto SadActor pode ser substituida em actor, e então o comportamento produzido por performPlay( ) se altera. Então você ganha flexibilidade dinâmica em tempo de execução. (Isto é também chamado de State Pattern. Veja Thinking in Patterns (with Java) em www.BruceEckel.com.) Em contraste, você não pode decidir herdar diferentemente em tempo de execução; isto deve ser completamente determinado em tempo de compilação. Comentários (em inglês)



Uma regra geral é “Use herança para expressar diferenças no comportamento, e campos para expressar variações no estado.” No exemplo anterior, ambos são usados; duas classes diferentes são herdadas para expressar a diferença no método act( ), e Stage usa composição para permitir que seu estado seja alterado. Neste caso, trocas no estado acontecem para produzir uma alteração no comportamento. Comentários (em inglês)

Herança pura x extensão



Quando estudando herança, poderia parecer que o caminho mais limpo para criar uma hierarquia de herança é seguir a abordagem “pura”. Que é, somente métodos que foram estabelecidos na classe básica ou interface são para sofrer override na classe derivada, como visto neste diagrama:

PEJ317.png



Isto pode ser chamado de um puro relacionamento “é-um” porque a interface de uma classe estabelece oque ela é. Herança garante que qualquer classe derivada terá a interface da classe básica e nada menos. Se você seguir este diagrama, classes derivadas terão sempre nada mais que a interface da classe básica. Comentários (em inglês)



Isto pode ser imaginado como uma substituição pura, porque os objetos da classe deriavada podem ser perfeitamente substituido pela classe básica, e você nunca precisa saber qualquer informação extra sobre as subclasses quando as está usando:

PEJ318.png



Isto é, a classe básica pode receber qualquer mensagem que você envia para a classe derivada porque as duas tem exatamente a mesma interface. Tudo que você precisa fazer é um upcast da classe derivada e nunca olher de volta para ver qual o tipo exato do objeto com o qual está negociando. Tudo é manuseado através do polimorfismo. Comentários (em inglês)



Quando você o vê desta maneira, parece que um puro relacionamento é-um é a única maneira sensível de fazer a coisas, e qualquer outro projeto indica raciocínio confuso e é falho por definição. Isto também é uma armadilha. Tão breve quanto você começar a pensar desta maneira, você voltará atrás e descobrirá que extender a interface (o que, desafortunadamente, a palavra chave extends parece encorajar) é a solução perfeita para um problema particular. Isto poderia ser designado de relacionamento “é-como-um”, porque a classe derivada é como a classe básica—ela tem a mesma interface fundamental—mas tem outros artifícios que requerem métodos adicionais para implementar:

PEJ319.png



Enquanto isto é também uma útil e sensível abordagem (dependendo da situação), tem um defeito. A parte extendida da interface na classe derivada não está disponível da classe básica, assim uma vez que você faça um upcast, você não pode chamar os métodos novos:

PEJ320.png



Se você não está fazendo um upcast neste caso, ele não incomodará você, mas frequentemente você chegará em uma situação na qual você precisa descobrir o tipo exato de objeto para que você possa acessar os métodos extendidos daquele tipo. A seção seguinte mostra como isto é feito.Comentários (em inglês)

Downcasting e identificação em
tempo de execução



Como você perde a informação específica do tipo via upcast (mover para cima na hierarquia da herança), faz sentido que para recuperar a informação do tipo—isto é, mover para baixo de volta na hierarquia da herança—você use um downcast. Contudo, você sabe que um upcast é sempre seguro; a classe básica não pode ter uma interface maior que a classe derivada. Então, toda mensagem que você enviar através da interface da classe básica será seguramente aceita. Mas com um downcast, você não sabe realmente que um shape (por exemplo) é atualmente um circle. Ele poderia ao invés ser um triângulo ou um quadrado ou algum outro tipo. Comentários (em inglês)

PEJ321.png



Para resolver este problema, deve haver algumas maneiras de garantir que a conversão para baixo está correta, assim você não converterá acidentalmente para o tipo errado e então enviar uma mensagem que o objeto não pode aceitar. Isto seria bastante inseguro. Comentários (em inglês)



Em algumas linguagens (como o C++) você deve executar uma operação especial em ordem para obter um tipo seguro para downcast, mas em Java, toda conversão é verificada! Assim embora pareça que você está executando uma converão ordinária entre parenteses, em tempo de execução esta conversão é verificada para assegurar que ela é de fato o tipo que você pensa que é. Se não for, você recebe uma ClassCastException. Este ato de checagem de tipos em tempo de execução é chamado de run-time type identification (RTTI). O exemplo seguinte demonstra o comportamento do RTTI:



//: c07:RTTI.java
// Downcasting e Run-Time Type Identification (RTTI).
// {ThrowsException}

class Useful {
  public void f() {}
  public void g() {}
}

class MoreUseful extends Useful {
  public void f() {}
  public void g() {}
  public void u() {}
  public void v() {}
  public void w() {}
}

public class RTTI {
  public static void main(String[] args) {
    Useful[] x = {
      new Useful(),
      new MoreUseful()
    };
    x[0].f();
    x[1].g();
    // Tempo de compilação: método não encontrado em Useful:
    //! x[1].u();
    ((MoreUseful)x[1]).u(); // Downcast/RTTI
    ((MoreUseful)x[0]).u(); // Exception thrown
  }
} ///:~




Como no diagrama, MoreUseful extende a interface de Useful. Mas como é herdado, ele também pode sofrer um upcast para um Useful. Você pode ver isto acontecendo na inicialização do array x em main( ). Como ambos os objetos no array são da classe Useful, você pode enviar os métodos f( ) e g( ) para ambos, e se você tentar chamar u( ) (o qual existe somente em MoreUseful), você receberá uma mensagem de erro em tempo de compilãção. Comentários (em inglês)



Se você quer acessar a interface extendida de um objeto MoreUseful, você pode tentar um downcast. Se for o tipo correto, terá sucesso. Caso contrário, você receberá uma ClassCastException. Você não precisa escrever qualquer código especial para esta exceção, pois ela indica um erro de programação que poderia acontecer em qualquer lugar de um programa. Comentários (em inglês)



Há mais em RTTI que uma simples conversão. Por exemplo, há uma maneira de ver com que tipo você negociando antes de tentar convertê-lo para baixo. Todo o Capítulo 10 está dirigido ao estudo dos diferentes aspectos da identificação de tipo do Java run-time. Comentários (em inglês)

Resumo



Polimorfismo significa “formas diferentes.” Na programação orientada a objetos, você tem a mesma face (a interface comum na classe básica) e diferentes formas usando aquela face: as versões diferentes dos métodos dinamicamente ligados. Comentários (em inglês)



Você tem visto neste capítulo que é impossível compreender, ou até criar, um exemplo de polimorfismo sem usar abstração de dados e herança. Polimorfismo é uma artifício que não pode ser visualizado isoladamente (como uma declaração switch pode, por exemplo), mas ao invés funciona somente em conjunto, como uma parte de uma “grande figura” de relacionamentos de classe. Pessoas são frequentemente confundidas por outras, artifícios não orientados a objeto de Java, como métodos sobrecarregados, os quais algumas vezes são apresentados como orientados a objeto. Não seja tolo: Se não é late binding, não é polimorfismo. Comentários (em inglês)



Para usar polimorfismo-e então técnicas orientadas a objeto-efetivamente em seus programas, você deve expandir sua visão de programação para incluir não só membros e mensagens de uma classe individual, mas também a comunidade entre classes e seus relacionamentos com cada outra. Apesar disto requerer significativos esforços, é um salutar esforço, porque os resultados serão um desenvolvimento rápido do programa, melhor organização do código, e manutenção mais fácil do código.Comentários (em inglês)

Exercícios



Soluções para os exercícios selecionados podem ser encontradas no documento eletrônico The Thinking in Java Annotated Solution Guide, disponível por uma pequena taxa em www.BruceEckel.com.

  1. Adicione um novo método na classe básica de Shapes.java que imprime uma mensagem, mas não faça um override dela nas classes derivadas. Explique o que acontece. Agora faça um override dela em uma das classes derivadas mas não nas outras, e veja o que acontece. Finalmente, faça override em todas as classes derivadas. Análise
  2. Adicione um novo tipo de Shape a Shapes.java e verifique em main( ) que polimorfismo funcionar para seu novo tipo como funcionava nos tipos antigos. Análise
  3. Altere Music3.java para que what( ) se torne o método toString( ) do Object raiz. Tente imprimir os objetos Instrument usando System.out.println( ) (sem qualquer conversão). Análise
  4. Adicione um novo tipo de Instrument a Music3.java e verifique que polimorfismo funcionar para seu novo tipo. Análise
  5. Modifique Music3.java para que ele crie randomicamente objetos Instrument da maneira como Shapes.java faz. Análise
  6. Crie uma hierarquia de herança para Rodent: Mouse, Gerbil, Hamster, etc. Na classe básica, providencie métodos que são comuns a todos os Rodents, e faça override destes na classe derivada para executar comportamentos diferentes dependendo do tipo específico de Rodent. Crie um array de Rodent, preencha-o com tipos específicos diferentes de Rodents, e chame seus métodos de classe básica para ver o que acontece. Análise
  7. Modifique o Exercício 6 para que Rodent seja uma classe abstract. Faça os métodos de Rodent abstratos tanto quanto possível. Análise
  8. Crie uma classe como abstract sem incluir quaisquer métodos abstract e verifique que você não pode criar quaisquer instâncias daquela classe. Análise
  9. Adicione a classe Pickle a Sandwich.java. Análise
  10. Modifique o Exercício 6 para que demonstre a ordem de inicialização das classes básicas e classes derivadas. Agora adicione objetos membros a ambas as classes básicas e classes derivadas e mostre a ordem em que a inicialização ocorre durante a construção. Análise
  11. Crie uma classe básica com dois métodos. No primeiro método, chame o segundo método. Herde a classe e faça um override do segundo método. Crie um objeto da classe derivada, faça upcast dele para o tipo básico, e chame o primeiro método. Explique oque acontece. Análise
  12. Crie uma classe básica com um método abstract print( ) que sofre um override na classe derivada. A versão com override do método imprime o valor de uma variável int definida na classe derivada. No ponto de definição desta variável, dê a ela um valor não zero. No construtor da classe básica, chame este método. Em main( ), crie um objeto do tipo derivado, e então chame seu método print( ). Explique os resultados.Análise
  13. Seguindo o exemplo em Transmogrify.java, crie uma classe Starship contendo uma referência a AlertStatus que pode indicar três estados diferentes. Inclua métodos para alterar os estados. Análise
  14. Crie uma classe abstract sem métodos. Derive uma classe e adicione um método. Crie um método static que toma uma referência a classe básica, faça um downcast dela para a classe derivada, e chame o método. Em main( ), demonstre que funciona. Agora coloque a declaração abstract para o método na classe básica, então eliminando a necessidade do downcast. Análise





[32] Para programadores C++, isto é análogo as funções virtuais puras do C++.


Anterior Próximo Página Inicial Índice Conteúdo