本文将对Java类实例化对象的过程进行详细的总结,在阅读本文后,你会了解JVM生成对象的执行过程。
1、普通的类/*** 一个简单的类* @author zhang xl**/public class SimpleObject{ private String name; private int age; public SimpleObject(){} public SimpleObject(String name,int age){ this.name=name; this.age=age; } /** * * 主函数用来测试 */ public static void main(String[] args) { //生成简单对象 SimpleObject simpleObject=new SimpleObject(); System.out.println("age="+simpleObject.age); System.out.println("name="+simpleObject.name); SimpleObject simpleObject2=new SimpleObject("lisi",15); System.out.println("age="+simpleObject2.age); System.out.println("name="+simpleObject2.name); }}此处,类中声明的两个成员变量没有显示的赋值,那么在执行SimpleObject simpleObject=new SimpleObject();的时候,将直接执行无参的构造函数,但是在执行之前,如果你断点调试,那会发现在进行入无参构造函数之前,你单击step into,会跳转到Object类的那个无参构造函数去执行(如果你的类文件没有关联到源文件,那么你会看到Class File Editor透视图,此时你若仔细观察,仍然可以看到提示The source attachment does not contain the source for the file Object.class.)为什么呢?因为SimpleObject默认继承自Object类,所以在执行对应的构造函数之前会默认先调用一下父类的那个构造函数(相当于super();这条语句在构造函数中被调用),执行 SimpleObject simpleObject2=new SimpleObject("lisi",15);的时候,也是先进入到Object类的无参构造方法中,然后才会执行构造函数。1.2如果此时给成员变量进行显示的初始化:private String name=”zhangsan”; private int age=”10”; 那么生成对象的流程与上面的流程基本相同,但是在执行父类的构造函数之后,会进阶着执行显示初始化,然后才会执行构造函数,因此如果构造函数中进行了赋值操作,那么就会覆盖掉显示初始化的赋值。2、含有静态成员变量private static String name="zhangsan";private int age=10;那么在断点调试的过程中,我们会发现,private static String name="zhangsan";这条语句在父类的构造函数执行之后,不会执行到,而是跳到private int age=10;这条语句上去执行,为什么呢?因为静态的成员变量和静态代码块是在类加载器加载对应类的时候就执行了,由于我们的main函数在这个类中,所以,在执行main函数之前,类加载器(实际上是sun.misc.Launcher$AppClassLoader这个类加载器),将Java编译器已经编译好的SimpleObject.class文件加载到内存,此时,静态代码块已经被执行(并且static修饰的代码只会被执行一次),所以我们在断点调试时,会发现,JVM将不会在执行那条语句。3、具有继承关系的类的对象的加载机制3.1在上面的类的基础上,添加一个其子类class ChildSimpleObject extends SimpleObject{ private char gender; public ChildSimpleObject(){} public ChildSimpleObject(String name,int age,char gender){ super(name,age); this.gender=gender; }}那么,在执行ChildSimpleObject child=new ChildSimpleObject();语句的时候,会先执行父类的父类的构造函数,如果父类还有父类,那么还要先执行父类的父类的构造函数,直到执行完Object的构造函数,在执行构造函数之前还会检查该类的成员变量是否进行了显示初始化,那么还要先执行显示初始化语句,然后在执行构造函数,按照次执行顺序,直到执行完当前类的构造函数。3.2如果有static修饰的成员变量或代码块,那么也都不在执行,原因同上面的解释。4、比较复杂的一种情况(上转型的多态): 我们都知道,如果子类重写了父类的某个方法,那么上转型对象在调用该方法时,执行的是子类重写后的方法,那么对于成员变量呢?public class ComplexObject{ int age=50; public ComplexObject(){} public int getAge() { return age; } public void setAge(int age) { this.age = age; } public static void main(String[] args) { ComplexObject obj=new ChildComplexObject(); System.out.println(obj.age); ChildComplexObject obj2=(ChildComplexObject) obj; System.out.println("obj2="+obj2.age); }}class ChildComplexObject extends ComplexObject{ public ChildComplexObject(){ } public ChildComplexObject(int age){ this.setAge(age); }}此时,上转型对象得到的age是父类中显示初始化的50.如果在子类中也添加一个相同的成员变量呢?即在子类中添加public int age=80;,此时,执行主函数中的方法,得到的仍是50,而此时,如果在将上转型对象在转回到ChildComplexObject类型时,此时,调用的age是子类中自己定义的80,但是如果将System.out.println("obj2="+obj2.age);这条语句改为System.out.println("obj2="+obj2.getAge());结果打印的仍然是50.这是为什么呢?因为如果子类中声明了一个和父类一样的成员变量,那么在生成子类对象的时候,这个子类对象对应的堆内存中会保留两份age空间,一份继承自父类,一份是自己的成员变量,上转型对象,其实质上仍然是子类new的对象,只不过用父类的引用来指向它,但是在访问成员变量,即直接访问.age的时候,其获取的是内存中父类那份的值50,而如果此时再向下转型,转为将刚new的子类的对象转为子类的引用,那么此时若再直接用.age来访问,那么访问的将是子类自己的那份age的值。而若用getAge的方法来访问,即使用子类对象的引用来访问,仍然是父类的值,因为getAge方法是从父类继承过来的,在执行该方法的时候,代码会转到父类去执行,在父类执行的时候,那个getAge方法中返回的age就是父类对象的那个age,值为50。如果在子类中写一个和父类一抹一样的getAge方法,那么再次执行的时候,得到的就是子类自己的age的值80.原因安在啊?因为在开辟内存的时候,类中的成员方法是只开辟一块内存的,即生成的多个对象中都有对成员方法的地址引用(每个对象中都有着对应类中成员方法的入口地址),当子类没有重写父类的方法时,子类对象保存的就是父类方法中方法地址,因此,如果访问该方法,那么就是访问父类的方法,对应的成员变量的内存空间也都是父类的。如果子类重写了父类的方法,那么子类中的地址则指向的是子类自己定义的方法的地址,那么此时子类对象(包括上转型对象)访问的都是子类中重写的方法。如果仅仅是声明一个对象,比如:User u;那么在断点调试的时候,我们可以发现,这条语句不会被单步执行到,为甚么?声明变量,只是说,这个变量名被占用了,而并没有开辟内存的动作,因此这句话不会执行加载User类的动作。