Thinking In Java笔记(第五章 初始化与清理(三))

第五章 初始化与清理

5.6 成员初始化

    Java尽力保证:所有变量在使用前都能得到恰当的初始化。对于方法的局部变量,Java以编译错误的形式来保证。如下:

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

    会得到一条错误的消息,提示i可能没有初始化。编译器可以给i赋初值,但是并没有这么做,因为没有初始化是程序员的疏忽,为了保证程序的稳定性,能帮助程序员找出程序的缺陷所在。

    但如果是类的数据成员是基本类型,类的每个基本类型成员变量都会保证有一个初值。如下:

public class Test {
    boolean t;
    char c;
    byte b;
    short s;
    int i;
    long l;
    float f;
    double d;
    InitialValues reference;
}

    上面的一系列数值分别为:false, (空白), 0, 0, 0, 0, 0.0, 0.0, null。char值为0,所以显示为空白。

5.6.1 指定初始化

    有一种很直接的方法给某个变量赋初值,就是在定义类成员变量的地方直接为其赋值,C++中不允许。对初始化非基本类型的对象也可以,例如下面的A类的对象,这样类Test的每个对象都会具有相同的初始值。如下:

class A{
}
public class Test {
    boolean bool = true;
    char ch = 'x';
    int i = 99;
    A a = new A();
}

5.7 构造器初始化

    还可以用构造器来进行初始化。在运行时刻,可以调用方法或执行某些动作来确定初值,这给编程带来了更大的灵活性。但是要记住,无法组织自动初始化的进行,也就是前面提到的编译器自动赋值,这个工作在构造器被调用之前就发生。例如:

public class Test { 
    int i;
    Test() {i = 7;} 
}

    i的值首先被置为0,再被赋值为7。

5.7.1 初始化顺序

    在类的内部,变量定义的先后顺序决定了初始化的顺序。即使变量定义散布与方法定义之间,它们人就会在任何方法(包括构造器)被调用之前得到初始化。例如:

class Window {
    Window(int maker) {
        System.out.println("Window(" + maker + ")");
    }
}

class House {
    Window w1 = new Window(1);
    House() {
        System.out.println("House");
        w3 = new Window(33);
    }
    Window w2 = new WIndow(2);
    void f() {
        System.out.println("f()");
    Window w3 = new Window(3);
    }
}

public class Test {
    public static void main(String[] args) {
        House h = new House();
        h.f();
    }   
}

    结果为Window(1) Window(2) Window(3) House() Window(33) f()

5.7.2 静态数据的初始化

    无论创建多少个对象,静态数据都至占用一份存储区域。static关键字不能应用于局部变量。看下面的例子:

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 Test {
    public static void main(String[] args){
        System.out.println("Creating new Cupboard() in main");
        new Cupboard();
        Syste.out.println("Creating new Cupboard() in main");
        new Cupboard();
        table.f2(1);
        cupboard.f3(1);
    }
    static Table table = new Table();
    static Cupboard cupboard = new Cupbpard();
}

    输出的结果依次为:

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)

    静态初始化只有在必要的时候才会进行,只有在第一个类对象被创建(或第一次访问静态数据)的时候,静态数据才会被初始化。之后,无论怎么创建对象,都不会再次被初始化。

    初始化的顺序是先静态对象(如果它们尚未被初始化),而后是”非静态对象”。

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

  1. 即使没有显式的使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog对象时(构造器可以看成静态方法),或者Dog类的静态方法、静态域首次被访问的时候,Java解释器必须查找类的路径,以定位Dog.class。
  2. 然后载入Dog.class,有关静态初始化的所有动作都会被执行,因此,静态初始化只在Class对象首次加载的时候进行一次。
  3. 当用new创建Dog的对象时候,首先在堆上给Dog对象分配足够的存储空间。
  4. 清零所分配的存储空间,这就自动的将Dog对象中所有的基本数据类型设置成了默认值(对数字来说就是0,对boolean和char类型的来说也类似),而引用则都被设置成了null。
  5. 执行所有出现于字段定时的初始化动作。
  6. 执行构造器方法。
5.7.3 显示的静态初始化

    Java允许多个静态初始化动作组织成一个特殊的”静态子句”(有时也叫做”静态块”)。就像下面这样:

public class Spoon {
    static int i;
    static {
        i = 47;
    }
}

    和其他静态初始化动作一样,这段代码只执行一次:当首次生成这个类的对象时,或首次访问属于这个类的静态成员数据的时候执行。例如:

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 Test {
    public static void main(String[] args) {
        System.out.println("Inside main()");
        Cups.cup1.f(99);  //  (1)
    }

    //static Cups cups1 = new Cups(); //  (2)
    //static Cups cup2 = new Cups();  //  (2)
}

    上面的结果为:Inside main() Cup(1) Cup(2) f(99)

5.7.4 非静态实例初始化

    Java中也有被成为实例初始化的类似语法,用来初始化每一个对象的非静态变量。和静态语句块一样的,只不过少了static。如果不创建类的实例,非静态语句块是不会被执行,只会触碰static变量和语句块。

下面用一个例子来总结下上述的顺序:

class Cup {
{
    System.out.println("Block - Cup");
}

static int c;
static {
    c = 1;
    System.out.println("Static Bolck - Cup");
}


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

class Cups {

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

    Cups() {
        System.out.println("Construct - Cups()");
    }
}
public class JavaTest {
    public static void main(String[] args) {
        System.out.println("Inside main()");
        Cups.cup1.f(99);  //  (1)
    }
}

    输出的结果为:

Inside main()
Static Bolck - Cup
Block - Cup
Construct - Cup(1)
Block - Cup
Construct - Cup(2)
Function - f(99)

