Java中的初始化


一、介绍

  初始化是一个语言十分重要的部分,许多C程序的错误就来自于编写者没有认真将每一个所定义的变量初始化,随着代码量的增加,某个变量的没有初始化往往会带来十分严重的后果,C++中引入的是构造器的概念,并提供了构造函数。Java也采用了构造器,并额外提供了垃圾回收器,对不再使用的内存进行自动回收。

二、 用构造器保证初始化

  Java中的构造器有两个特点:1、构造器的名字与类的名字相同,防止了与类方法名的冲突,构造器是没有返回值的。2、调用构造器是编译器的任务,编写者只需要告诉编译器调用哪个构造函数,那构造器之间如何区别呢?

  这就设计到方法的重载,构造器的名字是相同的,没有返回值,唯一的区别就是函数的参数不同,每一个构造函数都有一个独一无二的参数列表,而这个独一无二有两个含义:一方面是参数的类型是独一无二的,另一方面是参数的顺序同样也是独一无二的,如:

 1 class Gou{
 2     String name;
 3     Gou(int x,int y){
 4         System.out.println(x+y);
 5     }
 6 
 7     Gou(int y ,int x,char t){
 8         System.out.println(x+y+t);
 9     }
10 
11     Gou(char t ,int x, int y){
12         System.out.println(x+y+t+2);
13     }
14 }
15 class x{
16     public static void main(String[] args) {
17         Gou dog = new Gou(1,2);
18         Gou dog1 = new Gou(1,2,'x');
19         Gou dog2 = new Gou('x',1,2);
20     }
21 
22 }

  在17、18、19行三种初始化的方式调用了三种不同的构造函数,说明顺序也可以影响到构造函数的类型,如果你在类对象中定义了一个变量类型相同和顺序也相同的构造函数,编译器就会报错,当然这种换顺序改变构造器的方法并不是特别好,它会使程序难以去维护。

  这种构造器下如何设计到基本数据类型,如int、short等,如果构造器定义的是一个精度较大的数据类型,传入一个精度较小的数据会发生什么?或者反过来的情况。我们来尝试一下:

1、当构造器中定义的类型精度大于传入的数据类型,如:

 1 class Gou{
 2     Gou(int x ){
 3         System.out.println("int");
 4     }
 5     Gou(Float x){
 6         System.out.println("Float");
 7     }
 8     Gou(Double x){
 9         System.out.println("Double");
10     }
11     Gou(long x){
12         System.out.println("Long");
13     }
14 }
15 class x{
16     public static void main(String[] args) {
17         float x =1;
18         Gou c = new Gou(x);
19     }
20 }

  当有合适的数据类型进行匹配时,会优先匹配同样的,当我们尝试将此时的float类型的构造函数删除时,此时程序就会报错,需要进行强制转换将它的精度扩大成double,而对于char、shot等低于int型的变量而言,情况似乎又变化了,如果传入一个char类型的变量,而次数构造器最小的精度为int,此时编译器会自动将char扩展成int,使用int的构造器进行对象的初始化,这一点有一点特殊。

2、当传入的参数大于构造器定义参数的精度

 1 class Gou{
 2     Gou(int x ){
 3         System.out.println("int");
 4     }
 5 
 6     Gou(long x){
 7         System.out.println("Long");
 8     }
 9 }
10 class x{
11     public static void main(String[] args) {
12         float x =1;
13         double xt = 1
14         Gou c = new Gou((int)x);
15         Gou d = new Gou((double)x);//错误
16         Gou e = new Gou((int)xt);
17     }
18 }

  当我们传入一个float类型的值时,我们可以利用强制类型转换去使用低精度的构造器,如果你介意数据可能会出现错误,尽量不要用这种方法。而对于float和double的转换编译器依旧会报错。

  那如果我们没有写构造器呢?编译器会自动帮你创建一个默认的构造器,这个构造器没有参数,同时函数体也没有任何东西,但如果你已经创建了自己的构造器,编译器就不会创建这样一个无参构造器,如果你创建了有参构造器,仍然想无参数的去创建一个对象,就需要自己手动添加一个无参数的构造器。

三、this关键字

你是否思考过这样一个情况:

 1 class Gou{
 2     public  void eat(int x){
 3         
 4     }
 5 }
 6 class x{
 7     public static void main(String[] args) {
 8         Gou dog = new Gou();
 9         Gou dog1 = new Gou();
10         dog.eat(1);
11         dog1.eat(1);
12     }
13 }  

  当我们用不同对象调用同一个方法的时候,我们传的都是同一个参数,编译器是如何知道我们用的是不同的对象,其实这里编译器暗自把不同对象的一个引用当作参数传入了类对象:

1 dog.eat(dog,1);
2 dog1.eat(dog1,1);

真实发生的故事应该是这样,但这是内部发生的事情,我们不能这样书写,如果你想在类的内部截获这样一个偷偷传入进来的引用变量,就需要我将要介绍的this关键字,this引用变量并没有和其他引用变量有什么区别,如:

 1 class Gou{
 2     int x;
 3     public  Gou eat(int x){
 4         this.x = x;
 5         return this;//返回这个引用变量
 6     }
 7 }
 8 class x{
 9     public static void main(String[] args) {
10         Gou dog = new Gou().eat(10);
11     }
12 }

  这段代码里发生了一个很有意思的事情,我们用默认构造器创建一个对象后,我们直接调用了这个创建对象的eat方法,这个eat方法返回的是一个Gou类型的对象,所以我们直接返回this,注意这个返回的dog接受的对象是我们一开始所new的那个对象嘛?如果你看了上文,现在肯定有了答案,是的,我们在调用eat方法时会传入一个此时对象的引用变量,而这个对象就是我们才用new创建,所以就出现我们将对象无意识的传入再返回出来用我们的dog接受的情况。当然我们传入的引用变量肯定是没有了,我们重新用dog做了这个对象的引用变量。

  当我们需要在构造器里面调用其他构造器的时候,this也可以起作用,如:

1 class Gou{
2     int x;
3     Gou(int x){
4         this(x,x);
5     }
6     Gou(int p,int x){
7     }
8 }

在这段代码中我们在一个构造器中调用了另一个构造器,因为是在类内部,所以不需要new一个对象,直接this(参数)即可调用合适的构造器,而这样调用也不是随意的,如果你需要在构造器中调用另一个构造器,就需要把这句话写在第一行,并且你只能调用一次其他的构造器。this其实就是一个对象本身的属性,它存在于对象当中,等着合适的时候给对象去使用。

四、成员的初始化

  Java创建构造器的目的就是来初始化对象,而调用了构造器,成员内部变量又是如何初始化的呢?

  在Java中编译器尽量保证所有成员的初始化,对于类变量(与C语言里面的全局变量类似)来说,编译器会自动给它赋一个初值。对于局部变量,也就是类方法里定义的变量,就需要编写者给出一个初值,不然编译会报错。

  那对于类变量,我们去赋初值的方法有很多,最直接的就是直接在定义的时候去赋予它初值:

1 class Gou{
2     int x = 0;
3     int j = x;
4     String t = "";
5     float q = 0;
6 }
7 class s{
8     Gou x = new Gou();
9 }

