必须先初始化Java中的类和对象,然后才能使用它们。您以前已经了解到,在加载类时,将类字段初始化为默认值,并且通过构造函数初始化了对象,但是还有更多初始化内容。本文介绍了Java的所有用于初始化类和对象的功能。
如何初始化Java类
在探讨Java对类初始化的支持之前,让我们回顾一下初始化Java类的步骤。考虑清单1。
清单1.将类字段初始化为默认值
class SomeClass
{
static boolean b;
static byte by;
static char c;
static double d;
static float f;
static int i;
static long l;
static short s;
static String st;
}
清单1声明了class SomeClass。这个类声明的类型九个字段boolean,byte,char,double,float,int,long,short,和String。当SomeClass被加载,每个字段的位被设置为零,你解释如下:
false
0
\u0000
0.0
0.0
0
0
0
null
先前的类字段隐式初始化为零。但是,您还可以通过直接为它们分配值来显式初始化类字段,如清单2所示。
清单2.将类字段初始化为显式值
class SomeClass
{
static boolean b = true;
static byte by = 1;
static char c = 'A';
static double d = 2.0;
static float f = 3.0f;
static int i = 4;
static long l = 5000000000L;
static short s = 20000;
static String st = "abc";
}
每个分配的值必须与类字段的类型兼容。每个变量都直接存储值,除外st。变量st存储对String包含的对象的引用abc。
引用类字段
初始化类字段时,将其初始化为先前初始化的类字段的值是合法的。例如,清单3初始化y为x的值。两个字段均初始化为2。
清单3.引用先前声明的字段
class SomeClass
{
static int x = 2;
static int y = x;
public static void main(String[] args)
{
System.out.println(x);
System.out.println(y);
}
}
但是,相反的做法是不合法的:您不能将类字段初始化为随后声明的类字段的值。Java编译器illegal forward reference在遇到这种情况时将输出。考虑清单4。
清单4.尝试引用随后声明的字段
class SomeClass
{
static int x = y;
static int y = 2;
public static void main(String[] args)
{
System.out.println(x);
System.out.println(y);
}
}
illegal forward reference遇到时编译器将报告static int x = y;。这是因为源代码是从上至下编译的,而编译器尚未可见y。(如果y未显式初始化,它也会输出此消息。)
类初始化块
在某些情况下,您可能需要执行复杂的基于类的初始化。您将在加载类之后,从该类创建任何对象之前(假定该类不是实用程序类)执行此操作。您可以将类初始化块用于此任务。
甲类初始化块是由前面的语句块static即真实引入类的身体关键字。当类加载时,将执行这些语句。考虑清单5。
清单5.初始化正弦和余弦值的数组
class Graphics
{
static double[] sines, cosines;
static
{
sines = new double[360];
cosines = new double[360];
for (int i = 0; i < sines.length; i++)
{
sines[i] = Math.sin(Math.toRadians(i));
cosines[i] = Math.cos(Math.toRadians(i));
}
}
}
清单5声明了一个Graphics声明sines和cosines数组变量的类。它还声明了一个类初始化块,该块创建了360个元素的数组,其引用分配给sines和cosines。然后for,通过调用Math类的sin()和cos()方法,使用一条语句将这些数组元素初始化为适当的正弦和余弦值。(Math是Java标准类库的一部分。我将在以后的文章中讨论此类和这些方法。)
性能技巧
因为性能对图形应用程序很重要,并且访问数组元素比调用方法要快,所以开发人员会诉诸性能技巧,例如创建和初始化正弦和余弦数组。
组合类字段初始化器和类初始化块
您可以在应用程序中组合多个类字段初始化器和类初始化块。清单6提供了一个示例。
清单6.以自上而下的顺序执行类初始化
class MCFICIB
{
static int x = 10;
static double temp = 98.6;
static
{
System.out.println("x = " + x);
temp = (temp - 32) * 5.0/9.0; // convert to Celsius
System.out.println("temp = " + temp);
}
static int y = x + 5;
static
{
System.out.println("y = " + y);
}
public static void main(String[] args)
{
}
}
清单6声明并初始化一对类字段(x和y),并声明一对static初始化器。如下所示编译此清单:
javac MCFICIB.java
然后运行生成的应用程序:
java MCFICIB
您应该观察以下输出:
x = 10
temp = 37.0
y = 15
此输出显示以自顶向下的顺序执行类初始化。
()方法
编译类初始化程序和类初始化块时,Java编译器将编译的字节码(按自上而下的顺序)存储在名为的特殊方法中()。尖括号可防止名称冲突:您不能()在源代码中声明方法,因为字符在标识符上下文中是非法的。
加载类后,JVM会在调用之前main()(main()存在时)调用此方法。
让我们看看里面MCFICIB.class。下面的部分拆卸显示出用于存储的信息x,temp和y字段:
Field #1
00000290 Access Flags ACC_STATIC
00000292 Name x
00000294 Descriptor I
00000296 Attributes Count 0
Field #2
00000298 Access Flags ACC_STATIC
0000029a Name temp
0000029c Descriptor D
0000029e Attributes Count 0
Field #3
000002a0 Access Flags ACC_STATIC
000002a2 Name y
000002a4 Descriptor I
000002a6 Attributes Count 0
该Descriptor行标识该字段的JVM 类型描述符。该类型用一个字母表示:Ifor int和Dfor double。
以下部分反汇编揭示了该()方法的字节码指令序列。每行以一个十进制数字开头,该数字标识后续指令的从零开始的偏移地址:
0 bipush 10
2 putstatic MCFICIB/x I
5 ldc2_w #98.6
8 putstatic MCFICIB/temp D
11 getstatic java/lang/System/out Ljava/io/PrintStream;
14 new java/lang/StringBuilder
17 dup
18 invokespecial java/lang/StringBuilder/<init>()V
21 ldc "x = "
23 invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
26 getstatic MCFICIB/x I
29 invokevirtual java/lang/StringBuilder/append(I)Ljava/lang/StringBuilder;
32 invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
35 invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
38 getstatic MCFICIB/temp D
41 ldc2_w #32
44 dsub
45 ldc2_w #5
48 dmul
49 ldc2_w #9
52 ddiv
53 putstatic MCFICIB/temp D
56 getstatic java/lang/System/out Ljava/io/PrintStream;
59 new java/lang/StringBuilder
62 dup
63 invokespecial java/lang/StringBuilder/<init>()V
66 ldc "temp = "
68 invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
71 getstatic MCFICIB/temp D
74 invokevirtual java/lang/StringBuilder/append(D)Ljava/lang/StringBuilder;
77 invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
80 invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
83 getstatic MCFICIB/x I
86 iconst_5
87 iadd
88 putstatic MCFICIB/y I
91 getstatic java/lang/System/out Ljava/io/PrintStream;
94 new java/lang/StringBuilder
97 dup
98 invokespecial java/lang/StringBuilder/<init>()V
101 ldc "y = "
103 invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
106 getstatic MCFICIB/y I
109 invokevirtual java/lang/StringBuilder/append(I)Ljava/lang/StringBuilder;
112 invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
115 invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
118 return
从偏移量0到偏移量2的指令序列等效于以下类字段初始化器:
static int x = 10;
从偏移量5到偏移量8的指令序列等效于以下类字段初始化程序:
static double temp = 98.6;
从偏移量11到偏移量80的指令序列等效于以下类初始化块:
static
{
System.out.println("x = " + x);
temp = (temp - 32) * 5.0/9.0; // convert to Celsius
System.out.println("temp = " + temp);
}
从偏移量83到偏移量88的指令序列等效于以下类字段初始化程序:
static int y = x + 5;
从偏移量91到偏移量115的指令序列等效于以下类初始化块:
static
{
System.out.println("y = " + y);
}
最后,return偏移量为118 的指令将执行返回()到JVM调用此方法的部分。
不用担心字节码的含义
本练习的收获是看到清单6的类字段初始化器和类初始化块中的所有代码都位于该()方法中,并以自上而下的顺序执行。
如何初始化对象
加载并初始化一个类后,您通常会希望从该类创建对象。正如您在我最近对类和对象进行编程的介绍中所了解的那样,您可以通过放置在类的构造函数中的代码来初始化对象。考虑清单7。
清单7.使用构造函数初始化一个对象
class City
{
private String name;
int population;
City(String name, int population)
{
this.name = name;
this.population = population;
}
@Override
public String toString()
{
return name + ": " + population;
}
public static void main(String[] args)
{
City newYork = new City("New York", 8491079);
System.out.println(newYork); // Output: New York: 8491079
}
}
清单7声明一个City带有name和population字段的类。当一个City对象被创建时,City(String name, int population)调用构造函数初始化这些字段被叫构造函数的参数。(我也重写了Object的public String toString()方法,以方便地以字符串形式返回城市名称和人口值。System.out.println()最终调用此方法以返回对象的字符串表示形式,并由其输出。)
在调用构造函数之前,将执行name和population包含哪些值?您可以通过System.out.println(this.name); System.out.println(this.population);在构造函数的开始处插入来查找。编译源代码(javac City.java)并运行应用程序(java City)之后,您将观察nullfor name和0for population。该new运营商零对象的对象(实例)执行构造函数之前的字段。
与类字段一样,您可以显式初始化对象字段。例如,您可以指定String name = “New York”;或int population = 8491079;。但是,这样做通常无济于事,因为这些字段将在构造函数中初始化。我能想到的唯一好处是为对象字段分配了默认值。当您调用未初始化字段的构造函数时,将使用此值:
int numDoors = 4; // default value assigned to numDoorsCar(String make, String model, int year)
{
this(make, model, year, numDoors);
}
Car(String make, String model, int year, int numDoors)
{
this.make = make;
this.model = model;
this.year = year;
this.numDoors = numDoors;
}
对象初始化反映了类的初始化
除构造函数外,对象初始化还反映了类的初始化。您可以使用对象字段初始化程序来代替类字段初始化程序。此外,您可以使用对象初始化块来代替类初始化块。您还可以引用先前声明和初始化的对象字段,但不能引用后续声明和初始化的对象字段。清单8中展示了所有这些概念。
清单8.对象初始化如何反映类的初始化
class Mirror
{
int x = 2;
int y = x;
{
System.out.println("x = " + x);
System.out.println("y = " + y);
}
public static void main(String[] args)
{
Mirror mirror = new Mirror();
}
}
清单8揭示了对象初始化块是引入到类主体中的语句块。与类初始化块不同,对象初始化块没有任何前缀。因为您可以在构造函数中初始化对象,所以对象初始化块的唯一好用法是在匿名类的上下文中,该类没有构造函数,我将在以后的文章中进行讨论。
编译清单8(javac Mirror.java)并运行生成的应用程序(java Mirror)。您将发现以下输出:
x = 2
y = 2
组合构造函数和对象字段初始化程序以及对象初始化块
您可以在应用程序中组合多个构造函数,对象字段初始化程序和对象初始化块。清单9提供了一个示例。
清单9.一种奇怪的对象初始化方法
class MCOFIOIB
{
MCOFIOIB()
{
System.out.println("MCOFIOIB() called");
}
int x = 5;
{
x += 6;
}
int i;
MCOFIOIB(int i)
{
this.i = i;
System.out.println("MCOFIOIB(i) called: i = " + i);
}
{
System.out.println("i = " + i);
System.out.println("x = " + x);
}
public static void main(String[] args)
{
new MCOFIOIB();
System.out.println();
new MCOFIOIB(6);
}
}
清单9声明了一对构造函数(MCOFIOIB()和MCOFIOIB(int i)),一对对象字段(x和i)和一对对象初始化块。如下编译该清单:
javac MCOFIOIB.java
然后运行生成的应用程序:
java MCOFIOIB
您应该观察以下输出:
i = 0
x = 11
MCOFIOIB() called
i = 0
x = 11
MCOFIOIB(i) called: i = 6
此输出来自两个MCOFIOIB对象的创建。第一部分来自new MCOFIOIB();,第二部分来自new MCOFIOIB(6);。每个部分都揭示了在构造函数执行之前,对象字段初始化程序和对象初始化块已执行。此外,它揭示了对象字段初始化程序和对象初始化块以自上而下的顺序执行。(x必须先初始化到11才能x = 11输出。)
()方法
如果要检查编译器为其生成的字节码,MCOFIOIB.class则会观察到()方法而不是构造函数的存在。JVM调用这些方法而不是构造函数。
您还将观察到x和i字段的以下部分反汇编:
Field #1
0000026d Access Flags
0000026f Name x
00000271 Descriptor I
00000273 Attributes Count 0
Field #2
00000275 Access Flags
00000277 Name i
00000279 Descriptor I
0000027b Attributes Count 0
接下来,您将观察到以下有关MCOFIOIB()构造函数的信息和字节码序列:
0 aload_0
1 invokespecial java/lang/Object/()V
4 aload_0
5 iconst_5
6 putfield MCOFIOIB/x I
9 aload_0
10 dup
11 getfield MCOFIOIB/x I
14 bipush 6
16 iadd
17 putfield MCOFIOIB/x I
20 getstatic java/lang/System/out Ljava/io/PrintStream;
23 new java/lang/StringBuilder
26 dup
27 invokespecial java/lang/StringBuilder/()V
30 ldc "i = "
32 invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
35 aload_0
36 getfield MCOFIOIB/i I
39 invokevirtual java/lang/StringBuilder/append(I)Ljava/lang/StringBuilder;
42 invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
45 invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
48 getstatic java/lang/System/out Ljava/io/PrintStream;
51 new java/lang/StringBuilder
54 dup
55 invokespecial java/lang/StringBuilder/()V
58 ldc "x = "
60 invokevirtual java/lang/StringBuilder/append(Ljava/lang/String;)Ljava/lang/StringBuilder;
63 aload_0
64 getfield MCOFIOIB/x I
67 invokevirtual java/lang/StringBuilder/append(I)Ljava/lang/StringBuilder;
70 invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String;
73 invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
76 getstatic java/lang/System/out Ljava/io/PrintStream;
79 ldc "MCOFIOIB() called"
81 invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
84 return
从偏移量0到偏移量1的指令序列等效于调用Object超类的无参数构造函数:
new Object();
调用构造函数
当一个类不扩展另一个类时,该()方法以字节码开头以调用Object()构造函数。当构造函数以开头时super(),()以字节码开头以调用超类构造函数。当一个构造函数以this()(以调用同一类中的另一个构造函数)()开头时,以字节码序列开头以调用另一个构造函数;它不包含调用超类构造函数的代码。
从偏移量4到偏移量17的指令序列等效于以下类初始化块:
int x = 5;
{
x += 6;
}
从偏移量20到偏移量73的指令序列执行第二个对象初始化块:
{
System.out.println("i = " + i);
System.out.println("x = " + x);
}
从偏移量76到偏移量84的指令序列执行MCOFIOIB()构造函数代码,并将执行返回给构造函数的调用者。
同样,不必担心字节码的含义。要记住的重要事项是初始化顺序。调用时MCOFIOIB(),将执行以下任务:
首先调用超类的noargument构造函数。
然后,以自上而下的顺序执行对象字段初始化程序和对象初始化块。
构造函数的代码最后执行。
为简便起见,我将不介绍MCOFIOIB(int i)构造函数的字节码序列。与十分相似MCOFIOIB()。唯一的区别是要执行的最终代码是MCOFIOIB(int i)的代码,而不是MCOFIOIB()的代码。
结论
在本Java教程中,您学习了如何使用类字段初始化程序和类初始化块来初始化类,以及如何使用构造函数,对象字段初始化器和对象初始化块来初始化对象。尽管相对简单,但类和对象的初始化至关重要:JVM必须在使用类和对象之前对其进行初始化。
既然您知道了初始化的工作原理,那么您基本上已经完成了对Java类和对象语言功能的探索。本系列的下一篇教程将深入介绍接口,这是初级Java开发人员的高级话题。