    从上面的结果可以看出,没有新建Cups类的对象时,不会执行非静态语句块,也就是被{}包括起来的语句块。在第一次创建类对象或者使用到类的静态变量的时候,就会将.class文件加载进来,初始化static变量,执行static{}语句块。

5.8 数组初始化

    数组只是相同类型的、用一个标识符名称封装到一起的一个对象序列或基本类型数据序列。素组是通过方括号下标操作符【】来定义和使用的。定义数组只需要在类型名后面加上一对方括号:int[] a1;。方括号也可以放在后面:int a1[];

    两种格式的含义是一样的,后面一种格式符合C和C++程序员的习惯。前面一种格式能更直观的看出,其表明的类型是”一个int型数组”。

    编译器不允许指定数组的大小,现在拥有的只是对数组的一个引用(你已经为该引用分配了足够的存储空间),而且也没给数组对象本身分配任何空间。为了给数组创建相应的存储空间,需要对数组进行初始化。数组的初始化代码可以出现在代码的任何地方,但也可以使用一种特殊的初始化表达式,必须在创建数组的地方出现。这种特殊的初始化是由一对花括号括起来的,存储空间的分配(等价于使用new)将由编译器负责。例如:

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

    那么为什么还要在没有数组的时候定义一个数组的引用呢?Java中可以将数组赋值给另一个数组,int[] a2;,在Java中可以将一个数组赋值给另一个数组,所以可以这样:a2 = a1;直接复制一个引用。下面的例子:

public class ArrayTest {
    public static void main(String[] args) {
        int[] a1 = {1,2,3,4,5};
        int[] a2;
        a2 = a1;
        for(int i = 0;i < a2.length; i++)
            a2[i] = a2[i] + 1;
        for(int i = 0;i < a1.length; i++)
            System.out.println("a1[" + i + "]" + a[i]);
    }
}

    输出为a1[0]=1 a1[1]=2 a1[2]=3 a1[3]=4 a1[4]=5

    所有数组(无论元素始对象还是基本类型)都有一个固有成员length,可以通过它获得数组长度,但其不能直接被更改。和C与C++类似,Java数组计数从0开始,数组越界,C和C++默默接受,但Java直接出现运行时错误。

    可以在编程时,通过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)];
    }
}

    如果创建了一个非基本类型的数组,那么就是一个引用数组。以整型的包装器类Integer为例:

public class Test {
    public static void main(String[] args) {
        Random rand = new Random(47);
        Integer[] a = new Integer[rand.nextInt(20)];
    }
}

    这里即便用new创建了数组之后,也只是一个引用数组,并且直到通过创建新的Integer对象,并把对象和引用连接起来,初始化才算结束。如果忘记了创建对象,并链接对象和引用,那数组中就全是空引用,运行时会产生异常。

5.8.1 可变参数列表

    可变参数列表可用于参数个数或者类型未知的场合。例如void f(Object[] args)函数里的参数。这种在Java SE5之前出现,然而再Java SE5中,添加入了新特性,可以使用新特性来定义可变参数列表了,下面的例子:

public class NewVarArgs {
    static void printArray(Object... args) {
        for(Object obj : args) {
            System.out.println(obj + " ");
        }
    }
    public static void main(String[] args) {
        printArray(new Integer(47), new Float(3.14), new Double(11.11));
        printArray(47, 3.14F, 11.11);
        printArray("one", "two", "three");
    }
}

    有了可变参数,就不用显示的编写数组语法了,当指定参数的时候,编译器实际上会去填充数组,最后得到的仍然是一个数组。

5.9 枚举类型

    Java SE5中添加了一个看似很小的特性,即enum关键字,它使得我们在需要群组并使用枚举集时可以很方便的处理。C和C++以及以其他很多语言已经拥有枚举类型了,Java中枚举类型功能比C/C++的功能更加完备。下面是简单的示例:

public enum A {
    NOT, MILD, MEDIUM, HOT, FLAMING
}

    这里创建了一个名为A的枚举类型,它具有5种值,由于枚举类型的实例是常量,因此按照命名习惯通常用大写字母表示(有多个字母用下划线隔开)。

    为了使用enum,需要创建一个该类型的引用,并和某个实例连接起来。

public class Test {
    public static void main(String[] args) {
        A a = A.MEDUIM;
        System.out.println(a);
    }
}

    在创建枚举类型的时候,编译器会自动添加一些特性。例如:

  • 会创建toString()方法,一边可以很方便的显示某个enum实例的名字。
  • 创建ordinal()方法,用来表示某个特定enum常量的声明顺序。
  • static values()方法,用来按照enum常量的声明顺序,产生由这些常量值构成的数组。

    例子如下:

public class EnumOrder {
    public static void main(String[] args){
        for(A a : A.values) {
            System.out.println(s + ", oridinal " + a.ordinal());
        }
    }
}

输出结果为:
NOT, oridinal 0
MILD, oridinal 1
MEDIUM, oridinal 2
HOT, oridinal 3
FLAMING, oridinal 4

    enum的另一个特别实用的特性是能和switch语句一起使用。看下面的例子:

    enum Pet {
    Cat,
    Dog,
    Bird
}
public class JavaTest {
    Pet pet;
    public JavaTest(Pet p) {
        pet = p;
    }
    public void describe() {
        switch(pet) {
        case Cat :
            System.out.println("The pet is Cat");
            break;
        case Dog :
            System.out.println("The pet is Dog");
            break;
        case Bird :
            System.out.println("The pet is Bird");
            break;

        }
    }
    public static void main(String[] args) {
        Pet p1 = Pet.Bird;
        JavaTest test = new JavaTest(p1);
        test.describe();
    }
}

结果为:The pet is Bird
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值