第五章:初始化与清理

初始化与清理

  • C++引入了构造器(constructor)的概念,这是一个在创建对象时被自动调用的特殊方法。Java也采用了构造器,并额外提供了垃圾回收器

用构造器确保初始化

  • 可以假想为编写的每个类都定义一个initialize()方法。在我们使用对象之前,都应该先调用该方法。创建对象时,如果其类具有构造器,Java就会在用户有能力操作对象之前自动调用相应的构造器,从而保证了初始化的进行。
  • 因为调用构造器是编译器的责任,所以必须要让编译器知道应该调用哪个方法。Java采用了C++的解决方法:构造器采用与类相同的名称。不接收任何参数的构造器被称为默认构造器,如果一个类只有一个默认构造器,那么就可以省略这个构造方法。如果定义了一个带有形参的构造器,并且还需要默认构造器,则默认构造器还需要我们显式的编写出来。
  • 构造器是一种特殊方法,它没有返回值(错加返回类型会被当成成员方法)。

设计基本类型的重载

  • 基本类型能从一个较小的类型自动提升为较大的类型。这个性质会对调用重载方法时造成混淆。
public static void main(String[] args) {
    add2('a');//char 提升到int
    add2(1L);//long 提升到float
}
public static void add2(int a) {
    System.out.println("int");
}
public static void add2(float b) {
    System.out.println("float");
}

this关键字

  • 这个知识点很容易被我们忽略。其实在一个对象调用某个方法时,会将自己的引用也一起传进去。然后我们在方法内部就可以通过this关键字获得当前对象的引用。当我们在方法内部省略this关键字,直接调用一个方法或者获取一个成员值时,方法会悄悄为我们带上this。
public class Test {
    private String name = "miaoch";
    public static void main(String[] args) {
        Test t = new Test();
        t.t1();
        t.t2("hello");
        t.t3("hello");
    }
    private void t3(String str) {
        System.out.println(name);//"miaoch"
    }
    private void t2(String name) {
        //这里请求name会先查函数作用域中的name,如果没有才会去查this.name,如果还没有编译不通过
        System.out.println(name);//"hello"
    }
    private void t1() {
        System.out.println(name);//其实是this.name "miaoch"
    }
}
  • 所以如果定义的形参和成员变量名一致的话,我们就不得不用this了。但是this还有在构造器中调用别的构造器的功能。当然还有super,不过要注意必须写在第一行,不然它会告诉你Constructor call must be the first statement in a constructor
  • 关于垃圾回收,我打算等重新看一遍《深入理解Java虚拟机》再来讨论。

构造器初始化

  • 可以用构造器来进行初始化。但要牢记:无法阻止自动初始化的进行,它将在构造器被调用之前发生。比如:
public class Test {
    public int i;
    public Test() {
        i = 7;
    }
}
  • 即使我们在构造器中给i进行了初始化,但i其实会先被置0再被置7。置0的这个初始化是无法被阻止的。这个行为能保证即使你忘记在构造器中对成员进行初始化也不会造成错误。

初始化顺序

  • 在类的内部,变量定义的先后顺序决定了初始化的顺序(即使中间穿插着方法也无所谓)。
public class Test {
    private Test t1 = new Test();

    public Test() {
        t2 = new Test();
    } 
    private Test t2 = new Test();
}
  • 上面这个例子,t1会先被初始化为一个新的Test对象。然后t2也会被初始化为新的对象。接着构造器会重新为t2初始化一个新的对象。之前的对象由于没有引用则会被垃圾回收。虽然这种行为效率不高,但它保证了成员能得到初始化。
  • 静态变量只有在该类的第一个对象创建时或者第一次访问静态数据)才会被初始化,接着就不会再初始化了。且在第一个对象创建时,静态变量的初始化优先于成员变量。静态变量之间的顺序则是由代码顺序决定。

静态块和非静态块

  • 其实这两个东西和直接在字段定义处进行初始化没有很大区别,看例子:
public class Test {
    private Test t1 = new Test();

    //非静态块 可以操作静态变量和非静态变量
    {
        t1 = new Test();
    }

    private static Test t2 = new Test();

    //静态块 只能操作静态变量
    static {
        t2 = new Test();
    }
}
  • 也没有什么过多解释的,他们的性质和静态变量成员变量基本一致。唯一的好处就是可以执行一些操作,且这个操作是在执行构造器之前完成的(见下面的小结)。

小结

  • 对象创建过程,假设有个名为Dog的类:
    1. 即使没有显式地使用static关键字,构造器实际上也是静态方法。因此,当首次创建类型为Dog的对象时(构造器可以看成静态方法),或者Dog类的静态方法/静态变量首次被访问,Java解释器必须查找类路径,定位Dog.class文件。
    2. 然后载入Dog.class(这将会创建一个Class对象),有关静态初始化的所有动作都会执行(包括静态块)。因此,静态初始化只有在Class对象首次加载的时候进行一次。
    3. 当使用 new Dog()创建对象时,首先在上为Dog分配足够的存储空间(如果成员不需要初始化,也就是不需要分配空间,而后期又为其赋值,又得为其分配空间,那这个对象所占存储空间就会不规则,这会造成内存分配和清理很难维护。所以还不如一开始就分配其足够的空间)(这里还要说明一下,引用类型的成员只是分配了引用地址的空间,而不是引用类型的对象空间)。
    4. 这块存储空间会被清零,这也是为什么基本类型成员的初始值一般为0,而引用类型为null的原因。
    5. 执行所有出现于字段定义处的初始化动作(包括非静态块)。
    6. 执行构造器(里面也可能有初始化动作)。这个操作会牵涉到很多动作,尤其是涉及到继承的时候。

