java里的类继承
学了这么就java,今天重新看Java的类继承,被一个题敲醒了,java还是学了个皮毛,于是梳理一下学的过程中见过的一些坑。
1.先看下面的代码:
class Base{
private int i=2;
public Base() {
this.display();
}
public void display() {
System.out.println(i);
}
}
class Derived extends Base{
private int i=22;
public Derived() {
i=222; //②
}
public void display() {
System.out.println(i);
}
}
public class Test{
public static void main(String[] args) {
new Derived(); //①
}
}
会输出什么?2,22,还是222,正确答案是0,想不到吧?
上面的代码中main(),执行Derived()的构造函数,由于Derived在执行自己的构造之前会完成父类的构造,在父类构造器中调用了this.display()但结果却是0.
在①出创建Derived对象,系统会为其分配内存空间,但是这个Derived对象并不是只有一个i实例变量,而是两个(有一个是父类的)。
首先,Java对象是由构造器分配的?其实不是,在执行构造器代码之前。对该对象所占的内存就已经被分配下来了,这些值都是默认的0或者false;对于引用类型就是null。当程序调用①处的代码块时,系统会为Derived对象分配两块内存,分别存放Derived对象的两个i实例变量,其中有一个属于Base类定义的i的实例变量,这两个i实例变量的值都是0;对于父类构造器Base(),只有:this.display();但由于Base在定义i实例变量i时指定了初始值2,所以经过编译器处理之后应该是:
i=2;
this.display();
那么问题来了,this是谁?
如果对Base的构造器修改:
public Base{
System.out.println(this.i);
this.display();
}
对编译器处理后的构造器应该是:
i=2;
System.out.println(this.i);
this.display();
运行结果是2,0.那this到底是谁?
当this在构造器中时,this就代表正在初始化的Java对象,从之前的代码来看,this是在Base()中,但这些代码实际是在Derived()构造器中执行的,因此this应该是Derived对象。而不是Base对象那既然this是Derived对象那直接输出this.i为何会是2?这是因为this虽然代表了Derived对象,但是他却位于Base构造器中,编译时类型是Base,而他实际是一个Derived对象的引用,看下面代码:
public Base()
{
System.out.println(this.i);
this.display();
System.out.println(this.getClass());
}
它最后的输出是Derived类型,但是如果你再用this来调用Base里的其他方法就会提示出错,因为this编译时类型就是Base。当实际编译的类型与运行时的类型不一样时,通过该变量访问他的引用的对象的实例变量时,该实例变量的值由声明该变量的类型决定,但通过该变量调用它引用对象的实例方法时,该方法将由它实际应用的对象的类型决定,当访问this.i时,会访问Base类中定义的i实例变量,就是2,但执行this.display()时,则实际表现出Derived对象的行为,也就是Derived对象的实例变量0.
2.再看下面的代码:
class Animal{
private String aaa;
public Animal() {
this.aaa=getaaa(); //②
}
public String getaaa() {
return "Animal";
}
public String toString() {
return aaa;
}
}
public class Test extends Animal{
private String name;
private double weight;
public Test(String name,double weight) {
this.name=name; //③
this.weight=weight;
}
public String getaaa() {
return "dog[name="+name+",weight="+weight+"]";
}
public static void main(String[] args) {
System.out.println(new Test("二狗", 12.5)); //①
}
}
它的输出结果是dog[name=null,weight=0.0],这个代码的关键在②处,表面上是调用了父类的getaaa()方法,但是在实际运行的时候就会变成调用被子类重写的getaaa()方法,也就是说在main()中创建Test对象会先调用父类Animal的构造方法,但是此时Animal的构造方法调用了被子类对象Test重写的getaaa()方法对aaa赋值,而Test对象还没有对name和weight赋值,于是在tostring()方法中,返回的aaa是未赋值的name和weight,可以对以上代码中的Animal做如下修改:
class Animal{
private String aaa;
public Animal() {
this.aaa=getaaa(); //②
}
public String getaaa() {
return "Animal";
}
public String toString() {
return getaaa();
}
}
这样在返回sss结果之前就对Test对象name和weight赋值,得到结果:dog[name=二狗,weight=12.5]。
3.再看下面的例子:
class Base{
int i=2;
public void display() {
System.out.print("调用父类方法!");
System.out.println(this.i);
}
}
class Derived extends Base{
int i =20;
@Override
public void display() {
System.out.print("调用子类方法!");
System.out.println(this.i);
}
}
public class Test {
public static void main(String[] args) {
Base b=new Base();
Derived d =new Derived();
Base bd=new Derived();
Base e=bd;
Base c=d;
System.out.println("b:");
System.out.print("调用display:");
b.display();
System.out.println("d:");
System.out.print("调用display:");
d.display();
System.out.println("bd:");
System.out.print("直接用变量:");
System.out.println(bd.i);
System.out.print("调用display:");
bd.display();
System.out.println("e:");
System.out.print("直接用变量:");
System.out.println(e.i);
System.out.print("调用display:");
e.display();
System.out.println("c:");
System.out.print("直接用变量:");
System.out.println(c.i);
System.out.print("调用display:");
c.display();
}
}
运行结果:
b:
调用display:调用父类方法!2
d:
调用display:调用子类方法!20
bd:
直接用变量:2
调用display:调用子类方法!20
e:
直接用变量:2
调用display:调用子类方法!20
c:
直接用变量:2
调用display:调用子类方法!20
对于b,d的输出是毫无疑问的但是对于bd,在直接输出变量i的时候输出的是2(Base)中的赋值,但是调用display()时就把父类的方法改写,最后调用的是子类的方法。直接通过bd访问i实例变量,输出的是Base(声明时的类型),对象的i实例变量;如果通过bd来调用display(),该方法表现出Derived(运行时类型)对象的行为方法。
把bd赋给e,bd和e指向同一个java对象,不管声明时他们时什么类型,当通过这些变量调用方法时。方法的行为总是表现出他们的实际行为,如果通过这些变量来访问他们所指对象的实例变量,这些实例变量值总是表现出声明这些变量所用类型的行为。
对于成员(实例)变量和成员(实例)方法,有这样的区别:
如果子类中重写了父类方法,那子类里定义的方法就会把父类的方法覆盖掉,对于成员(实例)变量,即使子类定义了和父类完全同名的变量,子类变量也不会把父类中的变量覆盖。
因为这样的差别,所以对于一个引用变量来说,通过该变量访问它的引用的对象的成员(实例)变量时,该成员(实例)变量的值取决于声明该引用变量时的类型,通过该变量访问它的引用的对象的成员(实例)方法时,该成员(实例)方法的行为取决于实际引用的对象的类型。
4.再来个例子:
class Base{
int count=1;
}
class Mid extends Base{
int count=2;
}
class Sub extends Mid{
int count=3;
}
public class Test{
public static void main(String[] args) {
Sub s=new Sub();
Mid m=s;
Base b=s;
System.out.println(s.count);
System.out.println(m.count);
System.out.println(b.count);
System.out.println(s.getClass());
System.out.println(m.getClass());
System.out.println(b.getClass());
System.out.println(s==m);
System.out.println(b==m);
}
}
运行结果毫无疑问:
3
2
1
class lianxi.Sub
class lianxi.Sub
class lianxi.Sub
true
true
这个正是上一个例子的验证,但是在内存中不存在Mid和Base这两个对象,程序内存中只有一个Sub对象,只是这个Sub对象中不仅保存了在Sub类中定义的所有实例变量,还保持了所有父类所定义的全部实例变量。s,m,b,虽然指向了相同的java对象,但是在用到实例变量时却是体现出声明的类型的对象的实例变量值。
5.最后一个例子:
class price {
final static price FIRST=new price(1.2);
static double initprice=10; //①
double newprice;
public price(double discount) {
newprice=initprice-discount;
}
}
public class Test{
public static void main(String[] args) {
System.out.print (price.FIRST.newprice+" ");
System.out.println(new price(1.2).newprice);
}
}
运行结果:
-1.2 8.8
为何不是8.8和8.8 ?从内存分配的角度看:第一次用price程序会对price类进行初始化,初始化有两个阶段:
(1)系统为price的类变量分配内存空间
(2)按照初始化代码的排列顺序对类变量进行初始化
在(1)处,系统先为 FIRST,initprice分配内存空间,此时这两个变量的值分别时null(引用类型的初始值),0.0(double类型的初始值).然后是第二阶段,程序按次序对二者赋值先是FIRST,此时需要用到price的构造方法,对newprice赋值,但是此时还没有对静态变量initprice赋值(仍然是默认的0.0),因此newprice的值就是-1.2.那么如果去掉①处,initprice前面的static结果会是什么?
运行结果:
8.8 8.8
这是因为static类型的变量和普通变量存放的位置是不一样的,static类型的FIRST和非static类型的initprice,对于他们的声明顺序不是给他们初始化的顺序,这两变量没有先后顺序。
那么如果在①处,initprice前面的加上结果会是什么?
运行结果:
8.8 8.8
这是因为在设为final后,这个变量的值就在编译时被确定下来,这个final变量不再是一个值,系统把它当作时一个宏变量,在所有出现该变量的地方系统直接把他当作对应的值来处理。但是这种final变量必须在定义时即被赋值,并且被赋值的表达式只能是基本的算术运算表达式或字符串连接运算。