多态存在的三个前提:

  1. 要有继承关系
  2. 子类要重写父类的方法
  3. 父类引用指向子类

如下是多态的小实例:

父类Animal
class Animal{
int num=10;
static int age = 20;
 
public void eat(){
System.out.println(“动物吃饭”);
}
public static void sleep(){
System.out.println(“动物在睡觉”);
}
 
Public void run(){
System.out.println(“动物在奔跑”);
}
}

 

子类cat

Class Cat extends Animal{
Int num=80;
Static int age = “tomcat”;
 
Public void eat(){
System.out.println(“猫在吃饭”);
}
 
Public static void sleep(){
System.out.println(“猫在睡觉”);
}
 
Public void catchMouse(){
System.out.println(“猫在抓老鼠”);
}
}
 
测试类Demo
Class Demo{
Public static void main(String[] args){
Animal am = new Cat();
Am.eat();
Am.sleep();
Am.run();
 
System.out.println(am.num);
System.out.println(am.age);
}
}

1存在继承关系

Cat类继承了Aniaml类

2子类要重写父类的方法

子类重写了父类的两个成员方法eat(),sleep()。其中eat()是非静态的,sleep()是静态的

3父类数据类型的引用指向子类对象

测试类Demo中Animal am = new Cat();语句在堆内存中开辟了子类的对象,并把栈内存中的父类的引用指向了这个对象

 

输出结果为:

猫在吃饭

动物在睡觉

动物在奔跑

10

20

可以看出

子类Cat重写了父类Animal的非静态成员方法am.eat()的输出结果为:猫在吃饭

子类重写类父类的静态成员方法am.sleep();输出的结果为:动物在睡觉

未被子类重写的父类方法am.run()输出结果为:动物在奔跑

 

那么我们可以根据以上情况总结出多态成员访问的特点:

成员变量

编译看左边(父类),运行看左边(父类)

成员方法

编译看左边(父类),运行看右边(子类)。动态绑定

静态方法

编译看左边(父类),运行看左边(父类)

 

(静态和类相关,算不上重写,所以,访问还是左边的)

只有非静态的成员方法,编译看左边,运行看右边

 

当父类的数据类型的引用指向子类时:

不能使用子类特有的成员属性和子类特有的成员方法

 

 

如果想在代码执行过程中还想使用Cat类中特有的属性String name和它特有的成员方法

CatchMouse(),那我们就可以把这个父类引用指向子类对象的家伙am在强制变回Cat类型。这样am就是Cat类型的引用了,指向的也是Cat对象,自然也能使用cat类的一切属性和一切的成员方法

 

 

  初学Java时,在很长一段时间里,总觉得基本概念很模糊。后来才知道,在许多Java书中,把对象和对象的引用混为一谈。可是,如果我分不清对象与对象引用,那实在没法很好地理解下面的面向对象技术。把自己的一点认识写下来,或许能让初学Java的朋友们少走一点弯路。

       为便于说明,我们先定义一个简单的类:

class Vehicle {
        int passengers;      
        int fuelcap;
        int mpg;
}

有了这个模板,就可以用它来创建对象:

Vehicle veh1 = new Vehicle();

通常把这条语句的动作称之为创建一个对象,其实,它包含了四个动作。

1)右边的“new Vehicle”,是以Vehicle类为模板,在堆空间里创建一个Vehicle类对象(也简称为Vehicle对象)。

2)末尾的()意味着,在对象创建后,立即调用Vehicle类的构造函数,对刚生成的对象进行初始化。构造函数是肯定有的。如果你没写,Java会给你补上一个默认的构造函数。

3)左边的“Vehicle veh1”创建了一个Vehicle类引用变量。所谓Vehicle类引用,就是以后可以用来指向Vehicle对象的对象引用。

4)“=”操作符使对象引用指向刚创建的那个Vehicle对象。

我们可以把这条语句拆成两部分:

Vehicle veh1;
veh1 = new Vehicle();

