《On Java 中文版 基础卷》阅读笔记 初始化和清理 08

《On Java 中文版 基础卷》阅读笔记 初始化和清理 08

6.6 成员初始化

Java 会不遗余力地保证变量在使用之前被正确初始化。对于方法的局部变量,这种保证以编译时错误的形式出现。所以如果代码这样写:

java 复制代码
void f(){
	int i;
	i++;
}

你会收到一条错误消息,指出 i 可能未初始化。编译器可以给 i 一个默认值,但未初始化的局部变量更有可能是程序员的失误,而默认值会掩盖这一点。强制程序员提供初始化值更有可能避免错误。

然而,如果类的字段是基本类型的话,情况就会有所不同。

正如在第 3 章中所看到的那样,类的每个基本类型字段都会获得一个初始值。

下面的程序可以验证这一点,并显示了这个值:

java 复制代码
package housekeeping;
// housekeeping/InitialValues.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Shows default initial values

public class InitialValues {
  boolean t;
  char c;
  byte b;
  short s;
  int i;
  long l;
  float f;
  double d;
  InitialValues reference;
  void printInitialValues() {
    System.out.println("Data type   Initial value");
    System.out.println("boolean     " + t);
    System.out.println("char        [" + c + "]");
    System.out.println("byte        " + b);
    System.out.println("short       " + s);
    System.out.println("int         " + i);
    System.out.println("long        " + l);
    System.out.println("float       " + f);
    System.out.println("double      " + d);
    System.out.println("reference   " + reference);
  }
  public static void main(String[] args) {
    new InitialValues().printInitialValues();
  }
}
/* Output:
Data type   Initial value
boolean     false
char        []
byte        0
short       0
int         0
long        0
float       0.0
double      0.0
reference   null
*/

即使未指定值,类的字段也会自动被初始化(char 值为零,我的输出验证系统将其转换为 \[\] )​。所以至少不会有使用未初始化变量的危险。

当在类中定义了一个对象引用而不将其初始化时,这个引用就会被赋予一个特殊值 null。

指定初始化

如何给一个变量赋初始值呢?一种直接的方法就是在类中定义变量时分配值。

以下代码更改了类 InitialValues 中的字段定义,直接提供了初始值:

java 复制代码
package housekeeping;
// housekeeping/InitialValues2.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Providing explicit initial values

public class InitialValues2 {
  boolean bool = true;
  char ch = 'x';
  byte b = 47;
  short s = 0xff;
  int i = 999;
  long lng = 1;
  float f = 3.14f;
  double d = 3.14159;
}

也可以用同样的方式初始化非基本类型的对象。如果 Depth 是一个类,你可以创建一个变量,然后这样初始化:

java 复制代码
package housekeeping;
// housekeeping/Measurement.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
class Depth {}

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

如果没有给 d 一个初始值就尝试使用它,会得到一个叫作异常 (exception) 的运行时错误(第 15 章中会介绍)​。

还可以通过调用方法来提供初始值:

java 复制代码
package housekeeping;
// housekeeping/MethodInit.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
public class MethodInit {
  int i = f();
  int f() { return 11; }
}

这个方法可以有参数,但这些参数必须是已经初始化了的。因此,可以这样写:

java 复制代码
package housekeeping;
// housekeeping/MethodInit2.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
public class MethodInit2 {
  int i = f();
  int j = g(i);
  int f() { return 11; }
  int g(int n) { return n * 10; }
}

但不能这样写:

java 复制代码
package housekeeping;
// housekeeping/MethodInit3.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
public class MethodInit3 {
  //- int j = g(i); // Illegal forward reference 非法的前向引用
  int i = f();
  int f() { return 11; }
  int g(int n) { return n * 10; }
}

编译器对前向引用 (forward referencing) 发出了告警,这里的问题和初始化顺序有关,而不是程序的编译方式。

这种初始化方法简单明了,但有个限制:类 InitialValues 的每个对象都会有相同的初始值。

有时候这正是你想要的,但有时你可能需要更大的灵活性。

6.7 构造器初始化

可以用构造器来执行初始化,这为编程带来了更大的灵活性,让你可以在运行时调用方法来设置初始值。

不过这并不会阻止自动初始化的执行,因为它是在构造器执行前就发生了的。因此,如果这样写:

java 复制代码
package housekeeping;
// housekeeping/Counter.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
public class Counter {
  int i;
  Counter() { i = 7; }
  // ...
}

i 先被自动初始化为 0,然后被构造器初始化为 7。对于所有的基本类型和对象引用都是如此,包括那些在定义时就显式初始化了的。

出于这个原因,编译器不会强制你在构造器的某个特定位置或使用之前对元素进行初始化------因为初始化已经得到了保证。

6.7.1 初始化顺序

