1.初始化变量

正如您已经知道的,您可以在类中声明多个变量,而且不仅声明它们,还可以立即用它们的初始值初始化它们。

这些相同的变量也可以在构造函数中初始化。这意味着,理论上,这些变量可以被赋值两次。例子

代码 笔记
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";
   }
}



变量age被赋予初始值




初始值被覆盖


age 变量存储它的初始值。
 Cat cat = new Cat("Whiskers", 2);
这是允许的:将调用第一个构造函数
 Cat cat = new Cat();
这是允许的:将调用第二个构造函数

Cat cat = new Cat("Whiskers", 2);这是执行时发生的情况:

  • Cat创建了一个对象
  • 所有实例变量都用它们的初始值初始化
  • 调用构造函数并执行其代码。

换句话说,变量首先得到它们的初始值,然后才执行构造函数的代码。


2.类中变量的初始化顺序

变量不仅在构造函数运行之前被初始化——它们是按照明确定义的顺序初始化的:它们在类中声明的顺序。

让我们看一些有趣的代码:

代码 笔记
public class Solution
{
   public int a = b + c + 1;
   public int b = a + c + 2;
   public int c = a + b + 3;
}

这段代码不会编译,因为在a创建变量时还没有b 和c 变量。但是你可以像下面这样编写你的代码——这段代码将编译并运行得很好。

代码 笔记
public class Solution
{
   public int a;
   public int b = a + 2;
   public int c = a + b + 3;
}


0
0+2
0+2+3

但请记住,您的代码必须对其他开发人员透明。最好不要使用这样的技术,因为它会损害代码的可读性。

这里我们必须记住,在变量赋值之前,它们有一个默认值。对于int类型,这是零。

JVM在初始化a变量的时候,会简单的给int类型赋默认值:0。

当它到达 时b,a 变量将已知并具有一个值,因此 JVM 将为它分配值 2。

而当它到达c变量时,变量ab变量已经被初始化,所以JVM很容易计算出初始值c:0+2+3。

如果您在方法内部创建了一个变量,则您不能使用它,除非您之前已为其赋值。但这对于类的变量而言并非如此!如果一个类的变量没有被赋予初始值,那么它被赋予一个默认值。


3.常量

当我们分析对象是如何创建的时,值得一提的是常量的初始化,即带有修饰符的变量final

如果变量具有修饰符final,则必须为其分配初始值。你已经知道这一点,这并不奇怪。

但是你不知道的是,如果你在构造函数中赋值,你不必马上赋初值。这对于 final 变量来说工作得很好。唯一的要求是,如果你有多个构造函数,那么必须在每个构造函数中为一个最终变量赋值。

例子:

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.构造函数中的代码

还有一些关于构造函数的重要说明。后面随着你继续学习Java,你会遇到继承、序列化、异常等,它们都会不同程度地影响构造函数的工作。现在深入探讨这些主题毫无意义,但我们至少有义务触及它们。

例如,这里有一条关于构造函数的重要说明。理论上,您可以在构造函数中编写任何复杂的代码。但是不要这样做。例子:

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);
   }
}






打开文件读取流
将文件读入字节数组
将字节数组保存为字符串




在屏幕上显示文件内容

在 FilePrinter 类构造函数中,我们立即在文件上打开字节流并读取其内容。这是一种复杂的行为,可能会导致错误。

如果没有这样的文件怎么办?如果读取文件有问题怎么办?如果它太大了怎么办?

复杂的逻辑意味着出错的可能性很高,这意味着代码必须正确处理异常。

示例 1 — 序列化

在标准 Java 程序中,有很多情况下您不是创建类对象的人。例如,假设您决定通过网络发送一个对象:在这种情况下,Java 机器本身会将您的对象转换为一组字节,发送它,然后从该字节集重新创建对象。

但是假设您的文件在另一台计算机上不存在。构造函数会出错,没人会去处理。这很有可能导致程序终止。

示例 2 — 初始化类的字段

如果您的类构造函数可以抛出已检查的异常,即用 throws 关键字标记,那么您必须在创建对象的方法中捕获指示的异常。

但是如果没有这样的方法呢?例子:

代码  笔记
class Solution
{
   public FilePrinter reader = new FilePrinter("c:\\readme.txt");
}
这段代码不会编译。

FilePrinter构造函数可以抛出检查异常FilePrinter,这意味着如果不将对象包装在 try-catch 块中,则无法创建对象。而一个try-catch块只能写在一个方法中



5.基类构造器

在前面的课程中,我们稍微讨论了继承。不幸的是,我们对继承和 OOP 的完整讨论是为 OOP 专门的级别保留的,构造函数的继承已经与我们相关。

如果你的类继承了另一个类,父类的一个对象将被嵌入到你的类的一个对象中。更重要的是,父类有自己的变量和自己的构造函数。

这意味着当你的类有一个父类并且你继承了它的变量和方法时,了解和理解变量是如何初始化的以及构造函数是如何被调用的是非常重要的。

班级

我们如何知道初始化变量和调用构造函数的顺序?让我们从编写两个类的代码开始。一个将继承另一个:

代码 笔记
class ParentClass
{
   public String a;
   public String b;

   public ParentClass()
   {
   }
}

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

   public ChildClass()
   {
   }
}










ChildClass 继承ParentClass类。

我们需要确定初始化变量和调用构造函数的顺序。日志记录将帮助我们做到这一点。

记录

日志记录是通过将程序写入控制台或文件来记录程序在运行时执行的操作的过程。

确定构造函数已被调用非常简单:在构造函数的主体中,向控制台写入一条消息。但是你怎么知道一个变量是否已经被初始化了呢?

实际上,这也不是很困难:编写一个特殊的方法来返回用于初始化变量的值,并记录初始化。这就是代码的样子:

最终代码

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");
   }
}




创建ChildClass对象


此方法将传递的文本写入控制台并返回它。





声明ParentClass

Display text 并用它初始化变量。




写一条消息,说明构造函数已被调用。忽略返回值。


声明ChildClass

Display text 并用它初始化变量。




写一条消息,说明构造函数已被调用。忽略返回值。

如果执行这段代码,文本将显示在屏幕上,如下所示:

方法的控制台输出Main.print()
ParentClass.a
ParentClass.b
ParentClass.constructor
ChildClass.c
ChildClass.d
ChildClass.constructor

所以你总是可以亲自确保在调用构造函数之前初始化一个类的变量。基类在继承类初始化之前得到完全初始化。