数组初始化

  • 首先,数组只是相同类型的一些对象或基本类型数据的序列,它用一个标识符表示该序列,且通过方括号来定义和使用。
  • 编译器不允许指定数组的大小,例如你不能这么写:int a[6];,只能这么写int a[];(或int[] a;)要知道这么做其实是在声明一个数组引用a。而a指向的对象序列大小与a是无关的。a只是一个引用而已。如果要为对象序列创建相应的存储空间,必须写初始化表达式。
Integer[] a = {1,2,3};//本来里面应该写new Integer(1);的,这里有自动装箱操作。
// 备注,花括号中可以填写Integer的子类,所以可以有以下操作:
//Object[] a ={new Integer(1), new String("2")};
Integer[] b = new Integer[]{1,2,3};
  • 也许你不知道这两写法的区别。来看下面的代码
Integer[] a;
a = {1,2,3};//error
Integer[] b;
b = new Integer[]{1,2,3};
  • 现在知道了吧,后一种随时都可以为一个引用指向其相应的对象序列。而第一种只能在创建的时候指定,存在局限性。
  • 我们都有一个常识,当创建了一个数组以后,除非重新创建对象,否则不能再修改数组大小。这个原因我想是这样的:数组其实也是一个大的对象,就跟我们平常创建一个对象类似,你总不能再对这个对象进行扩容了吧?(还记得分配固定内存吗?)。另外数组创建完毕后,都有一个固有成员length,这个大家都知道。因为数组大小固定,所以length在为数组分配空间时已经被固定了,不能再去修改它。而我们每次访问数组的时候,都会自动检测是否越界,这种做法是有开销的,但是没有办法被禁用,好处就是比较安全,不会像C++中那样返回一个未知数据。
  • 也许你会说,除此之外,还有一种创建数组对象的方法:
int[] a = new int[10];
Integer[] b = new Integer[10];
  • 是的,这的确也是一个方法。不过要注意的是。对于基本类型。由于基本类型是存值,所以int[] a = new int[10];能够创建10个int类型的数据,并且初始化为0;但Integer[] b = new Integer[10];则不同了,它只是创建了10个Integer引用。这10个引用指向的对象依旧是null或者说没有指向对象,就跟类中的初始化一样)。我曾经就误以为第二种方法能直接对b[0]进行对象拥有的操作(我去问了老师,他也没解释清楚,现在感叹一下时光飞逝),然而事实告诉我并不行,这也是新手容易犯的错误。

可变的参数列表

  • 我们定义方法,可以定义数组类型的参数来实现参数个数可变的功能。在Java SE5之后,我们可以这样书写代码。貌似高大上许多,其实也没差多少:
public static void main(String[] args) {
    add("没用", 1,2,3,4);
    add("没用", 1,2,3);
    add("没用");//也可以不传,方便了许多,如果用数组,则需要传入new Integer[]{}
}
//Integer... integers 必须是最后一个参数,即不能写成Integer... integers,String x。也不能再用Integer[] integers重载
public static void add(String x, Integer... integers) {
    //在方法内部,integers的类型其实是Integer[]
    int sum = 0;
    for (int i=0; i<integers.length; i++) {
        sum += integers[i];
    }
    System.out.println("sum = " + sum);
}
  • 还要注意下面一种情况:
public static void main(String[] args) {
    add("没用");//error
}
public static void add(String x, Integer... integers) {
    System.out.println("int");
}
public static void add(String x, String... integers) {
    System.out.println("str");
}

枚举类型

  • Java SE5中添加了一个看似很小的特性,即enum关键字。
public class Test {
    public static void main(String[] args) {
        TestEnum miaoch = TestEnum.MIAOCH;
        System.out.println(miaoch);//自动调用toString()
        System.out.println(miaoch.name());
        System.out.println(miaoch.ordinal());//2 
        System.out.println(Arrays.toString(TestEnum.values()));
        switch (miaoch) {
            case HELLO://如果switch的是一个枚举类型,下面的case不加类名
            case WORLD:
                System.out.println("hello world!");
                break;
            case MIAOCH:
            case XIXI:
                System.out.println("miaoch xixi");
                break;
        }
    }
}
enum TestEnum {
    HELLO, WORLD, MIAOCH, XIXI//通常全大写
}
  • 当创建enum时,编译器会帮你添加一些有用的特性。例如toString()可以返回enum实例的名字,还有ordinal()可以返回下标。静态方法values可以返回一个数组。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值