Java 多态
- 多态通过分离做什么和怎么做,从另一个角度将接口实现分离开来
- 多态的作用是消除类型之间的耦合
- 多态方法允许一种类型表现出与其他类型之间的区别,只要它们都是从同一个基类(父类)导出而来的。这种区别是根据方法行为的不同二表示出来的,虽然这些方法都可以通过同一个基类(父类)来调用
1.向上转型
把某个对象的引用视为对其基类型的引用的作法被称为向上转型——因为继承树的画法中,基类是放置在上方的,类似一个金字塔。
2.方法动态绑定
2.1
如下代码,<code>tune()</code>方法接受一个<code>Father</code>引用,那么在这种情况下,编译器怎么才能知道这<code>Father</code>引用指向的是<code>ChildOne</code>或者<code>ChildTwo</code>对象呢?实际上,编译器无法得知。这就要引出绑定这个问题了。
public class Father(){
void getChild(){
System.out.println("I am Father");
};
}
public class ChildOne exends Father(){
void getChild(){
System.out.println("I am ChildOne");
}
}
public class ChildTwo exends Father(){
void getChild(){
System.out.println("I am ChildTwo ");
}
}
public class Test{
public static void tune(Father object){
object.getChild();
}
public void main(String[] args){
ChildOne childOne = new ChildOne();
ChileTwo childTwo = new ChildTwo();
tune(childOne);
tune(childTwo);
}
}
out:I am ChildOne
I am ChildTwo
- 将一个方法调用同一个方法主体关联起来被称作绑定。如果在程序执行之前进行绑定(如果有的话,由编译器和连接程序实现),叫做前期绑定。
- 但是上面的例子可以看到如果是前期绑定的话,当编译器只有一个<code>Father</code>引用时,它不知道究竟调用那个方法才对。解决办法就是后期绑定。意思就是在运行时根据对象的类型进行绑定。后期绑定也叫做动态绑定或运行时绑定。
- 如果一种语言想要实现后期绑定,就必须具有某种机制,以便在运行时能够判断对象的类型,从而调用恰当的方法。也就是说,编译器一直不知道对象的类型,但是方法调用机制能够找到正确的方法体。
- 其实我们平时写类似程序时候从来没有纠结过怎么绑定的,因为在Java中除了static方法和final方法(private方法属于final方法)之外,所有的方法都是后期绑定。所以我们不用去判定是否应该进行后期绑定——它自动会发生。
又有个问题来了,那我们为什么要用final来声明某一个方法呢?,一个解释是防止继承中这个方法被覆盖。但是知道了后期绑定之后就知道它最重要的一个作用是可以”关闭“动态绑定,告诉编译器不需要对其进行动态绑定。这样做的好处是编译器可以为final方法调用生成更有效的代码。
2.2
知道了Java中所有的方法都是通过动态绑定实现多态这个事实后,我们就可以编写只与基类打交道的程序代码了,并且这些代码对所有导出类都可以正确运行。或者换一种说法,发送消息给对象,让该对象去断定应该做什么事。
Paste_Image.png上面这个图不陌生,学JAVA的基本都见过,”圆是一种几何形状“,向上转型就可以变成下面这个代码:
<code>Shape s = new Cicle();</code>
当你调用<code>s.draw()</code>方法后,你可以认为是<code>Shape</code>的<code>draw()</code>方法,因为这毕竟是一个<code>Shape</code>引用,那么编译器是怎么知道去做其他的事情呢?由于后期绑定,还是正确调用了<code>Circle.draw()</code>方法。
2.3 缺陷:”覆盖“私有方法
如下代码:
public class PrivateOverride{
private void f() {
Systtem.out.println("private f()");
}
}
class Derived extends PrivateOverride(){
public void f(){
Systtem.out.println("public f()");
}
}
out:private f()
本来以为会输出<code>public f()</code>,但是由于<code>private</code>方法那个发被自动认为是<code>final</code>方法,而且对导出类是屏蔽的。因此,在这种情况下,<code>Derived</code>类中的<code>f()</code>就是一个全新的方法;既然基类中的<code>f()</code>方法在<code>Derived</code>中不可见,因此甚至也不能被重载。
虽然这样写没啥问题,编译器也不报错,但是容易产生混淆,所以最好还是采用不同名字。
2.3 缺陷:域与静态方法
有个问题,多态适用于方法调用,但是适用于某个域吗?,答案是不可以的,因为只有普通的方法调用可以使多态的。下面举个例子:
class Super{
public int field = 0;
public int getField(){
return field;
}
}
class Sub extends Super{
public int field= 1 ;
public int getField(){return fields;}
public int getSuperField(){return super.field};
}
public class FieldAccess{
public static void main(String[] args){
Super sup = new Sub();
System.out.println("sup.field"=+sup.field+",sup.getField()="+sup.getField());
Sub sub = new Sub();
System.out.println("sub.field"=+sub.field+",sub.getField()="+sub.getField()+",sub.getSuperField()="+sub.getSuperField());
}
}
out:
sup.field=0,sup.getField()=1;
sub.field=1,sub.getField()=1,sub.getSuperField()=0
上面的结果一目了然,证明了我们刚才的结论,但是一般情况下这种情况比较少出现,因为我们如果代码严谨的话很少会出现域是<code>public</code>的情况,所以最好把域设置成<code>private</code>的,这样保证了代码安全和防止混淆,但是你如果想访问这些<code>private</code>代码的话就必须要调用方法来访问了,类似JavaBean。
如果是静态方法,它的行为也不具有多态性,如下面的例子:
class StaticSuper{
public static String staticGet(){return "Base staticGet()";}
public Sring dynamicGet(){return "Base dynamicGet()";}
}
class StaticSub extends StaticSuper{
public static String staticGet(){return "Derived staticGet()";}
public static String dynamicGet(){return "Derived dynammicGet()";}
public class StaticPolymorphism{
public static void main(String[] args){
StaticSuper sup = new StaticSub();//向上转型
System.out.println(sup.staticGet());
System.out.println(sup.dynamicGet());
}
}
out:
Base staticGet()
Derived dynamicGet()
Ok,结果很明显,类似于上一个域问题的例子,我们得出一个结论:静态方法是与类,而并非与单个的对象相关联的。
3.构造器和多态
通常,构造器不同于其他种类的方法。涉及到多态时仍然如此。尽管构造器并不具有多态性(它们实际上是static方法,只不过该statuc声明是隐式的),但是还是要单独拿出来说明下构造器通过多态在复杂的层次结构中是如何运作的。
3.1构造器的调用顺序
基类的构造器总是在导出类的构造过程中被调用,而且按照继承层次逐渐链接,这样的话可以使基类的构造器得到调用。这样做的意义是:因为基类构造器具有一项特殊任务——检查对象是否被正确地构造。导出类(子类)只能访问它自己的成员,不能访问基类中的成员(基类成员通常都是private类型的)。只有基类的构造器才具有恰当的知识和权限来对自己的元素进行初始化。因此必须令所有的构造器都得到调用,否则就不能正确的构造完整的对象。
看下下面这个例子:
class Meal{
Meal(){print(print("Meal()");)
}
class Bread{
Bread(){print(print("Bread()"));)
}
class Cheese{
Cheese(){print(print("Cheese()");)
}
class Lettuce{
Lettuce(){print("Lettuce()");}
}
class Lunch extends Meal{
Lunch(){print("Lunch()");}
}
class PortableLunch extends Lunch{
PortableLunch(){print("PortableLunch()");}
}
public class Sandwich extends PortableLunch{
private Bread b = new Bread();
private Cheese c = new Cheese();
private Lettuce l = new Lettuce();
public Sandwich(){print("Sandwich()");}
public static void main(String[] args){
new Sandwich();
}
}
out:
Meal();
Lunch();
PortableLunch();
Bread();
Cheese();
Lettuce();
Sandwich();
通过这个例子我们看到了构造器的的调用顺序:
- 调用基类构造器。这个步骤会不断的反复递归下去,首先是构造这种层次结构的根,然后是下一层导出类,等等,直到最底层导出类。
- 按声明顺序调用成员的初始化方法。
- 调用导出类构造器的主体。
构造动作一经发生,那么对象所有部分的全体成员都会得到构建。而且要确保构造器内部所要使用的成员都已经构建完。为了确保这个目的,唯一办法就是首先调用基类的构造器。
4.协变返回类型
Java SE中添加了协变返回类型,它表示在导出类中的被覆盖方法可以返回基类方法的返回类型的某种到处类型。听着非常绕口,举个例子:
class Grain{
public String toString(){return "Grain";}
}
class Wheat extends Grain{
public String toString{return "Wheat";}
}
class Mill{
Grain process(){return new Grain();}
}
class WheatMill extends Mill{
Wheat process(){return new Wheat();}
}
public class CovariantReturn{
public static void main(String[] args){
Mill m = new Mill();
Grain g = m.process();
System.out.println(g);
m=new WheatMill();
g=m.process();
System.out.println(g);
}
}
out:
Grain
Wheat
例子一目了然 <code>process()</code>方法可以返回<code>Grain</code>的导出类。这就是协变返回类型
纯手敲,总结了一下,希望对大家有用,顺便自己可以当做笔记没事回顾下,有问题可以评论,看到会回答。