读thinking in java笔记(四):初始化

    随着计算革命的发展,“不安全”的编程方式已逐渐成为编程代价高昂的主因之一。
    初始化和清理(cleanup)正是涉及安全的两个问题。许多C程序的错误都源于程序员忘记初始化变量。清理也是一个问题,当使用完一个元素时,它对你也就不会有什么影响了,所以很容易把它忘记,所以,这样一来,这个元素占用的资源就会一直得不到释放,结果是资源(尤其是内存)用尽。
    C++引入了构造器(construtor)的概念,这是一个在创建对象时被自动调用的特殊方法。Java中也采用了构造器,并额外提供了“垃圾回收器”。对于不再使用的内存资源,垃圾回收器能自动将其释放。
1. 用构造器确保(对象)初始化
    可以假想为编写的每个类都定义一个initialize()方法。该方法的名称提醒你在使用其之前,应首先调用initialize()。然而,这同时意味着用户必须记得自己去调用次此方法,在Java中通过提供构造器,类的设计者可确保每个对象都会得到初始化。创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器。从而保证了初始化的进程。构造器的命名采用和类名相同的方式。[自己理解:使用对象前首先得创建对象,创建对象就是通过调用构造器,所以说用构造器确保对象的初始化。]
    接下来的问题就是如何命名这个方法?1.所取的任何名字都可能与类的某个成员名称相冲突;2.调用构造器是编译器的责任,所以必须让编译器知道应该调用哪个方法。Java中采用的方案是:即构造器采用与类相同的名称。

例子:
public class SimpleConstructor {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Rock();
        }
    }
}

class Rock {
    public Rock() {
        System.out.print("Rock ");
    }
}

    现在,在创建对象时:new Rock();将会为对象分配存储空间,并调用相应的构造器。这就确保了在你能操作对象之前,它已经被恰当的初始化了。
    请注意,由于构造器的名称和类名完全相同,所以,“每个方法首字母小写”的编码风格并不适用于构造器。
    不接受任何参数的构造器叫作默认构造器,Java文档中通常使用术语无参构造器。和其他方法一样,构造器也能带有形式参数,以便指定如何创建对象。对上述例子稍加修改,即可使构造器接受一个参数:

public class SimpleConstructor2 {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Rock2(i);
        }
    } 
}
class Rock2{
    public Rock2(int i) {
        System.out.print("Rock" + i + " ");
    }
}

    构造器有了形式参数,就可以在初始化对象时提供实际参数。构造器有助于减少错误,并使代码更易阅读。从概念上讲,“初始化”与“创建”是彼此独立的,然后在上面的代码中,你找不到对initialize()方法的明确调用,在Java中,初始化和创建捆绑在一起,两者不能分离。即,创建对象的过程也就初始化了对象。
    构造器是一种特殊类型的方法,因为它没有返回值。这与返回值为空(void)明显不同。对于空返回值,尽管方法本身不会自动返回什么,但仍可选择让它返回别的东西。构造器则不会返回任何东西,你别无选择(new表达式确实返回了对新建对象的引用,但构造器本身并没有任何返回值)。
2. 方法重载
    任何程序设计语言都具备的一项重要特性就是对名字的运用。当创建一个对象时,也就给此对象分配到的存储空间取了一个名字。所谓方法则是给某个动作取的名字。通过使用名字你可以使用所有的对象和方法。名字起的好可以使系统更易于理解和修改。
    在日常生活中,相同的词可以表达多种不同的含义,它们被“重载”了。在Java里,构造器是强制重载方法名的另一个原因。既然构造器的名字已经由类名所决定,就只能有一个构造器名。那么要想用多种方式创建一个对象该怎么办呢?假设你要创建一个类,即可以用标准方式进行初始化,也可以从文件里读取信息来初始化,这就需要两个构造器:一个默认构造器,另一个取字符串作为参数—该字符串表示初始化对象所需的文件名称。由于都是构造器,所以他们必须有相同的名字,即类名。为了让方法名相同而形式参数不同的构造器同时存在,必须用到方法重载。同时,尽管方法重载是构造器所必需的,但它也可应用于其他方法,且用法同样方便。
  2.1 区分重载方法
    要是几个方法有相同的名字,Java如何才能知道你指的是哪一个呢?其实方法很简单:每个重载的方法都必须有一个独一无二的参数类型列表(个数、类型、顺序—顺序一般不建议作为区分标准)。
    稍加思考,就会觉得这是合理的。毕竟,对于名字相同的方法,除了参数列表的差异以外,还有什么办法把它们区别开呢?甚至参数顺序的不同也足以区分两个方法。不过一般情况下别这么做,因为这会使代码难以维护。
  2.2 涉及基本类型的重载
    基本类型能从一个“较小”的类型自动提升至一个“较大”的类型,此过程一旦牵涉到重载,可能会造成一些混淆。

