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

6: Reusing Classes



Uma das mais importantes características de Java é a reutilização de código. Mas para ser revolucionário, você tem que estar hábil para fazer muito mais que copiar código e mudá-lo.



Esta abordagem é utilizada em lingugens procedurais como o C, e não tem trabalhado muito bem. Assim como tudo em Java, a solução gira em torno da classe. Você reutiliza o código pela criação de novas classes, mas no lugar de criá-las do desde o início, você usa classes existentes que alguém já contruiu e depurou. Comentários (em inglês)



O truque é usar a classe sem sujar o código existente. Nese capitulo, você verá duas formar de executar isto. A primeira é um pouco direta : você simplesmente cria objetos da sua classe existente dentro da nova classe. Isto é chamado composição, porque a nova classe é composta de objetos de classes existentes. Você simplismente reutiliza as funcionalidades do código, não seu método. Comentários (em inglês)



A segunda aproximação é mais sutil. Ela cria uma nova classe com o tipo de uma classe existente. Você literalmente utiliza o método de uma classe existente e adiciona código sem modifica-la. Este ato mágico é chamado herança e o compilador faz a maior parte do trabalho. Herança é um dos fundamentos da orientação a objetos. Comentários (em inglês)



Disto retiramos que a maior parte da sintaxe e comportamento são similares para ambas composição e herança ( que faz sentido porque ambas são formas de fazer novos de tipos existentes). Neste capitulo, você aprenderá sobre mecanismos de reutilização de código.Comentários (em inglês)

Sintaxe de composição



Até agora, a composição tem sido usada muito freqüentemente. Você simplesmente coloca a referência dos objetos dentro das novas classes.Por exemplo, suponha que você quisesse um objeto que controlasse muitos objetos String, dois objetos primitivos, e um objeto de outra classe. Para objetos não primitivos, você coloca as referências dentro da sua nova classe, porém você define os objetos primitivos diretamente:



//: c06:SprinklerSystem.java
// Composição para reutilização de código.
import com.bruceeckel.simpletest.*;

class WaterSource {
  private String s;
  WaterSource() {
    System.out.println("WaterSource()");
    s = new String("Constructed");
  }
  public String toString() { return s; }
}

public class SprinklerSystem {
  private static Test monitor = new Test();
  private String valve1, valve2, valve3, valve4;
  private WaterSource source;
  private int i;
  private float f;
  public String toString() {
    return
      "valve1 = " + valve1 + "\\n" +
      "valve2 = " + valve2 + "\\n" +
      "valve3 = " + valve3 + "\\n" +
      "valve4 = " + valve4 + "\\n" +
      "i = " + i + "\\n" +
      "f = " + f + "\\n" +
      "source = " + source;
  }
  public static void main(String[] args) {
    SprinklerSystem sprinklers = new SprinklerSystem();
    System.out.println(sprinklers);
    monitor.expect(new String[] {
      "valve1 = null",
      "valve2 = null",
      "valve3 = null",
      "valve4 = null",
      "i = 0",
      "f = 0.0",
      "source = null"
    });
  }
} ///:~




Um dos métodos definidos em ambas as classes é especial: toString( ). Você aprenderá mais tarde que todo objeto não primitivo tem um método toString( ) , e ele é chamado em condições especiais quando o compilador quer um String mas tem um objeto. Portanto na expressão em SprinklerSystem.toString( ):



"source = " + source;




o compilador vê você tentando incluir um objeto String("source = ") para WaterSource. Porque você somente pode adicionar um String num outro String, ele diz, "Vou transformar source num String chamando toString( )!” Depois de fazer isto ele pode combinar os dois Strings e passar o String resultante para System.out.println( ). Toda vez que você queira permitir esse comportamento para a classe que você tenha criado, você precisa somente escrever um método toString( ). Comentários (em inglês)



Primitivos que são campos em uma classe são automaticamente inicializados com valor zero, como podemos perceber no capítulo 2. Mas os objetos referência são inicializados com valor null, e se você tentar chamar métodos para qualquer um deles, você irá receber uma excecão. E isso é realmente bom (e útil), que você continue podendo imprimi-los sem provocar uma exceção.Comentários (em inglês)



