Java进阶---对象与内存控制(三)
3、父子实例的内存控制
在正是开始之前,先介绍一下面向对象的三大特征,有助于我们理解文章:
> 封装:属性可用来描述同一类事物的共同特征,行为可以描述同一类事物都可做的操作,而封装就是要把属于同一类事物的共性(包括共同的属性和行为)归到同一个类中,以方便使用。
> 继承,一个类如果拥有另一个封装类的所有属性和行为,且拥有区别于其他类属性或方法,那么可以让这个类继承这一个封装的类,这样我们就不用在这个类中定义封装类中所有的属性和行为却拥有了这些属性和行为,从而使代码复用,达到简化代码的效果。
> 多态:站在抽象的层面上去实施一个统一的行为,到个体(具体)的层面上时,这个统一的行为会因为个体(具体)的形态特征而实施自己的特征行为。父类层拥有统一行为,这个行为实际上是对此父类的多个子类所重写的该行为的抽象,即此行为可以代表其所有子类的行为;子类层重写的父类的行为拥有自己区别于其父类的其他子类的特征。这就是多态机制的体现。
1)、继承成员变量和继承方法的区别
我们都知道,当访问权限允许的情况下,子类可以直接访问父类中定义的成员变量和方法。但是这里是哪种继承方式呢?下面我们通过一个例子来分析讨论:
class Persion{
String name = "小武";
public void display(){
System.out.println("Persion中的display():" +this.name);
}
}
class Man extendsPersion{
String name = "灵灵";
@Override
public void display() {
System.out.println("Man中的display():" +this.name);
}
}
public classtest01 extends Persion{
public static void main(String[] args) {
Persion p = new Persion();
System.out.println("p.name"+ p.name);
p.display();
Man man = new Man();
System.out.println("man.name"+ man.name);
man.display();
Persion pm = new Man();
System.out.println("pm.name:"+ pm.name);
pm.display();
Persion pm2 = man;
System.out.println("pm2.name"+ pm2.name);
pm2.display();
}
}
输出结果:
下面分析程序执行过程。前四行输出大家都明白,从第五行开始讲起:
A. Persionpm = new Man();虽然我们声明了一个Persion对象,但始终要牢记此时初始化的应该是一个Man对象,此时Persion当中的name应该是Persion.name=”小武”,所以pm.name=”小武”,输出第五行;此时Persion当中的display()(即pm.display())被Man重写,所以应该称为Man.display(),所以输出的是”灵灵”,输出第六行;
B. Persionpm2 = man;man变量是对Man对象的引用,pm2变量是对Persion对象的引用,他们都指向同一块内存空间。但是pm2.name仍然是Persion对象中的name,pm2.display()却是Man.display(),输出第七八行;
2)、内存中子类的实例
下面再介绍一种一段情况,通过分析变量所引用的Java对象在内存中的分配,可以更好地理解Java的内存分配机制:
class Persion{
String name = "小武";
}
class Man extendsPersion{
String name = "灵灵";
}
public classtest01 extends Man{
String name = "小武灵灵";
public static void main(String[] args) {
test01 t = new test01();
Man m = t;
Persion p = t;
System.out.println("test01.name:"+ t.name);
System.out.println("Man.name"+ m.name);
System.out.println("Persion.name"+ p.name);
}
}
输出结果为:
这意味着t、m、p这3个变量所引用的Java对象(test01)所引用的Java对象拥有3个name实例变量,也就是说需要3块内存存储它们。
这个test01对象不仅存储了它自身的name实例变量,还需要存储从Man、Persion两个父类那里继承到的name实例变量,但这3个name实例变量在底层是有区别的,分别是test01.name、Man.name、Persion.name。
下面我么将程序改一下,看眼一下对super的了解:
class Persion{
String name = "小武";
int number = 4100;
}
class Man extendsPersion{
String name = super.name;
int number = 9160;
}
public classtest01 extends Man{
String name = "小武灵灵";
public static void main(String[] args) {
test01 t = new test01();
}
public test01() {
System.out.println("super.name:"+ super.name);
System.out.println("super.number:"+ super.number);
}
}
程序输出结果为”小武”和”9160”;
程序分析:输出小武可能没有问题,但是输出9160证明了一点,那就是super.number中的super代表引用该test01对象的关联的对象Man,即该对象的第一级继承对象。
所以,使用super应注意以下两个方面:
> 子类方法不能直接使用returnsuper;,但使用return this;返回调用该方法的对象是允许的;
> 程序不允许直接把super当成变量使用,例如判断super和一个变量是够引用同一个Java对象时不能使用super == a。
下面再给一个使用super的程序,我给出输出结果,大家自己分析吧:
class Persion{
String name = "小武";
int number = 4100;
}
class Man {
String name = "小武";
public Man getMan() {
return this;
}
public void output(){
System.out.println("Man中的output方法");
}
}
public classtest01 extends Man{
String name = "灵灵";
public void output() {
System.out.println("test01中的output方法");
}
public void getSuperVoid(){
super.output();
}
public Man getSuper(){
return super.getMan();
}
public static void main(String[] args) {
test01 t = new test01();
Man m = t.getSuper();
System.out.println("t和m所引用的对象是否相同:" + (t == m));
System.out.println("访问t所引用的name实例变量:" + t.name);
System.out.println("访问m所引用的name实例对象:" + m.name);
t.output();
m.output();
t.getSuperVoid();
}
}
输出结果:
总结父、子对象在内存中的存储规则:当程序创建一个子类对象时,系统不仅会为该类中定义的实例变量分配内存,也会为其父类中定义的所有实例变量分配内存,即使子类定义了与父类同名的实例变量。如果子类里定义了与父类中已有变量同名的变量,那么子类中定义的变量会隐藏父类中定义的变量并为其分配内存控件,而非覆盖父类中的变量。为了在子类方法中访问父类中定义的、被隐藏的实例变量,或者为了在子类方法中调用父类中被其重写的方法,可以通过super.作为限定来修饰这些实例变量和实例方法,例如上面的程序中调用Man中的output方法,使用super.output或者((Man)t).output();
3)、父、子类的类变量
上面讲了父、子类的实例在内存中的分配,其实父、子类的类变量基本与其类似。所不同的是类变量属于类本身,而实例变量则属于Java对象;类变量在类初始化阶段完成初始化,而实例变量在对象初始化阶段完成初始化。
下面是一个使用super.作为限定来访问父类中定义的类变量:
class Man {
static String name = "小武";
}
public classtest01 extends Man{
static String name = "灵灵";
public static void main(String[] args) {
test01 t = new test01();
t.output();
}
public void output(){
System.out.println("访问test01类的name变量:" + name);
System.out.println("访问test01的父类的name变量:" + Man.name);
System.out.println("通过super.访问test01的父类的name变量:"+ super.name);
}
}
输出结果中的name依次为灵灵、小武、小武。
总结访问父类的类变量的方法有三种:
>使用父类的类名作为主调来访问类变量;
>创建父类的对象的引用来访问类变量;
>使用super.作为限定来访问类变量。