浅析Java子类和父类的实例化顺序 及 陷阱

来源:互联网 发布:淘宝店页头尺寸 编辑:程序博客网 时间:2024/04/28 14:39

本文主要介绍Java里常用的子类和父类的变量实例化顺序及陷阱,并结合一个实例来探讨此问题。日后编程中应尽量避免此陷阱。

首先看下面一段代码:

定义一个虚类Server.java

[java] view plain copy
print?
  1. package org.yanzi.test1;  
  2.   
  3. public abstract class Server {  
  4.     private static final int DEFAULT_PORT = 900;  
  5.     public Server() {  
  6.         // TODO Auto-generated constructor stub  
  7.         int port = getPort();  
  8.         System.out.println("port = " + port + " DEFAULT_PORT = " + DEFAULT_PORT);  
  9.     }  
  10.     protected abstract int getPort();  
  11.   
  12. }  

然后定义一个子类SimpleServer.java

[java] view plain copy
print?
  1. package org.yanzi.test1;  
  2.   
  3. public class SimpleServer extends Server {  
  4. private int mPort = 100;  
  5.     public SimpleServer(int port) {  
  6.         // TODO Auto-generated constructor stub  
  7.         this.mPort = port;  
  8.     }  
  9.     @Override  
  10.     protected int getPort() {  
  11.         // TODO Auto-generated method stub  
  12.         return mPort;  
  13.     }  
  14.       
  15.   
  16. }  

测试代码:

[java] view plain copy
print?
  1. package org.yanzi.test1;  
  2.   
  3. public class Test1 {  
  4.   
  5.     /** 
  6.      * @param args 
  7.      */  
  8.     public static void main(String[] args) {  
  9.         // TODO Auto-generated method stub  
  10.         Server s = new SimpleServer(600);  
  11.     }  
  12.   
  13. }  

测试结果:

port = 0 DEFAULT_PORT = 900

在测试代码里,传了一个参数600,我们希望getPort得到也是600,但遗憾的是得到的是一个大大的0!!!出现这个问题是因对Java子类和父类实例化顺序存在模糊,下面来看下其正确顺序:

1、new一个SimpleServer,SimpleServer的构造函数接收参数600;

2、初始化父类Server的静态变量,DEFAULT_PORT赋值为900;

3、为了实例化子类,首先实例化其父类Server。子类有参数的构造中默认包含了super方法,即调用父类的无参构造函数。因此就到了int port = getPort();这一句,调用子类的getPort方法。子类的getPort方法返回mPort,此时mPort还没有赋值,因此还是0.这就是得到大大的0的原因!!!

4、父类初始化完毕后,开始初始化子类的实例变量,mPort赋值100;

5、执行子类的构造函数,mPort赋值600;

6、子类实例化完毕,对象创建完了!

真相大白了,再做一个测试。将SimpleServer里的实例变量mPort搞成静态变量如下:

[java] view plain copy
print?
  1. package org.yanzi.test1;  
  2.   
  3. public class SimpleServer extends Server {  
  4. private static int mPort = 100;  
  5.     public SimpleServer(int port) {  
  6.         // TODO Auto-generated constructor stub  
  7.         this.mPort = port;  
  8.     }  
  9.     @Override  
  10.     protected int getPort() {  
  11.         // TODO Auto-generated method stub  
  12.         return mPort;  
  13.     }  
  14.       
  15.   
  16. }  
测试结果:

port = 100 DEFAULT_PORT = 900

其执行顺序是:

1.第一个步骤同上,SimpleServer接收构造参数600

2.初始化父类的静态代码块,当然包括静态变量,然后初始化子类的静态变量

3.初始化父类的非静态代码,包括非静态的变量等;

4.执行父类的构造函数;

5.初始化子类的非静态代码

6.执行子类的构造函数。

    至此完毕,最终的结论就是构造函数越简单越好,不要在构造函数里做太多操作。父类构造函数中不要调用非final的方法即可,即不要在父类的构造函数中调用会被子类重写的方法,否则运行时会遇到意想不到的错误。看一个例子就会明白:

[java] view plain copy
print?
  1. import java.util.*;  
  2.   
  3. class Animal  
  4. {  
  5.    public Animal()  
  6.    {  
  7.      eat();  
  8.    }  
  9.      
  10.    protected void eat()  
  11.    {  
  12.      System.out.println("Eat something");  
  13.    }  
  14. }  
  15.   
  16. public class Bird extends Animal  
  17. {  
  18.   public Bird()  
  19.   {  
  20.      
  21.   }  
  22.   @Override  
  23.   protected void eat()  
  24.   {  
  25.     System.out.println("Just eat worm");  
  26.   }  
  27.     
  28.   public static void main(String[]args)  
  29.   {  
  30.     new Bird();  
  31.   }  
  32. }      
输出结果如下:


显然,在执行父类的构造方法时,调用的并非是父类中的eat()方法,而是子类中重写了的eat()方法。有人也许会怀疑是在父类中没有写上this.eat(),那么如果加上this,即代码如下所示:

[java] view plain copy
print?
  1. import java.util.*;  
  2.   
  3. class Animal  
  4. {  
  5.    public Animal()  
  6.    {  
  7.      this.eat();  
  8.    }  
  9.      
  10.    protected void eat()  
  11.    {  
  12.      System.out.println("Eat something");  
  13.    }  
  14. }  
  15.   
  16. public class Bird extends Animal  
  17. {  
  18.   public Bird()  
  19.   {  
  20.       
  21.   }  
  22.   @Override  
  23.   protected void eat()  
  24.   {  
  25.     System.out.println("Just eat worm");  
  26.   }  
  27.     
  28.   public static void main(String[]args)  
  29.   {  
  30.     new Bird();  
  31.   }  
  32. }      
  33.      
此时输出结果如下图所示:


显然,此时输出结果与上面的相同。那么原因是什么呢?

     这其实涉及到编译时类型和运行时类型的问题,在上面的例子中,虽然使用了this,即在调用父类的构造方法中,其编译时类型确实是Animal,但是运行时类型却是Bird,因而调用的是子类中重写的eat()方法。

     在上面的例子中,由于调用被子类重写的方法只是导致输出出错,严重的例子可能导致运行时错误,代码如下:

[java] view plain copy
print?
  1. import java.util.*;  
  2.   
  3. class Animal  
  4. {  
  5.    public Animal()  
  6.    {  
  7.      this.eat();  
  8.    }  
  9.      
  10.    protected void eat()  
  11.    {  
  12.      System.out.println("Eat something");  
  13.    }  
  14. }  
  15.   
  16. public class Bird extends Animal  
  17. {  
  18.   private String wormType;  
  19.   public Bird()  
  20.   {  
  21.     wormType="Leafworm";  
  22.   }  
  23.   @Override  
  24.   protected void eat()  
  25.   {  
  26.     System.out.println("The length of wormType is "+wormType.length());  
  27.   }  
  28.     
  29.   public static void main(String[]args)  
  30.   {  
  31.     new Bird();  
  32.   }  
  33. }      
  34.      
运行结果为:


由上图可知,在编译时未出现错误,但是在运行时却出现空指针错误,原因就在于wormType还没被初始化,从而调用String.length()方法是出错。

       综上可得到结论:在父类的构造方法中,如果要调用其他方法,绝对不能调用可能会被子类重写的方法,否则会出现意想不到的错误。这也就意味着,如果要父类的构造方法中要调用其他成员方法,那么要么调用private方法,要么调用final修饰的方法,因为它们都是不能被子类重写的方法。

阅读全文
0 0
原创粉丝点击