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

4: Inicialização
& Limpeza



À medida que a revolução da informática progride, a programação "insegura" tem se tornado uma das maiores culpadas pelo encarecimento da programação.



Duas dessas questões de segurança são inicialização e limpeza. inicialização e limpeza. Muitos bugs em C ocorrem quando os programadores esquecem de inicializar uma variável. Isso é especialmente verdade em bibliotecas onde os usuários não sabem como inicializar um componente da biblioteca, mesmo que ele precise. Limpeza é um problema especialmente porque é fácil de se esquecer de um elemento / componente quando você já conseguiu fazê-lo funcionar, uma vez que você não vai mais se preocupar com ele. Dessa forma, os recursos utilizados por esse elemento ficarão "presos", e você rapidamente terá falta de recursos (em geral falta de memória).Comentários (em inglês)



C++ introduziu o conceito de construtor, um método especial chamado automaticamente quando um objeto é criado. Java também adotou o construtor, e em adição tem um coletor de lixo que libera recursos de memória automaticamente quando eles não estiverem sendo usados por muito tempo. Este capítulo examina os assuntos da inicialização e limpeza, e seu suporte em Java. Comentários (em inglês)

Inicialização garantida
com o construtor



Você pode se imaginar criando um método chamado initialize( ) para toda classe que você escrever. Seu nome é uma dica que informa que deveria ser chamado antes do uso do objeto. Infelizmente, isto significa que o usuário deve lembrar de chamar o método. Em Java, o modelo das classes pode garantir inicialização de todos os objetos providenciando um método especial chamado construtor. Se uma classe tem um construtor, Java automaticamente chama este construtor quando um objeto é criado, antes até que o usuário possa colocar sua mãoes para isso. Assim a inicialização é garantida. Comentários (em inglês)



O próximo desafio é como nomear este método. Há duas observações. A primeira é que qualquer nome que você use poderia conflitar com um nome que você poderia gostar de usar como um membro nesta classe. A segunda é que em virtude do compilador ser responsável por chamar o construtor, ele deve sempre saber qual método chamar. A solução C++ parece ser a mais fácil e lógica, assim é usada também em Java: O nome do construtor é o mesmo nome da classe. E faz sentido que este método será chamado automaticamente na inicialização. Comentários (em inglês)



Aqui está uma classe simples com um construtor:



//: c04:SimpleConstructor.java
// Demonstração de um construtor.
import com.bruceeckel.simpletest.*;

class Rock {
  Rock() { // Este é o construtor
    System.out.println("Creating Rock");
  }
}

public class SimpleConstructor {
  static Test monitor = new Test();
  public static void main(String[] args) {
    for(int i = 0; i < 10; i++)
      new Rock();
    monitor.expect(new String[] {
      "Creating Rock",
      "Creating Rock",
      "Creating Rock",
      "Creating Rock",
      "Creating Rock",
      "Creating Rock",
      "Creating Rock",
      "Creating Rock",
      "Creating Rock",
      "Creating Rock"
    });
  }
} ///:~




Agora, quando o objeto está criado: Comentários (em inglês)



new Rock();




memória é alocada e o construtor é chamado. Isto garante que o objeto será inicializado apropriadamente antes que você coloque suas mãos nele. Comentários (em inglês)



Note que o estilo de codificação que recomenda que a primeira letra de todos os métodos seja minúscula não se aplica ao construtor, pois o nome do construtor deve ser exatamente igual ao nome da classe. Comentários (em inglês)



Como qualquer método, o construtor pode ter argumentos para permitir que você especifique como um objeto deva ser criado. O exemplo anterior pode ser alterado facilmente assim o construtor recebe um argumento:



//: c04:SimpleConstructor2.java
// Construtor pode ter argumentos
import com.bruceeckel.simpletest.*;

class Rock2 {
  Rock2(int i) {
    System.out.println("Criando Rock número " + i);
  }
}

public class SimpleConstructor2 {
  static Test monitor = new Test();
  public static void main(String[] args) {
    for(int i = 0; i < 10; i++)
      new Rock2(i);
    monitor.expect(new String[] {
      "Criando Rock número 0",
      "Criando Rock número 1",
      "Criando Rock número 2",
      "Criando Rock número 3",
      "Criando Rock número 4",
      "Criando Rock número 5",
      "Criando Rock número 6",
      "Criando Rock número 7",
      "Criando Rock número 8",
      "Criando Rock número 9"
    });
  }
} ///:~




Argumentos do construtor dão a você uma maneira de prover parâmetros para a inicialização de um objeto. Por exemplo, se a classe Tree tem um construtor que recebe um parâmetro único integer informando a altura da árvore, você poderia criar um objeto Tree como este: Comentários (em inglês)



Tree t = new Tree(12);  // 12 metros de altura.




Se Tree(int) é seu único construtor, então o compilador não deixará você criar um objeto Tree de qualquer outra maneira. Comentários (em inglês)



Construtores eliminam uma grande quantidade de problemas e tornam o código fácil de ler. No fragmento de código anterior, por exemplo, você não vê uma chamada explícita a algum método initialize( ) que é conceitualmente separado da criação. Em Java, criação e inicialização são conceitos unificados—você não pode ter um sem o outro. Comentários (em inglês)



O construtor não é um tipo usual de método porque não retorna valor. Isto é distintamente diferente de um valor de retorno void , em qual o método retorna nada mas você ainda tem a opção de fazê-lo retornar algo. Construtores retornam nada e não tem uma opção (a expressão new retorna uma referência ao objeto novo criado, mas o construtor mesmo não tem valor de retorno). Se havia um valor de retorno, e você poderia selecionar seu próprio, o compilador necessitaria saber o que fazer com aquele valor retornado. Comentários (em inglês)

Sobrecarga de métodos



Um traço importante em qualquer linguagem de programação é o uso de nomes. Quando você cria um objeto, você dá nome para uma região da memória. Um método é um nome para uma ação. Pelo uso de nomes para descrever seu sistema, você cria um programa que é mais fácil das pessoas entenderem e alterarem. Isto é muito parecido com escrever prosas—o objetivo é se comunicar com seus leitores. Comentários (em inglês)



Você se refere a todos os objetos pelos seus nomes. Nomes bem escolhidos tornam mais fácil para você e outros entenderem seu código. Comentários (em inglês)



Os problemas começam quando mapeamos os conceitos de nuance da linguagem humana em uma linguagem de programação. Frequentemente, a mesma palavra expressa um número diferente de significados—ele são sobrecarregados. Isto é útil, especialmeente quando vem para diferenças triviais. Você pode “lavar a camisa,” “lavar o carro,” e “lavar o cachoro.” Seria tolo ser forçado a dizer, “camisaLavar a camisa,” “carroLavar o carro,” e “cachorroLavar o cachorro” só para que o ouvinte não precisasse fazer qualquer distinção sobre a ação executada. Comentários (em inglês)



Muitas linguagens de programação (C especialmente) exigem que você tenha um identificador único para cada função. Assim você não poderia ter uma função chamada print( ) para imprimir inteiros e outra chamada print( ) para imprimir decimais—cada função exige um nome único. Comentários (em inglês)



Em Java (e C++), outro fator força a sobrecarga de nomes de métodos: o construtor. Como o nome do construtor é pré-determinado pelo nome da classe, pode haver somente um nome de construtor. Mas se você quizer criar um objeto de mais de uma maneira? Por exemplo, suponha que você construiu uma classe e que pode inicializá-la de uma maneira padrão ou pela leitura de informações de um arquivo. Você precisa de dois construtores, um que não recebe argumentos (O construtor padrão ,[19] também chamado de construtor sem-argumentos ),e um que recebe um argumento String , o quela é o nome do arquivo de onde se inicializa o objeto. Ambos são construtores, assim eles devem ter o mesmo nome—o nome da classe. Assim, sobrecarga de método é essencial para permitir que o mesmo nome seja usado com tipos diferentes de argumentos. E embora a sobrecarga de método seja mais indicada para construtores, é uma conveniência geral e pode ser usada com qualquer método. Comentários (em inglês)



Aqui está um exemplo que mostra ambos, construtores sobrecarregados e métodos ordinários sobrecarregados:



//: c04:Overloading.java
// Demonstração de ambos, construtor
// e métodos ordinários sobrecarregados.
import com.bruceeckel.simpletest.*;
import java.util.*;

class Tree {
  int height;
  Tree() {
    System.out.println("Plantando uma semente");
    height = 0;
  }
  Tree(int i) {
    System.out.println("Criando uma nova Arvore que tem  "
      + i + " metros de altura");
    height = i;
  }
  void info() {
    System.out.println("A arvore tem " + height + " metros de altura");
  }
  void info(String s) {
    System.out.println(s + ": Arvore tem "
      + height + " metros de altura");
  }
}

public class Overloading {
  static Test monitor = new Test();
  public static void main(String[] args) {
    for(int i = 0; i < 5; i++) {
      Tree t = new Tree(i);
      t.info();
      t.info("método sobrecarregado");
    }
    // Construtor sobrecarregado:
    new Tree();
    monitor.expect(new String[] {
      "Criando uma nova Arvore que tem 0 metros de altura",
      "Arvore tem 0 metros de altura",
      "método sobrecarregado: Arvore tem 0 metros de altura",
      "Criando uma nova Arvore que tem 1 metros de altura",
      "Arvore tem 1 metros de altura",
      "método sobrecarregado: Arvore tem 1 metros de altura",
      "Criando uma nova Arvore que tem 2 metros de altura",
      "Arvore tem 2 metros de altura",
      "método sobrecarregado: Arvore tem 2 metros de altura",
      "Criando uma nova Arvore que tem 3 metros de altura",
      "Arvore tem 3 metros de altura",
      "método sobrecarregado: Arvore tem 3 metros de altura",
      "Criando uma nova Arvore que tem 4 metros de altura",
      "Arvore tem 4 metros de altura",
      "método sobrecarregado: Arvore tem 4 metros de altura",
      "Plantando uma semente"
    });
  }
} ///:~




Um objeto Tree pode ser criado ou como uma semente, sem argumentos, ou como uma planta crescida em um viveiro, com uma altura já existente. Para suportar isto, há um construtor padrão, e um que recebe a altura existente.Comentários (em inglês)



Você pode também querer chamar o método info( ) de mais de uma maneira. Por exemplo, se você tem uma mensagem extra que quer que seja impressa, você pode usar info(String), e info( ) se você não tem nada mais a dizer. Poderia parecer estranho dar dois nomes separados para o que obviamente tem o mesmo conceito. Afortunadamente, sobrecarga de métodos permite que você use o mesmo nome para ambos. Comentários (em inglês)

Distinguindo métodos sobrecarregados



Se os métodos tem o mesmo nome, como Java pode saber qual método você precisa? Há uma regra simples: cada método sobrecarregado deve receber uma lista única de tipos de argumentos. Comentários (em inglês)



Se você pensar a respeito por um segundo, faz sentido. Como poderia um programador perceber a diferença entre dois métodos que tem o mesmo nome, senão pelo tipo de seus argumentos? Comentários (em inglês)