类中的变量定义顺序决定了初始化的顺序。即使分散到方法定义之间,变量定义仍然会在任何方法(包括构造器)调用之前就被初始化。

例如:

java 复制代码
package housekeeping;
// housekeeping/OrderOfInitialization.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Demonstrates initialization order

// When the constructor is called to create a
// Window object, you'll see a message:
class Window {
  Window(int marker) {
    System.out.println("Window(" + marker + ")");
  }
}

class House {
  Window w1 = new Window(1); // Before constructor
  House() {
    // Show that we're in the constructor:
    System.out.println("House()");
    w3 = new Window(33); // Reinitialize w3
  }
  Window w2 = new Window(2); // After constructor
  void f() { System.out.println("f()"); }
  Window w3 = new Window(3); // At end
}

public class OrderOfInitialization {
  public static void main(String[] args) {
    House h = new House();
    h.f(); // Shows that construction is done
  }
}
/* Output:
Window(1)
Window(2)
Window(3)
House()
Window(33)
f()
*/

在 House 中,Window 对象的定义被故意分散到方法之间,以证明它们都会在构造器或其他任何事情发生之前就执行初始化。

此外,w3 在构造器中被重新初始化。

输出显示 w3 引用被初始化了两次:一次在构造器调用之前,另一次在构造器调用期间(第一个对象被丢弃,因此稍后可能会被垃圾收集)​。

这看起来可能效率不高,但它保证了正确的初始化。

试想,如果定义了一个重载的构造器,它没有初始化 w3,而 w3 定义时也没有指定默认的初始化值,程序运行时会产生什么后果呢?

6.7.2 静态数据的初始化

无论创建了多少对象,静态数据都只有一份内存空间。static 关键字不能用于局部变量,而仅适用于字段(成员变量)​。

如果一个字段是 static 的基本类型,并且没有初始化,那它就会获得基本类型的标准初始值。如果它是一个对象引用,则默认初始值为 null。

如果将静态数据的初始化放在定义时,采取的方式和非静态变量没有什么不同。

下面的示例显示了 static 何时被初始化:

java 复制代码
package housekeeping;
// housekeeping/StaticInitialization.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Specifying initial values in a class definition

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

class Table {
  static Bowl bowl1 = new Bowl(1);
  Table() {
    System.out.println("Table()");
    bowl2.f1(1);
  }
  void f2(int marker) {
    System.out.println("f2(" + marker + ")");
  }
  static Bowl bowl2 = new Bowl(2);
}

class Cupboard {
  Bowl bowl3 = new Bowl(3);
  static Bowl bowl4 = new Bowl(4);
  Cupboard() {
    System.out.println("Cupboard()");
    bowl4.f1(2);
  }
  void f3(int marker) {
    System.out.println("f3(" + marker + ")");
  }
  static Bowl bowl5 = new Bowl(5);
}

public class StaticInitialization {
  public static void main(String[] args) {
    System.out.println("main creating new Cupboard()");
    new Cupboard();
    System.out.println("main creating new Cupboard()");
    new Cupboard();
    table.f2(1);
    cupboard.f3(1);
  }
  static Table table = new Table();
  static Cupboard cupboard = new Cupboard();
}
/* Output:
Bowl(1)
Bowl(2)
Table()
f1(1)
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
main creating new Cupboard()
Bowl(3)
Cupboard()
f1(2)
main creating new Cupboard()
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
*/

Bowl 代码里的提示信息揭示了一个类的创建过程,

Table 和 Cupboard 类把 Bowl 类型的静态字段分散到各处定义。

注意 Cupboard 类还在 static 定义之前创建了一个非静态的 bowl3。

输出显示了 static 初始化仅在必要时发生。如果不创建 Table 对象并且也不引用 Table.bowl1 或 Table.bowl2,

则 Bowl 类型的静态字段 bowl1 和 bowl2 永远都不会被创建。

它们仅在第一个 Table 对象创建(或第一次访问静态数据)时被初始化。

之后,这些静态对象不会被重新初始化。

初始化的顺序是从静态字段开始(如果它们还没有被先前的对象创建触发初始化的话)​,然后是非静态字段,然后执行构造方法。

可以从输出中看到这一点。要执行静态的 main() 方法,必须先加载 StaticInitialization 类,然后初始化它的静态字段 table 和 cupboard,这会导致它们对应的类被加载,并且因为它们都包含了静态的 Bowl 对象,所以 Bowl 也被加载。

因此,这个特定程序中所有的类都在 main() 方法开始执行前加载。通常情况下并非如此,这是因为常见的程序中不会像此例那样通过 static 将所有内容链接在一起。

为了总结对象创建的过程,假设有一个名为 Dog 的类。

1.尽管没有显式使用 static 关键字,但构造器实际上也是静态方法。

因此,第一次创建类型为 Dog 的对象时,或者第一次访问类 Dog 的静态方法或静态字段时,