可以看到,无论是基本数据类型,还是自定义的类都可以在定义的时候直接给一个初值,当然也可以另用方法来对类变量进行初始化,如:

 1 class Gou{
 2     int x = f();
 3     int t = g(x);
 4     int p = h(n);//错误
 5     int f(){
 6         return 1;
 7     }
 8     int g(int x){
 9         return  x;
10     }
11 
12     int h(int x ) {
13         return x;
14     }
15 
16 }

  可以看到可以用无参方法去初始化,也可以用有参数的,而这个参数必须已经被初始化,不然编译器会报错。那如果你执行了你的初始化,编译器的初始化会不执行吗?答案是不会,你无法阻止自动初始化的进行。在上述代码中对于x的初始化,首先定义x的时候编译器会自动给一个初值,这里是int给的是0(数据类似默认的初值),给了这个初值后,才会执行你定义的初始化动作,将x赋值为0。所以对于类成员而言,编译器似乎不在意你是否给出了初始化的动作,因为初始化早已得到保证。

  那初始化的顺序是怎么样的呢?在类的内部,变量定义的先后顺序决定了初始化的顺序,即使变量的定义散落在各个方法定义之间,它们仍然会在各个类方法被调用之前被初始化。这里也有一个特殊的情况,万一成员变量是Static类型的呢?Static型的变量其实在类加载的时候就已经初始化完成,而这种初始化是编译器默认的,当然如果你想自己初始化,基本方法与普通变量没有什么区别。说了比较多的情况我们总结一下一个类是如何初始化的,假如有一个叫Dog的类:

  • 如果没有显式的使用Static关键字,构造器实际上就是一个Static类型的方法,因此,当首次创建类型为Dog的对象的时候,或者调用Dog里的Static方法的时候,此时java的解释器必须查找这个类的位置,也就是在内存里的路径,以定位Dog.class文件。
  • 然后载入Dog.class(其实创建了一个Class对象),有关静态初始化的动作都会执行,因此,静态初始化只会出现在Class对象首先加载的时候(也就是这个类加载的时候),并且只会进行这一次。
  • 当用new Dog()时,也就是真正创建一个类的对象的时候,首先会在堆上为Dog的对象分配足够的空间。
  • 然后这块存储空间会被清0,清0的意思就是自动将Dog对象里的所有基本数据类型都设置成默认的初值(如int型为0 等等)。
  • 然后再会执行字段定义处所有初始化的动作(也就是编写者希望得到的初值)。
  • 执行类的构造器,至此一个类对象所有的初始化动作全部完成。

五、finalize方法

  谈完了对象创建和初始化的过程,我想简单谈一下一个对象的销毁,Java性能强大的原因与其垃圾回收机制密不可分,编写者用完一个对象后,可以丢给回收机制去处理,并不会发生忘记delete一个变量或者对象而产生内存泄漏的情况,当然这也不是一直有效,当你的对象获得一块特殊的内存地址,这个地址不是由new过来的,在这种情况垃圾回收就失效了,所以java提供一个finalize方法,一旦回收器准备来回收对象了,就先调用这个finalize方法,让编写者先把回收器处理不了的垃圾处理掉,再进行正常的回收。

  Java垃圾回收器有自己的机制,这里介绍三点:

  1. 对象可能不被垃圾回收
  2. 垃圾回收不等于C++里面的析构函数
  3. 垃圾回收只与内存相关

  对于第一点对象可能不被垃圾回收的意思就是如果程序员将对象丢弃后,垃圾回收器不是一定会将其立马回收,它有自己的回收机制,当内存足够的时候,它认为不用浪费时间去回收你丢弃的这个对象,所以这就导致你的finalize方法的调用也不是一定发生的,如果垃圾回收器一直没有回收你这个对象,对象里的finalize方法就永远不会被调用。这就解释了与析构函数的区别,你写了释放特定区域的方法,但不一定会调用。第三点的意思就是使用垃圾回收的唯一原因就是回收程序不再使用的内存,只要是发生了类似new的情况,垃圾回收都会去释放它们,而对于使用finalize方法来释放内存这个主要发生在“本地方法”这个情况当中。

  那既然垃圾回收不保证发生,那finalize有其他用途嘛?有!

  当一个对象成员不被需要后,需要被清理时,而这个对象可能打开了某个文件,我们要再这个程序消失前去关闭这个文件,而如果没有执行这个动作造成的错误又很难去发现,所以这里就可以用finalize来去保证这个动作一定进行,也就是说在对象被垃圾回收前,一定会执行finalize方法,就很容易发现这个对象所打开的文件是否关闭。