Até diferenças na ordem dos argumentos são suficientes para distinguir dois métodos: (Embora você não vá querer normalmente chegar a esta perspectiva, pois produz dificuldades para manutenção do código.) Comentários (em inglês)



//: c04:OverloadingOrder.java
// Sobrecarga baseada na ordem dos argumentos.
import com.bruceeckel.simpletest.*;

public class OverloadingOrder {
  static Test monitor = new Test();
  static void print(String s, int i) {
    System.out.println("String: " + s + ", int: " + i);
  }
  static void print(int i, String s) {
    System.out.println("int: " + i + ", String: " + s);
  }
  public static void main(String[] args) {
    print("String primeiro", 11);
    print(99, "Int primeiro");
    monitor.expect(new String[] {
      "String: String primeiro, int: 11",
      "int: 99, String: Int primeiro"
    });
  }
} ///:~




Os dois métodos print tem argumentos idênticos, mas em ordem diferente e é isto que os faz distintos. Comentários (em inglês)

Sobrecarga com primitivos



Um primitivo pode ser promovido automaticamente de um tipo menor para um maior, e isto pode ser um pouco confuso quando combinado com a sobrecarga. O exemplo seguinte demonstra o que acontece quando um primitivo é manuseado por um método sobrecarregado:



//: c04:PrimitiveOverloading.java
// Promoção de primitivos e sobrecarga.
import com.bruceeckel.simpletest.*;

public class PrimitiveOverloading {
  static Test monitor = new Test();
  void f1(char x) { System.out.println("f1(char)"); }
  void f1(byte x) { System.out.println("f1(byte)"); }
  void f1(short x) { System.out.println("f1(short)"); }
  void f1(int x) { System.out.println("f1(int)"); }
  void f1(long x) { System.out.println("f1(long)"); }
  void f1(float x) { System.out.println("f1(float)"); }
  void f1(double x) { System.out.println("f1(double)"); }

  void f2(byte x) { System.out.println("f2(byte)"); }
  void f2(short x) { System.out.println("f2(short)"); }
  void f2(int x) { System.out.println("f2(int)"); }
  void f2(long x) { System.out.println("f2(long)"); }
  void f2(float x) { System.out.println("f2(float)"); }
  void f2(double x) { System.out.println("f2(double)"); }

  void f3(short x) { System.out.println("f3(short)"); }
  void f3(int x) { System.out.println("f3(int)"); }
  void f3(long x) { System.out.println("f3(long)"); }
  void f3(float x) { System.out.println("f3(float)"); }
  void f3(double x) { System.out.println("f3(double)"); }

  void f4(int x) { System.out.println("f4(int)"); }
  void f4(long x) { System.out.println("f4(long)"); }
  void f4(float x) { System.out.println("f4(float)"); }
  void f4(double x) { System.out.println("f4(double)"); }

  void f5(long x) { System.out.println("f5(long)"); }
  void f5(float x) { System.out.println("f5(float)"); }
  void f5(double x) { System.out.println("f5(double)"); }

  void f6(float x) { System.out.println("f6(float)"); }
  void f6(double x) { System.out.println("f6(double)"); }

  void f7(double x) { System.out.println("f7(double)"); }