效果是一样的。这样写,就比较清楚了,有两个实体:一是对象引用变量,一是对象本身。

       在堆空间里创建的实体,与在数据段以及栈空间里创建的实体不同。尽管它们也是确确实实存在的实体,但是,我们看不见,也摸不着。不仅如此,我们仔细研究一下第二句,找找刚创建的对象叫什么名字?有人说,它叫“Vehicle”。不对,“Vehicle”是类(对象的创建模板)的名字。一个Vehicle类可以据此创建出无数个对象,这些对象不可能全叫“Vehicle”。

       对象连名都没有,没法直接访问它。我们只能通过对象引用来间接访问对象。

       为了形象地说明对象、引用及它们之间的关系,可以做一个或许不很妥当的比喻。对象好比是一只很大的气球,大到我们抓不住它。引用变量是一根绳,可以用来系汽球。

       如果只执行了第一条语句,还没执行第二条,此时创建的引用变量veh1还没指向任何一个对象,它的值是null。引用变量可以指向某个对象,或者为null。它是一根绳,一根还没有系上任何一个汽球的绳。执行了第二句后,一只新汽球做出来了,并被系在veh1这根绳上。我们抓住这根绳,就等于抓住了那只汽球。

       再来一句:

Vehicle veh2;

就又做了一根绳,还没系上汽球。如果再加一句:

 

veh2 = veh1;

系上了。这里,发生了复制行为。但是,要说明的是,对象本身并没有被复制,被复制的只是对象引用。结果是,veh2也指向了veh1所指向的对象。两根绳系的是同一只汽球。

       如果用下句再创建一个对象:

veh2 = new Vehicle();

则引用变量veh2改指向第二个对象。

       从以上叙述再推演下去,我们可以获得以下结论:(1)一个对象引用可以指向0个或1个对象(一根绳子可以不系汽球,也可以系一个汽球);(2)一个对象可以有N个引用指向它(可以有N条绳子系住一个汽球)。

       如果再来下面语句:

    

veh1 = veh2;

按上面的推断,veh1也指向了第二个对象。这个没问题。问题是第一个对象呢?没有一条绳子系住它,它飞了。多数书里说,它被Java的垃圾回收机制回收了。这不确切。正确地说,它已成为垃圾回收机制的处理对象。至于什么时候真正被回收,那要看垃圾回收机制的心情了。

       由此看来,下面的语句应该不合法吧?至少是没用的吧?

new Vehicle();

不对。它是合法的,而且可用的。譬如,如果我们仅仅为了打印而生成一个对象,就不需要用引用变量来系住它。最常见的就是打印字符串:

System.out.println(“I am Java!”);

字符串对象“I am Java!”在打印后即被丢弃。有人把这种对象称之为临时对象。

       对象与引用的关系将持续到对象回收。但是,关于这一点,打算在下文“简述Java回收机制”再说。

 

 

java多态性,父类引用指向子类对象

父类引用指向子类对象指的是:

例如父类Animal,子类Cat,Dog。其中Animal可以是类也可以是接口,Cat和Dog是继承或实现Animal的子类。

Animal animal = new Cat();


即声明的是父类,实际指向的是子类的一个对象。

那我们从内存角度来理解试试. 
假设现在有一个父类Father,它里面的变量需要占用1M内存. 
有一个它的子类Son,它里面的变量需要占用0.5M内存. 
现在通过代码来看看内存的分配情况:

Father f = new Father();//系统将分配1M内存.
Son s = new Son();//系统将分配1.5M内存!

因为子类中有一个隐藏的引用super会指向父类实例,所以在实例化子类之前会先实例化一个父类,也就是说会先执行父类的构造函数.由于s中包含了父类的实例,所以s可以调用父类的方法.

Son s1 = s; //s1指向那1.5M的内存.
Father f1 = (Father)s;//这时f1会指向那1.5M内存中的1M内存

f1只是指向了s中实例的父类实例对象,所以f1只能调用父类的方法(存储在1M内存中),而不能调用子类的方法(存储在0.5M内存中).

S

on s2 = (Son)f;//这句代码运行时会报ClassCastException.

因为 f 中只有1M内存,而子类的引用都必须要有1.5M的内存,所以无法转换.

Son s3 = (Son)f1;//这句可以通过运行,这时s3指向那1.5M的内存.

由于f1是由s转换过来的,所以它是有1.5M的内存的,只是它指向的只有1M内存. 