例子:
public class PrimitiveOverloading {
  void f1(char x) { printnb("f1(char) "); }
  void f1(byte x) { printnb("f1(byte) "); }
  void f1(short x) { printnb("f1(short) "); }
  void f1(int x) { printnb("f1(int) "); }
  void f1(long x) { printnb("f1(long) "); }
  void f1(float x) { printnb("f1(float) "); }
  void f1(double x) { printnb("f1(double) "); }

  void f2(byte x) { printnb("f2(byte) "); }
  void f2(short x) { printnb("f2(short) "); }
  void f2(int x) { printnb("f2(int) "); }
  void f2(long x) { printnb("f2(long) "); }
  void f2(float x) { printnb("f2(float) "); }
  void f2(double x) { printnb("f2(double) "); }

  void f3(short x) { printnb("f3(short) "); }
  void f3(int x) { printnb("f3(int) "); }
  void f3(long x) { printnb("f3(long) "); }
  void f3(float x) { printnb("f3(float) "); }
  void f3(double x) { printnb("f3(double) "); }

  void f4(int x) { printnb("f4(int) "); }
  void f4(long x) { printnb("f4(long) "); }
  void f4(float x) { printnb("f4(float) "); }
  void f4(double x) { printnb("f4(double) "); }

  void f5(long x) { printnb("f5(long) "); }
  void f5(float x) { printnb("f5(float) "); }
  void f5(double x) { printnb("f5(double) "); }

  void f6(float x) { printnb("f6(float) "); }
  void f6(double x) { printnb("f6(double) "); }

  void f7(double x) { printnb("f7(double) "); }