  void testConstVal() {
    System.out.println("Testing with 5");
    f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5);
  }
  void testChar() {
    char x = 'x';
    System.out.println("char argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testByte() {
    byte x = 0;
    System.out.println("byte argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testShort() {
    short x = 0;
    System.out.println("short argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testInt() {
    int x = 0;
    System.out.println("int argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testLong() {
    long x = 0;
    System.out.println("long argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testFloat() {
    float x = 0;
    System.out.println("float argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  void testDouble() {
    double x = 0;
    System.out.println("double argument:");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x);
  }
  public static void main(String[] args) {
    PrimitiveOverloading p =
      new PrimitiveOverloading();
    p.testConstVal();
    p.testChar();
    p.testByte();
    p.testShort();
    p.testInt();
    p.testLong();
    p.testFloat();
    p.testDouble();
    monitor.expect(new String[] {
      "Testing with 5",
      "f1(int)",
      "f2(int)",
      "f3(int)",
      "f4(int)",
      "f5(long)",
      "f6(float)",
      "f7(double)",
      "char argument:",
      "f1(char)",
      "f2(int)",
      "f3(int)",
      "f4(int)",
      "f5(long)",
      "f6(float)",
      "f7(double)",
      "byte argument:",
      "f1(byte)",
      "f2(byte)",
      "f3(short)",
      "f4(int)",
      "f5(long)",
      "f6(float)",
      "f7(double)",
      "short argument:",
      "f1(short)",
      "f2(short)",
      "f3(short)",
      "f4(int)",
      "f5(long)",
      "f6(float)",
      "f7(double)",
      "int argument:",
      "f1(int)",
      "f2(int)",
      "f3(int)",
      "f4(int)",
      "f5(long)",
      "f6(float)",
      "f7(double)",
      "long argument:",
      "f1(long)",
      "f2(long)",
      "f3(long)",
      "f4(long)",
      "f5(long)",
      "f6(float)",
      "f7(double)",
      "float argument:",
      "f1(float)",
      "f2(float)",
      "f3(float)",
      "f4(float)",
      "f5(float)",
      "f6(float)",
      "f7(double)",
      "double argument:",
      "f1(double)",
      "f2(double)",
      "f3(double)",
      "f4(double)",
      "f5(double)",
      "f6(double)",
      "f7(double)"
    });
  }
} ///:~




Você verá que o valor constante 5 é tratado como um int, assim se está disponível um método sobrecarregado que recebe um int, ele é usado. Em todos os outros casos, se você tem um tipo de dados que é menor que o argumento do método, aquele tipo de dado é promovido. char produz como efeito uma leve diferença, então se ele não encontra um char exatamente igual, ele é promovido para int. Comentários (em inglês)



O que acontece se seu argumento é maior que o argumento esperado pelo método sobrecarregado? Uma modificação do programa anterior dá a resposta:



//: c04:Demotion.java
// Rebaixamento de primitivos e sobrecarga.
import com.bruceeckel.simpletest.*;

public class Demotion {
  static Test monitor = new Test();
  void f1(char x) { System.out.println("f1(char)"); }
  void f1(byte x) { System.out.println("f1(byte)"); }
  void f1(short x) { System.out.println("f1(short)"); }
  void f1(int x) { System.out.println("f1(int)"); }
  void f1(long x) { System.out.println("f1(long)"); }
  void f1(float x) { System.out.println("f1(float)"); }
  void f1(double x) { System.out.println("f1(double)"); }

  void f2(char x) { System.out.println("f2(char)"); }
  void f2(byte x) { System.out.println("f2(byte)"); }
  void f2(short x) { System.out.println("f2(short)"); }
  void f2(int x) { System.out.println("f2(int)"); }
  void f2(long x) { System.out.println("f2(long)"); }
  void f2(float x) { System.out.println("f2(float)"); }

  void f3(char x) { System.out.println("f3(char)"); }
  void f3(byte x) { System.out.println("f3(byte)"); }
  void f3(short x) { System.out.println("f3(short)"); }
  void f3(int x) { System.out.println("f3(int)"); }
  void f3(long x) { System.out.println("f3(long)"); }

  void f4(char x) { System.out.println("f4(char)"); }
  void f4(byte x) { System.out.println("f4(byte)"); }
  void f4(short x) { System.out.println("f4(short)"); }
  void f4(int x) { System.out.println("f4(int)"); }

  void f5(char x) { System.out.println("f5(char)"); }
  void f5(byte x) { System.out.println("f5(byte)"); }
  void f5(short x) { System.out.println("f5(short)"); }

  void f6(char x) { System.out.println("f6(char)"); }
  void f6(byte x) { System.out.println("f6(byte)"); }

  void f7(char x) { System.out.println("f7(char)"); }

  void testDouble() {
    double x = 0;
    System.out.println("double argument:");
    f1(x);f2((float)x);f3((long)x);f4((int)x);
    f5((short)x);f6((byte)x);f7((char)x);
  }
  public static void main(String[] args) {
    Demotion p = new Demotion();
    p.testDouble();
    monitor.expect(new String[] {
      "double argument:",
      "f1(double)",
      "f2(float)",
      "f3(long)",
      "f4(int)",
      "f5(short)",
      "f6(byte)",
      "f7(char)"
    });
  }
} ///:~




Aqui, o método recebe valores primitivos reduzidos. Se seu argumento está fora, então você precisa converter para o tipo necessário colocando o nome do tipo dentro de parenteses. Se você não fizer isso, o compilador irá emitir uma mensagem de erro. Comentários (em inglês)



Você deve ter cuidado com estas conversões redutoras, as quais significam que você pode perder informações durante a conversão. Isto por que o compilador força você a fazê-lo—para indicar a conversão redutoraComentários (em inglês)

Sobrecarga nos valores de retorno



É comum ter a curiosidade “Por que somente nomes de classes e listas de argumentos de métodos? Por que nao distinguir entre métodos baseado em seus valores de retorno?” Por exemplo, estes dois métodos, os quais tem o mesmo nome e argumentos, são facilmente distinguidos um do outro: Comentários (em inglês)



void f() {}
int f() {}




Isto funciona bem quando o compilador pode inequivocamente determinar o significado do contexto, como em int x = f( ). Contudo, você pode também chamar um método e ignorar o valor de retorno. Isto é frequentemente referenciado como chamar um método por seu efeito lateral , desde que você não precise do valor de retorno, mas ao invés disso quer outros efeitos do método chamado. Assim se você chamar o método deste modo: Comentários (em inglês)



f();




como pode Java determinar qual f( ) deveria ser chamado? E como poderia alguém lendo o código e vê-lo? Por causa desta sorte de problemas, você não pode usar tipos de valores de retorno para distinguir métodos sobrecarregados. Comentários (em inglês)

Construtores padrão



Como mencionado anteriormente, um construtor padrão (também conhecido como um construtor “no-arg”) é um sem argumentos que é usado para criar um “objeto básico.” Se você cria uma classe que não tem construtores, o compilador criará automaticamente um construtor padrão para você. Por exemplo:Comentários (em inglês)



//: c04:DefaultConstructor.java

class Bird {
  int i;
}

public class DefaultConstructor {
  public static void main(String[] args) {
    Bird nc = new Bird(); // Padrão!
  }
} ///:~




A linha Comentários (em inglês)



new Bird();




cria um novo objeto e chama o construtor padrão, até um que não estiver explicitamente definido. Sem ele, nós não teríamos método para chamar para construir um objeto. Contudo, se você definir qualquer construtor (com ou sem argumentos), o compilador não sintetizará um para você: Comentários (em inglês)



class Hat {
  Hat(int i) {}
  Hat(double d) {}
}




Agora se você disser: Comentários (em inglês)



new Hat();




o compilar irá entender que não pode encontrar um construtor compatível. Isto é como se quando você não coloca nenhum construtor, o compilador diz “Você é obrigado a ter algum construtor, assim deixe-me fazer um para você.” Mas se você escrever um construtor, o compilador diz “Você está escrevendo um construtor então você sabe o que está fazendo; se você não colocou em um padrão é por que você pensa em deixá-lo fora.” Comentários (em inglês)

O palavra chave this



Se você tem dois objetos do mesmo tipo chamados a e b, você pode querer saber como é que possível chamar um método f( ) para ambos os objetos:Comentários (em inglês)



class Banana { void f(int i) { /* ... */ } }
Banana a = new Banana(), b = new Banana();
a.f(1);
b.f(2);




Se há somente um método chamado f( ), como pode aquele método saber se está sendo chamado pelo objeto a ou b? Comentários (em inglês)



Para permitir que você escreva o código em uma sintaxe orientada a objeto conveniente na qual você “envia uma mensagem a um objeto,” o compilar faz algum trabalho secreto para você. Há um primeiro argumento secreto passado para o método f( ), e este argumento é a referência ao objeto que está sendo manipulado. Assim os dois métodos chamados tornam-se algo como: Comentários (em inglês)



Banana.f(a,1);
Banana.f(b,2);




Isto é interno e você não pode escrever estas expressões e esperar que o compilador as aceite, mas lhe dão uma idéia do que está acontecendo. Comentários (em inglês)



Suponha que você esteja dentro de um método e gostaria de obter a referência para o objeto corrente. Mas como a referência é passada secretamente pelo compilador, não há identificador para isso. Contudo, para este propósito há uma palavra chave: this. A palavra chave this —a qual pode ser usada somente dentro de um método—produz a referência para o objeto do método que está sendo chamado. Você pode tratar esta referência como qualquer outra referência a objeto. Tenha em mente que se você está chamando um método de sua classe de dentro de outro método de sua classe, você não precisa usar this. Você simplismente chama o método. A referência this corrente é automaticamente usada pelo outro método. Assim você pode dizer: Comentários (em inglês)



class Apricot {
  void pick() { /* ... */ }
  void pit() { pick(); /* ... */ }
}




Dentro de pit( ), você poderia dizer this.pick( ) mas não é necessário.[20] O compilador faz isso por você automaticamente. A palavra chave this é usada somente para aqueles casos especiais em quais você precisa explicitamente usar a referência para o objeto corrente. Por exemplo, é frequentemente usado em declarações return quando você quer retornar a referência para o objeto corrente: Comentários (em inglês)



//: c04:Leaf.java
// Uso simples da palavra chave "this".
import com.bruceeckel.simpletest.*;

public class Leaf {
  static Test monitor = new Test();
  int i = 0;
  Leaf increment() {
    i++;
    return this;
  }
  void print() {
    System.out.println("i = " + i);
  }
  public static void main(String[] args) {
    Leaf x = new Leaf();
    x.increment().increment().increment().print();
    monitor.expect(new String[] {
      "i = 3"
    });
  }
} ///:~




Por que increment( ) retorna a referência do objeto corrente via this , operações multiplas podem ser facilmente executadas no mesmo objeto. Comentários (em inglês)

Chamando construtores de construtores



Quando você escreve diversos construtores para um classe, há vezes que você gostaria de chamar um construtor de outro para evitar duplicação de código. Você pode fazer algo assim chamado por por usando a palavra chave this . Comentários (em inglês)



Normalmente, quando você diz this, é no sentido de “este objeto” ou “o objeto corrente,” por que ele mesmo produz para si uma referência ao objeto corrente. Em um construtor, a palavra chave this recebe um significado diferente quando você dá a ela uma lista de argumentos. Isto faz uma chamada explícita ao construtor que tiver aquela lista de argumentos. Então você tem um caminho direto para chamar outros construtores:Comentários (em inglês)



//: c04:Flower.java
// Chamando construtores com "this."
import com.bruceeckel.simpletest.*;

public class Flower {
  static Test monitor = new Test();
  int petalCount = 0;
  String s = new String("null");
  Flower(int petals) {
    petalCount = petals;
    System.out.println(
      "Constructor w/ int arg only, petalCount= "
      + petalCount);
  }
  Flower(String ss) {
    System.out.println(
      "Constructor w/ String arg only, s=" + ss);
    s = ss;
  }
  Flower(String s, int petals) {
    this(petals);
//!    this(s); // Não pode chamar dois!
    this.s = s; // Outro uso de "this"
    System.out.println("String & int args");
  }
  Flower() {
    this("hi", 47);
    System.out.println("default constructor (no args)");
  }
  void print() {
//! this(11); // Não dentro de não-construtor!
    System.out.println(
      "petalCount = " + petalCount + " s = "+ s);
  }
  public static void main(String[] args) {
    Flower x = new Flower();
    x.print();
    monitor.expect(new String[] {
      "Constructor w/ int arg only, petalCount= 47",
      "String & int args",
      "default constructor (no args)",
      "petalCount = 47 s = hi"
    });
  }
} ///:~




O construtor Flower(String s, int petals) mostra que, enquanto você pode chamar um construtor usando this, você não pode chamar dois. Adicionalmente, o construtor chamado deve ser a primeira coisa feita, ou você irá receber uma mensagem de erro do compilador. Comentários (em inglês)



Este exemplo também mostra outra maneira de você ver o uso de this . Como o nome do argumento s e o nome do atributo s são o mesmo, há uma ambiguidade. Você pode resolvê-la com this.s, para dizer que você estã se referindo ao atributo. Você verá frequentemente esta forma usada em código Java, e é usada em muitos lugares neste livro. Comentários (em inglês)



Em print( ) você pode ver que o compilador não deixa você chamar um construtor de dentro de qualquer outro método que não um construtor. Comentários (em inglês)

O significado de static



Com a palavra this em mente, você pode entender mais completamente o que significa fazer um método static. Significa que não há this para este método em particular.Você não pode chamar métodos não-static de dentro de métodos static [21] (entretanto o inverso é possível), e você pode chamar um método static da mesma classe, sem qualquer objeto. De fato, este é a finalidade primária de se ter um método static . É como se você estivesse criando o equivalente de uma função global (em C). Contudo, funções globais não são permitidas em Java, e colocando o método static dentro de uma classe permite seu acesso a outros métodos static e a atributos static também. Comentários (em inglês)



Algumas pessoas arguem que métodos static não são orientados a objeto, pois eles tem a semântica de uma função global; com um método static , você não envia um mensagem para um objeto, pois não há this. Isto é provavelmente um argumento justo, e se você se achar usando uma porção de métodos static, você deveria provavelmente repensar sua estratégia. Contudo, statics são pragmaticos, e há vezes em que você genuinamente precisa deles, assim se eles são ou não são “POO propriamente” deveria ser deixado para os teóricos. Sem dúvida, até a linguagem Smalltalk tem o equivalente a estes “métodos classe.” Comentários (em inglês)

Limpeza: finalização e
coleta de lixo



Programadores sabem a importância da inicialização, mas frequentemente esquecem a importância da limpeza. Depois de tudo, quem precisa limpar um int? Mas com bibliotecas, “deixar seguir” simplesmente um objeto sem que você faça algo com ele não é muito seguro. Naturalmente, Java tem um coletor de lixo para resgatar a memória de objetos que não estão em uso a muito. Agora considere um caso incomum: suponha que seu objeto tenha alocado memória “especial” sem usar new. O coletor de lixo só sabe como liberar memória alocada com new, assim ele não saberá como liberar a memória “especial” do objeto. Para resolver este caso, Java provê um método chamado finalize( ) que você pode definir para a sua classe. Aqui está como supostamente ele funciona. Quando o coletor de lixo está pronto para liberar a memória usada pelo seu objeto, ele irá primeiro chamar finalize( ), e somento na próxima passada do coletor de lixo irá resgatar a memória do objeto. Assim se você escolher usar finalize( ), ele dá a você a habilidade para executar algumas limpezas importantes na hora da coleta de lixo. Comentários (em inglês)



Esta é uma armadilha de programação potencial pois alguns programadores, especialmente de C++ , podem inicialmente confundir finalize( ) com o destrutor em C++, o qual é uma função que é sempre chamada quando um objeto é destruido. Mas é importante distringuir aqui entre C++ e Java, por que em C++, os objetos são sempre destruidos (em um programa livre de bug), Mas em Java, objetos nem sempre são coletados para o lixo. Ou, colocando de outra forma: Comentários (em inglês)



1. Seus objetos podem não ser coletados como lixo.



2. Coleta de lixo não é destruição.



Se você se lembrar disso, você ficará fora dos problemas. Oque significa é que se há alguma atividade que deve ser executada antes de você não mais precisar de um objeto, você deve executar esta atividade você mesmo. Java não tem destrutor ou conceito similar, assim você deve criar um método ordinário para executar esta limpeza. Por exemplo, suponha que no processo de criação de seu objeto, ele se desenhou na tela. Se você não o apagar explicitamente da tela, ele pode nunca ser limpo. Se você colocar algum tipo de funcionalidade de apagamento dentro do finalize( ), então se um objeto é coletado para o lixo e finalize( ) é chamado (não há garantia de que isso acontecerá), então a imagem será primeiro removida da tela, mas se não, a imagem permanecerá. Comentários (em inglês)



Você pode achar que a memória para um objeto nunca precisa ser liberada porque seus programas nunca chegam perto do ponto de ultrapassar a capacidade de memória. Se seu programa termina e o coletor de lixo nunca precisa circular para liberar a memória de qualquer de seus objetos, esta memória retornará para o sistema operacional em massa como saídas do programa. Isto é uma boa coisa, porque o coletor de lixo tem prioridade elevada, e se você nunca usa, você nunca incorrerá neste custo. Comentários (em inglês)

Para que é o finalize( )?



Assim, se você não usasse finalize( ) como um método de limpeza de propósito geral, para que ele é bom ?Comentários (em inglês)



O terceiro ponto a lembrar é:



3. O coletor de lixo é apenas para memória.



Isto é, a única razão para a existência do coletor de lixo é recuperar memória que seu programa não está mais usando. Assim qualquer atividade que é associada com o coletor de lixo, de forma especial seu método finalize( ) , deve também ser só sobre a memória e sua desalocação. Comentários (em inglês)



Pode isto significar que se seu objeto contêm outros objetos, finalize( ) deveria explicitamente liberar aqueles objetos? Bem, não—o coletor de lixo cuida da liberação da memória de todos os objetos sem considerar como o objeto foi criado. Mostra que a necessidade de finalize( ) é limitada para casos especiais nos quais seu objeto pode alocar alguma memória de outra forma que na criação do objeto. Mas, você pode observar, tudo em Java é um objeto, então como pode ser isto? Comentários (em inglês)



Poderia parecer que finalize( ) existe por causa da possibilidade de você fazer algo como em C-alocando memória-usando um outro mecanismo diferente do normal de Java. Isto pode acontecer primeiramente através de métodos nativos, os quais são uma forma para chamar códigos não-Java do Java. (Métodos nativos são comentados no Apêndice B da segunda edição eletrônica deste livro, disponível no CD ROM do livro e em www.BruceEckel.com.) C e C++ são as únicas linguagens que correntemente suportam métodos nativos, mas como elas podem chamar sub-programas em outras linguagens, você pode efetivamente chamar qualquer coisa. Dentro do código não-Java, a familia de funções malloc( ) do C pode ser chamada para alocar memória, e a menos que você chame free( ), aquela memória não será liberada, causando um escapamento de memória. Naturalmente, free( ) é uma função C e C++ , assim você precisa chamá-la de um método nativo de dentro de seu finalize( ). Comentários (em inglês)



Depois de ler isto, você provavelmente tem a idéia de que você não vai usar finalize( ) muito.[22] Você está correto; Não é o lugar apropriado para ocorrer limpeza comum. Assim onde deveria a limpeza comum ser executada? Comentários (em inglês)

Você pode executar limpezas



Para limpar um objeto, o usuário do objeto deve chamar um método de limpeza no ponto em que a limpeza for desejada. Isto soa bonito e direto, mas colide um pouco com o conceito C++ do destrutor. Em C++, todos os objetos são destruidos. Ou melhor, todos os objetos deveriam ser destruidos. Se o objeto C++ é criado como local (i.e., na pilha-não possível em Java), então a destruição acontece no fechamento da chave corrente do escopo no qual o objeto foi criado. Se o objeto foi criado usando new (como em Java), o destrutor é chamado quando o programador chama o operador C++ delete (o qual não existe em Java). Se o programador C++ esquece de chamar delete, o destrutor nunca é chamado, e você tem um vazamento de memória, e mais, as outras partes do objeto nunca serão limpas. Este tipo de problema pode ser muito difícil de localizar, e é uma das razões que compelem mudanças de C++ para Java. Comentários (em inglês)



Em contraste, Java não permite que você crie objetos locais—você deve sempre usar new. Mas em Java, Não há “delete” para chamar para liberar o objeto, porque o coletor de lixo libera a memória para você. Assim de um ponto de vista simplista, você poderia dizer que por causa do coletor de lixo, Java não tem destrutor. Você verá na sequência deste livro, contudo, que a presença do coletor de lixo não remove a necessidade ou a utilidade de destrutores. (E você poderia nunca chamar finalize( ) diretamente, assim este não é um caminho apropriado para uma solução.) Se você quer algum tipo de execução de limpeza que não seja a liberação de memória, você deve ainda chamar explicitamente um método apropriado em Java, o qual seja equivalente do destrutor C++ sem a conveniência. Comentários (em inglês)



Lembre que nem o coletor de lixo nem a finalização é garantida. Se a JVM não está próxima de chegar a um estouro de memória, então ela pode não gastar tempo recuperando memórias através do coletor de lixo. Comentários (em inglês)

A condição de término



Em geral , você não pode contar com finalize( ) sendo chamado, e você deve criar métodos separados de “limpeza” e chamá-los explicitamente. Então parece que finalize( ) é somente para obscuras limpezas de memória que muitos programadores nunca usarão. Contudo, há um uso muito interessante de finalize( ) que não confiando nele o chama todas as vezes. Este é a verificação de condição de término [23]de um objeto.Comentários (em inglês)



No ponto em que você não está mais interessado em um objeto—quando está pronto para ser limpo—aquele objeto deveria estar em um estado onde sua memória poderia ser liberada com segurança. Por exemplo, se o objeto representa um arquivo aberto, aquele arquivo deveria ser fechado pelo programador antes do objeto estar no coletor de lixo. Se qualquer parte do objeto não estiver apropriadamente limpa, então você tem um problema em seu programa que poderia ser muito difícil de achar. O valor de finalize( ) é que ele pode ser usado eventualmente para descobrir esta condição, até mesmo se não for sempre chamado. Se uma das finalizações acontece para revelar o problema, então você descobre o problema, o qual é tudo que você realmente quer. Comentários (em inglês)



Aqui está um exemplo simples de como você pode usar isto:



//: c04:TerminationCondition.java
// Usando finalize() para detectar se um objeto 
// foi apropriadamente limpo.
import com.bruceeckel.simpletest.*;

class Book {
  boolean checkedOut = false;
  Book(boolean checkOut) {
    checkedOut = checkOut;
  }
  void checkIn() {
    checkedOut = false;
  }
  public void finalize() {
    if(checkedOut)
      System.out.println("Error: checked out");
  }
}

public class TerminationCondition {
  static Test monitor = new Test();
  public static void main(String[] args) {
    Book novel = new Book(true);
    // Limpeza apropriada:
    novel.checkIn();
    // Referência quebrada, esquecer para limpeza:
    new Book(true);
    // Forçar coleta de lixo e finalização:
    System.gc();
    monitor.expect(new String[] {
      "Error: checked out"}, Test.WAIT);
  }
} ///:~




A condição de término é aquela em que todos os objetos Book são passados para serem verificados antes que sejam coletados como lixo, mas em main( ), um erro de programador não verifica um dos livros. Sem finalize( ) para verificar a condição de término, isto poderia ser um problema difícil de achar. Comentários (em inglês)



Note que System.gc( ) é usado para forçar a finalização (e você poderia fazer isso durante o desenvolvimento do programa para uma rápida correção). Mas sempre que não fizer, é muito provavel que um Book errante será eventualmente descoberto através de execuções repetidas do programa (assumindo que o programa aloca memória suficiente para causar a execução do coletor de lixo). Comentários (em inglês)

Como funciona um coletor de lixo



Se você vem de uma linguagem de programação onde alocar objeto na área de recursos é muito caro, você pode naturalmente presumir que o esquema Java de alocar tudo (exceto primitivas) na área de recursos é bastante custoso. Entretanto, mostra que o coletor de lixo pode ter um impacto significativo no aumento da velocidade na criação de objetos. Isto pode soar um pouco estranho de início—que liberação de memória afeta alocação de memória—mas é a forma como algumas JVMs funcionam, e significa que alocação de objetos na área de memória para recursos em Java pode estar mais rápida que quando armazenamos na pilha em outras linguagens. Comentários (em inglês)



Por exemplo, você pode pensar na área de recursos do C++ como um terreno onde cada objeto toma posse de seu próprio pedaço de relva. Esta posição pode se abandonada algumas vezes depois e pode ser reutilizada. Em algumas JVMs, a área de recursos Java é bastante diferente; é mais como uma cinta condutora que se move para frente toda vez que um novo objeto é alocado. Isto significa que a alocação de um objeto na memória é remarcada rapidamente. O “ponteiro da área de recursos” é simplesmente movido para frente no território virgem, assim é efetivamente o mesmo que a pilha de alocação do C++. (Naturalmente, há uma pequena demora extra devido a paginação, mas não é nada como pesquisar pela memória.) Comentários (em inglês)



Agora você pode observar que a área de recursos não é de fato uma esteira rolante, e se você a tratar desta maneira irá eventualmente iniciar paginando bastante memória (que é um grande golpe na performance) e mais tarde fugindo. O engano é que o coletor de lixo caminha dentro, e enquanto ele coleta o lixo ele compacta todos os objetos na área de recursos assim você está efetivamente movendo o “ponteiro da área” próximo ao início da esteira rolante e mais longe de uma página falha. O coletor de lixo rearranja as coisas e faz o possível para alcançar a alta velocidade, o modelo infinita-área-livre pode ser usado enquando alocando memória. Comentários (em inglês)



Para entender como isto funciona, você precisa ter uma melhor idéia de quão diferente é o esquema de trabalho do coletor de lixo (GC). Uma simples mas lenta técnica de coleta de lixo é chamada contagem de referências . Isto significa que cada objeto contem um contador de referência, e toda vez que uma referência for anexada a um objeto, o contador de referência é acrescido. Toda vez que uma referência vai fora de seu escopo ou atribui-se null a ela, o contador de referências é decrescido. Então, gerenciar contadores de referência é um pequeno mas constante atraso que acontece durante a vida útil de seu programa. O coletor de lixo se move através de lista inteira de objetos, e quando encontra um com o contador zerado ele libera aquela memória. O único defeito é que se objetos se referem circularmente a outros, estes podem ter contadores não zerados enquanto ainda sendo jogados no lixo. Localizar algo como grupos auto-referenciados requer trabalho extra significativo para o coletor de lixo. Contadores de referência são comumente usado para explicar um tipo de coletor de lixo, mas eles não parecem ter sido usados em qualquer JVM implementada. Comentários (em inglês)



Em esquemas mas rápidos, o coletor de lixo não é baseado no contador de referências. Ao invés disso, é baseado na idéia de que qualquer objeto não morto, pode no final das contas, ser seguido de volta para a referência viva, na pilha ou na memória estática. A cadeia pode ir através de diversas camadas de objetos. Então, se você começar na pilha e na área de memória estática e seguir através de todas as referências, você encontrará todos os objetos vivos. Para cada referência que você encontra, você deve achar o objeto daqueles pontos para e então seguir todas as referências daquele objeto, achando dentro dos objetos os pontos para outro, etc., até que você tenha se movido através da rede inteira que se originou com a referência na pilha ou na memória estática. Cada objeto que você atravessa deve ainda estar vivo. Note que não há problema com com grupos auto-referenciados separados-eles simplesmente não são encontrados, e portanto automáticamente coletados como lixo. Comentários (em inglês)



Da abordagem descrita aqui, a JVM usa um esquema adaptado de coleta de lixo, e que faz com que objetos vivos que estão localizados dependam da variante correntemente em uso. Uma destas variantes é para-e-copie. Isto significa que—por razões que se tornarão aparentes—o programa é primeiro parado (isto não é um esquema de coleta de tarefa secundária). Então, cada objeto vivo que é encontrado é copiado de uma pilha para outra, deixando para trás todo o lixo. Em adição, como os objetos são copiados na nova pilha, eles são comprimidos do-fim-ao-fim, então compactando a nova pilha (e permitindo novos armazenamentos simplesmente rolando fora o fim como descrito previamente). Comentários (em inglês)



Naturalmente, quando um objeto é movido de um lugar para outro, todas as referências aquele ponto do(i.e., aquela referência ) objeto devem ser trocadas. A referência que vai da pilha ou da área de memória estática para o objeto pode ser trocada imediatamente, mas pode haver outras referências apontando para aquele objeto que serão encontradas mais tarde durante a “caminhada.” Estas são consertadas conforme são encontradas (você pode imaginar uma tabela que mapeia velhos endereços para novos correspondentes). Comentários (em inglês)



Há dois motivos que fazem estes assim chamados “coletores de cópia” ineficientes. O primeiro é a idéia que você tem duas pilhas e você espalha todas a memória de volta e avante entre estas duas pilhas separadas, mantendo o dobro da memória que você precisa atualmente. Algumas JVMs gerenciam isto alocando a pilha em blocos conforme necessitem e simplesmente copiam de um bloco para outro. Comentários (em inglês)



O segundo motivo é a cópia. Uma vez que seu programa se torne estável, pode estar gerando pouco ou nenhum lixo. A despeito disso, um coletor de cópia ainda copiará toda a memória de um lugar para outro, o que é desperdício. Para prevenir isto, algumas JVMs detectam que nenhum lixo novo estão sendo gerado e alternam para um esquema diferente (que é aquele “adaptado”). Este outro esquema é chamado de marcar-e-varrer, e é o que as últimas versões da JVM Sun usa todo o tempo. Para uso geral, marcar-e-varrer é bastante lento, mas quando você está gerando pouco ou nenhum lixo, é rápido. Comentários (em inglês)



Marcar-e-varrer segue a mesma lógica de partida da pilha e memória estática e vasculha através de todas as referências para procurar objetos vivos. Contudo, cada vez que encontra um objeto vivo, este objeto é marcado através de um indicador nele, mas o objeto não é coletado ainda. Somente quando o processo de marcação termina, ocorre o varrer. Durante o varrer, os objetos mortos são liberados. Portanto, cópias não acontecem, assim se o coletor escolhe compactar um fragmento da pilha, faz isso sem prejudicar objetos em volta. Comentários (em inglês)



O “parar-e-copiar” refere-se a idéia de que este tipo de coleta de lixo não é feita como tarefa secundária; ao invés, o programa é parado enquanto a coleta de lixo ocorre. Na literatura da Sun você encontrará muitas referências a coleta de lixo como um processo secundário de baixa prioridade, mas bem explicado que o coletor de lixo não foi implementado desta forma, ao menos nas últimas versões da JVM da Sun. Ao invés, o coletor de lixo da Sun roda quando há pouca memória. Em adição, marcar-e-varrer precisa que o programa seja parado. Comentários (em inglês)



Como mencionado anteriormente, Na JVM descrita aqui a memória é alocada em blocos grandes. Se você aloca um objeto grande, ele tem seu próprio bloco. Parar-e-copiar estrito precisa copiar todo objeto vivo de uma pilha de origem para uma nova pilha antes que você possa liberar a velha, o que representa bastante memória. Com blocos, o coletor de lixo pode simplesmente copiar objetos para blocos mortos tanto quanto coletá-los. Cada bloco tem um contador de geração para manter seu rastro se estiver vivo. Em casos normais, somente os blocos criados desde a última coleta de lixo são compactados; todos os outros blocos tem colisões em seus contadores de geração se eles estiverem referenciados em algum lugar. Estes indicam a situação normal de uma porção de pequenos objetos temporariamente vivos. Periodicamente, uma varredura completa é feita—grandes objetos ainda não são copiados (ele apenas tem feridos seus contadores de geração), e blocos contendo pequenos objetos são copiados e compactados. A JVM monitora a eficiência do coletor de lixo e se ele se torna um esbanjador de tempo porque todos os objetos estão vivos a muito tempo, então ela alterna para marcar-e-varrer. Similarmente, a JVM mantem pistas de como fazer um marcar-e-varrer com sucesso, e se a pilha começa a se tornar fragmentada, ele alterna de volta para parar-e-copiar. Isto é onde a forma “adaptada” começa, assim você acaba com a boca cheia de um: “Geracional parar-e-copiar marcar-e-varrer adaptado.” Comentários (em inglês)



Há um número adicional de aceleradores possíveis na JVM. Um especialmente importante envolve a operação de um carregador e que é chamado de compilador just-in-time (JIT). Um compilador JIT converte parcialmente ou completamente um programa em código nativo de máquina assim ele não precisa ser interpretado pela JVM e então roda mais rápido. Quando uma classe deve ser carregada (normalmente, a primeira vez que você quer criar um objeto daquela classe), o arquivo .class é localizado, e os códigos byte para aquela classe são trazidos na memória. Neste ponto, uma abordagem é simplesmente compilar com o JIT todo o código, mas isto tem dois defeitos: toma um pouco mais de tempo, o que, compondo com o tempo de vida do programa, pode adicionar muito; e incrementa o tamanho do executável (códigos byte são significativamente mais compactos que código JIT expandido), e isto pode causar paginação, que definitivamente retarda um programa. Uma proposta alternativa é avaliação vagarosa, a qual significa que o código não será compilado JIT até que seja necessário. Então, código que nunca for executado pode nunca ser compilado. As tecnologias Java HotSpot em JDKs recentes usaram uma proposta similar pelo incremento otimizado de um pedaço de código a cada vez que é executado, assim quanto mais o código é executado, mais rápido se torna. Comentários (em inglês)

Inicialização de membros



Java saiu por este caminho para garantir que variáveis sejam inicializadas apropriadamente antes que sejam usadas. No caso de variáveis que estão definidas localmente para um método, isto garante vir na forma de um erro em tempo de compilação. Assim se você diz: Comentários (em inglês)



  void f() {
    int i;
    i++; // Erro -- i não inicializado
  }




você receberá uma mensagem de erro que diz que i pode não ter sido inicializado. É claro que, o compilador poderia dar a i um valor padrão, mas é muito parecido com um erro de programação e um valor padrão poderia ocultá-lo. Forçando o programador a providenciar a inicialização do valor é parecido com a solução de um problema. Comentários (em inglês)



Se um primitivo é um campo de uma classe, contudo, as coisas são um pouco diferentes. Desde que qualquer método pode inicializar ou usar aquela informação, não é prático forçar o usuário a inicializá-lo com um valor apropriado antes que o dado seja usado. Contudo, não é seguro deixá-lo com um valor de lixo, assim cada campo primitivo de uma classe é garantido para obter um valor inicial. Estes valores podem ser vistos aqui: Comentários (em inglês)



//: c04:InitialValues.java
// Mostrar valores iniciais padrão.
import com.bruceeckel.simpletest.*;

public class InitialValues {
  static Test monitor = new Test();
  boolean t;
  char c;
  byte b;
  short s;
  int i;
  long l;
  float f;
  double d;
  void print(String s) { System.out.println(s); }
  void printInitialValues() {
    print("Tipo de dado      Valor inicial");
    print("boolean        " + t);
    print("char           [" + c + "]");
    print("byte           " + b);
    print("short          " + s);
    print("int            " + i);
    print("long           " + l);
    print("float          " + f);
    print("double         " + d);
  }
  public static void main(String[] args) {
    InitialValues iv = new InitialValues();
    iv.printInitialValues();
    /* Você poderia também dizer:
    new InitialValues().printInitialValues();
    */
    monitor.expect(new String[] {
      "Data type      Initial value",
      "boolean        false",
      "char           [" + (char)0 + "]",
      "byte           0",
      "short          0",
      "int            0",
      "long           0",
      "float          0.0",
      "double         0.0"
    });
  }
} ///:~




Você pode ver isso mesmo que pense que os valores não estejam especificados, eles são automaticamente inicializados (Um valor char é um zero, o qual é impresso como um espaço). Assim ao menos não haverá nenhuma ameaça de trabalhar com variáveis não inicializadas. Comentários (em inglês)



Você verá mais tarde que quando você define uma referência para um objeto dentro de uma classe sem inicializá-lo como um novo objeto, àquela referência será dado um valor especial null (o qual é uma palavra chave Java). Comentários (em inglês)

Especificando a inicialização



O que acontece se você quiser dar a uma variável um valor inicial? Uma maneira direta de fazer isso é simplesmente atribuir o valor no ponto em que você define a variável na classe. (Note que você não pode fazer isso em C++, mesmo que novatos em C++ sempre tentem.) Aqui a definição de campos na classe InitialValues está alterada para providenciar valores iniciais:



class InitialValues {
  boolean b = true;
  char c = 'x';
  byte B = 47;
  short s = 0xff;
  int i = 999;
  long l = 1;
  float f = 3.14f;
  double d = 3.14159;
  //. . .




Você pode também inicializar objetos não-primitivos da mesma maneira. Se Depth é uma classe, você pode criar uma variável e inicializá-la assim: Comentários (em inglês)



class Measurement {
  Depth d = new Depth();
  // . . .




Se você não deu a d um valor inicial e você tentar usá-lo mesmo assim, você irá obter um erro em tempo de execução chamado de uma exceção (discutida no capítulo 9). Comentários (em inglês)



Você pode até chamar um método para providenciar a inicialização de um valor:



class CInit {
  int i = f();
  //...
}




Este método pode ter argumentos, naturalmente, mas estes argumentos não podem ser outros membros da classe que não tenham sido inicializados ainda. Então, você pode fazer isto: Comentários (em inglês)



class CInit {
  int i = f();
  int j = g(i);
  //...
}




Mas você não pode fazer assim: Comentários (em inglês)



class CInit {
  int j = g(i);
  int i = f();
  //...
}




Este é um lugar no qual o compilador, apropriadamente, se queixa do referênciamento antecipado, pois isto tem a ver com a ordem de inicialização e não com o modo com que o programa é compilado Comentários (em inglês)



Esta abordagem para inicialização é simples e direta. Tem a limitação de que todo tipo de objeto InitialValues irá obter estes mesmos valores de inicialização. Algumas vezes isto é exatamente o que você precisa, mas em outras você precisa de mais flexibilidade. Comentários (em inglês)

Inicialização do construtor



O construtor pode ser usado para executar inicialização, e isto dá a você maior flexibilidade em seus programas porque você pode chamar métodos e executar ações em tempo de execução para determinar os valores iniciais. Há uma coisa para lembrar, contudo: Você não está evitando a inicialização automática, A qual acontece antes de entrar no construtor. Assim, por exemplo, se você diz:



class Counter {
  int i;
  Counter() { i = 7; }
  // . . .




Então i será primeiro inicializado para 0, então para 7. Isto é verdadeiro com todos tipos primitivos e com referências a objetos, incluindo aqueles aos quais é dada inicialização explícita no ponto de definição. Por esta razão, o compilador não tenta forçar a inicialização de elementos no construtor em qualquer lugar particular, ou antes deles serem usados—inicialização já está garantida.[24] Comentários (em inglês)

Ordem de inicialização



Dentro de uma classe, a ordem de inicialização é determinada pela ordem que as variáveis são definidas dentro da classe. As definições de variáveis podem ser espalhadas por toda a parte e entre definições de métodos, mas variáveis são inicializadas antes que qualquer método possa ser chamado—mesmo que seja o construtor. Por exemplo:Comentários (em inglês)



//: c04:OrderOfInitialization.java
// Demonstra ordem de inicialização.
import com.bruceeckel.simpletest.*;

// Quando o construtor é chamado para criar um 
// objeto Tag você verá a mensagem:
class Tag {
  Tag(int marker) {
    System.out.println("Tag(" + marker + ")");
  }
}

class Card {
  Tag t1 = new Tag(1); // Antes do construtor
  Card() {
    // Indica que não estamos no construtor:
    System.out.println("Card()");
    t3 = new Tag(33); // Reinicializa t3
  }
  Tag t2 = new Tag(2); // Depois do construtor
  void f() {
    System.out.println("f()");
  }
  Tag t3 = new Tag(3); // No fim
}

public class OrderOfInitialization {
  static Test monitor = new Test();
  public static void main(String[] args) {
    Card t = new Card();
    t.f(); // Mostra que a construção está feita
    monitor.expect(new String[] {
      "Tag(1)",
      "Tag(2)",
      "Tag(3)",
      "Card()",
      "Tag(33)",
      "f()"
    });
  }
} ///:~




Em Card, as definições de objetos Tag são intencionalmente espalhadas para provar que elas serão inicializadas antes de entrar no construtor ou antes que outra coisa aconteça. Em adição, t3 é reinicializado dentro do construtor. Comentários (em inglês)



Das impressões você pode ver que, a referência t3 foi inicializada duas vezes: um antes e um durante a chamada do construtor. (O primeiro objeto é rebaixado, assim pode ser coletado como lixo mais tarde.) Isto pode não parecer eficiente a primeira vista, mas garante inicialização apropriada—oque aconteceria se um construtor sobrecarregado fosse definido para não inicializar t3 e não houvesse uma inicialização “padrão” para t3 em sua definição? Comentários (em inglês)

Inicialização de dados estáticos



Quando os dados são static, a mesma coisa acontece; Se é um primitivo e você o inicializou, ele recebe um valor inicial primitivo padronizado. Se é uma referência a um objeto, é null a menos que você crie um objeto novo e ligue sua referência a ele.Comentários (em inglês)



Se você quizer colocar inicialização no ponto de definição, parece o mesmo que para não-statics. Há somente uma simples peça de memória para um static, desconsiderando como muitos objetos são criados. Mas a questão aparece quando a memória static volta inicializada. Um exemplo tornará esta questão mais clara: Comentários (em inglês)



//: c04:StaticInitialization.java
// Especificando valors iniciais na definição de classe.
import com.bruceeckel.simpletest.*;

class Bowl {
  Bowl(int marker) {
    System.out.println("Bowl(" + marker + ")");
  }
  void f(int marker) {
    System.out.println("f(" + marker + ")");
  }
}

class Table {
  static Bowl b1 = new Bowl(1);
  Table() {
    System.out.println("Table()");
    b2.f(1);
  }
  void f2(int marker) {
    System.out.println("f2(" + marker + ")");
  }
  static Bowl b2 = new Bowl(2);
}

class Cupboard {
  Bowl b3 = new Bowl(3);
  static Bowl b4 = new Bowl(4);
  Cupboard() {
    System.out.println("Cupboard()");
    b4.f(2);
  }
  void f3(int marker) {
    System.out.println("f3(" + marker + ")");
  }
  static Bowl b5 = new Bowl(5);
}

public class StaticInitialization {
  static Test monitor = new Test();
  public static void main(String[] args) {
    System.out.println("Creating new Cupboard() in main");
    new Cupboard();
    System.out.println("Creating new Cupboard() in main");
    new Cupboard();
    t2.f2(1);
    t3.f3(1);
    monitor.expect(new String[] {
      "Bowl(1)",
      "Bowl(2)",
      "Table()",
      "f(1)",
      "Bowl(4)",
      "Bowl(5)",
      "Bowl(3)",
      "Cupboard()",
      "f(2)",
      "Creating new Cupboard() in main",
      "Bowl(3)",
      "Cupboard()",
      "f(2)",
      "Creating new Cupboard() in main",
      "Bowl(3)",
      "Cupboard()",
      "f(2)",
      "f2(1)",
      "f3(1)"
    });
  }
  static Table t2 = new Table();
  static Cupboard t3 = new Cupboard();
} ///:~




Bowl permite que você observe a criação de uma classe, e Table e Cupboard criam membros static de Bowl espalhados através de sua definição de classe. Note que Cupboard cria um não-static Bowl b3 antes das definições static . Comentários (em inglês)



Das impressões, você pode ver que a inicialização static ocorre somente se é necessário. Se você não criar um objeto Table e você nunca se referir a Table.b1 ou Table.b2, o static Bowl b1 e b2 nunca serão criados. Eles estão inicializados somente quando o primeiro objeto Table está criado (ou o primeiro acesso static ocorrer). Depois disso, os objetos static não estão reinicializados. Comentários (em inglês)



A ordem de inicialização é primeiro statics, se eles não estão já inicializados pela criação de um objeto anterior, e depois objetos não-static . Você pode ver evidências disso nas impressões. Comentários (em inglês)



Isto auxilia a resumir o processo de criação de um objeto. Considere um classe chamada Dog: Comentários (em inglês)

  1. A primeira vez que um objeto do tipo Dog for criado (O construtor é atualmente um método static ), ou a primeira vez que um método static ou campo static da classe Dog é acessado, o interpretador Java deve localizar Dog.class, oque ele faz por pesquisa através do classpath. Feedback
  2. Como Dog.class é carregada (criando um objeto de Classe , sobre o qual você vai aprender mais tarde),todos os seus inicializadores static estão funcionando. Então, a inicialização static toma lugar somente uma vez, como o objeto de Classe é carregado da primeira vez. Feedback
  3. Quando você cria um new Dog( ), o processo de construção para um objeto Dog primeiro aloca bastante memória para um objeto Dog na pilha. Feedback
  4. Este armazenamento é criado a limpo, atribuindo automaticamente a todas as primitivas daquele objeto Dog seus valores padrão (zero para números e o equivalente para boolean e char) e referências para null. Feedback
  5. Quaisquer inicializações que ocorrer no ponto da definição de campo será executada.Feedback
  6. Construtores são executados. Como você verá no Capítulo 6, isto pode involver atualmente um bonito montante de atividade, especialmente quando herança está involvida.Feedback

Inicialização estática explícita



Java permite que você agrupe outras inicializações static dentro de uma “clausula static ” (algumas vezes chamada de bloco static) em uma classe. Parece com isso: Comentários (em inglês)



class Spoon {
  static int i;
  static {
    i = 47;
  }
  // . . .




Parece ser um método, mas é só a palavra chave static seguida por um bloco de código. Este código, como outras inicializações static , é executado somente uma vez: a primeira vez que você faz um objeto da classe ou que você acesse um membro static daquela classe (Mesmo se você nunca faça um objeto daquela classe). Por exemplo: Comentários (em inglês)



//: c04:ExplicitStatic.java
// Inicialização estática explícita com uma clausula "static".
import com.bruceeckel.simpletest.*;

class Cup {
  Cup(int marker) {
    System.out.println("Cup(" + marker + ")");
  }
  void f(int marker) {
    System.out.println("f(" + marker + ")");
  }
}

class Cups {
  static Cup c1;
  static Cup c2;
  static {
    c1 = new Cup(1);
    c2 = new Cup(2);
  }
  Cups() {
    System.out.println("Cups()");
  }
}

public class ExplicitStatic {
  static Test monitor = new Test();
  public static void main(String[] args) {
    System.out.println("Inside main()");
    Cups.c1.f(99);  // (1)
    monitor.expect(new String[] {
      "Inside main()",
      "Cup(1)",
      "Cup(2)",
      "f(99)"
    });
  }
  // static Cups x = new Cups();  // (2)
  // static Cups y = new Cups();  // (2)
} ///:~




Os inicializadores static para Cups rodam quando ocorrer acesso a este ou aquele objeto static c1 na linha marcada (1), ou se a linha (1) estiver comentada e a linha marcada (2) não estiver comentada. Se ambas (1) e (2) estiverem comentadas, a inicialização static para Cups nunca ocorrerá. Tambem, não acontecerá se uma ou ambas as linhas marcadas (2) estiverem não comentadas; a inicialização estática somente ocorrerá uma vez. Comentários (em inglês)

Inicialização de instância não estática



Java provê uma similar sintaxe para inicialização de variáveis não-staticas para cada objeto. Aqui está um exemplo:



//: c04:Mugs.java
// "Inicialização de instância" Java.
import com.bruceeckel.simpletest.*;

class Mug {
  Mug(int marker) {
    System.out.println("Mug(" + marker + ")");
  }
  void f(int marker) {
    System.out.println("f(" + marker + ")");
  }
}

public class Mugs {
  static Test monitor = new Test();
  Mug c1;
  Mug c2;
  {
    c1 = new Mug(1);
    c2 = new Mug(2);
    System.out.println("c1 e c2 inicializados");
  }
  Mugs() {
    System.out.println("Mugs()");
  }
  public static void main(String[] args) {
    System.out.println("Dentro de main()");
    Mugs x = new Mugs();
    monitor.expect(new String[] {
      "Inside main()",
      "Mug(1)",
      "Mug(2)",
      "c1 e c2 inicializados",
      "Mugs()"
    });
  }
} ///:~




Você pode ver esta clausula de inicialização de instância: Comentários (em inglês)



  {
    c1 = new Mug(1);
    c2 = new Mug(2);
    System.out.println("c1 e c2 inicializados");
  }




parece exatamente como a clausula de inicialização estática exceto pela perda da palavra chave static . Esta sintaxe é necessária para suportar a inicialização de classes internas anônimas (veja Capítulo 8). Comentários (em inglês)

Inicialização de array



Inicializar arrays em C é tender a erro e ao tédio. C++ usa inicialização agregada para torná-la mais segura.[25] Java não tem “agregados” como o C++ faz, já que tudo é objeto em Java. Ela tem arrays, e estes são suportados coma a inicialização de array. Comentários (em inglês)



Um array é simplesmente uma sequência de outros objetos ou primitivos que são todos do mesmo tipo e empacotados juntos sob um nome identificador. Arrays são definidos e usados com o operador indexado colchetes fechados [ ]. Para definir um array, você simplesmente segue seu nome de tipo com colchetes fechados vazios: Comentários (em inglês)



int[] a1;




Você pode também colocar colchetes após o identificador para produzir exatamente a mesma idéia: Comentários (em inglês)



int a1[];




Isto obedece as expectativas de programadores C e C++. O primeiro estilo, contudo, é provavelmente uma sintaxe mais sensível, pois diz que o tipo é “um array de int .” Este estilo será usado neste livro. Comentários (em inglês)



O compilador não permite que você descubra o quão grande é um array. Isto nos tras de volta ao assunto das “referências.” Tudo que você tem neste ponto é a referência para um array, e não houve espaço alocado para o array. Para criar armazenamento para o array, você deve escrever uma expressão de inicialização. Para arrays, inicialização pode aparecer em qualquer lugar de seu código, mas você pode tambem usar um tipo especial de expressão de inicialização que pode ocorrer no ponto onde o array é criado. Esta inicialização especial é um conjunto de valores cercados por chaves fechadas. A alocação de memória (o equivalente de usar new) terá os cuidados tomados pelo compilador neste caso. Por exemplo:Comentários (em inglês)



int[] a1 = { 1, 2, 3, 4, 5 };




Assim por que você deveria sempre definir uma referência array sem um array? Comentários (em inglês)



int[] a2;




Bem, é possível atribuir um array a outro em Java, assim você pode dizer: Comentários (em inglês)



a2 = a1;




Oque você está realmente fazendo é copiando uma referência, como demonstrado aqui: Comentários (em inglês)



//: c04:Arrays.java
// Arrays de primitivos.
import com.bruceeckel.simpletest.*;

public class Arrays {
  static Test monitor = new Test();
  public static void main(String[] args) {
    int[] a1 = { 1, 2, 3, 4, 5 };
    int[] a2;
    a2 = a1;
    for(int i = 0; i < a2.length; i++)
      a2[i]++;
    for(int i = 0; i < a1.length; i++)
      System.out.println(
        "a1[" + i + "] = " + a1[i]);
    monitor.expect(new String[] {
      "a1[0] = 2",
      "a1[1] = 3",
      "a1[2] = 4",
      "a1[3] = 5",
      "a1[4] = 6"
    });
  }
} ///:~




Você pode ver que à a1 é dado um valor de inicialização mas à a2 não é; a2 é atribuido mais tarde—neste caso, para outro array. Comentários (em inglês)



Há algo novo aqui: Todos os arrays tem um membro intrínseco (se eles são arrays de objetos ou arrays de primitivos) que você pode perguntar—mas não alterar—para contar quantos elementos existem no array. Este é membro é o length. Tanto arrays em Java, como C e C++, iniciam a contagem do elemento zero, o maior elemento que você pode indexar é length - 1. Se você sair dos limites, C e C++ silenciosamente aceitam isto e você invade sobre toda a sua memória, oque é a origem de muitos problemas infames. No entanto, Java protege você contra este tipo de problemas causando um erro em tempo de execução (uma exceção, o assunto do Capítulo 9) se você ultrapassar os limites. Naturalmente, verificar todos os acessos de arrays gastam tempo e código e não há maneira de desligá-los, o que significa que acessos a arrays podem ser a origem da ineficiência em seu programa se eles ocorrem em uma conjuntura crítica. Para segurança na Internet e produtividade de programação, os designers Java acham que este é um tarefa extra que vale a pena. Comentários (em inglês)



E se você não sabe quantos elementos você precisa em seu array enquanto está escrevendo o programa? Você simplesmente usa new para criar os elementos do array. Aqui, new funciona mesmo que esteja criando um array de primitivos (new não criará um primitivo não-array): Comentários (em inglês)



//: c04:ArrayNew.java
// Criando array com new.
import com.bruceeckel.simpletest.*;
import java.util.*;

public class ArrayNew {
  static Test monitor = new Test();
  static Random rand = new Random();
  public static void main(String[] args) {
    int[] a;
    a = new int[rand.nextInt(20)];
    System.out.println("comprimento de a = " + a.length);
    for(int i = 0; i < a.length; i++)
      System.out.println("a[" + i + "] = " + a[i]);
    monitor.expect(new Object[] {
      "%% comprimento de a = \\\\d+",
      new TestExpression("%% a\\\\[\\\\d+\\\\] = 0", a.length)
    });
  }
} ///:~




A declaração expect( ) contem algo novo neste exemplo: a classe TestExpression . Um objeto TestExpression recebe uma expressão, tanto uma string ordinária como uma expressão regular como mostrado aqui, e um segundo argumento inteiro que indica que a expressão precedente será repetida tantas vezes. TestExpression não só previne duplicação desnecessária no código, mas neste caso, permite que o número de repetições seja determinado durante a execução. Comentários (em inglês)



O tamanho do array é escolhido aleatóriamente pelo uso do método Random.nextInt( ) , o qual produz um valor entre 0 e aquele do argumento. Por causa do método randômico, está claro que a criação do array acontece atualmente em tempo de execução. Em adição, a saída do programa mostra que os elementos de tipo primitivo do array são inicializados automaticamente para valores “vazios”. (Para numéricos e char, este é zero, e para booleanos, é false.) Comentários (em inglês)



Naturalmente, o array também poderia ter sido definido e inicializado na mesma declaração:



int[] a = new int[rand.nextInt(20)];




Este é a maneira preferida de fazer isto, se você puder. Comentários (em inglês)



Se você está operando com um array de objetos não primitivos, você deve sempre usar new. Aqui, o assunto referência surge novamente, porque o que você cria é um array de referências. Considere o tipo envelope Integer, que é uma classe e não um primitivo: Comentários (em inglês)



//: c04:ArrayClassObj.java
// Criando um array de objetos não primitivos.
import com.bruceeckel.simpletest.*;
import java.util.*;

public class ArrayClassObj {
  static Test monitor = new Test();
  static Random rand = new Random();
  public static void main(String[] args) {
    Integer[] a = new Integer[rand.nextInt(20)];
    System.out.println("comprimento de a = " + a.length);
    for(int i = 0; i < a.length; i++) {
      a[i] = new Integer(rand.nextInt(500));
      System.out.println("a[" + i + "] = " + a[i]);
    }
    monitor.expect(new Object[] {
      "%% comprimento de a = \\\\d+",
      new TestExpression("%% a\\\\[\\\\d+\\\\] = \\\\d+", a.length)
    });
  }
} ///:~




Aqui, mesmo depois new é chamado para criar o array: Comentários (em inglês)



Integer[] a = new Integer[rand.nextInt(20)];




é somente um array de referências, e não a própria referência até que seja inicializada pela criação de um novo objeto Integer e a inicialização esteja completa: Comentários (em inglês)



a[i] = new Integer(rand.nextInt(500));




Se você se esquecer de criar um objeto, no entanto, você receberá uma exceção em tempo de execução quando você tentar usar a posição vazia do array. Comentários (em inglês)



Observe bem a formação do objeto String dentro da declaração de impressão. Você pode ver que a referência ao objeto Integer é automaticamente convertida para produzir uma representação String do valor de dentro do objeto. Comentários (em inglês)



é possível também inicializar array de objetos pelo uso de listas em chaves fechadas. Há duas formas:



//: c04:ArrayInit.java
// Inicialização de arrays.

public class ArrayInit {
  public static void main(String[] args) {
    Integer[] a = {
      new Integer(1),
      new Integer(2),
      new Integer(3),
    };
    Integer[] b = new Integer[] {
      new Integer(1),
      new Integer(2),
      new Integer(3),
    };
  }
} ///:~




A primeira forma é útil algumas vezes, mas é muito limitada pois o tamanho do array é determinado em tempo de compilação. A vírgula final na lista de inicializadores é opcional. (Esta característica torna mais fácil a manutenção de listas longas.) Comentários (em inglês)



A segunda forma oferece uma sintaxe conveniente para criar e chamar métodos que podem produzir o mesmo efeito que as listas de argumentos variáveis do C(conhecidas como “varargs” em C). Isto pode incluir quantidades desconhecidas de argumentos tanto quanto tipos desconhecidos. Como no final das contas todas as classes herdam da classe raiz comum Object (um assunto sobre o qual você aprenderá mais conforme progredir no livro), você pode criar um método que receba um array de Object e o chame assim: Comentários (em inglês)



//: c04:VarArgs.java
// Usando a sintaxe array para criar uma lista variável de argumentos.
import com.bruceeckel.simpletest.*;

class A { int i; }

public class VarArgs {
  static Test monitor = new Test();
  static void print(Object[] x) {
    for(int i = 0; i < x.length; i++)
      System.out.println(x[i]);
  }
  public static void main(String[] args) {
    print(new Object[] {
      new Integer(47), new VarArgs(),
      new Float(3.14), new Double(11.11)
    });
    print(new Object[] {"one", "two", "three" });
    print(new Object[] {new A(), new A(), new A()});
    monitor.expect(new Object[] {
      "47",
      "%% VarArgs@\\\\p{XDigit}+",
      "3.14",
      "11.11",
      "one",
      "two",
      "three",
      new TestExpression("%% A@\\\\p{XDigit}+", 3)
    });
  }
} ///:~




Você pode ver que print( ) recebe um array de Object, então passa através do array e imprime cada um. A biblioteca de classes padrão do Java produz saídas sensíveis, mas os objetos das classes criadas aqui—A e VarArgs—imprimem o nome da classe, seguido por um sinal ‘@’, e mais ainda constrói uma expressão regular , \\p{XDigit}, a qual indica um dígito hexadecimal. A sequência ‘+’ significa que haverá um ou mais dígitos hexadecimais. Então, o comportamento padrão (se você não definir um método toString( ) para sua classe, o qual será descrito mais tarde neste livro) é imprimir o nome da classe e o endereço do objeto. Comentários (em inglês)

Arrays multidimensionais



Java permite que você crie facilmente arrays multidimensionais:



//: c04:MultiDimArray.java
// Criando arrays multidimensionais.
import com.bruceeckel.simpletest.*;
import java.util.*;

public class MultiDimArray {
  static Test monitor = new Test();
  static Random rand = new Random();
  public static void main(String[] args) {
    int[][] a1 = {
      { 1, 2, 3, },
      { 4, 5, 6, },
    };
    for(int i = 0; i < a1.length; i++)
      for(int j = 0; j < a1[i].length; j++)
        System.out.println(
          "a1[" + i + "][" + j + "] = " + a1[i][j]);
    // array 3D com tamanho fixo:
    int[][][] a2 = new int[2][2][4];
    for(int i = 0; i < a2.length; i++)
      for(int j = 0; j < a2[i].length; j++)
        for(int k = 0; k < a2[i][j].length; k++)
          System.out.println("a2[" + i + "][" + j + "][" +
            k + "] = " + a2[i][j][k]);
    // array 3D com vetor de tamanho variável:
    int[][][] a3 = new int[rand.nextInt(7)][][];
    for(int i = 0; i < a3.length; i++) {
      a3[i] = new int[rand.nextInt(5)][];
      for(int j = 0; j < a3[i].length; j++)
        a3[i][j] = new int[rand.nextInt(5)];
    }
    for(int i = 0; i < a3.length; i++)
      for(int j = 0; j < a3[i].length; j++)
        for(int k = 0; k < a3[i][j].length; k++)
          System.out.println("a3[" + i + "][" + j + "][" +
            k + "] = " + a3[i][j][k]);
    // Array de objetos não primitivos:
    Integer[][] a4 = {
      { new Integer(1), new Integer(2)},
      { new Integer(3), new Integer(4)},
      { new Integer(5), new Integer(6)},
    };
    for(int i = 0; i < a4.length; i++)
      for(int j = 0; j < a4[i].length; j++)
        System.out.println("a4[" + i + "][" + j +
            "] = " + a4[i][j]);
    Integer[][] a5;
    a5 = new Integer[3][];
    for(int i = 0; i < a5.length; i++) {
      a5[i] = new Integer[3];
      for(int j = 0; j < a5[i].length; j++)
        a5[i][j] = new Integer(i * j);
    }
    for(int i = 0; i < a5.length; i++)
      for(int j = 0; j < a5[i].length; j++)
        System.out.println("a5[" + i + "][" + j +
            "] = " + a5[i][j]);
    // Teste de saída
    int ln = 0;
    for(int i = 0; i < a3.length; i++)
      for(int j = 0; j < a3[i].length; j++)
        for(int k = 0; k < a3[i][j].length; k++)
          ln++;
    monitor.expect(new Object[] {
      "a1[0][0] = 1",
      "a1[0][1] = 2",
      "a1[0][2] = 3",
      "a1[1][0] = 4",
      "a1[1][1] = 5",
      "a1[1][2] = 6",
      new TestExpression(
        "%% a2\\\\[\\\\d\\\\]\\\\[\\\\d\\\\]\\\\[\\\\d\\\\] = 0", 16),
      new TestExpression(
        "%% a3\\\\[\\\\d\\\\]\\\\[\\\\d\\\\]\\\\[\\\\d\\\\] = 0", ln),
      "a4[0][0] = 1",
      "a4[0][1] = 2",
      "a4[1][0] = 3",
      "a4[1][1] = 4",
      "a4[2][0] = 5",
      "a4[2][1] = 6",
      "a5[0][0] = 0",
      "a5[0][1] = 0",
      "a5[0][2] = 0",
      "a5[1][0] = 0",
      "a5[1][1] = 1",
      "a5[1][2] = 2",
      "a5[2][0] = 0",
      "a5[2][1] = 2",
      "a5[2][2] = 4"
    });
  }
} ///:~




O código usado para imprimir usa length assim ele não depende de fixar o tamanho do array. Comentários (em inglês)



O primeiro exemplo mostra um array multidimensional de primitivos. Você delimita cada vetor no array pelo uso de chaves:



    int[][] a1 = {
      { 1, 2, 3, },
      { 4, 5, 6, },
    };




Cada conjunto de colchetes move você para o próximo nível do array. Comentários (em inglês)



O segundo exemplo mostra um array tridimensional alocado com new. Aqui, o array inteiro é alocado de uma vez:



int[][][] a2 = new int[2][2][4];




Mas o terceiro exemplo mostra que cada vetor nos arrays que constituem a matriz pode ser de qualquer tamanho:



    int[][][] a3 = new int[rand.nextInt(7)][][];
    for(int i = 0; i < a3.length; i++) {
      a3[i] = new int[rand.nextInt(5)][];
      for(int j = 0; j < a3[i].length; j++)
        a3[i][j] = new int[rand.nextInt(5)];
    }




O primeiro new cria um array com um primeiro elemento de tamanho randômico e o resto indeterminado. O segundo new dentro do loop for preenche os elementos mas deixa o terceiro índice indeterminado até que você alcance o terceiro new. Comentários (em inglês)



Você verá nas saídas que os valores daquele array são inicializados automaticamente com zero se você não der a eles um valor explícito de inicialização.



Você pode operar com arrays de objetos não primitivos de maneira similar, daquela mostrada no quarto exemplo, demonstrando a habilidade para coletar muitas expressões new com chaves:



    Integer[][] a4 = {
      { new Integer(1), new Integer(2)},
      { new Integer(3), new Integer(4)},
      { new Integer(5), new Integer(6)},
    };




O quinto exemplo mostra como um array de objetos não primitivos pode ser construido peça por peça:



    Integer[][] a5;
    a5 = new Integer[3][];
    for(int i = 0; i < a5.length; i++) {
      a5[i] = new Integer[3];
      for(int j = 0; j < a5[i].length; j++)
        a5[i][j] = new Integer(i*j);
    }




O i*j é só para colocar um valor interessante no Integer. Comentários (em inglês)

Sumário



Este aparentemente elaborado mecanismo de inicialização, o construtor, pode dar a você a forte impressão a respeito da crítica importância colocada na inicialização em uma linguagem. Quando Bjarne Stroustrup, o inventor do C++, desenhou aquela linguagem, uma das primeiras observações que ele fêz sobre a produtividade em C foi que a inicialização imprópria de variáveis causam uma parte significativa dos problemas de programação. Estes tipos de problemas são difíceis de localizar, e casos similares são aplicados a limpeza imprópria. Porque construtores permitem que você garanta inicialização e limpeza apropriados (o compilador não permitirá que um objeto seja criado sem a chamada de um construtor apropriado), você obtem controle completo e seguro.Comentários (em inglês)



Em C++, a destruição é bastante importante porque objetos criados com new devem ser explicitamente destruidos. Em Java, o coletor de lixo libera automaticamente a memória de todos os objetos, assim um método equivalente de limpeza em Java não necessário muitas vezes (mas quando o é, como observado neste capítulo, você deve fazê-lo você mesmo). Em casos onde você não precisa de comportamento de um destrutor, o coletor de lixo do Java simplifica grandemente a programação e adiciona a necessária segurança no manuseio da memória. Alguns coletores de lixo podem até limpar outros recursos como imagens e identificadores de arquivos. No entanto, o coleto de lixo adiciona um custo em tempo de execução, despesa esta que é difícil de colocar na perspectiva por causa da histórica lentidão dos interpretadores Java. Embora Java tenha tido um significativo aumento de performance no correr do tempo, o problema da velocidade tem tornado seu preço alto na adoção da linguagem para certos tipos de problemas de programação. Comentários (em inglês)



Por causa da garantia de que todos os objetos serão construidos, há atualmente mais sobre o construtor do que é mostrado aqui. Em particular, quando você cria uma nova classe usando ou composição ou herança, a garantia de construção também é mantida, e algumas sintaxes adicionais são necessárias para suportar isto. Você aprenderá sobre composição, herança, e como eles afetam construtores nos próximos capítulosComentá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 uma classe com um construtor padrão (um que não recebe argumentos) que imprime uma mensagem. Crie um objeto desta classe.Feedback
  2. Adicione um construtor sobrecarregado para o Exercício 1 que receba um argumento String e o imprima mais a frente com sua mensagem. Feedback
  3. Criar um array de referências a objetos da classe criada por você no Exercício 2, mas não criar agora objetos para atribuir ao array. Quando você rodar o programa, observe se mensagens de inicialização chamadas pelo construtor são impressas. Feedback
  4. Complete o Exercício 3 pela criação de objetos para ligar ao array de referências.Feedback
  5. Criar um array de objetos String e atribuir uma string para cada elemento. Imprimir o array pelo uso de um laço for . Feedback
  6. Criar uma classe chamada Dog com um método sobrecarregado bark( ) . Este método deveria ser sobrecarregado baseado em vários tipos de dados primitivos, e imprimir diferentes tipos de latidos, uivos, etc., dependendo de qual versão sobrecarregada é chamada. Escrever um main( ) que chama todas as versões diferentes.Feedback
  7. Modificar o Exercício 6 para que dois dos métodos sobrecarregados tenham dois argumentos (de dois tipos diferentes),mas na ordem inversa relativa ao outro. Verificar que isto funciona.Feedback
  8. Criar uma classe sem um construtor, e então criar um objeto daquela classe no main( ) para verificar que o construtor padrão é automaticamente sintetizado.Feedback
  9. Criar uma classe com dois métodos. Dentro do primeiro método, chamar o segundo método duas vezes: a primeira vez sem usar this, e a segunda vez usando this. Feedback
  10. Criar uma classe com dois construtores (sobrecarregados). Usando this, chamar o segundo construtor dentro do primeiro. Feedback
  11. Criar uma classe com um método finalize( ) que imprime uma mensagem. No main( ), criar um objeto de sua classe. Explicar o comportamento de seu programa.Feedback
  12. Modificar o Exercício 11 para que seu finalize( ) seja sempre chamado. Feedback
  13. Criar um classe chamada Tank que pode ser preenchida e esvaziada, e tem uma condição de término que pode ser vazia quando o objeto está limpo. Escreva um finalize( ) que verifica esta condição de término. No main( ), teste os cenários possíveis que podem ocorrer quando seu Tank é usado. Feedback
  14. Criar uma classe contendo um int e um char que não são inicializados, e imprima seus valores para verificar que Java executa uma inicialização padrão. Feedback
  15. Criar uma classe contendo uma referência String não inicializada. Demonostre que esta referência é inicializada pelo Java com um null. Feedback
  16. Criar uma classe com um campo String que é inicializado no ponto de definição, e outro que é inicializado pelo construtor. Qual é a diferença entre estas duas propostas?Feedback
  17. Criar uma classe com um campo static String que é inicializado no ponto de definição, e outro que é inicializado por um bloco static . Adicione um método static que imprima ambos os campos e demonstre que eles são ambos inicializados antes de serem usados. Feedback
  18. Criar uma classe com uma String que é inicializada usando “inicialização de instância.” Descreva um uso para este artifício (outro que o especificado neste livro). Feedback
  19. Escrever um método que cria e inicializa um array bidimensional de double. O tamanho do array é determinado pelos argumentos do método, e os valores de inicialização são de uma faixa determinada pelos valores iniciais e finais que também são argumentos do método. Criar um segundo método que imprimirá o array gerado pelo primeiro método. No teste main( ) os métodos criam e imprimem vários diferentes tamanhos de arrays. Feedback
  20. Repita o Exercício19 para um array tridimensional.Feedback
  21. Comente a linha marcada (1) no ExplicitStatic.java e verifique que a clausula de inicialização estática não é chamada. Agora descomente uma das linhas marcadas (2) e verifique the a inicialização estática é chamada. Agora descomente a outra linha marcada (2) e verifique que a a inicialização estática somente ocorre uma vez. Feedback



[19] Em algumas literaturas Java da Sun, eles ao invés de se referir a aqueles com o estranho mas descritivo nome “construtores sem-arg.” O termo “construtor padrão” tem sido usado por muitos anos, assim eu o uso assim.



[20] Algumas pessoas colocarão obsessivamente this em frente de todos os métodos chamados e referências de campos, arguindo que isto torna-o “claro e mais explícito.” Não faça isso. Há uma razão para nós usarmos linguagens de alto nível: elas fazem coisas por nós. Se você coloca this quando ele não é necessário, você confundirá e aborrecerá todos que lerem seu código, pois todo o resto do código que eles tem lido em todos os lugares não usa this . Seguindo um estilo de codificação consistente e direto economizamos tempo e dinheiro.



[21] Um caso em que isto é possível ocorre se você passar uma referência a um objeto para um método static . Então, via referência ( o qual agora é efetivamente this), você pode chamar métodos não-static e acessar campos não-static . Mas normalmetne, se você quer fazer algo parecido, você deve só fazer um simples método não-static .



[22] Joshua Bloch mais a frente em sua seção entitulada “evite finalizadores”: “Finalizadores são imprevisíveis, frequentemente danosos, e geralmente desnecessários.” Effective Java, página 20 (Addison-Wesley 2001).



[23] Um termo cunhado por Bill Venners (www.artima.com) durante um seminário que ele e eu estavamos dando juntos.



[24] Em contraste, C++ tem a lista de inicializadores de construtor que fazem com que a inicialização ocorra antes da entrada do corpo do construtor, e é forçado para objetos. Veja Thinking in C++, 2nd edition (avaliável no CD ROM deste livro e em www.BruceEckel.com).



[25] Veja Thinking in C++, 2nd edition para uma completa descrição da inicialização agregada do C++ .


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