本文共 16447 字,大约阅读时间需要 54 分钟。
Java 尽量保证所有变量在使用前都能得到恰当的初始化。如果方法的局部变量未被初始化,那么会报编译时错误:
void f() { int i; i++;}
// housekeeping/InitialValues.java// Shows default initial valuespublic 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(); }}
输出:
Data type Initial valueboolean falsechar[ ]byte 0short 0int 0long 0float 0.0double 0.0reference null
可见尽管没有初始化这些数据成员,但它们确实有初值(char 值为 0,所以显示为空白)。所以这样至少不会出现"未初始化变量"的风险了。
在类里定义一个对象引用时,如果不将其初始化,那么引用就会被赋值为 null。
一种给变量赋初值的方法是在定义类成员变量的地方为其赋值。以下代码修改了上面 InitialValues 类成员变量的定义,直接提供了初值:
// housekeeping/InitialValues2.java// Providing explicit initial valuespublic 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 是一个类,那么可以像下面这样创建一个对象并初始化它:
// housekeeping/Measurement.javaclass Depth { }public class Measurement { Depth d = new Depth(); // ...}
如果没有为 d 赋予初值就尝试使用它,就会出现运行时错误,告诉你产生了一个异常(详细见"异常"章节)。
我们也可以通过调用某个方法来提供初值:
// housekeeping/MethodInit.javapublic class MethodInit { int i = f(); int f() { return 11; }}
这个方法可以带有参数,但这些参数不能是未初始化的类成员变量。因此,可以这么写:
// housekeeping/MethodInit2.javapublic class MethodInit2 { int i = f(); int j = g(i); int f() { return 11; } int g(int n) { return n * 10; }}
但是不能这么写:
// housekeeping/MethodInit3.javapublic class MethodInit3 { int j = g(i); // Illegal forward reference int i = f(); int f() { return 11; } int g(int n) { return n * 10; }}
默认初始化的方式简单直观,但是有限制,那就是每次初始化的值后对象的值是写死的,缺少了一些灵活性。
与指定初始化相对,我们还可以用构造器进行初始化,这种方式给了我们更大的灵活性,因为通过这种方式我们可以在运行时调用方法进行初始化。但是,这无法阻止自动初始化的进行,因为自动初始化会在构造器被调用之前发生。因此,如果使用如下代码:
// housekeeping/Counter.javapublic class Counter { int i; Counter() { i = 7; } // ...}
i 首先会被初始化为 0,然后变为 7。对于所有的基本类型和引用,包括在定义时已明确指定初值的变量,这种情况都是成立的。因此,编译器不会强制要求我们在构造器的某个地方或在使用它们之前初始化类的成员变量——因为初始化早已得到了保证。
在类中变量定义的顺序决定了它们初始化的顺序。即使变量定义散布在方法定义之间,它们仍会在任何方法(包括构造器)被调用之前得到初始化。例如:
// housekeeping/OrderOfInitialization.java// 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 }}
输出:
Window(1)Window(2)Window(3)House()Window(33)f()
在 House 类中,故意把几个 Window 对象的定义散布在各处,以证明它们全都会在调用构造器或其他方法之前得到初始化。此外,w3 在构造器中被再次赋值。
由输出可见,引用 w3 被初始化了两次:一次在调用构造器前,一次在构造器调用期间(第一次引用的对象将被丢弃,并作为垃圾回收)。
无论创建多少个对象,静态数据都只占用一份存储区域。static 关键字不能应用于局部变量,只能作用于属性(字段、域)。如果一个字段是静态的基本类型,并且没有被初始化,那么它就会进行默认初始化。如果它是对象引用,那么它的默认初值就是 null。
下面例子显示了静态存储区是何时初始化的,其中Bowl 类是我们要创建的类,而 Table 和 Cupboard 在它们的类定义中包含了 Bowl 类型的静态数据成员。注意一点,在Cupboard类中非静态成员b3的定义是在静态数据成员定义前面的。:
// housekeeping/StaticInitialization.java// Specifying initial values in a class definitionclass 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();}
输出:
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)
由输出可见,初始化的顺序先是静态对象(如果它们之前没有被初始化的话),然后是非静态对象,从输出中可以看出。要执行 main() 方法,必须加载 StaticInitialization 类,它的静态属性 table 和 cupboard 随后被初始化,这会导致它们对应的类也被加载,而由于它们都包含静态的 Bowl 对象,所以 Bowl 类也会被加载。因此,在这个特殊的程序中,所有的类都会在 main() 方法之前被加载。
同时注意静态初始化只有在必要时刻才会进行。如果不创建 Table 对象,也不引用 Table.bowl1 或 Table.bowl2,那么静态的 Bowl 类对象 bowl1 和 bowl2 永远不会被创建。只有在第一个 Table 对象被创建(或被访问)时,它们才会被初始化。此后,静态对象不会再次被初始化。
概括下创建对象的过程,假设有一个名为Dog的类:
我们可以将一组静态初始化动作放在类里面的一个静态块中,像下面这样:
// housekeeping/Spoon.javapublic class Spoon { static int i; static { i = 47; }}
静态块看起来像个方法,但实际上它只是一段跟在static关键字后面的代码块。与其他静态初始化动作一样,这段代码仅当首次创建这个类的对象或首次访问这个类的静态成员(甚至不需要创建该类的对象)时才会执行一次,例如:
// housekeeping/ExplicitStatic.java// Explicit static initialization with "static" clauseclass 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]}
输出:
Inside mainCup(1)Cup(2)f(99)
无论是通过标为 [1] 的行访问静态的 cup1 对象,还是把标为 [1] 的行注释掉,让它去运行标为 [2] 的那行代码(去掉 [2] 的注释),Cups 的静态初始化动作都会执行。如果同时注释 [1] 和 [2] 处,那么 Cups 的静态初始化就不会进行。此外,如果把标为 [2] 处的注释都去掉,静态初始化也只会执行一次。
Java 提供了被称为实例初始化的类似语法,用来初始化每个对象的非静态变量,例如:
// housekeeping/Mugs.java// Instance initializationclass 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"); }}
输出:
Inside mainMug(1)Mug(2)mug1 & mug2 initializedMugs()new Mugs() completedMug(1)Mug(2)mug1 & mug2 initializedMugs(int)new Mugs(1) completed
看起来它很像静态代码块,只不过少了 static 关键字。这种语法对于支持"匿名内部类"(参见"内部类"一章)的初始化是必须的,但是你也可以使用它保证某些操作一定会发生,而不管哪个构造器被调用。从输出看出,实例初始化子句是在两个构造器之前执行的。
关于数组的基本使用,可以参考:
如果在编写程序时,不确定数组中需要多少个元素,可以使用 new 在程序运行期间动态创建元素:
// housekeeping/ArrayNew.java// Creating arrays with newimport java.util.*;public class ArrayNew { public static void main(String[] args) { int[] a; Random rand = new Random(47); a = new int[rand.nextInt(20)]; System.out.println("length of a = " + a.length); System.out.println(Arrays.toString(a)); } }
输出:
length of a = 18[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
数组的大小是通过 Random.nextInt() 随机确定的,这个方法会返回 0 到输入参数之间的一个值。 由于随机性,很明显数组的创建确实是在运行时进行的。此外,程序输出表明,数组元素中的基本数据类型值会自动初始化为默认值(对于数字和字符是 0;对于布尔型是 false)。Arrays.toString() 是 java.util 标准类库中的方法,会产生一维数组的可打印版本。
在本例中,数组也可以在定义的同时进行初始化:
int[] a = new int[rand.nextInt(20)];
我们还可以创建一个引用数组,以整型的包装类型Integer为例,它是一个类而非基本类型:
// housekeeping/ArrayClassObj.java// Creating an array of nonprimitive objectsimport java.util.*;public class ArrayClassObj { public static void main(String[] args) { Random rand = new Random(47); Integer[] a = new Integer[rand.nextInt(20)]; System.out.println("length of a = " + a.length); for (int i = 0; i < a.length; i++) { a[i] = rand.nextInt(500); // Autoboxing } System.out.println(Arrays.toString(a)); }}
输出:
length of a = 18[55, 193, 361, 461, 429, 368, 200, 22, 207, 288, 128, 51, 89, 309, 278, 498, 361, 20]
这里,即使使用 new 创建数组之后:
Integer[] a = new Integer[rand.nextInt(20)];
它只是一个引用数组,直到通过创建新的 Integer 对象(通过自动装箱),并把对象赋值给引用,初始化才算结束:
a[i] = rand.nextInt(500);
如果忘记了创建对象,但试图使用数组中的空引用,就会在运行时产生异常。
我们可以灵活使用利用花括号括起来的列表来初始化数组的初始方式,比如下面这个例子,我们创建了一个String数组,并将其传递给另一个类的main()方法:
// housekeeping/DynamicArray.java// Array initializationpublic class DynamicArray { public static void main(String[] args) { Other.main(new String[] { "fiddle", "de", "dum"}); }}class Other { public static void main(String[] args) { for (String s: args) { System.out.print(s + " "); } }}
输出:
fiddle de dum
我们可以以一种类似 C 语言中的可变参数列表(C 通常把它称为"varargs")来创建和调用方法。这可以应用在参数个数或类型未知的场合。由于所有的类都最后继承于 Object 类,我们可以创建一个以 Object 数组为参数的方法,并像下面这样调用:
// housekeeping/VarArgs.java// Using array syntax to create variable argument listsclass A { }public class VarArgs { static void printArray(Object[] args) { for (Object obj: args) { System.out.print(obj + " "); } System.out.println(); } public static void main(String[] args) { printArray(new Object[] { 47, (float) 3.14, 11.11}); printArray(new Object[] { "one", "two", "three"}); printArray(new Object[] { new A(), new A(), new A()}); }}
输出:
47 3.14 11.11 one two three A@15db9742 A@6d06d69c A@7852e922
printArray() 的参数是 Object 数组,使用 for-in 语法遍历和打印数组的每一项。标准 Java 库能输出有意义的内容,但这里创建的是类的对象,打印出的内容是类名,后面跟着一个 @ 符号以及多个十六进制数字。因而,默认行为(如果没有定义 toString() 方法的话,后面会讲这个方法)就是打印类名和对象的地址。
你可能看到像上面这样编写的 Java 5 之前的代码,它们可以产生可变的参数列表。在 Java 5 中,这种期盼已久的特性终于添加了进来,就像在 printArray() 中看到的那样:
// housekeeping/NewVarArgs.java// Using array syntax to create variable argument listspublic class NewVarArgs { static void printArray(Object... args) { for (Object obj: args) { System.out.print(obj + " "); } System.out.println(); } public static void main(String[] args) { // Can take individual elements: printArray(47, (float) 3.14, 11.11); printArray(47, 3.14F, 11.11); printArray("one", "two", "three"); printArray(new A(), new A(), new A()); // Or an array: printArray((Object[]) new Integer[] { 1, 2, 3, 4}); printArray(); // Empty list is OK }}
输出:
47 3.14 11.11 47 3.14 11.11 one two three A@15db9742 A@6d06d69c A@7852e922 1 2 3 4
有了可变参数,你就再也不用显式地编写数组语法了,当你指定参数时,编译器实际上会为你填充数组。你获取的仍然是一个数组,这就是为什么 printArray() 可以使用 for-in 迭代数组的原因。但是,这不仅仅只是从元素列表到数组的自动转换。注意程序的倒数第二行,一个 Integer 数组(通过自动装箱创建)被转型为一个 Object 数组(为了移除编译器的警告),并且传递给了 printArray()。显然,编译器会发现这是一个数组,不会执行转换。因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法会把它们当作可变参数列表来接受。
程序的最后一行表明,可变参数的个数可以为 0。当具有可选的尾随参数时,这一特性会有帮助:
// housekeeping/OptionalTrailingArguments.javapublic class OptionalTrailingArguments { static void f(int required, String... trailing) { System.out.print("required: " + required + " "); for (String s: trailing) { System.out.print(s + " "); } System.out.println(); } public static void main(String[] args) { f(1, "one"); f(2, "two", "three"); f(0); }}
输出:
required: 1 one required: 2 two three required: 0
这段程序展示了如何使用除了 Object 类之外类型的可变参数列表。这里,所有的可变参数都是 String 对象。可变参数列表中可以使用任何类型的参数,包括基本类型。下面例子展示了可变参数列表变为数组的情形,并且如果列表中没有任何元素,那么转变为大小为 0 的数组:
// housekeeping/VarargType.javapublic class VarargType { static void f(Character... args) { System.out.print(args.getClass()); System.out.println(" length " + args.length); } static void g(int... args) { System.out.print(args.getClass()); System.out.println(" length " + args.length) } public static void main(String[] args) { f('a'); f(); g(1); g(); System.out.println("int[]: "+ new int[0].getClass()); }}
输出:
class [Ljava.lang.Character; length 1class [Ljava.lang.Character; length 0class [I length 1class [I length 0int[]: class [I
getClass() 方法属于 Object 类,将在"类型信息"一章中全面介绍。它会产生对象的类,并在打印该类时,看到表示该类类型的编码字符串。前导的 [ 代表这是一个后面紧随的类型的数组,I 表示基本类型 int;为了进行双重检查,我在最后一行创建了一个 int 数组,打印了其类型。这样也验证了使用可变参数列表不依赖于自动装箱,而使用的是基本类型。
然而,可变参数列表与自动装箱可以和谐共处,如下:
// housekeeping/AutoboxingVarargs.javapublic class AutoboxingVarargs { public static void f(Integer... args) { for (Integer i: args) { System.out.print(i + " "); } System.out.println(); } public static void main(String[] args) { f(1, 2); f(4, 5, 6, 7, 8, 9); f(10, 11, 12); }}
输出:
1 24 5 6 7 8 910 11 12
注意吗,你可以在单个参数列表中将类型混合在一起,自动装箱机制会有选择地把 int 类型的参数提升为 Integer。
可变参数列表使得方法重载更加复杂了,尽管乍看之下似乎足够安全:
// housekeeping/OverloadingVarargs.javapublic class OverloadingVarargs { static void f(Character... args) { System.out.print("first"); for (Character c: args) { System.out.print(" " + c); } System.out.println(); } static void f(Integer... args) { System.out.print("second"); for (Integer i: args) { System.out.print(" " + i); } System.out.println(); } static void f(Long... args) { System.out.println("third"); } public static void main(String[] args) { f('a', 'b', 'c'); f(1); f(2, 1); f(0); f(0L); //- f(); // Won's compile -- ambiguous }}
输出:
first a b csecond 1second 2 1second 0third
在每种情况下,编译器都会使用自动装箱来匹配重载的方法,然后调用最明确匹配的方法。
但是如果调用不含参数的 f(),编译器就无法知道应该调用哪个方法了。尽管这个错误可以弄清楚,但是它可能会使客户端程序员感到意外。
你可能会通过在某个方法中增加一个非可变参数解决这个问题:
// housekeeping/OverloadingVarargs2.java// {WillNotCompile}public class OverloadingVarargs2 { static void f(float i, Character... args) { System.out.println("first"); } static void f(Character... args) { System.out.println("second"); } public static void main(String[] args) { f(1, 'a'); f('a', 'b'); }}
{WillNotCompile} 注释把该文件排除在了本书的 Gradle 构建之外。如果你手动编译它,会得到下面的错误信息:
OverloadingVarargs2.java:14:error:reference to f is ambiguous f('a', 'b');\^both method f(float, Character...) in OverloadingVarargs2 and method f(Character...) in OverloadingVarargs2 match 1 error
如果你给这两个方法都添加一个非可变参数,就可以解决问题了:
// housekeeping/OverloadingVarargs3public class OverloadingVarargs3 { static void f(float i, Character... args) { System.out.println("first"); } static void f(char c, Character... args) { System.out.println("second"); } public static void main(String[] args) { f(1, 'a'); f('a', 'b'); }}
输出:
firstsecond
你应该总是在重载方法的一个版本上使用可变参数列表,或者压根不用它。
转载地址:http://yntx.baihongyu.com/