问题引出
在子类中可以通过使用super关键字来调用父类的被重写的方法,与构造方法中调用父类的构造方法不同,此处调用语句放在哪里是由程序逻辑决定的,这是因为在构造方法中要首先创建父类的对象,所以需要将显式调用父类构造方法的语句放于子类构造方法的第一行(默认系统会添加对父类无参构造函数的调用),而在子类的非构造方法中调用父类的被重写方法时,父类的对象已经创建了,就没有像调用父类构造方法那样的限制了。其实这两种super调用的意义是没有任何关系的。
this所指的是当前调用方法的对象,而super所指的则是当前调用方法的对象的直接父类的对象。
当在子类的构造方法中使用super关键字调用父类的构造方法时,对于被重写属性的操作是针对父类属性的,对于子类重写的属性没有影响。
下面给出一个程序,运行这个程序就能体会上文所描述的状况。最后主要说明一下这个程序中的对象实例化过程。
public class TestInstanceObject
{
public static void main(String[] args)
{
Apple a = new Apple(, );
System.out.println("苹果的重量是" + a.weight + "苹果的单价是" + a.price);
System.out.println("子类中的weight属性值为" + a.getWeight());
System.out.println("父类中的weight属性值为" + a.getSuperWeight());
}
}
class Fruit
{
int weight;
int price;
// constructors overload
public Fruit()
{
System.out.println("这是Fruit类的无参构造方法");
}
public Fruit(int weight, int price)
{
this.weight = weight;
this.price = price;
System.out.println("我是Fruit类具有两个整型参数的构造方法,我完成对从我这里继承的属性的初始化,即使weight属性被隐藏了");
}
}
class Apple extends Fruit
{
int weight = ;
//constructors overload
public Apple()
{
System.out.println("我是Apple类的无参构造方法,我会隐式调用父类的无参构造方法来完成相应于此处所产生的子类对象的父类对象");
}
public Apple(int weight, int price)
{
super(weight, price); //call the parent constructormethod
System.out.println("我是Apple类具有两个整型参数的构造方法,我显式调用了父类的具有两个整型参数的构造方法");
}
public int getWeight()
{
return weight;
}
public int getSuperWeight()
{
return super.weight;
}
}
背景知识
背景知识一 类初始化时机
第一:生成该类对象的时候,会初始化该类及该类的所有父类;
第二:访问该类的静态成员的时候(子类中访问父类中定义的静态变量时,只会初始化父类,不会造成子类的初始化);
第三:
class.forName("类名");
背景知识二 JVM类加载机制
加载、链接与初始化
加载:由类的加载器执行,将类的.class文件载入内存,并根据字节码创建Class对象;
链接:验证该类中的字节码,为静态变量分配内存并赋以默认初始值,如果必要,会解析该类创建的对其他类的引用;(验证<可优化>、准备<分配内存>、解析<不一定>)
初始化:初始化是类加载的最后一步,其本质上是执行类构造方法,而类构造方法则是由编译器按照类变量赋值语句、静态语句块的出现顺序所收集生成的(所以不一定每个类都有类构造方法)。并且,JVM保证在子类的类构造方法执行前,完成对父类类构造方法的执行,也就是对超类进行初始化(自然包括对超类进行加载、链接),包括为静态函数分配入口地址。
初始化被放到了初次使用类的静态方法(构造器隐式地是静态的)或非常数静态域时才执行。
完成这三步后,就能够去创建这个类的对象了,也就是去对实例变量进行初始化,执行对实例变量进行操作的语句或语句块,然后是执行实例构造器。
其实对于使用new关键字隐式调用构造方法完成对象的创建这一过程而言,构造方法并非独自完成了创建对象的全部任务。实际上,当程序员调用构造函数时,系统首先会为该对象分配内存空间,并为这个对象执行默认初始化,这之后对象已经存在了。也就是说在真正执行构造方法之前对象就已经产生了,只是这个对象还不能为外部程序所访问,只能在构造函数中通过this关键字引用。
当构造方法的执行体执行结束后,这个对象作为构造器的返回值被返回,通常还会赋给一个引用类型的变量,从而让外部程序可以访问该对象。所以当创建一个对象时,如:
p只不过是一个引用类型的变量而已,并不是对象,真正的对象是
new People();
所创建的我们看不见、摸不着的,在内存中存储着的实体。
归纳总结一下,实例化一个对象的过程是:
1.为对象分配内存,执行默认初始化;
2.执行显式初始化,即类成员变量声明时的赋值语句与实例初始化块。
两者顺序执行,并且初始化块中可以对在其之后声明的属性进行操作(但不能引用)。即:
class Car
{
{
brand= "Ford";
}
String brand;
}
3.执行构造方法;
4.返回对象在内存中的首地址等重要信息。
子类对象创建过程
好了,接下来就对上面程序中的
Apple a = new Apple(50, 2);
语句执行过程进行分析。
首先是加载类阶段,键入java命令后开始执行程序(首先会加载main方法所在的类-主类,本示例代码主类中没有其余变量与操作,故而不进行分析),执行到语句
Apple a = new Apple(50, 2);
时,系统发现类Apple还没有加载,于是JVM就去加载该类,当执行完加载、链接,执行初始化时,JVM又发现Apple类的直接父类Fruit类也未加载,于是又去加载Fruit类,同样,JVM对Fruit类完成加载、链接,进行初始化时又发现Fruit类的父类Object类也未加载,于是又去加载Object类(JVM加载类的前两步,加载是去加载class文件,链接是对static成员进行默认初始化,第三步初始化是去顺序执行对静态变量的简单赋值语句以及静态初始化语句块),加载完毕Object类后,返回对Fruit类完成初始化,然后再对Apple类进行初始化,就完成了对所有涉及到的类的加载,整个过程是递归进行的。随后就进入创建对象阶段,没有父亲就没有孩子,所以会去自上而下创建父类的对象,其实这是因为在子类的构造器中至少会隐式调用父类的默认构造器。实际上并没有真正如同new一般创建直接或间接父类的对象,因为子类会继承父类的属性,所以对父类对象的创建最主要的目的就是对子类所继承的属性的初始化。根据我的理解,首先是为属性(所有的属性,包括继承而得的,即使是被隐藏的)分配内存空间并赋以默认初始值,然后从上到下递归进行两个操作:执行简单赋值语句与初始化块,执行构造方法。也就是说,这一系列步骤之后,由上至下所有的属性都会分配内存与初始化,即使存在隐藏覆盖的情形。至于在构造方法重载的情况下调用哪一个构造方法,又需要根据直接子类的构造方法调用而定。我们知道,当子类的构造方法不使用super关键字显式调用父类的构造方法时,编译时会在子类的构造方法中自动加上一条
super();
语句来实现对父类无参构造方法的调用,所以在调用父类的构造方法时还需要根据直接子类所调用父类构造方法的情况来进行调用,依次类推。
对于上述程序,new关键字之后是对于Apple类的带有两个整型参数的构造函数的隐式调用-new调用构造方法就叫隐式调用,所以就会去Apple类中查找执行带有两个整型参数的构造方法,即
public Apple(int weight, int price);
,在该构造方法又中显式调用了父类中有两个整型参数的构造方法,所以就会到父类中去查找并执行带有两个整型参数的构造方法,即
public Fruit(int weight, int price);
,在该构造方法中,通过this关键字完成了对当前父类对象属性的初始化,当然这两个属性被Apple类继承了,也就是属于Apple类的属性,就好像他们是定义在Apple类中的。
以上所描述的只是如何选择所调用的构造方法,实际上由上到下每一个类中执行简单赋值语句以及初始化块与调用构造方法之间是按顺序递归进行的,一个类完成之后其直接子类再去完成,直至到进行实例化的子类。在本程序中,Apple类定义了一个weight属性,由于这个属性与从父类中继承而来的属性重名,所以父类中的属性被隐藏,在子类中是看不到的,除非使用super关键字进行调用。自此Apple类的对象a就创建完成了。在主函数中分别调用Apple类中getWeight与getSuperWeigth方法,显示出子类中定义的weight属性值与所隐藏的父类中的weight属性值。运行结果如下: