1. Inicializando variáveis

Como você já sabe, você pode declarar diversas variáveis ​​em sua classe, e não apenas declará-las, mas também inicializá-las imediatamente com seus valores iniciais.

E essas mesmas variáveis ​​também podem ser inicializadas em um construtor. Isso significa que, em teoria, essas variáveis ​​podem receber valores duas vezes. Exemplo

Código Observação
class Cat
{
   public String name;
   public int age = -1;

   public Cat(String name, int age)
   {
     this.name = name;
     this.age = age;
   }

   public Cat()
   {
     this.name = "Nameless";
   }
}



A agevariável recebe um valor inicial




O valor inicial é substituído


A variável idade armazena seu valor inicial.
 Cat cat = new Cat("Whiskers", 2);
Isso é permitido: o primeiro construtor será chamado
 Cat cat = new Cat();
Isso é permitido: o segundo construtor será chamado

Isto é o que acontece quando Cat cat = new Cat("Whiskers", 2);é executado:

  • Um Catobjeto é criado
  • Todas as variáveis ​​de instância são inicializadas com seus valores iniciais
  • O construtor é chamado e seu código é executado.

Ou seja, primeiro as variáveis ​​recebem seus valores iniciais, e só então o código do construtor é executado.


2. Ordem de inicialização das variáveis ​​em uma classe

As variáveis ​​não são apenas inicializadas antes da execução do construtor — elas são inicializadas em uma ordem bem definida: a ordem em que são declaradas na classe.

Vejamos um código interessante:

Código Observação
public class Solution
{
   public int a = b + c + 1;
   public int b = a + c + 2;
   public int c = a + b + 3;
}

Este código não irá compilar, pois no momento em que a avariável é criada ainda não existem variáveis b ​​e c . Mas você pode escrever seu código da seguinte maneira - esse código será compilado e executado perfeitamente.

Código Observação
public class Solution
{
   public int a;
   public int b = a + 2;
   public int c = a + b + 3;
}


0
0+2
0+2+3

Mas lembre-se que seu código deve ser transparente para outros desenvolvedores. É melhor não usar técnicas como essa, pois prejudica a legibilidade do código.

Aqui devemos lembrar que, antes que as variáveis ​​recebam um valor, elas têm um valor padrão . Para o inttipo, isso é zero.

Quando a JVM inicializar a avariável, ela simplesmente atribuirá o valor padrão para o tipo int: 0.

Quando atingir b, a variável a já será conhecida e terá um valor, então a JVM atribuirá a ela o valor 2.

E quando atingir a cvariável, as variáveis a​​e bjá estarão inicializadas, então a JVM calculará facilmente o valor inicial para c: 0+2+3.

Se você criar uma variável dentro de um método, não poderá usá-la, a menos que tenha atribuído um valor a ela anteriormente. Mas isso não é verdade para as variáveis ​​de uma classe! Se um valor inicial não for atribuído a uma variável de uma classe, será atribuído um valor padrão a ela.


3. Constantes

Enquanto estamos analisando como os objetos são criados, vale a pena tocar na inicialização de constantes, ou seja, variáveis ​​com o finalmodificador.

Se uma variável tiver o finalmodificador, deve ser atribuído um valor inicial. Você já sabe disso e não há nada de surpreendente nisso.

Mas o que você não sabe é que não precisa atribuir o valor inicial imediatamente se atribuí-lo no construtor. Isso funcionará muito bem para uma variável final. O único requisito é que, se você tiver vários construtores, uma variável final deve receber um valor em cada construtor.

Exemplo:

public class Cat
{
   public final int maxAge = 25;
   public final int maxWeight;

   public Cat (int weight)
   {
     this.maxWeight = weight; // Assign an initial value to the constant
   }
}


4. Código em um construtor

E mais algumas notas importantes sobre construtores. Mais tarde, ao continuar a aprender Java, você encontrará coisas como herança, serialização, exceções, etc. Todos eles influenciam o trabalho dos construtores em graus variados. Não faz sentido nos aprofundarmos nesses temas agora, mas somos obrigados a pelo menos abordá-los.

Por exemplo, aqui está uma observação importante sobre construtores. Em teoria, você pode escrever código de qualquer complexidade em um construtor. Mas não faça isso. Exemplo:

class FilePrinter
{
   public String content;

   public FilePrinter(String filename) throws Exception
   {
      FileInputStream input = new FileInputStream(filename);
      byte[] buffer = input.readAllBytes();
      this.content = new String(buffer);
   }

   public void printFile()
   {
      System.out.println(content);
   }
}






Abra um fluxo de leitura de arquivo
Lê o arquivo em uma matriz de bytes
Salve a matriz de bytes como uma string




Exiba o conteúdo do arquivo na tela