如果能够理解对象在内存的分布,那我们接下来就介绍java的多态性

方法的重写、重载和动态链接构成了java的多态性。Java之所以引入多态的概念,原因之一是它在类的继承问题上和C++不同,后者允许多继承,这确实给其带来的非常强大的功能,但是复杂的继承关系也给C++开发者带来了更大的麻烦,为了规避风险,Java只允许单继承,派生类与基类间有IS-A的关系(即“猫”is a “动物”)。这样做虽然保证了继承关系的简单明了,但是势必在功能上有很大的限制,所以,Java引入了多态性的概念以弥补这点的不足。

理解多态性,首先要理解的就是“向上转型”

Animal c = new Cat();


它表示我定义了一个Animal类型的引用,指向新建的Cat类型的对象。由于Cat是继承自它的父类Animal,所以Animal类型的引用是可以指向Cat类型的对象的。这就是“向上转型”。

注意:java中“向上转型”是自动的。但是“向下转型”却不是自动的。需要我们用强制类型转化。

Animal c = new Cat();
Cat c1 = (Cat)c;    //不允许“向下转型”,需用强制类型转化。

那么这样做有什么意义呢?因为子类是对父类的改进和扩充。定义一个父类类型的引用指向一个子类的对象既可以使用子类强大的功能,又可以抽取父类的共性。 但是父类类型的引用可以调用父类中定义的所有属性和方法,而对于子类中定义而父类中没有的方法,父类引用是无法调用的;

要做到父类引用调用子类的属性或者方法,就还要有动态连接。那什么是动态链接呢?当父类中的一个方法只有在父类中定义而在子类中没有被重写的情况下,才可以被父类类型的引用调用; 对于父类中定义的方法,如果子类中重写了该方法,那么父类类型的引用将会调用子类中的这个方法,这就是动态连接。

JAVA里没有多继承,一个类只能有一个父类。而继承的表现就是多态。一个父类可以有多个子类,而在子类里可以重写父类的方法,这样每个子类里重写的代码不一样,自然表现形式就不一样。这样用父类的变量去引用不同的子类,在调用这个相同的方法的时候得到的结果和表现形式就不一样了,这就是多态,相同的消息(也就是调用相同的方法)会有不同的结果。举例说明:
下面看一个多态性的例子

//父类 
public class Father{ 
//父类有一个打孩子方法 
public void hitChild(){ 
    } 
} 
//子类1 
public class Son1 extends Father{ 
//重写父类打孩子方法 
public void hitChild(){ 
      System.out.println("为什么打我?我做错什么了!"); 
    } 
} 
//子类2 
public class Son2 extends Father{ 
//重写父类打孩子方法 
public void hitChild(){ 
      System.out.println("我知道错了,别打了!"); 
    } 
} 
//子类3 
public class Son3 extends Father{ 
//重写父类打孩子方法 
public void hitChild(){ 
      System.out.println("我跑,你打不着!"); 
    } 
} 
//测试类 
public class Test{ 
public static void main(String args[]){ 
      Father father; 
      father = new Son1(); 
      father.hitChild(); 
      father = new Son2(); 
      father.hitChild(); 
      father = new Son3(); 
      father.hitChild(); 
    } 
} 
上面程序调用同一个方法,去

出现不同的结果。这就是多态。 
对于多态性的实现有必要重视方法重载(overloading)和方法重写(override)区别

class Father{ 
public void func1(){ 
       System.out.println("AAA"); 
    } 
} 
 
class Child extends Father{ 
//func1(int i)是对func1()方法的一个重载,主要不是重写!
//由于在父类中没有定义这个方法,所以它不能被父类类型的引用调用 
//所以在下面的main方法中child.func1(68)是不对的 
public void func1(int i){ 
        System.out.println("BBB"); 
    }  
} 
 
public class PolymorphismTest { 
public static void main(String[] args) { 
        Father child = new Child(); 
        child.func1(68);//错误
    } 
}

上面的程序是个很典型的多态的例子。子类Child继承了父类Father,并重载了父类的func1()方法。重载后的func1(int i)和func1()不再是同一个方法,由于父类中没有func1(int i),那么,父类类型的引用child就不能调用func1(int i)方法。

