如果你对于java中的上下转型,多态有些困惑或者感觉难以记住的话,相信这篇博客一定会让你的理解更加深入。
(下面从逻辑方面进行阐述,真实内存到底如何存放数据,请勿以此为依据。)
1.引用类型与实例类型
2.上转型
3.下转型
4.字段影响
5.方法影响
6.什么情况下才能进行上下转型
7.多态
8.总结
先放两张图,然后开始慢慢说起,图在后面会用到。
1.引用类型与实例类型
为什么要先说这个呢?
我们知道,java中的引用类型与实例对象类型是不一样的。
S s = new S();
有一个一个类S,S s 是在内存中新建了一个S类引用类型对象s,接着 new S() 是在内存中新建了一个S类实例对象, = 将这个实例对象赋给引用类型对象s,也就是s现在指向了这个实例对象。
S s1 = s;
现在我们又新建了一个S类引用对象s1,将s赋给s1,这个又是什么意思呢,跟我们想象中的拷贝不一样,内存中的S类实例对象只有一个,这行代码是将 s指向的对象赋给了新建的引用类型s1,也就是说,现在s1跟s都指向了这个实例对象。
做个小小的实验验证一下:
public class S {
int age = 1;
public static void main(String[] args) {
S s = new S();
System.out.println("s.age:"+s.age);
S s1 = s;
s1.age = 2;
System.out.println("s.age:"+s.age);
}
}
输出:
s.age:1
s.age:2
可以看到,我们并没有直接改变s的age属性,但是值却发生了改变,说明两个引用类型确实指向的是一个实例对象。
在上面的基础上现在可以说说上下转型的逻辑了。
现在有两个类,F类和S类,S类继承于F类。
2.上转型
S s = new S();
F f = s;
这个就是上转型,它的逻辑是什么呢?
我们先新建了S类引用类型对象s,并指向一个S类实例对象。第二行代码:新建了一个S基类F类引用类型对象f,并将它指向引用类型s指向的S类实例对象。
3.下转型
S s = new S();
F f = s;
S s1 = (S)f;
这个是下转型。
前面两行于=与上面一样,重点是第三行,又新建了一个S类引用类型对象s1,并将它指向引用类型f指向的S类实例对象。
上面只是介绍了上下转型的大致流程,你应该还是很疑惑,那么到底它对于类中字段,方法会带来什么影响?以及为什么使用上下转型?什么时候才能进行上下转型?通过在内存中的执行我们可以慢慢看到答案
我们可以想象引用类型对象在内存中包含很多个指针,每个指针分别指向实例对象中的一个字段或方法,那么指针到底有多少个?进行不同的上下转型操作时,指针的是怎么改变指向的内容的呢?
4.字段影响
先放代码:
public class F {
int age = 1;
String name = "F";
}
public class S extends F {
int age = 2;
String id = "123";
public static void main(String[] args) {
F f = new F();
S s = new S();
System.out.println("age:"+s.age+" id:"+s.id+" name:"+s.name);
f = s;
// System.out.println("id:"+f.id); 这行代码出错,说明上转型后并不能调用id属性
System.out.println("age:"+f.age+" name:"+f.name);
S s1 = (S)f;
System.out.println("age:"+s1.age+" id:"+s1.id+" name:"+s1.name);
//下转型后,age值再次改变,说明在S实例对象内存区存在两个“age”,
}
}
是不是感觉很难理解,我们需要配合上面的第一张内存图来理解
F f = new F();
f引用类型中包含两个字段指针:age和name,分别指向了实例对象的age字段和name字段
S s = new S();
由于S类继承了F,所以它的实例对象字段区会存在F类的字段age(F)和name,然后还会存在自己新增的两个字段age(S)和id,共4个字段。然而,S类引用类型s中只有三个指针,这是因为S新增的age字段和F类字段同名,所以,关键的是,要看清这个age字段指针指向的是实例对象中的age(S),也就是子类新增的age字段。
f = s;
现在将基类F的引用类型对象f指向s指向的实例对象,f中有两个字段指针,name字段指针指向name字段很容易理解,本来就是继承基类的属性,而age字段指针呢,指向的是age(F).
S s1 = (S)f;
这一句就很好理解了,新建了S类引用类型对象,指向f指向的S类型实例对象,跟上图中间的指向是一样的嘛,也合乎常理。
小结:
基类的引用类型中的字段指针与基类实例对象中的字段个数是相同的。
基类的子类,实例对象的字段,不管与基类是否同名,其个数永远等于 基类字段个数+子类字段个数,而子类的引用类型中的字段指针,如果碰到同名的字段,指向的都是子类新增的字段。
上转型时,基类字段指针遇到同名字段,指向的是基类创建的字段,其他都一一对应。
下转型时,子类字段指针遇到同名字段,再次指向子类新增的字段,其他也一一对应。
5.方法影响
public class F {
public void eat() {
System.out.println("F-eat");
}
public void drink() {
System.out.println("F-drink");
}
}
public class S extends F{
@Override
public void eat() {
System.out.println("S-eat");
}
public void walk() {
System.out.println("S-walk");
}
public static void main(String[] args) {
F f = new F();
S s = new S();
f = s;
f.eat();
f.drink();
// f.walk(); 代码错误,说明上转型不能调用原子类非重写方法
}
}
方法与字段流程相似,不在细致分析,我们只找比较关键的分析。
与字段不同的是,子类如果新增方法与基类同名,在内存中此新增方法将会覆盖掉原来基类创建的方法,也可以说基类的方法在子类对象的内存中不再存在。
上转型时,基类方法指针在子类实例对象中只能找到子类覆盖后的方法,因此只能指向这个被覆盖的方法,因此在调用时会出现多态后面再详细阐述。
下转型时,所以方法指针都指回原来一一对应的方法,没用什么特殊的地方。
小结:
基类的引用类型中的方法指针与基类实例对象中的方法个数是相同的。
基类的子类,实例对象的方法,其个数等于 基类方法个数+子类与基类不同名方法个数,子类对象如果方法与基类方法同名,会将其覆盖掉,因此子类的引用类型中的方法指针与子类实例对象中的方法个数是相同的,一一对应。
上下转型时,方法指针与方法都是一一对应的。
6.什么情况下才能进行上下转型
A a = new B();
对于上转型,只要引用类型对象a是实例对象b的祖先类,就可以进行上转型,很容易理解,基类中的指针数少于子类中的指针数,且每个指针都能在其中找到名字一一对应的。
A a = (A)b;
对于下转型,只有引用类型A是b所指向的实例化对象的类型的基类或者相同,就可以进行下转型与上面原理相似。
上下转型的本质原则都是基类引用类型对象能够指向子类实例类型对象 !!!!!!!!
7.多态
到了这里就要说一说多态了,上面其实已经涵盖了多态,我们之所以单独拿出来讲讲,是因为它的用途实在是广泛。
public class F {
public void speak() {
System.out.println("我是father");
}
}
public class S1 extends F {
public void speak() {
System.out.println("我是son-1");
}
}
public class S2 extends F {
public void speak() {
System.out.println("我是son-2");
}
}
public class Test {
public static void main(String[] args) {
F f = new F();
S1 s1 = new S1();
S2 s2 = new S2();
f = s1;
f.speak();
f = s2;
f.speak();
}
}
输出:
我是son-1
我是son-2
同一个基类引用类型对象f,在指向不同子类实例对象时,执行同一个函数,会出现不同的结果,这就是多态。
经过上面我们的分析,你现在还看不懂多态吗?
8.总结
我们从内存的角度,对执行过程特殊的地方来进行一次总结。
1.基类建立:
指针与实例对象中字段和方法一一对应。
2.子类建立:
新增字段(包括重名)都会建立内存,字段指针除了与基类同名的,其他也会新增,字段指针指向的时候一一对应,遇到内存中存在两个相同名字的字段时,指向子类新增的那个。
新增方法(除了重名)都会建立内存,重名的方法,新增方法会覆盖原来方法,方法指针除了与基类同名的,其他也会新增,方法指针指向的时候一一对应。
3.上转型时:
基类字段指针一一指向对应名字字段,当遇到存在两个相同名字的字段时,指向基类创建的那个。
基类方法指针一一指向对应名字方法。
4.下转型时:
子类字段指针对应名字一一指向子类对象字段,遇到内存中存在两个相同名字的字段时,指向子类新增的那个。
子类方法指针一一指向对应名字方法。
对于字段:可调用自己或者父类以上创建的字段,哪种类型引用对象调用就指向那种类型创建的字段。
对于方法:可调用自己或者父类以上创建的字段,不管哪种引用类型对象调用,根据实例对象类型重写后的方法执行(未被重写就按原来的执行)。
现在你还有什么不理解的地方吗?