No construtor da classe FilePrinter, abrimos imediatamente um fluxo de bytes em um arquivo e lemos seu conteúdo. Este é um comportamento complexo e pode resultar em erros.

E se não houvesse tal arquivo? E se houver problemas com a leitura do arquivo? E se fosse muito grande?

Lógica complexa implica em alta probabilidade de erros e isso significa que o código deve tratar as exceções corretamente.

Exemplo 1 — Serialização

Em um programa Java padrão, há muitas situações em que não é você quem cria os objetos de sua classe. Por exemplo, suponha que você decida enviar um objeto pela rede: nesse caso, a própria máquina Java irá converter seu objeto em um conjunto de bytes, enviá-lo e recriar o objeto a partir do conjunto de bytes.

Mas suponha que seu arquivo não exista no outro computador. Haverá um erro no construtor e ninguém o tratará. E isso é capaz de fazer com que o programa seja encerrado.

Exemplo 2 — Inicializando campos de uma classe

Se o construtor de sua classe pode lançar exceções verificadas, ou seja, está marcado com a palavra-chave throws, então você deve capturar as exceções indicadas no método que cria seu objeto.

Mas e se não houver tal método? Exemplo:

Código  Observação
class Solution
{
   public FilePrinter reader = new FilePrinter("c:\\readme.txt");
}
Este código não irá compilar.

O FilePrinterconstrutor de classe pode lançar uma exceção verificada , o que significa que você não pode criar um FilePrinterobjeto sem envolvê-lo em um bloco try-catch. E um bloco try-catch só pode ser escrito em um método



5. Construtor de classe base

Nas lições anteriores, discutimos um pouco sobre herança. Infelizmente, nossa discussão completa sobre herança e OOP está reservada para o nível dedicado a OOP, e a herança de construtores já é relevante para nós.

Se sua classe herdar outra classe, um objeto da classe pai será inserido dentro de um objeto de sua classe. Além do mais, a classe pai tem suas próprias variáveis ​​e seus próprios construtores.

Isso significa que é muito importante para você saber e entender como as variáveis ​​são inicializadas e os construtores são chamados quando sua classe tem uma classe pai e você herda suas variáveis ​​e métodos.

Aulas

Como sabemos a ordem em que as variáveis ​​são inicializadas e os construtores são chamados? Vamos começar escrevendo o código para duas classes. Um herdará o outro:

Código Observação
class ParentClass
{
   public String a;
   public String b;

   public ParentClass()
   {
   }
}

class ChildClass extends ParentClass
{
   public String c;
   public String d;

   public ChildClass()
   {
   }
}










A ChildClass classe herda a ParentClassclasse.

Precisamos determinar a ordem na qual as variáveis ​​são inicializadas e os construtores são chamados. O registro nos ajudará a fazer isso.

Exploração madeireira

Logging é o processo de gravação de ações executadas por um programa enquanto ele é executado, gravando-as no console ou em um arquivo.

É bastante simples determinar que o construtor foi chamado: no corpo do construtor, escreva uma mensagem para o console. Mas como você pode saber se uma variável foi inicializada?

Na verdade, isso também não é muito difícil: escreva um método especial que retorne o valor usado para inicializar a variável e registre a inicialização. Isto é o que o código pode parecer:

código final

public class Main
{
   public static void main(String[] args)
   {
      ChildClass obj = new ChildClass();
   }

   public static String print(String text)
   {
      System.out.println(text);
      return text;
   }
}

class ParentClass
{
   public String a = Main.print("ParentClass.a");
   public String b = Main.print("ParentClass.b");

   public ParentClass()
   {
      Main.print("ParentClass.constructor");
   }
}

class ChildClass extends ParentClass
{
   public String c = Main.print("ChildClass.c");
   public String d = Main.print("ChildClass.d");

   public ChildClass()
   {
      Main.print("ChildClass.constructor");
   }
}




Criar um ChildClassobjeto


Este método grava o texto passado no console e também o retorna.





Declare a ParentClassclasse

Display text e também inicialize as variáveis ​​com ela.




Escreva uma mensagem informando que o construtor foi chamado. Ignore o valor de retorno.


Declare a ChildClassclasse

Display text e também inicialize as variáveis ​​com ela.




Escreva uma mensagem informando que o construtor foi chamado. Ignore o valor de retorno.

Se você executar este código, o texto será exibido na tela da seguinte forma:

Saída do console do métodoMain.print()
ParentClass.a
ParentClass.b
ParentClass.constructor
ChildClass.c
ChildClass.d
ChildClass.constructor

Portanto, você sempre pode garantir pessoalmente que as variáveis ​​de uma classe sejam inicializadas antes que o construtor seja chamado. Uma classe base é totalmente inicializada antes da inicialização da classe herdada.