最后,对于多态做出几点总结

1、使用父类类型的引用指向子类的对象; 
2、该引用只能调用父类中定义的方法和变量; 
3、如果子类中重写了父类中的一个方法,那么在调用这个方法的时候,将会调用子类中的这个方法;(动态连接、动态调用) 
4、变量不能被重写(覆盖),”重写“的概念只针对方法,如果在子类中”重写“了父类中的变量,那么在编译时会报错。

为什么要区分编译时类型和运行时类型?

看这样一句代码:Person p=new Women()(Women类继承自Person类)那么,假如p的属性修饰符为public 访问属性时得到的是Person类的属性还是Women类的属性,方法调用又是哪个类?答案:会得到Person类的属性,调用Women类的方法。为什么会这样呢?这里就需要知道什么是编译时类型和运行时类型,java程序状态会分为编译和运行这两种状态,编译时,JVM会在栈中静态创建基本数据变量,和引用数据变量的引用,回到刚刚那句代码,显然,p这个引用就是在编译时创建的,那么,p的编译时类型就是Person了,当运行这句java代码时,JVM在堆中为p新建一块内存,对应new Women()这句代码,所以p的运行时类型就是Women。有这样一条规则,对象调用编译时类型的属性和运行时类型的方法。下面先用代码表示这样的结果,然后再说明我个人的一些理解。

code1

public class TestDemo1 {
 
public static void main(String[] args) {
// TODO Auto-generated method stub
        Person p=new Women();
        System.out.println("p.name:"+p.name);
        p.show();
    }
 
}
class Person{
public String name;
public Person()
    {
        name="person";
    }
public void show()
    {
        System.out.println("class person's show()");
    }
}
 
class Women extends Person
{
public String name;
public Women()
    {
        name="women";
    }
public void show()
    {
        System.out.println("class women's show()");
    }
}结果如下

从代码运行结果可以看出,p调用的属性属于Person类,而调用的方法是Women类的,验证了上面的规则–对象调用编译时类型的属性和运行时类型的方法

个人理解

这里属于我个人的理解,可能有错,以后发现会重新修正 
根据上述规则 
根据继承的特点我们可以知道,子类会继承父类非私有的属性和方法,也就是说,父类的(非私有)属性也会出现在子类中,当然,这是显而易见的,然而关键在于,如果子类重新定义了这一属性,会怎么样呢?实际上,父类的属性并不会被覆盖,为了方便起见,我把从父类继承来的属性记为– 属性<父类> 而自己重新定义的同名属性为–属性<子类> 这样,在子类中,会有两个属性 即:属性<父类> 属性<子类>,那么如何调用呢?–解答:<>中的内容对应着调用该属性的对象的编译时类型,编译时类型为父类,调用属性<父类> ,另一种情况就是调用子类的属性了。

Class A中定义属性a,Class B继承自A,重新定义了属性a,此时,B中有编译时类型为A的属性a和编译时类型为B的属性a,Class C继承自B,自己重新定义了属性a,这时,C具有三种编译时类型的属性a。这样就好看多了,不知道应该调用的属性是哪个类的,就只要分析自己的编译时类型就可以了,调用方法其实不用在意,直接调用运行时类型的方法即可(运行时类型还是比较容易看的)。 
就上图的例子我们用代码测试如下

code2

 

public class TestDemo2 {
 
public static void main(String[] args) {
// 编译时类型为A,输出应该是A
        System.out.println("编译时类型为A,输出应该是A");
        A a=new A();
        System.out.println(a.name);
        A ab=new B();
        System.out.println(ab.name);
        A ac=new C();
        System.out.println(ac.name);
// 编译时类型为B,输出应该是B
        System.out.println("编译时类型为B,输出应该是B");
        B b=new B();
        System.out.println(b.name);
        B bc=new B();
        System.out.println(bc.name);
// 编译时类型为C,输出应该是C
        System.out.println("编译时类型为C,输出应该是C");
        C c=new C();
        System.out.println(c.name);
    }
 
}
class A
{
    String name="A";
}
class B extends A
{
    String name="B";
}
class C extends B
{
    String name="C";
}37

根据运行结果上述解释应该是合理的!