Java 解释器会搜索类路径来定位 Dog.class 文件。

2.当 Dog.class 被加载后(这将创建一个 Class 对象,后面会介绍)​,它的所有静态初始化工作都会执行。

因此,静态初始化只在 Class 对象首次加载时发生一次。

3.当使用 new Dog() 创建对象时,构建过程首先会在堆上为 Dog 对象分配足够的存储空间。

4.这块存储空间会被清空,然后自动将该 Dog 对象中的所有基本类型设置为其默认值(数值类型的默认值是 0, boolean 和 char 则是和 0 等价的对应值)​,而引用会被设置为 null。

5.执行所有出现在字段定义处的初始化操作。

6.执行构造器。正如将在第 8 章中看到的,这实际上可能涉及相当多的动作,尤其是在涉及继承时。

6.7.3 显式的静态初始化

Java 允许在一个类里将多个静态初始化语句放在一个特殊的 "静态子句" 里(有时称为静态块)​。它看起来像这样:

java 复制代码
package housekeeping;
// housekeeping/Spoon.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
public class Spoon {
  static int i;
  static {
    i = 47;
  }
}

尽管看起来有点像一个方法,但它只是在 static 关键字后加了一段代码。

这段代码和其他静态初始化语句一样,只执行一次:

第一次创建该类的对象时,或第一次访问该类的静态成员时(即使从未创建过该类的对象)​。

例如:

java 复制代码
package housekeeping;
// housekeeping/ExplicitStatic.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Explicit static initialization with "static" clause

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

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

public class ExplicitStatic {
  public static void main(String[] args) {
    System.out.println("Inside main()");
    Cups.cup1.f(99);                  // [1]
  }
  // static Cups cups1 = new Cups();  // [2]
  // static Cups cups2 = new Cups();  // [2]
}
/* Output:
Inside main()
Cup(1)
Cup(2)
f(99)
*/

无论是通过方式 1 访问静态对象 cup1,还是注释掉方式 1 并取消方式 2 的注释来运行,Cups 的静态初始化动作都会发生。

如果把方式 1 和方式 2 同时注释掉,Cups 的静态初始化动作就不会发生。

此外,方式 2 运行一次还是两次无关紧要,静态初始化动作只会发生一次。

6.7.4 非静态实例初始化

Java 提供了一种称为实例初始化 (instance initialization) 的类似语法,用于初始化每个对象的非静态变量。下面是一个例子:

java 复制代码
package housekeeping;
// housekeeping/Mugs.java
// (c)2021 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// Instance initialization

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

public class Mugs {
  Mug mug1;
  Mug mug2;
  {                                         // [1]
    mug1 = new Mug(1);
    mug2 = new Mug(2);
    System.out.println("mug1 & mug2 initialized");
  }
  Mugs() {
    System.out.println("Mugs()");
  }
  Mugs(int i) {
    System.out.println("Mugs(int)");
  }
  public static void main(String[] args) {
    System.out.println("Inside main()");
    new Mugs();
    System.out.println("new Mugs() completed");
    new Mugs(1);
    System.out.println("new Mugs(1) completed");
  }
}
/* Output:
Inside main()
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs()
new Mugs() completed
Mug(1)
Mug(2)
mug1 & mug2 initialized
Mugs(int)
new Mugs(1) completed
*/

1 除了缺少 static 关键字外,实例初始化子句看起来与静态初始化子句完全相同。

此语法对于支持匿名内部类的初始化是必需的(参见第 11 章)​,但也可以用来保证无论调用哪个显式的构造器,某些操作都会发生。

从输出可以看到,实例初始化子句在构造器之前执行。

相关推荐
鬼手点金1 小时前
KV Cache 未必是一次性的:可编辑、可组合的“笔记”
java·人工智能·笔记·学习·自动化·1024程序员节
xiangyun611 小时前
C++ 手撕 shared_ptr:引用计数与控制块到底长什么样?
开发语言·c++·算法·面试·stl
海鸥-w1 小时前
Redis 配置与排错说明
java·数据库·redis·bootstrap
XiYang-DING1 小时前
P4009.和为K的子数组个数 & P5522.异或和为 k 的子数组个数
java·开发语言
TinyKey1 小时前
malloc_debug_notes
java
郭小李1 小时前
spring @PathVariable 注解用法
java·后端·spring
用户3126874877201 小时前
Java 锁全景:synchronized、ReentrantLock、ReadWriteLock、StampedLock
java
鱼宵2 小时前
CSV 怎么做销售报表?Stream 第 9 课:读洗聚排打五段管道一次跑通
java·报表·stream
jaysee-sjc2 小时前
【苍穹外卖】Day12:工作台数据看板与 Excel 报表导出
java·开发语言·数据库·spring boot·spring·微信·excel