将改变的事物与未变的事物分离开来
多态简单的说就是一个行为具有不同的表现形式。
多态方法调用允许一种类型表现出与其它相似类型之间的区别,只要它们都是同一个基类导出而来。这种区别是根据方法行为的不同而表现出来的,虽然这些方法都可以通过同一个基类来调用。
class Animal {
void eat() {
System.out.println("eat something...");
}
}
class Dog extends Animal {
@Override
void eat() {
System.out.println("eat meat...");
}
}
class Cat extends Animal {
@Override
void eat() {
System.out.println("eat fish...");
}
}
public class PolymorphismTest {
public static void main(String[] args) {
Random random = new Random();
Animal[] animals = new Animal[5];
for (int i = 0; i < animals.length; i++) {
switch (random.nextInt(3)) {
case 0:
animals[i] = new Animal();
break;
case 1:
animals[i] = new Dog();
break;
case 2:
animals[i] = new Cat();
break;
}
}
for (Animal animal : animals) {
animal.eat();
}
}
}
/*
* Output:
eat meat...
eat fish...
eat fish...
eat meat...
eat something...
*/
Java之所以能实现这种看起来像是忘记了类型的引用,但调用时却能准确调用不同类型的方法的行为,是因为Java的动态绑定机制。
JAVA虚拟机调用一个类方法时,它会基于对象引用的类型(通常在编译时
可知)来选择所调用的方法。相反,当虚拟机调用一个实例方法时,它会
基于对象实际的类型(只能在运行时得知)来选择所调用的方法,这就是动态绑定。
所以可以看出要实现多态就需要满足下面三个条件:
1.继承/实现接口
2.重写父类方法
3.向上转型(故意的“忘记类型”)
public class Father {
public int field = 0;
private void f() {
System.out.println("father private method");
}
final void g(){
System.out.println("father final method");
}
static void staticMethod() {
System.out.println("father static method");
}
public static void main(String[] args) {
Father child = new Child();
System.out.println(child.field);
child.f();
child.staticMethod();
}
}
class Child extends Father {
public int field = 1;
void f() {
System.out.println("child method");
}
//final方法不能被重写
// void g(){
// System.out.println("child method");
// }
static void staticMethod() {
System.out.println("child static method");
}
}
/*
* Output:
0
father private method
father static method
*/
可以看到我们用了父类的引用指向了一个子类的对象(向上转型),获取了成员属性,调用了一个父类用private修饰,子类也存在的一个方法,调用了一个父类用static修饰,子类也存在的一个方法。输出却显示获取了父类的成员属性,调用了父类的成员方法。刚才动态绑定不是说,调用一个实例方法时,它会基于对象实际的类型(只能在运行时得知)来选择所调用的么?
因为他们根本就不是动态绑定的。
成员属性、private、static、final 方法或者是构造器都是不能够被重写的,都是属于静态绑定,在编译时就已经确认了调用。
再看一个特殊的例子
(以下例子及分析来自重新认识java(五) —- 面向对象之多态(向上转型与向下转型))
class A {
public String show(D obj) {
return ("A and D");
}
public String show(A obj) {
return ("A and A");
}
}
class B extends A{
public String show(B obj){
return ("B and B");
}
public String show(A obj){
return ("B and A");
}
}
class C extends B{
}
class D extends B{
}
public class Demo {
public static void main(String[] args) {
A a1 = new A();
A a2 = new B();
B b = new B();
C c = new C();
D d = new D();
System.out.println("1--" + a1.show(b));
System.out.println("2--" + a1.show(c));
System.out.println("3--" + a1.show(d));
System.out.println("4--" + a2.show(b));
System.out.println("5--" + a2.show(c));
System.out.println("6--" + a2.show(d));
System.out.println("7--" + b.show(b));
System.out.println("8--" + b.show(c));
System.out.println("9--" + b.show(d));
}
}
//结果:
//1--A and A
//2--A and A
//3--A and D
//4--B and A
//5--B and A
//6--A and D
//7--B and B
//8--B and B
//9--A and D
//能看懂这个结果么?先自分析一下。
前三个,强行分析,还能看得懂。但是第四个,大概你就傻了吧。为什么不是b and b呢?
这里就要学点新东西了。
当父类对象引用变量引用子类对象时,被引用对象的类型决定了调用谁的成员方法,引用变量类型决定可调用的方法。
如果子类中没有覆盖该方法,那么会去父类中寻找。
可能读起来比较拗口,我们先来看一个简单的例子:
class X {
public void show(Y y){
System.out.println("x and y");
}
public void show(){
System.out.println("only x");
}
}
class Y extends X {
public void show(Y y){
System.out.println("y and y");
}
public void show(int i){
}
}
class main{
public static void main(String[] args) {
X x = new Y();
x.show(new Y());
x.show();
}
}
//结果
//y and y
//only x
Y继承了X,覆盖了X中的show(Y y)方法,但是没有覆盖show()方法。
这个时候,引用类型为X的x指向的对象为Y,这个时候,调用的方法由Y决定,会先从Y中寻找。执行x.show(new Y());,该方法在Y中定义了,所以执行的是Y里面的方法;
但是执行x.show();的时候,有的人会说,Y中没有这个方法啊?它好像是去父类中找该方法了,因为调用了X中的方法。
事实上,Y类中是有show()方法的,这个方法继承自X,只不过没有覆盖该方法,所以没有在Y中明确写出来而已,看起来像是调用了X中的方法,实际上调用的还是Y中的。
这个时候再看上面那句难理解的话就不难理解了吧。X是引用变量类型,它决定哪些方法可以调用;show()和show(Y y)可以调用,而show(int i)不可以调用。Y是被引用对象的类型,它决定了调用谁的方法:调用y的方法。
上面的是一个简单的知识,它还不足以让我们理解那个复杂的例子。我们再来看这样一个知识:
继承链中对象方法的调用的优先级:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
如果你能理解这个调用关系,那么多态你就掌握了。我们回到那个复杂的例子:
abcd的关系是这样的:C/D —> B —> A
我们先来分析4 : a2.show(b)
- 首先,a2是类型为A的引用类型,它指向类型为B的对象。A确定可调用的方法:show(D obj)和show(A obj)。
- a2.show(b) ==> this.show(b),这里this指的是B。
- 然后.在B类中找show(B obj),找到了,可惜没用,因为show(B obj)方法不在可调用范围内,this.show(O)失败,进入下一级别:super.show(O),super指的是A。
- 在A 中寻找show(B obj),失败,因为没用定义这个方法。进入第三级别:this.show((super)O),this指的是B。
- 在B中找show((A)O),找到了:show(A obj),选择调用该方法。
- 输出:B and A
public class Father {
public int field = 0;
public Father() {
System.out.println("father construct");
}
public Father(int field) {
this.field = field;
System.out.println("father construct with arg");
}
private void f() {
System.out.println("father private method");
}
public static void staticMethod() {
System.out.println("father static method");
}
public static void main(String[] args) {
Father child = new Child();
child = new Child(1);
}
}
class Child extends Father {
public int field = 1;
public Child() {
System.out.println("child construct");
}
public Child(int field) {
this.field = field;
System.out.println("child construct with arg");
}
public void f() {
System.out.println("child method");
}
public static void staticMethod() {
System.out.println("child static method");
}
}
/*
* Output:
father construct
child construct
father construct
child construct with arg
*/
可以看到在每次调用子类的构造器之前都调用了父类的默认构造器,即使并没有显示的调用父类构造器。
这是因为每当创建一个子类的时候,该对象包含了一个父类的对象(也就是在子类构造器、方法中、成员变量初始化时可以引用的super),这个父类的对象被包装在子类对象的内部。
既然知道了这一点其实就很好理解了。既然在子类中的构造器都可以引用那个父类的对象(super),那也就是说在子类构造之前这个父类的对象就已经被构造出来了——通过隐式(或显示)的调用父类构造器而构造。这也解释了为什么显示调用父类构造器必须写在子类构造器的最前面。如果没有显示的调用父类构造器,就会“偷偷的”调用父类的默认构造器,如果父类的默认构造器不存在,编译器就会报错。
Implicit super constructor Father() is undefined. Must explicitly invoke another constructor
提示当父类默认构造器不存在的时候子类需要显示的调用父类的其他构造器。
如果在一个构造器内部调用正在构造对象的某个动态绑定方法,会是什么样子呢?
class Glyph{
Glyph() {
System.out.println("Glyph() before draw()");
draw();
System.out.println("Glyph() after draw()");
}
void draw(){
System.out.println("Glyph.draw()");
}
}
class RoundGlyph extends Glyph{
private int radius=1;
public RoundGlyph(int radius) {
super();
this.radius = radius;
System.out.println("RoundGlyph.RoundGlyph(),radius = "+radius);
}
void draw(){
System.out.println("RoundGlyph.draw(),radius="+radius);
}
}
public class PolyConstructors {
public static void main(String[] args){
new RoundGlyph(5);
}
}
/*
* Output:
Glyph() before draw()
RoundGlyph.draw(),radius=0
Glyph() after draw()
RoundGlyph.RoundGlyph(),radius = 5
*/
因为子类重写了父类的方法,运行时动态绑定调用了子类的方法,可以看到子类中的方法在子类对象完全构造完成之前就被调用了。但是其中成员属性的值却显得有些匪夷所思,为什么不是初始化时的1?因为我们知道成员属性的初始化执行顺序应该优先于构造函数的。
但是初始化的实际过程是:
1)在其他任何事物发生之前,将分配给对象的存储空间初始化城二进制的零
2)调用父类构造器
3)按照声明顺序调用成员初始化方法
4)调用子类构造器主体
因为成员初始化前的分配时将空间置零(基本变量值0,对象引用为null),而且父类构造器的执行顺序优于子类构造器甚至优于成员变量的初始化。毕竟在成员变量初始化的时候就已经可以引用super了。
欢迎批评指正^ ^