E realmente faz sentido que o compilador não cria somente um objeto padrão para cada referência, porque isso poderia implicar em um overhead desnecessário em muitos cassos. Se você quer inicializar as referências, podesse fazer: Comentários (em inglês)

  1. No ponto onde os objetos são definidos. Isso significa que eles seriam sempre inicializados antes do construtor sert chamado.Comentários (em inglês
  2. No construtos para aquela classe.Comentários (em inglês
  3. Exatamente antes você atualmente precisar usar o objeto. Isso é conhecido como lazy initialization. É possível reduzir o overhead em situações onde a criação do objeto é cara e o objeto não necessita ser criado toda as vezes.Comentários (em inglês


Todas as três abordagens são demonstradas aqui: Comentários (em inglês)



//: c06:Bath.java
// Inicialização do construtor com composição.
import com.bruceeckel.simpletest.*;

class Soap {
  private String s;
  Soap() {
    System.out.println("Soap()");
    s = new String("Constructed");
  }
  public String toString() { return s; }
}

public class Bath {
  private static Test monitor = new Test();
  private String // Inicializando no ponto de definição:
    s1 = new String("Happy"),
    s2 = "Happy",
    s3, s4;
  private Soap castille;
  private int i;
  private float toy;
  public Bath() {
    System.out.println("Inside Bath()");
    s3 = new String("Joy");
    i = 47;
    toy = 3.14f;
    castille = new Soap();
  }
  public String toString() {
    if(s4 == null) // Inicialização atrasada:
      s4 = new String("Joy");
    return
      "s1 = " + s1 + "\\n" +
      "s2 = " + s2 + "\\n" +
      "s3 = " + s3 + "\\n" +
      "s4 = " + s4 + "\\n" +
      "i = " + i + "\\n" +
      "toy = " + toy + "\\n" +
      "castille = " + castille;
  }
  public static void main(String[] args) {
    Bath b = new Bath();
    System.out.println(b);
    monitor.expect(new String[] {
      "Inside Bath()",
      "Soap()",
      "s1 = Happy",
      "s2 = Happy",
      "s3 = Joy",
      "s4 = Joy",
      "i = 47",
      "toy = 3.14",
      "castille = Constructed"
    });
  }
} ///:~




Note que no construtor Bath , uma declaração é executada antes de qualquer inicialização tomar lugar. Quando você não inicializa no ponto de definição, não há ainda garantias de que você executará qualquer inicialização antes de enviar uma mensagem para uma referência de um objeto—exceto de uma inevitável exceção em tempo de execução. Comentários (em inglês)



Quando toString( ) é chamado ele preenche s4 assim todos aqueles campos são apropriadamente inicializados antes do momento em que serão usados. Comentários (em inglês)

Sintaxe da herança



Herança é uma parte integral de Java (e de todas as linguagens POO). Isto denota que você está sempre fazendo herança quando você cria uma classe, porque a menos que você explicitamente herde de uma outra classe, você implicitamente herda da classe raiz Java padrão Object. Comentários (em inglês)



A sintaxe para composição é óbvia, mas para fazer herança a forma é diferente e distinta. Quando você herda de uma classe, você diz “Esta classe nova é como aquela classe antiga.” Você faz isso no código colocando o nome da classe normalmente, mas antes da chave que inicia seção do corpo da classe , põe a palavra-reservada extends seguido pelo nome da classe base. Quando você faz isso, você automaticamente herda todos os campos e métodos da classe base. Aqui está um exemplo: Comentários (em inglês)



//: c06:Detergent.java
// Sintaxe de herança e propriedades.
import com.bruceeckel.simpletest.*;

class Cleanser {
  protected static Test monitor = new Test();
  private String s = new String("Cleanser");
  public void append(String a) { s += a; }
  public void dilute() { append(" dilute()"); }
  public void apply() { append(" apply()"); }
  public void scrub() { append(" scrub()"); }
  public String toString() { return s; }
  public static void main(String[] args) {
    Cleanser x = new Cleanser();
    x.dilute(); x.apply(); x.scrub();
    System.out.println(x);
    monitor.expect(new String[] {
      "Cleanser dilute() apply() scrub()"
    });
  }
}

public class Detergent extends Cleanser {
  // Alterar um método:
  public void scrub() {
    append(" Detergent.scrub()");
    super.scrub(); // Chamar versão da classe base
  }
  // Adicionar métodos a interface:
  public void foam() { append(" foam()"); }
  // Testar uma nova classe:
  public static void main(String[] args) {
    Detergent x = new Detergent();
    x.dilute();
    x.apply();
    x.scrub();
    x.foam();
    System.out.println(x);
    System.out.println("Testing base class:");
    monitor.expect(new String[] {
      "Cleanser dilute() apply() " +
      "Detergent.scrub() scrub() foam()",
      "Testing base class:",
    });
    Cleanser.main(args);
  }
} ///:~




Isto demonstra um número de artifícios. Primeiro, no método Cleanser append( ) , Strings são concatenadas com s usando o operador += , o qual é um dos operadores (junto com ‘+’) que os projetistas Java “sobrecarregaram” para funcionar com Strings. Comentários (em inglês)



Segundo, ambos Cleanser e Detergent contêm um método main( ) . Você pode criar um main( ) para cada uma de suas classes, e é fortemente recomendado codificar desta maneira assim que seu código teste está arrumado na classe. Mesmo que você tenha uma porção de classes no programa, somente a main( ) da classe invocada na linha de comando será chamada. (contanto que main( ) seja public, não interessa se a classe de qual faz parte é public.) Assim neste caso, quando você diz java Detergent, Detergent.main( ) será chamado. Mas você pode também dizer java Cleanser para invocar Cleanser.main( ), mesmo que Cleanser não seja uma classe public . Esta técnica de colocar um main( ) em cada classe permite teste fácil da unidade de cada classe. E você não precisa remover o main( ) quando você tiver terminado os testes; você pode deixá-lo lá para futuros testes. Comentários (em inglês)



Aqui, você pode ver que Detergent.main( ) chama Cleanser.main( ) explicitamente, passando-lhe os mesmos argumentos da linha de comando (contudo, você poderia passar a ele qualquer array String ). Comentários (em inglês)



É importante que todos os métodos em Cleanser sejam public. Lembre que se você deixar desligado qualquer especificador de acesso, o padrão para membros será o do acesso a package, o qual permite acesso somente para membros da package. Então, dentro desta package, qualquer um usaria estes métodos se não houver especificador de acesso. Detergent não teria problemas, por exemplo. Contudo, se uma classe de alguma outra package herdasse de Cleanser, ela acessaria apenas membros public . Assim para planejar para herança, como uma regra geral torne todos os campos private e todos os métodos public. (membros protected também permitem acesso por classes derivadas; você aprenderá sobre isso mais tarde.) Naturalmente, em casos particulares você precisa fazer ajustes, mas esta é uma regra útil. Comentários (em inglês)



Note que Cleanser tem um conjunto de métodos em sua interface: append( ), dilute( ), apply( ), scrub( ), e toString( ). Porque Detergent é derivada de Cleanser (via palavra chave extends ), ela automaticamente recebe todos aqueles métodos em sua interface, mesmo que você não os veja explicitamente definidos em Detergent. Você pode pensar em herança, então, como reutilização de classes. Comentários (em inglês)



Como visto em scrub( ), é possível tomar métodos que foram definidos na classe base e modificá-los. Neste caso, você pode querer chamar o método da classe base de dentro de sua nova versão. Mas dentro de scrub( ), você não pode simplesmente chamar scrub( ), pois isto produziria uma chamada recursiva, a qual não é o que você quer. Para resolver este problema, Java tem a palavra chave super que refere a “superclasse” de que a classe corrente foi herdada. Então a expressão super.scrub( ) chama a versão da classe base do método scrub( ). Comentários (em inglês)



Quando herdando você não está restrito a usar somente os métodos da classe base. Você pode também adicionar novos métodos a classe derivada exatamente da mesma maneira que você coloca qualquer método em uma classe: é só definí-lo. O método foam( ) é um exemplo disto. Comentários (em inglês)



Em Detergent.main( ) você pode ver que de um objeto Detergent , você pode chamar todos os métodos que estão disponíveis em Cleanser tão bem quanto em Detergent (i.e., foam( )). Comentários (em inglês)

Inicializando a classe base



Desde que há agora duas classes envolvidas—a classe base e a classe derivada—ao invés de só uma, pode haver um pouco de confusão ao tentar imaginar o objeto resultante produzido pela classe derivada. Por um lado, parece que a nova classe tem a mesma interface que a classe base e talvez alguns métodos e campos adicionais. Mas herança não é só cópia da interface da classe base. Quando você cria um objeto da classe derivada, ele contêm dentro dele um sub-objeto da classe base. Este sub-objeto é o mesmo que se você tivesse criado um objeto da classe base através dela mesma. Só que por outro lado, o sub-objeto da classe base está escondido detnro do objeto da classe derivada. Comentários (em inglês)



Naturalmente, é essencial que o sub-objeto da classe base seja inicializado corretamente, e há somente uma maneira para garantir isto: executar a inicialização no construtor chamando o construtor da classe base, o qual tem todo o conhecimento apropriado e privilégios para executar a inicialização da classe base. Java insere automaticamente chamadas ao construtor da classe base no construtor da classe derivada. O próximo exemplo mostra isto funcionando com três níveis de herança: Comentários (em inglês)



//: c06:Cartoon.java
// Chamada de construtor durante a herança
import com.bruceeckel.simpletest.*;

class Art {
  Art() {
    System.out.println("Art constructor");
  }
}

class Drawing extends Art {
  Drawing() {
    System.out.println("Drawing constructor");
  }
}

public class Cartoon extends Drawing {
  private static Test monitor = new Test();
  public Cartoon() {
    System.out.println("Cartoon constructor");
  }
  public static void main(String[] args) {
    Cartoon x = new Cartoon();
    monitor.expect(new String[] {
      "Art constructor",
      "Drawing constructor",
      "Cartoon constructor"
    });
  }
} ///:~




Você pode ver que a construção acontece da base “exterior,” assim a classe base é inicializada antes dos construtores da classe derivada poderem acessá-la. Mesmo que você não crie um construtor para Cartoon( ), o compilador sintetizará um construtor padrão para você que chama o construtor da classe base. Comentários (em inglês)

Construtor com argumentos.



O precedente exemplo possui um construtor padrão; isto é, ele não possui quaisquer argumentos. É fácil para o compilador chamá-lo porque não há questões sobre que argumentos passar. Se sua classe não tem argumentos padrão, ou se desejar chamar um contrutor da classe-base que possui um argumento, você deve explicitamente escrever a chamada para o construtor da classe-base usando a palavra chave super e a lista de argumentos apropriada:



//: c06:Chess.java
// Herança, construtores e argumentos.
import com.bruceeckel.simpletest.*;

class Game {
  Game(int i) {
    System.out.println("Game constructor");
  }
}

class BoardGame extends Game {
  BoardGame(int i) {
    super(i);
    System.out.println("BoardGame constructor");
  }
}

public class Chess extends BoardGame {
  private static Test monitor = new Test();
  Chess() {
    super(11);
    System.out.println("Chess constructor");
  }
  public static void main(String[] args) {
    Chess x = new Chess();
    monitor.expect(new String[] {
      "Game constructor",
      "BoardGame constructor",
      "Chess constructor"
    });
  }
} ///:~




Se você não chamar o construtor da classe base em BoardGame( ), o compilador reclamará que ele não encontra um construtor na forma de Game( ). Em adição, a chamada ao construtor da classe base deve ser a primeira coisa a fazer no construtor da classe derivada. (O compilador lembrará você se você errar nisso.) Comentários (em inglês)

Capturando exceções do construtor base



Como já notado, o compilador força você a colocar a chamada ao construtor da classe base em primeiro lugar no corpo do construtor da classe derivada. Isto significa que nada mais pode aparecer antes dela. Como você verá no Capítulo 9, isto também previne o construtor de classe derivada de capturar qualquer exceção que venha da classe base. Isto pode ser inconveniente algumas vezes. Comentários (em inglês)

Combinando composição
e herança



É muito comum usar composição e herança juntas. O exemplo seguinte mostra a criação de uma classe mais complexa, usando tanto herança quanto composição, durante a inicialização necessária do construtor:



//: c06:PlaceSetting.java
// Combinando composição e herança.
import com.bruceeckel.simpletest.*;

class Plate {
  Plate(int i) {
    System.out.println("Plate constructor");
  }
}

class DinnerPlate extends Plate {
  DinnerPlate(int i) {
    super(i);
    System.out.println("DinnerPlate constructor");
  }
}

class Utensil {
  Utensil(int i) {
    System.out.println("Utensil constructor");
  }
}

class Spoon extends Utensil {
  Spoon(int i) {
    super(i);
    System.out.println("Spoon constructor");
  }
}

class Fork extends Utensil {
  Fork(int i) {
    super(i);
    System.out.println("Fork constructor");
  }
}

class Knife extends Utensil {
  Knife(int i) {
    super(i);
    System.out.println("Knife constructor");
  }
}

// Uma maneira culta de fazer algo:
class Custom {
  Custom(int i) {
    System.out.println("Custom constructor");
  }
}

public class PlaceSetting extends Custom {
  private static Test monitor = new Test();
  private Spoon sp;
  private Fork frk;
  private Knife kn;
  private DinnerPlate pl;
  public PlaceSetting(int i) {
    super(i + 1);
    sp = new Spoon(i + 2);
    frk = new Fork(i + 3);
    kn = new Knife(i + 4);
    pl = new DinnerPlate(i + 5);
    System.out.println("PlaceSetting constructor");
  }
  public static void main(String[] args) {
    PlaceSetting x = new PlaceSetting(9);
    monitor.expect(new String[] {
      "Custom constructor",
      "Utensil constructor",
      "Spoon constructor",
      "Utensil constructor",
      "Fork constructor",
      "Utensil constructor",
      "Knife constructor",
      "Plate constructor",
      "DinnerPlate constructor",
      "PlaceSetting constructor"
    });
  }
} ///:~




Embora o compilador force você a inicializar as classes base, e exija que você faça-o imediatamente no início do construtor, ele não fica vigiando para ter certeza de que você inicializou os objetos membro, assim você deve se lembrar de prestar atenção a isto. Comentários (em inglês)

Garantindo a limpeza apropriada



Java não tem o conceito que C++ tem de um destrutor, um método que é chamado automaticamente quando um objeto é destruído. A razão é que, em Java, a idéia é simplesmente esquecer dos objetos, no lugar de destruí-los, permitindo que o coletor de lixo recupere a memória conforme necessário.Comentários (em inglês)



Frequentemente isto está correto, mas há vezes quando sua classe pode executar algumas atividades durante seu tempo de vida que necessitam limpeza. Como mencionado no Capítulo 4, você não pode saber quando o coletor de lixo será chamado, ou se ele será chamado. Assim se você quer algo limpo de uma classe, você deve escrever explicitamente um método especial para fazê-lo, e estar certo que o programador cliente sabe que ele deve chamar este método. No topo deste—como descrito no Capítulo 9 (“Manuseando erros com Exceptions”)—você deve se proteger contra uma exceção por colocar esta limpeza em uma clausula finally. Comentários (em inglês)



Considere um exemplo de sistema de projeto auxiliado por computador que desenha figuras na tela:



//: c06:CADSystem.java
// Assegurando limpeza apropriada.
package c06;
import com.bruceeckel.simpletest.*;
import java.util.*;

class Shape {
  Shape(int i) {
    System.out.println("Shape constructor");
  }
  void dispose() {
    System.out.println("Shape dispose");
  }
}

class Circle extends Shape {
  Circle(int i) {
    super(i);
    System.out.println("Drawing Circle");
  }
  void dispose() {
    System.out.println("Erasing Circle");
    super.dispose();
  }
}

class Triangle extends Shape {
  Triangle(int i) {
    super(i);
    System.out.println("Drawing Triangle");
  }
  void dispose() {
    System.out.println("Erasing Triangle");
    super.dispose();
  }
}

class Line extends Shape {
  private int start, end;
  Line(int start, int end) {
    super(start);
    this.start = start;
    this.end = end;
    System.out.println("Drawing Line: "+ start+ ", "+ end);
  }
  void dispose() {
    System.out.println("Erasing Line: "+ start+ ", "+ end);
    super.dispose();
  }
}

public class CADSystem extends Shape {
  private static Test monitor = new Test();
  private Circle c;
  private Triangle t;
  private Line[] lines = new Line[5];
  public CADSystem(int i) {
    super(i + 1);
    for(int j = 0; j < lines.length; j++)
      lines[j] = new Line(j, j*j);
    c = new Circle(1);
    t = new Triangle(1);
    System.out.println("Combined constructor");
  }
  public void dispose() {
    System.out.println("CADSystem.dispose()");
    // A ordem de limpeza é inversa 
    // da ordem de inicialização
    t.dispose();
    c.dispose();
    for(int i = lines.length - 1; i >= 0; i--)
      lines[i].dispose();
    super.dispose();
  }
  public static void main(String[] args) {
    CADSystem x = new CADSystem(47);
    try {
      // Código e manuseio de exceção...
    } finally {
      x.dispose();
    }
    monitor.expect(new String[] {
      "Shape constructor",
      "Shape constructor",
      "Drawing Line: 0, 0",
      "Shape constructor",
      "Drawing Line: 1, 1",
      "Shape constructor",
      "Drawing Line: 2, 4",
      "Shape constructor",
      "Drawing Line: 3, 9",
      "Shape constructor",
      "Drawing Line: 4, 16",
      "Shape constructor",
      "Drawing Circle",
      "Shape constructor",
      "Drawing Triangle",
      "Combined constructor",
      "CADSystem.dispose()",
      "Erasing Triangle",
      "Shape dispose",
      "Erasing Circle",
      "Shape dispose",
      "Erasing Line: 4, 16",
      "Shape dispose",
      "Erasing Line: 3, 9",
      "Shape dispose",
      "Erasing Line: 2, 4",
      "Shape dispose",
      "Erasing Line: 1, 1",
      "Shape dispose",
      "Erasing Line: 0, 0",
      "Shape dispose",
      "Shape dispose"
    });
  }
} ///:~




Tudo neste sistema é algum tipo de Shape (o qual é ele mesmo um tipo de Object, desde que ele é implicitamente herdado da classe raiz). Cada classe faz um override do método dispose( ) de Shape em adição chamando a versão da classe base do método usando super. As classes específicas de ShapeCircle, Triangle, e Line—todas tem construtores que “desenham,” embora qualquer método chamado durante o tempo de vida do objeto seria responsável por fazer algo que precisasse de limpeza. Cada classe tem seu próprio método dispose( ) para restaurar coisas não-memória de volta a forma que eram antes do objeto ter existido. Comentários (em inglês)



Em main( ), você pode ver duas palavras chaves que são novas, e não será apresentadas oficialmente até o Capítulo 9: try e finally. A palavra chave try indica que o bloco que segue (delimitado por chaves fechadas) é uma região protegida , o que significa que a ela é dado um tratamento especial. Um destes tratamentos especiais é que o código na clausula finally que segue esta região protegida é sempre executado, não importando como se saia do bloco try . (Com manuseio de exceções é possível deixar um bloco try de um número não ordinário de maneiras.) Aqui, a clausula finally está dizendo “sempre chame dispose( ) de x, não importando o que aconteça.” Estas palavras chaves será explicadas detalhadamente no Capítulo 9.Comentários (em inglês)



Note que no seu método de limpeza, você deve também ficar atento a ordem de chamada de métodos de limpeza da classe base e objetos membros no caso de um sub-objeto depender de outro. Em geral, você deveria seguir a mesma forma imposta por um compilador C++ em seus destrutores: primeiro execute todos os trabalhos de limpeza específicos para sua classe, na ordem inversa de criação. (Em geral, isto requer que elementos da classe base ainda estejam viáveis.) Então chame o método de limpeza da classe base, como demonstrado aqui.Comentários (em inglês)



Pode haver muitos casos nos quais o assunto limpeza não é um problema; você só deixa o coletor de lixo fazer o trabalho. Mas quando você deve fazê-lo explicitamente, diligência e atenção são necessários, porque não há muito com que poder contar quando a coleta de lixo começa. O coletor de lixo não pode nunca ser chamado. Se for, pode reclamar objetos em qualquer ordem que quiser. O melhor é não contar com a coleta de lixo para nada além da falta de memória. Se você quer que a limpeza aconteça, faça seu próprio método de limpeza e não conte com finalize( ). Comentários (em inglês)

Escondendo nomes



Se uma classe base Java tem um nome de método que é sobrecarregado diveras vezes, redefinir aquele nome de método na classe derivada não ocultará nada da versão da classe base (exceto em C++). Trabalhos assim sobrecarregam mesmo se o método foi definido a este nível ou em uma classe básica:



//: c06:Hide.java
// Sobrecarregar um nome de método da classe básica em uma classe derivada 
// não oculta as versões da classe básica.
import com.bruceeckel.simpletest.*;

class Homer {
  char doh(char c) {
    System.out.println("doh(char)");
    return 'd';
  }
  float doh(float f) {
    System.out.println("doh(float)");
    return 1.0f;
  }
}

class Milhouse {}

class Bart extends Homer {
  void doh(Milhouse m) {
    System.out.println("doh(Milhouse)");
  }
}

public class Hide {
  private static Test monitor = new Test();
  public static void main(String[] args) {
    Bart b = new Bart();
    b.doh(1);
    b.doh('x');
    b.doh(1.0f);
    b.doh(new Milhouse());
    monitor.expect(new String[] {
      "doh(float)",
      "doh(char)",
      "doh(float)",
      "doh(Milhouse)"
    });
  }
} ///:~




Você pode ver que todos os métodos sobrecarregados de Homer são avaliáveis em Bart, mesmo que Bart introduza um novo método sobrecarregado (em C++ fazer isto ocultaria os métodos da classe básica). Como você verá no próximo capítulo, é bem mais comum fazer override de métodos do mesmo nome, usando exatamente a mesma assinatura e tipo de retorno da classe básica. Pode ser mais confuso contudo (o que é por que C++ o proíbe—para prevenir que você faça o que é provavelmente um erro). Comentários (em inglês)

Escolhendo composição
ou herança



Ambos, composição e herança permitem que você coloque sub-objetos dentro de sua nova classe (composição faz isso explicitamente—com herança isto está implícito). Você pode ficar surpreso sobre a diferença entre os dois, e na dúvida sobre quando escolher um ou outro. Comentários (em inglês)



Composição é geralmente usada quando você quer os artifícios de uma classes existente dentro de sua nova classe, mas não sua interface. Isto é, você embute um objeto assim você pode usá-lo para implementar funcionalidade em sua nova classe, mas o usuário de sua nova classe vê a interface que você definiu para a nova classe no lugar da interface do objeto embutido. Para este efeito, você embute objetos private de classes existentes dentro de sua nova classe. Comentários (em inglês)



Algumas vezes faz sentido permitir que a classe do usuário acesse diretamente a composição de sua nova classe; Isto é, tornar os objetos do membro public. Os objetos membro usam implementação oculta a eles mesmos, assim esta é a coisa segura a fazer. Quando o usuário sabe que você está montando um feixe de partes, torna a interface mais fácil de compreender. Um objeto car é um bom exemplo: Comentários (em inglês)



//: c06:Car.java
// Composiçao com objetos públicos.

class Engine {
  public void start() {}
  public void rev() {}
  public void stop() {}
}

class Wheel {
  public void inflate(int psi) {}
}

class Window {
  public void rollup() {}
  public void rolldown() {}
}

class Door {
  public Window window = new Window();
  public void open() {}
  public void close() {}
}

public class Car {
  public Engine engine = new Engine();
  public Wheel[] wheel = new Wheel[4];
  public Door
    left = new Door(),
    right = new Door(); // duas portas
  public Car() {
    for(int i = 0; i < 4; i++)
      wheel[i] = new Wheel();
  }
  public static void main(String[] args) {
    Car car = new Car();
    car.left.window.rollup();
    car.wheel[0].inflate(72);
  }
} ///:~




Porque neste caso, a composição de um carro é parte da análise do problema (e não simplesmente parte do projeto fundamental), tornar os membros public ajuda os programadores cliente a entender como usar a classe e requer menos complexidade do código para o criador da classe. Contudo, tenha em mente que é um caso especial, e que em geral você poderia fazer os campos private. Comentários (em inglês)



Quando você herda, você toma uma classe existente e faz uma versão especial dela. Em geral, isto significa que você está tomando uma classe de propósito geral e a especializando para uma necessidade particular. Com uma breve idéia, você verá que não faria sentido compor um carro usando um objeto veículo, ele é um veículo. O é-um relacionamento que expressa uma herança, e o tem-um relacionamento que expressa uma composição. Comentários (em inglês)

Protegido



Agora que você foi introduzido na herança, a palavra chave protected finalmente tem significado. Em um mundo ideal, a palavra chave private seria suficiente. Em projetos reais, há vezes em que você quer fazer algo oculto do resto do mundo e ainda permitir acesso a membros de classes derivadas. A palavra chave protected é um nódulo de pragmatismo. Ela diz “Isto é tão private quanto a classe do usuário permitir, mas avaliável para quem herde desta classe ou alguém na mesma package.” (Em Java, protected também promove acesso a package.) Comentários (em inglês)



A melhor abordagem é deixar os campos private; você sempre preservaria seus direitos de alterar a implementação fundamental. Você pode então permitir acesso controlado para herdeiros de sua classe através de métodos protected :



//: c06:Orc.java
// A palavra chave protected.
import com.bruceeckel.simpletest.*;
import java.util.*;

class Villain {
  private String name;
  protected void set(String nm) { name = nm; }
  public Villain(String name) { this.name = name; }
  public String toString() {
    return "I'm a Villain and my name is " + name;
  }
}

public class Orc extends Villain {
  private static Test monitor = new Test();
  private int orcNumber;
  public Orc(String name, int orcNumber) {
    super(name);
    this.orcNumber = orcNumber;
  }
  public void change(String name, int orcNumber) {
    set(name); // Avaliável porque é protected
    this.orcNumber = orcNumber;
  }
  public String toString() {
    return "Orc " + orcNumber + ": " + super.toString();
  }
  public static void main(String[] args) {
    Orc orc = new Orc("Limburger", 12);
    System.out.println(orc);
    orc.change("Bob", 19);
    System.out.println(orc);
    monitor.expect(new String[] {
      "Orc 12: I'm a Villain and my name is Limburger",
      "Orc 19: I'm a Villain and my name is Bob"
    });
  }
} ///:~




Você pode ver que change( ) tem acesso a set( ) porque ele é protected. também note a maneira que o método toString( ) de Orc é definido em termos de versão de classe básica de toString( ). Comentários (em inglês)

Desenvolvimento incremental



Uma das vantagens da herança é que ela suporta desenvolvimento incremental. Você pode introduzir código novo sem causar bugs no código existente; de fato, você isola novos bugs dentro do código novo. Pela herança de uma classe funcional existente e adicionando campos e métodos (e redefinindo métodos existentes), você deixa o código existente—que ainda pode ser usado por alguém—intocado e sem problemas. Se um problema acontece, você sabe que é em seu novo código, o qual é muito menor e mais fácil de ler que se você tivesse modificado o corpo do código existente. Comentários (em inglês)



É muito impressionante o quão limpamente as classes são separadas. Você nunca precisará do código fonte dos métodos para poder reutilizar o código. Se muito, você só importa a package. (Isto é verdade para ambos, herança e composição.) Comentários (em inglês)



É importante realizar que o desenvolvimetno de um programa é um processo incremental, assim como o aprendizado humano. Você pode fazer tantas análises quantas quiser, mas você ainda não saberá todas as respostas quando tiver terminado um projeto. Você terá muito mais sucesso—e mais feedback imediato—se você começar a “cultivar” seu projeto como uma criatura orgânica evolutiva do que construindo-o todo de uma vez como um arranha-céu . Comentários (em inglês)



Embora herança por experiência pode ser considerada uma técnica útil, em algum ponto depois das coisas se estabilizarem você precisa tomar uma nova visão da hierarquia de sua classe com um olho na quebra dela em uma estrutura sensível. Lembre que por baixo dela toda, herança está representando a expressão de um relacionamento que diz: “Esta nova classe é um tipo daquela velha classe.” Seu programa não deveria ter tido a ver com puxar pedaços em volta, mas ao invés, com criação e manipulação de objetos de vários tipos para expressar um modelo nos termos que vieram do espaço do problema. Comentários (em inglês)

Upcasting [Conversão para cima]



O aspecto mais importante da herança não é que ela provê métodos para a nova classe. É o relacionamento expresso entre a nova classe a a classe básica. Este relacionamento pode ser resumido pelo dito, “A nova classe é um tipo da classe existente.” Comentários (em inglês)



Esta descrição não é só uma maneira estranha de explicar herança—ela é suportada diretamente pela linguagem. Como um exemplo, considere uma classe básica chamada Instrument que representa instrumentos musicais, e uma classe derivada chamada Wind. Porque herança significa que todos os métodos da classe básica são também avaliáveis na classe derivada, qualquer mensagem que você pode mandar para a classe básica pode também ser enviada a classe derivada. Se a classe Instrument tem um método play( ) , assim será nos instrumentos Wind . Isto significa que nós podemos dizer com certeza que um objeto Wind é também um tipo de Instrument. O exemplo seguinte mostra como o compilador suporta esta noção: Comentários (em inglês)



//: c06:Wind.java
// Herança e upcasting.
import java.util.*;

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

// Objetos Wind são instrumentos
// porque eles tem a mesma interface:
public class Wind extends Instrument {
  public static void main(String[] args) {
    Wind flute = new Wind();
    Instrument.tune(flute); // Upcasting
  }
} ///:~




O que é interessante neste exemplo, é o método tune( ) , o qual aceita uma referência a Instrument . Contudo, em Wind.main( ) o método tune( ) é chamado dando-lhe uma referência a Wind . Dado que Java é rigorosa sobre a checagem de tipos, parece estranho que um método que aceita um tipo irá prontamente aceitar outro, até que você entenda que um objeto Wind é também um objeto Instrument , e não há método que tune( ) poderia chamar para um Instrument que também não seja para Wind. Dentro de tune( ), o código funciona para Instrument e qualquer coisa derivada de Instrument, e o ato de converter uma referência Wind em uma referência Instrument é chamada upcasting. Comentários (em inglês)

Por que “up[para cima]casting”?



A razão do termo é histórica, e baseada na forma com que os diagramas de herança de classe tem sido tradicionalmente desenhados: com a raiz no topo da página, desenvolvendo para baixo. (Naturalmente, você pode desenhar seus diagramas de qualquer forma que você achar útil.) O diagrama de herança para Wind.java é então: Comentários (em inglês)

PEJ313.png



Conversão de um tipo derivado para um tipo básico se move para cima [up] no diagrama de herança, assim isto é comumente referido como sendo um upcasting. Upcasting é sempre seguro porque você está indo de tipo mais específico para um tipo mais geral. Isto é, a classe derivada é um super-conjunto da classe básica. Ela pode conter mais métodos que a classe básica, mas deve conter ao menos os métodos da classe básica. A única coisa que pode ocorrer para a interface da classe durante a conversão para cima é que ela pode perder métodos, e não ganhá-los. Isto é por que o compilador permite conversões para cima sem qualquer tipagem explícita ou outras notações especiais. Comentários (em inglês)



Você pode também executar o inverso do upcasting, chamado downcasting, mas isto involve um dilema que é o assunto do Capítulo 10. Comentários (em inglês)

Composição x herança revisitada



Em programação orientada a objetos, a maneira mais provavel que você criará e usará código é pelo simples empacotamento de dados e métodos junto em uma classe, e usando objetos daquela classe. Você também usará classes existentes para construir novas classes com composição. Menos frequentemente, você usará herança. Assim embora herança receba muita ênfase enquanto aprendendo POO, não significa que você a usaria em todos os lugares que você possivelmente possa. Do contrário, você a usaria de forma reduzida, somente quanto estiver claro que herança é útil. Uma das maneiras mais claras de determinar se você deveria usar composição ou herança é perguntar se você sempre precisará fazer um upcast de sua nova classe para a classe básica. Se você deve fazer upcast, então herança é necessária, mas se você não precisa fazer upcast, então você deveria observar cuidadosamente se você precisa de herança. No próximo capítulo (em polimorfismo) haverá uma das mais convincentes razões para upcasting, mas se você lembrar de perguntar “Eu preciso fazer upcast?” você terá uma boa ferramenta para decidir entre composição e herança. Comentários (em inglês)

A palavra chave final



A palavra chave final de Java tem significados um pouco diferentes dependendo do contexto, mas em geral ela diz “Isto não pode ser alterado.” Você pode querer prevenir alterações por duas razões: design ou eficiência. Porque estas duas razões são bastante diferentes, é possível usar inapropriadamente a palavra chave final . Comentários (em inglês)



As sessões seguintes discutem três situações onde final pode ser utilizada: para dados, métodos, e classes. Comentários (em inglês)

Dados final



Muitas linguagens de programação tem uma maneira de contar ao compilador que um pedaço de dados é “constante.” Uma constante é útil por duas razões:

  1. Ela pode ser uma constante em tempo de compilação que nunca se alterará. Feedback
  2. Ela pode ser um valor inicializado em tempo de execução que você não quer que seja trocado. Feedback


No caso de uma constante em tempo de compilação, o compilador permite “dobrar” o valor constante dentro de qualquer cálculo em qual ele está sendo usado; Isto é, o cálculo pode ser executado em tempo de compilação, eliminando alguns atrasos em tempo de execução. Em Java, os tipos destas constantes devem ser primitivos e são expressos com a palavra chave final . Um valor deve ser dado no momento da definição de cada uma das constantes. Comentários (em inglês)



Um campo que é ao mesmo tempo static e final tem somente um pedaço de memória que não pode ser alterado. Comentários (em inglês)



Quando usando final com referências a objetos mais que com primitivos, o significado fica um pouco confuso. Com um primitivo, final torna o value uma constante, mas com uma referência a um objeto, final torna a referência uma constante. Uma vez que a referência está inicializada para um objeto, ele não pode nunca ser trocada para apontar outro objeto. Contudo, o objeto mesmo pode ser modificado; Java não provê uma maneira de tornar qualquer objeto arbitrário em uma constante. (Você pode, contudo, escrever sua classe de forma que objetos tenham o mesmo efeito de ser constantes.) Este restrição inclui arrays, os quais também são objetos. Comentários (em inglês)



Aqui está um exemplo que demonstra campos final :



//: c06:FinalData.java
// O efeito de final em campos.
import com.bruceeckel.simpletest.*;
import java.util.*;

class Value {
  int i; // Acesso package
  public Value(int i) { this.i = i; }
}

public class FinalData {
  private static Test monitor = new Test();
  private static Random rand = new Random();
  private String id;
  public FinalData(String id) { this.id = id; }
  // Podem ser constantes em tempo de compilação:
  private final int VAL_ONE = 9;
  private static final int VAL_TWO = 99;
  // Tipica constante pública:
  public static final int VAL_THREE = 39;
  // Não podem ser constantes em tempo de compilação:
  private final int i4 = rand.nextInt(20);
  static final int i5 = rand.nextInt(20);
  private Value v1 = new Value(11);
  private final Value v2 = new Value(22);
  private static final Value v3 = new Value(33);
  // Arrays:
  private final int[] a = { 1, 2, 3, 4, 5, 6 };
  public String toString() {
    return id + ": " + "i4 = " + i4 + ", i5 = " + i5;
  }
  public static void main(String[] args) {
    FinalData fd1 = new FinalData("fd1");
    //! fd1.VAL_ONE++; // Erro: o valor não poder ser alterado
    fd1.v2.i++; // Objeto não é constante!
    fd1.v1 = new Value(9); // OK -- não final
    for(int i = 0; i < fd1.a.length; i++)
      fd1.a[i]++; // Objeto não é constante!
    //! fd1.v2 = new Value(0); // Erro: Não pode
    //! fd1.v3 = new Value(1); // trocar referência
    //! fd1.a = new int[3];
    System.out.println(fd1);
    System.out.println("Creating new FinalData");
    FinalData fd2 = new FinalData("fd2");
    System.out.println(fd1);
    System.out.println(fd2);
    monitor.expect(new String[] {
      "%% fd1: i4 = \\\\d+, i5 = \\\\d+",
      "Creating new FinalData",
      "%% fd1: i4 = \\\\d+, i5 = \\\\d+",
      "%% fd2: i4 = \\\\d+, i5 = \\\\d+"
    });
  }
} ///:~




Como VAL_ONE e VAL_TWO são final primitivos com valores em tempo de compilação, eles podem ambos ser usados como constantes em tempo de compilação e não são diferentes de qualquer outra forma importante. VAL_THREE é a maneira mais típica de definição de constantes que você verá: public assim elas são usáveis fora da package, static para enfatizar que há somente uma, e final para dizer que é uma constante. Note que primitivos final static com valores iniciais constantes (isto é, constantes em tempo de compilação) são nomeadas com todas as letras maiúsculas por convenção, com palavras separadas por sublinhados. (Isto é igual como as constantes C, de onde a convenção se originou.) Também note que i5 não pode ser conhecido em tempo de compilação, assim não é colocado em maiúsculas. Comentários (em inglês)



Só porque algo é final não significa que seu valor é conhecido em tempo de compilação. Isto é demonstrado pela inicialização de i4 e i5 em tempo de execução usando números gerados randomicamente. Esta parte do exemplo também mostra a diferença entre tornar um valor final static ou não-static. Esta diferença se apresente somente quando os valores são inicializados em tempo de execução, pois os valores em tempo de compilação são tratados como o mesmo pelo compilador. (E presumivelmente otimizados fora da existência.) A diferença é mostrada quando você executa o programa. Note que os valores de i4 para fd1 e fd2 são idênticos, mas o valor para i5 não é alterado pela criação do segundo objeto FinalData . Isto é porque é um static e é inicializado uma vez após o carregamento e não a cada vez que um novo objeto é criado. Comentários (em inglês)



As variáveis v1 até v3 demonstram o significado de uma referência final . Como você pode ver em main( ), só porque v2 é final não significa que você não pode alterar seu valor. Porque é uma referência, final significa que você não pode re-ligar v2 a um novo objeto. Você pode também ver que o mesmo significado permanece verdadeiro para um array, o qual é só um outro tipo de referência. (Não há forma que eu conheça de tornar final as referências a elas mesmas .) Tornar referências final parece menos útil que tornar primitivos final. Comentários (em inglês)

Constantes vazias



Java permite a criação de finals vazias, que são campos que são declarados como final mas não é dado um valor de inicialização. Em todos os casos, a constante vazia deve ser inicializada antes de ser usada, e o compilador assegura isto. Contudo, constantes vazias provêm muito mais flexibilidade no uso da palavra chave final pois, por exemplo, um campo final dentro de uma classe pode agora ser diferente para cada objeto,e ainda ela mantêm sua qualidade imutável. Aqui está um exemplo: Comentários (em inglês)



//: c06:BlankFinal.java
// campos final "Vazios".

class Poppet {
  private int i;
  Poppet(int ii) { i = ii; }
}

public class BlankFinal {
  private final int i = 0; // final inicializado
  private final int j; // final vazio
  private final Poppet p; // referência final vazia
  // finals vazios DEVEM ser inicializados no construtor:
  public BlankFinal() {
    j = 1; // Inicializa final vazio
    p = new Poppet(1); // Inicializa referência final vazia
  }
  public BlankFinal(int x) {
    j = x; // Inicializa final vazia
    p = new Poppet(x); // Inicializa referência final vazia
  }
  public static void main(String[] args) {
    new BlankFinal();
    new BlankFinal(47);
  }
} ///:~




Você é forçado a executar atribuições a finals ou com uma expressão no ponto de definição de um campo ou em todo o construtor. Desta maneira é garantido que campos final estarão sempre inicializados antes do seu uso. Comentários (em inglês)

Argumentos constantes



Java permite que você torne argumentos final declarando-os como na lista de argumentos. Isto significa que dentro do método você não pode alterar aquilo para o qual a referência do argumento aponta:



//: c06:FinalArguments.java
// Usando "final" como argumentos de método.

class Gizmo {
  public void spin() {}
}

public class FinalArguments {
  void with(final Gizmo g) {
    //! g = new Gizmo(); // Illegal -- g é final
  }
  void without(Gizmo g) {
    g = new Gizmo(); // OK -- g não é final
    g.spin();
  }
  // void f(final int i) { i++; } // Não pode alterar
  // Você pode somente ler de um final primitivo:
  int g(final int i) { return i + 1; }
  public static void main(String[] args) {
    FinalArguments bf = new FinalArguments();
    bf.without(null);
    bf.with(null);
  }
} ///:~




Os métodos f( ) e g( ) mostram o que acontece quando argumentos primitivos são final: você pode ler o argumento, mas você não pode alterá-lo. Este artifício parece somente útil de forma marginal, e não é provável que algo irá usá-lo. Comentários (em inglês)

Métodos final



Há duas razões para métodos final . A primeira é colocar um “fecho” no método para prevenir qualquer classe que o herde de alterar seu significado. Isto é feito por razões de projeto quando você quer ter certeza que o comportamento de um método é mantido durante a herança e não poder ser suprimido. Comentários (em inglês)



A segunda razão para métodos final é a eficiência. Se você torna um método final, você está permitindo ao compilador tornar qualquer chamada àquele método em chamadas inline . Quando o compilador vê uma chamada a um método final , ele pode (em sua sabedoria) pular a abordagem normal de inserção de código para executar o mecanismo da chamada do método (colocando argumentos na pilha, saltando direto ao código do método e executando-o, voltando e limpando-o da pilha de argumentos, e negociando com o valor de retorno) e ao invés substituindo a chamada do método por uma cópia do código atual no corpo do método. Isto elimina a demora na chamada do método. Naturalmente, se um método é grande, então seu código começa a inchar, e você provavelmente não verá qualquer ganho de performance do inlining, pois qualquer implementação será deteriorada pelo montante de tempo gasto dentro do método. Está implícito que o compilador Java está habilitado a detectar estas situações e escolher sabiamente se deve tornar inline um método final . Contudo, é melhor deixar ao compilador e a JVM manusear assuntos de eficiência e tornar um método final somente se você quiser explicitamente prevenir overriding.[31] Comentários (em inglês)

final e private



Qualquer método private em uma classe é implicitamente final. Porque você não pode acessar um método private , você não pode fazer o override dele. Você pode adicionar o especificador final a um método private , mas ele não dá àqueles métodos nenhum significado extra.Comentários (em inglês)



Este assunto pode causar confusão, porque se você tentar fazer um override de um método private (o qual é implicitamente final), parece funcionar, e o compilador não dá uma mensagem de erro:



//: c06:FinalOverridingIllusion.java
// Apenas parece que você pode fazer override de 
// um método private ou private final.
import com.bruceeckel.simpletest.*;

class WithFinals {
  // Idêntico ao "private" sozinho:
  private final void f() {
    System.out.println("WithFinals.f()");
  }
  // Também automaticamente "final":
  private void g() {
    System.out.println("WithFinals.g()");
  }
}

class OverridingPrivate extends WithFinals {
  private final void f() {
    System.out.println("OverridingPrivate.f()");
  }
  private void g() {
    System.out.println("OverridingPrivate.g()");
  }
}

class OverridingPrivate2 extends OverridingPrivate {
  public final void f() {
    System.out.println("OverridingPrivate2.f()");
  }
  public void g() {
    System.out.println("OverridingPrivate2.g()");
  }
}

public class FinalOverridingIllusion {
  private static Test monitor = new Test();
  public static void main(String[] args) {
    OverridingPrivate2 op2 = new OverridingPrivate2();
    op2.f();
    op2.g();
    // Você pode converter para cima:
    OverridingPrivate op = op2;
    // Mas você não pode chamar os métodos:
    //! op.f();
    //! op.g();
    // Mesmo aqui:
    WithFinals wf = op2;
    //! wf.f();
    //! wf.g();
    monitor.expect(new String[] {
      "OverridingPrivate2.f()",
      "OverridingPrivate2.g()"
    });
  }
} ///:~




“Overriding” só pode ocorrer se algo é parte da interface da classe básica. Isto é, você deve estar habilitado a fazer um upcast de um objeto ao seu tipo básico e chamar o mesmo método (Este ponto se tornará claro no próximo capítulo). Se um método é private, ele não é parte da interface de sua classe básica. É só algum código que está escondido dentro da classe, e só acontece para ter aquele nome, mas se você criar um método public, protected, ou de acesso a package com o mesmo nome na classe derivada, não há conexão para o método que pode acontecer de ter aquele nome na classe básica. Você não fez override do método; você só criou um novo método. Como um método private é inalcançavel e efetivamente invisível, ele não importa em nada com exceção da organização de código da classe para a qual foi definido. Comentários (em inglês)

Classes final



Quando você diz que uma classe inteira é final (precedendo sua definição com a palavra chave final ), você declara que você não quer herdar desta classe ou permitir que alguém faça isso. Em outras palavras, por alguma razão o projeto de sua classe é como aquele que nunca haverá necessidade de fazer qualquer alteração, ou por razões de segurança você não quer subclassificação. Comentários (em inglês)



//: c06:Jurassic.java
// Tornando um classe inteira final

class SmallBrain {}

final class Dinosaur {
  int i = 7;
  int j = 1;
  SmallBrain x = new SmallBrain();
  void f() {}
}

//! class Further extends Dinosaur {}
// erro: Não pode extender uma classe final 'Dinosaur'

public class Jurassic {
  public static void main(String[] args) {
    Dinosaur n = new Dinosaur();
    n.f();
    n.i = 40;
    n.j++;
  }
} ///:~




Note que os campos de uma classe final podem ser final ou não conforme você escolher. As mesmas regras aplicam-se a final para campos indiferentemente se a classe está definida como final. Contudo, porque previne herança, todos os métodos em uma classe final são implicitamente final, pois não há maneira de fazer override deles. Você pode adicionar o especificador final a um método em uma classe final , mas não adiciona nenhum significado. Comentários (em inglês)

Cuidados com final



Pode parecer que seja sensato tornar um método final enquanto você está projetando uma classe. Você pode achar que possivelmente ninguém vai querer fazer override de seus métodos. Algumas vezes isto verdade. Comentários (em inglês)



Mas seja cuidadoso com suas suposições. Em geral, é difícil antecipar o quanto uma classe pode ser reutilizada, especialmente uma classe de proposta geral. Se você define um método como final, você pode prevenir a possibilidade de reuso de sua classe através de herança em alguns projetos de outros programadores simplesmente porque você não poderia imaginá-la sendo usada daquela forma. Comentários (em inglês)



A biblioteca padrão Java é um bom exemplo disto. Em particular, a classe Vector Java 1.0/1.1 foi comumente usada e poderia ter sido sempre muito útil se, em nome da eficiência (a qual era quase com certeza uma ilusão), todos os métodos não tivessem sido feitos final. É facilmente concebível que você pode querer herdar e sobrescrever cada uma das classes fundamentais úteis, mas os projetistas decidem que isto não é apropriado. Isto é irônico por duas razões. Primeiro, Stack é herdado de Vector, o qual diz que um Stack é um Vector, o que não é realmente verdade de um ponto de vista lógico. Segundo, muitos dos mais importantes métodos de Vector, como addElement( ) e elementAt( ), são synchronized. Como você verá no Capítulo 11, isto incorre em um significativo atraso de performance que provavelmente apaga qualquer ganho obtido por final. Isto empresta crédito a teoria de que programadores são consistentemente ruins em visualizar onde otimizações poderiam ocorrer. E é muito ruim aquele projetista desajeitado que fez isto na biblioteca padrão, onde todos tem de conviver com isto. (Afortunadamente, o conteúdo da biblioteca Java 2 realocou Vector com ArrayList, o qual comporta-se muito mais civilizadamente. Desafortunadamente, há ainda novos códigos sendo escritos que usam o conteúdo da antiga biblioteca.) Comentários (em inglês)



É também interessante notar que Hashtable, outra importante classe da biblioteca padrão Java 1.0/1.1 , não tem qualquer método final . Conforme mencionado em algum lugar deste livro, é bastante óbvio que algumas classes foram projetadas por pessoas completamente diferentes umas das outras. (Você verá que os nomes de métodos em Hashtable são muito mais abreviados se comparados com aqueles em Vector, outra peça de evidência.) Isto é precisamente o tipo da coisa que não deveria ser óbvio para consumidores de uma biblioteca de classes. Quando coisas são inconsistentes, só traz mais trabalho para o usuário—além de outro peso para o valor do projeto e comportamento do código. (Veja que o conteúdo da biblioteca Java 2 realoca Hashtable em HashMap.) Comentários (em inglês)

Inicialização e
carregamento da classe



Nas mais tradicionais linguagens, programas são carregados uma vez como parte do processo de partida. Isto é seguido pela inicialização, e então o programa começa. O processo de inicialização nestas linguagens deve ser cuidadosamente controlado para que a ordem de inicialização de statics não cause problemas. C++, por exemplo, tem problemas se um static aguardar que outro static seja validado antes do segundo ter sido inicializado. Comentários (em inglês)



Java não tem este problema porque ela toma uma abordagem diferente para o carregamento. Porque tudo em Java é um objeto, muitas atividades tornam-se mais fáceis, e esta é uma delas. Como você aprenderá mais completamente no próximo capítulo, o código compilado para cada classe existe em seu próprio arquivo separado. Este arquivo não é carregado até que o código seja necessário. Em geral, você pode dizer que “o código da classe é carregado no ponto de seu primeiro uso.” Isto é, frequentemente não até que o primeiro objeto da classe seja construido, mas o carregamento também ocorre quando um campo static ou um método static é acessado.Comentários (em inglês)



O ponto do primeiro uso é também onde a inicialização do static acontece. Todos os objetos static e o bloco de código static serão inicializados na ordem textual (Isto é, a ordem que você os escreve na definição da classe) no ponto de carregamento. Os statics, naturalmente, são inicializados somente uma vez.Comentários (em inglês)

Inicialização com herança



É útil observar o todo do processo de inicialização, incluindo herança, para obter a imagem completa do que acontece. Considere o seguinte exemplo:



//: c06:Beetle.java
// O processo completo da inicialização.
import com.bruceeckel.simpletest.*;

class Insect {
  protected static Test monitor = new Test();
  private int i = 9;
  protected int j;
  Insect() {
    System.out.println("i = " + i + ", j = " + j);
    j = 39;
  }
  private static int x1 =
    print("static Insect.x1 initialized");
  static int print(String s) {
    System.out.println(s);
    return 47;
  }
}

public class Beetle extends Insect {
  private int k = print("Beetle.k initialized");
  public Beetle() {
    System.out.println("k = " + k);
    System.out.println("j = " + j);
  }
  private static int x2 =
    print("static Beetle.x2 initialized");
  public static void main(String[] args) {
    System.out.println("Beetle constructor");
    Beetle b = new Beetle();
    monitor.expect(new String[] {
      "static Insect.x1 initialized",
      "static Beetle.x2 initialized",
      "Beetle constructor",
      "i = 9, j = 0",
      "Beetle.k initialized",
      "k = 47",
      "j = 39"
    });
  }
} ///:~




A primeira coisa que acontece quando você executa Java em Beetle é que você tenta acessar Beetle.main( ) (um método static ), assim o carregador vai a procura do código compilado para a classe Beetle (isto acontece em um arquivo chamado Beetle.class). No processo de carregá-lo, o carregador avisa que ele tem uma classe básica (isto é o que a palavra chave extends diz), a qual é então carregada. Isto acontecerá se ou não você for fazer um objeto da classe básica. (Tente comentar o código da criação do objeto para provar isso a você mesmo.)Comentários (em inglês)



Se a classe básica tem uma classe básica, aquela segunda classe básica seria então carregada, e assim por diante. Depois, a inicialização do static na raiz da classe básica (neste caso, Insect) é executado, e então a próxima classe derivada, e assim por diante. Isto é importante porque a inicialização static da classe derivada depende dos membros da classe básica terem sido inicializados apropriadamente.Comentários (em inglês)



Neste ponto, as classes necessárias estão todas carregadas assim o objeto pode ser criado. Primeiro, todas as primitivas neste objeto são atribuidas com seus valores padrão e as referências a objetos são atribuidas com null-isto acontece rapidamente pela atribuição de zeros binários à memória no objeto. Então o construtor da classe básica será chamado. Neste caso a chamada é automatica, mas você pode também especificar a chamada do construtor da classe básica (como a primeira operação no construtor de Beetle( ) ) pelo uso de super. A construção da classe básica vai através do mesmo processo na mesma ordem que o construtor da classe derivada. Depois do construtor da classe básica completar, as variáveis de instância são inicializadas na ordem textual. Finalmente, o resto do corpo do construtor é executado. Comentários (em inglês)

Resumo



Ambos herança e composição permitem que você crie um novo tipo dos tipos existentes. Normalmente, contudo, composição reutiliza tipos existentes como parte da implementação fundamental de um novo tipo, e herança reutiliza a interface. Como a classe derivada tem a interface da classe básica, ela pode sofrer um upcast para a básica, a que é crítico para o polimorfismo, como você verá no próximo capítulo. Comentários (em inglês)



Apesar da forte ênfase a herança na programação orientada a objetos, quando você inicia um projeto você geralmente preferiria composição durante o primeiro corte e usar herança somente quando é claramente necessário. Composição tende a ser mais flexível. Em adição, pelo uso de artifício adicionado da herança com seu tipo de membro, você pode alterar o tipo exato, e então o comportamento, daqueles objetos membros em tempo de execução. Além disso, você pode alterar o comportamento do objeto composto em tempo de execução. Comentários (em inglês)



Quando projetando um sistema, seu objetivo é encontrar ou criar um conjunto de classes nas quais cada classe tem um uso específico e não é nem tão grande (cercando tantas funcionalidades que ela seja imanejável para reutilização) nem aborrecidamente pequena (você não pode usá-la só ou sem adicionar funcionalidade). 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. Criar duas classes, A e B, com construtores padrão (sem lista de argumentos) que anunciam a elas mesmas. Herde uma nova classe chamada C de A, e crie um membro da classe B dentro de C. Não crie um construtor para C. Crie um objeto da classe C e observe os resultados. Feedback
  2. Modifique o Exercício1 para que A e B tenham construtores com argumentos ao invés de construtores padrão. Escreva um construtor para C e execute toda a inicialização dentro do construtor de C. Feedback
  3. Crie uma classe simples. Dentro de uma segunda classe, define uma referência a um objeto da primeira classe. Use inicialização lenta para instânciar este objeto. Feedback
  4. Herde uma nova classe da classe Detergent. Override scrub( ) e adicione um novo método chamado sterilize( ). Feedback
  5. Tome o arquivo Cartoon.java e comente o construtor para a classe Cartoon . Explicando o que acontece. Feedback
  6. Tome o arquivo Chess.java e comente o construtor para a classe Chess . Explicando o que acontece. Feedback
  7. Prove que construtores padrão são criados para você pelo compilador. Feedback
  8. Prove que os construtores da classe básica são (a) sempre chamados e (b) chamados antes do construtor da classe derivada. Feedback
  9. Vrie uma classe básica com somente um construtor não-padrão, e uma classe derivada com ambos um padrão (sem-argumentos) e um construtor não-padrã. nos construtores da classe derivada , chame o construtor da classe básica. Feedback
  10. Crie uma classe chamada Root que contêm uma instância de cada uma das classes (que você também cria) chamadas Component1, Component2, e Component3. Derive uma classe Stem de Root que também contêm uma instância de cada “componente.” Todas as classes deveriam ter construtores padrão que imprimem uma mensagem sobre aquela classe. Feedback
  11. Modifique o Exercício 10 para que cada classe somente tenha construtores não padrão. Feedback
  12. Adicione uma hierarquia apropriada dos métodos dispose( ) para todas as classes do Exercício 11. Feedback
  13. Crie uma classe com um método que é sobrecarregado três vezes. Herde uma nova classe, adicione uma nova sobrecarga do método, e mostre que todos os quatro métodos estão disponíveis na classe derivada. Feedback
  14. Em Car.java adicione um método service( ) para Engine e chame este método em main( ). Feedback
  15. Crie uma classe dentro de uma package. Sua classe deveria conter um método protected . Do lado de fora da package, tente chamar o método protected e explique os resultados. Agora herde de sua classe e chame o método protected de dentro de um método de sua classe derivada. Feedback
  16. Crie uma classe chamada Amphibian. Dela, herde uma classe chamada Frog. Coloque método apropriados na classe básica. Em main( ), crie um Frog e faça um upcast dele para Amphibian e demonstre que todos os métodos ainda funcionam. Feedback
  17. Modifique o Exercício 16 de forma que Frog faça override das definições de método da classe básica (providencie novas definições usando as mesmas assinaturas de método). Note o que acontece em main( ). Feedback
  18. Crie uma classe com um campo static final e um campo final e demonstre a diferença entre os dois.Feedback
  19. Crie uma classe com uma referência final vazia a um objeto. Execute a inicialização da final vazia dentro de todos os construtores. Demonstre a garantia de que final deve ser inicializada antes do uso, e que ela não pode ser alterada depois de inicializadaonce initialized. Feedback
  20. Crie uma classe com um método final . Herde daquela classe e tente fazer um override daquele método. Feedback
  21. Crie uma classe final e tente herdar dela. Feedback
  22. Prove que o carregamento de classe acontece somente uma vez. Prove que o carregamento pode ser causado tanto pela criação da primeira instância daquela classe quanto pelo acesso a um de membro static . Feedback
  23. Em Beetle.java, herde um tipo específico de beetle da classe Beetle, seguindo o mesmo formato como o das classes existentes. Acompanhe e explique as saídas. Feedback





[31] Não se torne presa do desejo do aperfeiçoamento prematuro. Se você conseguiu seu sistema funcionando e ele é muito lento, é duvidoso que você possa consertá-lo com a palavra chave final . Contudo, o Capítulo 15 tem informações sobre modelagem, o qual pode ser útil no aumento da velocidade de seu programa.


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