  void testConstVal() {
    printnb("5: ");
    f1(5);f2(5);f3(5);f4(5);f5(5);f6(5);f7(5); print();
  }
  void testChar() {
    char x = 'x';
    printnb("char: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testByte() {
    byte x = 0;
    printnb("byte: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testShort() {
    short x = 0;
    printnb("short: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testInt() {
    int x = 0;
    printnb("int: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testLong() {
    long x = 0;
    printnb("long: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testFloat() {
    float x = 0;
    printnb("float: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  void testDouble() {
    double x = 0;
    printnb("double: ");
    f1(x);f2(x);f3(x);f4(x);f5(x);f6(x);f7(x); print();
  }
  public static void main(String[] args) {
    PrimitiveOverloading p =
      new PrimitiveOverloading();
    p.testConstVal();
    p.testChar();
    p.testByte();
    p.testShort();
    p.testInt();
    p.testLong();
    p.testFloat();
    p.testDouble();
  }
} /* Output:
5: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
char: f1(char) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
byte: f1(byte) f2(byte) f3(short) f4(int) f5(long) f6(float) f7(double)
short: f1(short) f2(short) f3(short) f4(int) f5(long) f6(float) f7(double)
int: f1(int) f2(int) f3(int) f4(int) f5(long) f6(float) f7(double)
long: f1(long) f2(long) f3(long) f4(long) f5(long) f6(float) f7(double)
float: f1(float) f2(float) f3(float) f4(float) f5(float) f6(float) f7(double)
double: f1(double) f2(double) f3(double) f4(double) f5(double) f6(double) f7(double)
*/

    你会发现常数值5被当作int值处理,所以如果有某个重载方法接受int型参数,它就会被调用。至于其他情况,如果传入的数据类型(实际参数类型)小于方法中声明的形式参数类型,实际数据类型就会被提升。char型略有不同,如果无法找到恰好接受char参数的方法,就会把char直接提升至int。总结:1. 实际参数类型 < 形式参数类型 类型提升;2. 实际参数类型 > 形式参数类型 强制类型转换;3. char型:如果无刚好接受char参数的方法,直接提升到int;
    如果传入的实际参数大于重载方法声明的形式参数,会出现的什么情况呢?

例子:
public class Demotion {
  void f1(char x) { print("f1(char)"); }
  void f1(byte x) { print("f1(byte)"); }
  void f1(short x) { print("f1(short)"); }
  void f1(int x) { print("f1(int)"); }
  void f1(long x) { print("f1(long)"); }
  void f1(float x) { print("f1(float)"); }
  void f1(double x) { print("f1(double)"); }

  void f2(char x) { print("f2(char)"); }
  void f2(byte x) { print("f2(byte)"); }
  void f2(short x) { print("f2(short)"); }
  void f2(int x) { print("f2(int)"); }
  void f2(long x) { print("f2(long)"); }
  void f2(float x) { print("f2(float)"); }

  void f3(char x) { print("f3(char)"); }
  void f3(byte x) { print("f3(byte)"); }
  void f3(short x) { print("f3(short)"); }
  void f3(int x) { print("f3(int)"); }
  void f3(long x) { print("f3(long)"); }

  void f4(char x) { print("f4(char)"); }
  void f4(byte x) { print("f4(byte)"); }
  void f4(short x) { print("f4(short)"); }
  void f4(int x) { print("f4(int)"); }

  void f5(char x) { print("f5(char)"); }
  void f5(byte x) { print("f5(byte)"); }
  void f5(short x) { print("f5(short)"); }

  void f6(char x) { print("f6(char)"); }
  void f6(byte x) { print("f6(byte)"); }

  void f7(char x) { print("f7(char)"); }

  void testDouble() {
    double x = 0;
    print("double argument:");
    f1(x);f2((float)x);f3((long)x);f4((int)x);
    f5((short)x);f6((byte)x);f7((char)x);
  }
  public static void main(String[] args) {
    Demotion p = new Demotion();
    p.testDouble();
  }
} /* Output:
double argument:
f1(double)
f2(float)
f3(long)
f4(int)
f5(short)
f6(byte)
f7(char)
*/

    在这里,方法接受较小的基本类型作为参数。如果传入的实际参数较大,就得通过类型转换来执行窄化转换。如果不这么做,编译器就会报错。
  2.3 以返回值区分重载方法
    读者可能会想,在区分重载方法的时候,为什么只能以方法的形参列表作为标准呢?能否根据方法的返回值来区分呢?比如:void f(){} int f(){return 1;} 这样两个方法,虽然它们有相同的名字和形式参数,但却很容易区分它们。只要编译器可以根据语境判断出语义,比如在int x = f()中,那么的确可以据此区分重载方法。不过,有时你并不关心方法的返回值,你想要的是方法调用的其他效果(这常被称为“为了副作用而调用”),这时你可能会调用方法而忽略其返回值。所以,如果这样调用方法:f(); 此时Java如何才能判断该调用时哪一个f()呢?  所以得出结论:根据方法的返回值来区分重载方法是行不通的。
3. 默认构造器
    如前所述,默认构造器(又名“无参”构造器)是没有形式参数的—它的作用是创建一个“默认对象”。如果你写的类中没有构造器,则编译器会自动帮你创建一个默认构造器。

例子:
class Bird {}

public class DefaultConstructor {
  public static void main(String[] args) {
    Bird b = new Bird(); // Default!
  }
} ///:~

    表达式new Bird() 行创建了一个新对象,并调用其默认构造器—即使你没有明确定义它。没有它的话,就没有方法可调用,就无法创建对象。但是,如果已经定义了一个构造器(无论是否有参数),编译器就不会帮你自动创建默认的构造器:

例子:
class Bird2 {
  Bird2(int i) {}
  Bird2(double d) {}
}

public class NoSynthesis {
  public static void main(String[] args) {
    //! Bird2 b = new Bird2(); // No default
    Bird2 b2 = new Bird2(1);
    Bird2 b3 = new Bird2(1.0);
  }
} ///:~

    要是你这样写:new Bird2() 编译器就会报错:没有找到匹配的构造器。这就好比,要是你没有提供任何构造器,编译器会认为“你需要一个构造器,让我给你制造一个吧”;但假如你已写了一个构造器,编译器则会认为“啊,你已写了一个构造器,所以你知道你在做什么;你是刻意省略了默认构造器”。
4. this关键字
    如果有同一类型的两个对象,分别是a和b。一个peel()方法是如何知道是被a还是被b所调用的呢?

例子:
public class BananaPeel {
  public static void main(String[] args) {
    Banana a = new Banana(),
           b = new Banana();
    a.peel(1);
    b.peel(2);
  }
} ///:~

    为了能用简便、面向对象的语法来编写代码—即“发送消息给对象”,编译器做了一些幕后工作。它暗自把“说操作对象的引用”作为第一个参数传给peel()。所以,上述两个方法的调用就变成了这样:Banana.peel(a,1);Banana.peel(b,2);这是内部的表示形式,我们并不能这样书写代码,并试图通过编译;但这种写法的确能帮你了解实际所发生的事情。
    假设你希望在方法的内部获得对当前对象的引用。由于这个引用是由编译器“偷偷”传入的,所以没有标识符可用。但是为此有个专门的关键词this。this关键字只能在方法内部使用,表示对“调用方法的那个对象”的引用。this的用法和其他对象的引用并无不同。但要注意,如果在方法内部调用同一个类的另一个方法,就不必使用this。直接调用即可。当前方法中的this引用会自动应用于同一类中的其他方法。所以可以这样

写代码:
public class Apricot {
  void pick() { /* ... */ }
  void pit() { pick(); /* ... */ }
} ///:~

    在pit()内部,你可以写this.pick(),但无此必要。编译器能帮你自动添加。只有当需要明确指出对当前对象的引用时,才需要使用this关键字。例如,当需要返回对当前对象的引用时,就常常在return语句里这样写:

public class Leaf {
  int i = 0;
  Leaf increment() {
    i++;
    return this;
  }
  void print() {
    System.out.println("i = " + i);
  }
  public static void main(String[] args) {
    Leaf x = new Leaf();
    x.increment().increment().increment().print();
  }
} /* Output:
i = 3
*/

    由于increment()通过this关键字返回了对当前对象的引用,所以很容易在一条语句里对同一个对象执行多次操作。
    this关键字对于将当前对象传递给其他方法也很有用:

class Person {
  public void eat(Apple apple) {
    Apple peeled = apple.getPeeled();
    System.out.println("Yummy");
  }
}
class Peeler {
  static Apple peel(Apple apple) {
    // ... remove peel
    return apple; // Peeled
  }
}
class Apple {
  Apple getPeeled() { 
      return Peeler.peel(this); 
  }
}
public class PassingThis {
  public static void main(String[] args) {
    new Person().eat(new Apple());
  }
} /* Output:
Yummy
*/

  4.1 在构造器中调用构造器
    可能为一个类写了多个构造器,有时可能想在一个构造器中调用另一个构造器,以避免重复代码。可用this关键字做到这一点。
    通常写this的时候,都是指“这个对象”或者“当前对象”,而且它本身表示对当前对象的引用。在构造器中,如果为this添加了参数列表,那么就有了不同含义。这将产生对符合此参数列表的某个构造器的明确调用;这样,调用其他构造器就有了直接的途径:

public class Flower {
  int petalCount = 0;
  String s = "initial value";
  Flower(int petals) {
    petalCount = petals;
    print("Constructor w/ int arg only, petalCount= "
      + petalCount);
  }
  Flower(String ss) {
    print("Constructor w/ String arg only, s = " + ss);
    s = ss;
  }
  Flower(String s, int petals) {
    this(petals);
//!    this(s); // Can't call two!
    this.s = s; // Another use of "this"
    print("String & int args");
  }
  Flower() {
    this("hi", 47);
    print("default constructor (no args)");
  }
  void printPetalCount() {
//! this(11); // Not inside non-constructor!
    print("petalCount = " + petalCount + " s = "+ s);
  }
  public static void main(String[] args) {
    Flower x = new Flower();
    x.printPetalCount();
  }
} /* Output:
Constructor w/ int arg only, petalCount= 47
String & int args
default constructor (no args)
petalCount = 47 s = hi
*/

    构造器Flower(String s,int petals)表明:尽管可以用this调用一个构造器,但却不能调用两个。此外,必须将构造器调用置于最起始行,否则编译器会出错。这个例子也展示了this的另外一种用法。由于参数s的名称和数据成员s的名称相同,所以会产生歧义。使用this.s来代表数据成员就能解决这个问题。在Java程序代码中经常出现这种写法,本书中也经常这样写。printPetalCount()方法表明,除构造器之外,编译器禁止在其他任何地方调用构造器。
  4.1 static的含义
    了解this关键字之后,就能更全面地理解static(静态)方法含义。static方法就是没有this的方法。在static方法的内部不能调用非静态方法,反过来倒是可以。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。它很像全局方法。Java中禁止使用全局方法,但你在类中置入static方法就可以访问其他static方法和static域。
    有些人认为static方法不是“面向对象”的,因为它们的确具有全局函数的语义;使用static方法时,由于不存在this,所以不是通过“向对象发送消息”的方式来完成的。的确,要是在代码中出现了大量的static方法,就该重新考虑自己的设计了。然而,static的概念有它的实用之处,许多时候都要用到它。至于它是否真的“面向对象”就留给理论家去讨论吧。
5. 清理
    有关清理将通过单独一篇来记录。
6. 成员初始化
    Java尽力保证:所有变量在使用前都能得到恰当初始化。对于方法的局部变量,Java以编译时错误的形式来贯彻这种保证。所以如果写成:

void f(){
    int i;
    i++; //Error -- i not initialized
}

    就会得到一条出错消息,告诉你i可能尚未初始化。当然,编译器也可以为i赋一个默认值,但是未初始化的局部变量更有可能是程序员的疏忽,所以采用默认值反而会掩盖这种失误。因此,强制程序员提供一个初始值,往往能够帮助找出程序里的缺陷。
    要是类的数据成员(即字段)是基本类型,情况就会变得有些不同。正如在“一切都是对象”一章中所看到的,类的每个基本类型数据成员保证都会有一个初始值。

例子:
import static net.mindview.util.Print.*;
public class InitialValues {
  boolean t;
  char c;
  byte b;
  short s;
  int i;
  long l;
  float f;
  double d;
  InitialValues reference;
  void printInitialValues() {
    print("Data type      Initial value");
    print("boolean        " + t);
    print("char           [" + c + "]");
    print("byte           " + b);
    print("short          " + s);
    print("int            " + i);
    print("long           " + l);
    print("float          " + f);
    print("double         " + d);
    print("reference      " + reference);
  }
  public static void main(String[] args) {
    InitialValues iv = new InitialValues();
    iv.printInitialValues();
    /* You could also say:
    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值为0,所以显示为空白)。这样至少不会冒“未初始化变量”的风险了。
    在类里定义一个对象引用时,如果不将其初始化,此引用就会获得一个特殊值null。
    总结:1.基本类型成员变量如果不对其进行显示初始化,那么编译器会为其赋予一个默认初始化值。2.引用型成员变量如果不对其进行初始化,此应用将会获得一个特殊值null。3.局部变量如果不对其进行初始化在编译器即报错。
  6.1 指定(显示)初始化
    如果想为某个变量赋初值,该怎么做呢?有一种很直接的办法,就是在定义类成员变量的地方为其赋值,以下代码片段修改了InitialValues类成员变量的定义,直接提供了初值。

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是一个类,那么可以像下面这样创建一个对象并初始化它:

class Depth {}

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

    如果没有为d指定初始值就尝试使用它,就会出现运行时错误,告诉你产生了一个异常。
    甚至可以通过调用某个方法来提供初值:

public class MethodInit {
  int i = f();
  int f() { return 11; }
} ///:~

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

public class MethodInit2 {
  int i = f();
  int j = g(i);
  int f() { return 11; }
  int g(int n) { return n * 10; }
} ///:~

    但是像下面这样写就不对了:

public class MethodInit3 {
  //! int j = g(i); // Illegal forward reference
  int i = f();
  int f() { return 11; }
  int g(int n) { return n * 10; }
} ///:~

    显然,上述程序的正确性取决于初始化的顺序,而与其编译方式无关。所以,编译器恰当地对“向前引用”发出了警告。
7. 构造器初始化
    可以用构造器来进行初始化。在运行时刻,可以调用方法或执行某些动作来确定初值,这为编程带来了很大的灵活性。但要牢记:无法阻止自动初始化(默认初始化)的进行,它将在构造器调用之前发生。因此,假如使用下述代码:

public class Counter {
  int i;
  Counter() { i = 7; }
  // ...
} ///:~

    那么i首先会被置0,然后变成7。对于所有基本类型和对象引用,包括在定义时已经指定初值的变量,这种情况都是成立的;因此,编译器不会强制你一定要在构造器的某个地方或在使用它们之前对元素进行初始化—因为初始化早已得到了保证。
  7.1 (成员变量)初始化顺序
    在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布于方法定义之间,他们仍旧会在任何方法(包括构造器)被调用之前得到初始化。例如:

class Window {
  Window(int marker) { print("Window(" + marker + ")"); }
}

class House {
  Window w1 = new Window(1); // Before constructor
  House() {
    // Show that we're in the constructor:
    print("House()");
    w3 = new Window(33); // Reinitialize w3
  }
  Window w2 = new Window(2); // After constructor
  void f() { print("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这个引用会被初始化两次:一次再调用构造器前,一次在调用期间(第一次引用的对象将被丢弃,并作为垃圾回收)。
  7.2 静态数据的初始化
    无论创建多少个对象,静态数据都只占用一份存储区域。static关键字不能应用于局部变量,因此它只能作用于域。如果一个域是静态的基本类型域,且也没有对它进行初始化,那么它就会获得基本类型的标准初值;如果它是一个对象引用,那么它的默认初始化值就是null。
    如果想在定义处进行初始化,采取的方法和非静态数据没什么不同。
    要想了解静态存储区域是何时初始化,就请看下面的这个例子:

import static net.mindview.util.Print.*;
class Bowl {
  Bowl(int marker) {
    print("Bowl(" + marker + ")");
  }
  void f1(int marker) {
    print("f1(" + marker + ")");
  }
}
class Table {
  static Bowl bowl1 = new Bowl(1);
  Table() {
    print("Table()");
    bowl2.f1(1);
  }
  void f2(int marker) {
    print("f2(" + marker + ")");
  }
  static Bowl bowl2 = new Bowl(2);
}
class Cupboard {
  Bowl bowl3 = new Bowl(3);
  static Bowl bowl4 = new Bowl(4);
  Cupboard() {
    print("Cupboard()");
    bowl4.f1(2);
  }
  void f3(int marker) {
    print("f3(" + marker + ")");
  }
  static Bowl bowl5 = new Bowl(5);
}
public class StaticInitialization {
  public static void main(String[] args) {
    print("Creating new Cupboard() in main");
    new Cupboard();
    print("Creating new Cupboard() in main");
    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)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
*/

    由输出可见,静态初始化只有在必要时刻才会进行。如果不创建Table对象,也不引用Tabel.b1或Table.b2,那么静态的Bowl b1和b2永远不会被创建,只有第一个table对象被创建(或者第一次访问静态数据)的时候,它们才会被初始化。此后,静态对象不会再次被初始化。
    初始化的顺序是先静态对象(如果它们尚未因前面的对象创建过程而被初始化),而后是“非静态”对象。从输出结果可以观察到这一点,要执行main(静态方法),必须加载StaticInitialization类,然后其静态域table和cupboard被初始化,这将导致它们对应的类也被加载,并且由于它们也都包含静态的Bowl对象,因此Bowl随后也被加载。这样,在这个特殊的程序中的所有类在main()开始之前就都被加载了。总结:成员变量在方法调用前被初始化,将类加载进内存时,成员变量就会进行初始化,如果没有对其显示初始化,则执行默认初始化。
    总结一下对象创建的过程,假设有个名为Dog的类:
    1. 即使没有显示的使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态域首次被访问时,Java解释器必须查找类路径,以定位Dog.class文件。
    2. 然后载入Dog.class文件(后面会学到,这将创建一个Class对象),有关静态初始化的动作都会执行。因此,静态初始化只在Class对象首次加载的时候进行一次。
    3. 当用new Dog()创建对象的时候,首先将在堆上为Dog对象分配足够的存储空间。
    4. 这块存储空间会被清零,这就自动的将Dog对象中的所有基本类型数据都设置成了默认值(对数字来说就是0,对布尔型和字符型也相同),而引用则被设置为null。
    5. 执行所有出现在字段处的初始化动作。
    6. 执行构造器。
  7.3 使用静态代码块显式的静态初始化
    Java允许将多个静态初始化动作组织成一个特殊的“静态子句”(有时也叫“静态块”)。就像下面这样:

public class Spoon {
  static int i;
  static {
    i = 47;
  }
} ///:~

    尽管上面的代码看起来像个方法,但它实际只是一段跟在static关键字后面的代码。与其他静态初始化动作一样,这段代码仅执行一次:当首次生成这个类的一个对象时,或者首次访问属于那个类的静态数据成员时(即便从未生成过那个类对象)。例如:

class Cup {
  Cup(int marker) {
    print("Cup(" + marker + ")");
  }
  void f(int marker) {
    print("f(" + marker + ")");
  }
}

class Cups {
  static Cup cup1;
  static Cup cup2;
  static {
    cup1 = new Cup(1);
    cup2 = new Cup(2);
  }
  Cups() {
    print("Cups()");
  }
}

public class ExplicitStatic {
  public static void main(String[] args) {
    print("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)的那行代码(即解除标为(2)的行的注释),Cups的静态初始化动作都会得到执行。如果把标为(1)和(2)的行同时注释掉,Cups的静态初始化动作就不会进行,就像在输出中看到的那样。此外,激活一行还是两行标为(2)的代码都无关紧要,静态初始化动作只进行一次。
  7.4 非静态实例初始化
    Java中也有被称为实例初始化的类似语法,用来初始化每一个对象的非静态变量。例如:

class Mug {
  Mug(int marker) {
    print("Mug(" + marker + ")");
  }
  void f(int marker) {
    print("f(" + marker + ")");
  }
}

public class Mugs {
  Mug mug1;
  Mug mug2;
  {
    mug1 = new Mug(1);
    mug2 = new Mug(2);
    print("mug1 & mug2 initialized");
  }
  Mugs() {
    print("Mugs()");
  }
  Mugs(int i) {
    print("Mugs(int)");
  }
  public static void main(String[] args) {
    print("Inside main()");
    new Mugs();
    print("new Mugs() completed");
    new Mugs(1);
    print("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
*/

    你可以看到实例初始化子句:

{
    mug1 = new Mug(1);
    mug2 = new Mug(2);
    print("mug1 & mug2 initialized");
}

    看起来它与静态初始化子句一模一样,只不过少了static关键字。这种语法对于支持“匿名内部类”的初始化时必须的,但是它使得你可以保证无论调用了哪个显式构造器,某些操作都会发生。从输出可以看到,实例初始化子句是在两个构造器之前执行的。总结:在方法调用前,进行成员变量的初始化。
8. 数组的初始化
    数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。数组是通过方括号下标操作符[ ]来定义和使用的。要定义一个数组,只需在类型名后加上一对空方括号即可:
int[] al;
方括号也可以置于标识符的后面:
int al[];
    两种格式的含义是一样的,后一种格式符合C和C++程序员的习惯。不过,前一种格式或许更合理,毕竟它表明类型是“一个int型数组”。本书将采用这种格式。
    编译器不允许指定数组的大小。这就又把我们带回到有关“引用”的问题上。现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间。为了给数组创建相应的存储空间,必须写初始化表达式。对于数组,初始化动作可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,它必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的值组成的。在这种情况下,存储空间的分配(等价于使用new)将由编译器负责。
例如:

int[] a1 = {1,2,3,4,5};  

    那么为什么还要在没有数组的时候定义一个数组的引用呢?

int[] a2;

    在Java中可以将一个数组赋值给另一个数组,所以可以这样:a2 = a1;其实真正做的只是复制了一个引用。
    如果在编写程序时,并不能确定在数组里需要多少个元素,那么该怎么办呢?可以直接用new在数组里创建元素。尽管创建的是基本类型数组,new仍然可以工作(不能用new创建单个的基本类型数据)。

public class ArrayNew {
  public static void main(String[] args) {
    int[] a;
    Random rand = new Random(47);
    a = new int[rand.nextInt(20)];
    print("length of a = " + a.length);
    print(Arrays.toString(a));
  }
} /* Output:
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)。当然在本例中,数组也可以在定义的同时进行初始化:int[] a = new int[rand.nextInt()];如果可能的话,应该尽量这么做。如果你创建了一个非基本类型的数组,那么你就创建了一个引用数组。以整型的包装器类Interger为例,它是一个类而不是基本类型:

public class ArrayClassObj {
  public static void main(String[] args) {
    Random rand = new Random(47);
    Integer[] a = new Integer[rand.nextInt(20)];
    print("length of a = " + a.length);
    for(int i = 0; i < a.length; i++)
      a[i] = rand.nextInt(500); // Autoboxing
    print(Arrays.toString(a));
  }
} /* Output: (Sample)
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]; 它还只是一个引用数组,并且直到通过创建新的Interger对象,并把对象赋值给引用,初始化进程才算结束:a[i] = rand.nextInt(500); 如果忘记了创建对象,并且试图使用数组中的空引用,就会在运行时产生异常。
    也可以用花括号括起来的列表来初始化对象数组。有两种形式:

public class ArrayInit {
  public static void main(String[] args) {
    Integer[] a = {
      new Integer(1),
      new Integer(2),
      3, // Autoboxing
    };
    Integer[] b = new Integer[]{
      new Integer(1),
      new Integer(2),
      3, // Autoboxing
    };
    System.out.println(Arrays.toString(a));
    System.out.println(Arrays.toString(b));
  }
} /* Output:
[1, 2, 3]
[1, 2, 3]
*/

    在这两种形式中,初始化列表的最后一个逗号都是可选的。
    尽管第一种形式很有用,但是它更加受限,因为它只能用于数组被定义处。你可以在任何地方使用第二种和第三种形式,甚至是在方法调用的内部。例如,你可以创建一个String对象数组,将其传递给另一个main()方法,以提供参数,用阿里替换传递给该main()方法的命令行参数。

public 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 + " ");
  }
} /* Output:
fiddle de dum
*/

    为Other.main()的参数而创建的数组是在方法调用处创建的,因此你甚至可以在调用时提供可以替换的参数。

数组的定义方式总结:
1.Integer[] arr = {1,2,3};
2.Integer[] arr = new Integer[3];
3.Integer[] arr = new Integer[]{1,2,3};

  8.1 可变参数列表
    由于所有的类都直接间接继承Object类,所以可以创建以Object数组为参数的方法,并像下面这样调用:

class 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[]{
      new Integer(47), new Float(3.14), new Double(11.11)
    });
    printArray(new Object[]{"one", "two", "three" });
    printArray(new Object[]{new A(), new A(), new A()});
  }
} /* Output: (Sample)
47 3.14 11.11
one two three
A@1a46e30 A@3e25a5 A@19821f
*/

    可以看到printArray()方法使用Object数组作为参数,然后使用foreach语法遍历数组,打印每个对象。标准Java库中类能输出有意义的内容,但这里建立的类的对象,打印出的内容只是类的名称以及后面紧跟着的一个@符号及多个十六进制数字。于是,默认行为(如果没有定义toString()方法的话,后面会讲到这个方法)就是打印类的名字和对象的地址。
    你可能看到过像上面这样编写的Java SE5之前的代码,它们可以产生可变的参数列表。然而在JavaSE5中,这种盼望已久的特性终于加了进来,因此你现在可以使用它们来定义可变参数列表了。

public 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(new Integer(47), new Float(3.14),
      new Double(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
  }
} /* Output: (75% match)
47 3.14 11.11
47 3.14 11.11
one two three
A@1bab50a A@c3c749 A@150bd4d
1 2 3 4
*/

    有了可变参数,就再也不用显式的编写数组语法了,当你指定参数时,编译器实际上会为你去填充数组。你获取的仍旧是一个数组,这就是为什么printArray()可以使用foreach来迭代该数组的原因。但是,这不仅仅只是从元素列表到数组的自动转换,请注意程序中倒数第二行,一个Interger数组(通过使用自动包装而创建的)被转型为一个Object数组(以便移除编译器警告信息),并且传递给printArray()。很明显,编译器会发现它已经是一个数组了,所以不会在其上执行任何转换。因此,如果你有一组事物,可以把它们当作列表传递,而如果你已经有了一个数组,该方法可以把它们当作可变参数列表来接受。
    该程序的最后一行表名将0个参数传递给可变参数列表是可行的。

public 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);
  }
} /* Output:
required: 1 one
required: 2 two three
required: 0
*/

    这个程序还展示了你可以如何使用具有Object之外类型的可变参数列表。在可变参数列表中可以使用任何类型的参数,包括基本类型。下面的例子也展示了可变参数列表变为数组的情形,并且如果在该列表中没有任何元素,那么转变成的数据的尺寸为0:

public 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());
  }
} /* Output:
class [Ljava.lang.Character; length 1
class [Ljava.lang.Character; length 0
class [I length 1
class [I length 0
int[]: class [I
*/

    然而,可变参数列表与自动包装机制可以和谐共处,例如:

public 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(new Integer(1), new Integer(2));
    f(4, 5, 6, 7, 8, 9);
    f(10, new Integer(11), 12);
  }
} /* Output:
1 2
4 5 6 7 8 9
10 11 12
*/

    请注意,你可以在单一的参数列表中将类型混合在一起,而自动包装机制将由选择地将int参数提升为Interger。
    可变参数列表使得重载过程变得复杂了:

public 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't compile -- ambiguous
  }
} /* Output:
first a b c
second 1
second 2 1
second 0
third
*/

    在每一个中情况中,编译器都会使用自动包装机制来匹配重载的方法,然后调用最明确匹配的方法。
    但是在不使用参数调用f()时,编译器就无法知道应该调用哪一个方法了。如果你给每个方法中增加一个非可变参数,就可以解决问题了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值