多态的由来
class Dog{
public void eat(){
System.out.println("啃骨头");
}
public void lookHome(){
System.out.println("看家");
}
}
class Cat{
public void eat(){
System.out.println("吃鱼");
}
public void catchMouse(){
System.out.println("抓老鼠");
}
}
class DuoTaiDemo{
public static void main(String[] args) {
/*
Dog d = new Dog();
d.eat();
有多个狗和猫对象都要调用吃饭这个行为,这样会导致d.eat();代码重复性非常严重
为了提高代码的复用性,可以将d.eat();代码进行封装
public static void method(Dog d) {
d.eat();
}
然后创建对象,直接调用method方法即可
method(new Dog());
method(new Dog());
但当创建Cat对象时,同样需要调用eat方法,同样可以将eat方法进行封装
public static void method(Cat c) {
c.eat();
}
*/
}
}
如果后期有了牛对象,同样需要封装eat方法,当每多一个动物,都要单独定义功能。封装方法就是让动物的对象去做事,会发现代码的扩展性很差。
如何提高代码的扩展性呢?
可以发现既然是让动物去eat,无论是dog,还是cat,eat是他们的共性,那么将eat进行抽取,抽取到父类中。
对Java当中的对象而言,对象的多态指的是啥?
指的就是对象可以当做其父类数据类型的一个对象来使用,不能当做子类数据类型的对象来使用
abstract class Animal{
//由于每一个小动物的eat方式都不一样,因此在父类中无法准确描述eat的具体行为
//因此只能使用抽象方法描述,从而导致这个类也为抽象类
abstract public void eat();
}
当有了Animal抽象类之后,狗和猫只要继承这个类,实现他们特有的eat方法即可。
//描述狗,狗有吃饭,看家的行为
class Dog extends Animal{
public void eat(){
System.out.println("啃骨头");
}
public void lookHome(){
System.out.println("看家");
}
}
//描述猫,猫有吃饭,抓老鼠行为
class Cat extends Animal{
public void eat(){
System.out.println("吃鱼");
}
public void catchMouse(){
System.out.println("抓老鼠");
}
}
既然Dog属于Animal中一种,Cat也属于Animal中一种,那么不用具体面对具体的动物,而只要面对Animal即可。
Dog d = new Dog();
Animal a = new Dog();
Cat c = new Cat();
Animal aa = new Cat();
通过上述代码发现,Animal类型既可以接受Dog类型,也可以接受Cat类型,当再让动物去做事时,不用面对具体的动物,而只要面对Animal即可。因此上述method方法可以修改为:
public static void method(Animal a){
a.eat();
}
method(Animal a)可以接受Animal的子类型的所有小动物,而method方法不用再关心是具体的哪一个类型。即就是只建立Animal的引用就可以接收所有的Dog和Cat对象进来,让它们去eat。从而提高了程序的扩展性。
其实上述代码就已经形成了多态父类的引用或者接口的引用指向了自己的子类对象。
Dog d = new Dog();//Dog对象的类型是Dog类型。
Animal a = new Dog();//Dog对象的类型右边是Dog类型,左边Animal类型。
多态的特点
优点:
提高了程序的扩展性,降低程序的冗余
弊端:
通过父类引用操作子类对象时,只能使用父类中已有的方法,或者被子类重写的方法,不能操作子类特有的方法
实现要求:
- 必须有关系:继承,实现。
- 通常都有重写操作
转型
当父类的引用指向子类对象时,就发生了向上转型,即把子类类型对象转成了父类类型。向上转型的好处是隐藏了子类类型,提高了代码的扩展性。但向上转型也有弊端,只能使用父类共性的内容,而无法使用子类特有功能,功能有限制。向上转型并不改变对象自身原本的数据类型
//描述动物这种事物的共性eat
abstract class Animal{
abstract void eat();
}
//描述dog这类事物
class Dog extends Animal{
void eat(){
System.out.println("啃骨头");
}
void lookHome(){
System.out.println("看家");
}
}
//描述小猫
class Cat extends Animal{
void eat(){
System.out.println("吃鱼");
}
void catchMouse(){
System.out.println("抓老鼠");
}
}
public class Test {
public static void main(String[] args) {
Animal a = new Dog(); //这里形成了多态
a.eat();
//a.lookHome();//使用Dog特有的方法,需要向下转型
Dog d = (Dog)a;
d.lookHome();
Animal a1 = new Cat();
a1.eat();
/*
由于a1具体指向的是Cat的实例,而不是Dog实例,这时将a1强制转成Dog类型,将会发生 ClassCastException异常,在转之前需要做健壮性判断
if(!Dog instanceof a1){
// 判断当前对象是否是Dog类型
System.out.println("类型不匹配,不能转换");
return;
// 方法执行中止
}
Dog d1 = (Dog)a1;
d1.catchMouse();
*/
}
}
什么时候使用向上转型
当不需要面对子类类型时,通过提高扩展性,或者使用父类的功能就能完成相应的操作,这时就可以使用向上转型
什么时候使用向下转型
当要使用子类特有功能时,就需要使用向下转型(强制转换 将大类型转为小类型)。
在转换的时候,就得需要先判断数据类型,否则会发生ClassCastException 类型转换异常
Animal a = new Dog();
Cat cat = (Cat) a; //Error a本身的对象Dog类型 Dog类型不能转为Cat类型
Animal a = new Animal();
Dog dog = (Dog) a; //Error a本身的对象Animal类型 Animal不能转为Dog类型
D extends C extends B extends A
对于C c而言,c可以向上转型为B b或A a
那么a b向下转型时 只能强制到C这个数据类型
C不能穿D的衣服 同样C也不能向下转型为D
以下为一个转型的示例:
/*
描述张老师和张姥爷
张老师拥有讲课和看电影功能
张姥爷拥有讲课和钓鱼功能
*/
class 张姥爷{
void 讲课(){
System.out.println("政治");
}
void 钓鱼(){
System.out.println("钓鱼");
}
}
//张老师继承了张姥爷,就有拥有了张姥爷的讲课和钓鱼的功能,
//但张老师和张姥爷的讲课内容不一样,因此张老师要覆盖张姥爷的讲课功能
class 张老师 extends 张姥爷{
void 讲课(){
System.out.println("Java");
}
void 看电影(){
System.out.println("看电影");
}
}
public class Test {
public static void main(String[] args) {
//多态形式
张姥爷 a = new 张老师(); //向上转型
a.讲课(); // 这里表象是张姥爷,其实真正讲课的仍然是张老师,因此调用的也是张老师的讲课功能
a.钓鱼(); // 这里表象是张姥爷,但对象其实是张老师,而张老师继承了张姥爷,即张老师也具体钓鱼功能
// 当要调用张老师特有的看电影功能时,就必须进行类型转换
张老师 b = (张老师)a; //向下转型
b.看电影();
}
}
总结:转型过程中,至始至终只有张老师对象做着类型转换,父类对象是无法转成子类对象的。
多态中成员的特点
多态出现后会导致子父类中的成员变量有微弱的变化。
代码如下:
class Fu{
int num = 4;
}
class Zi extends Fu{
int num = 5;
}
class Demo{
public static void main(String[] args){
Fu f = new Zi();
System.out.println(f.num);
Zi z = new Zi();
System.out.println(z.num);
}
}
多态成员变量
当子父类中出现同名的成员变量时,多态调用该变量时:
- 编译时期:参考的是引用型变量所属的类中是否有被调用的成员变量。没有,编译失败。
- 运行时期:也是调用引用型变量所属的类中的成员变量。
- 简单记:编译运行看左边。
class Fu{
int num = 4;
void show(){
System.out.println("Fu show num");
}
}
class Zi extends Fu{
int num = 5;
void show(){
System.out.println("Zi show num");
}
}
class Demo{
public static void main(String[] args){
Fu f = new Zi();
f.show();
}
}
多态成员函数
- 编译时期:参考引用变量所属的类,如果没有类中没有调用的函数,编译失败。
- 运行时期:参考引用变量所指的对象所属的类,并运行对象所属类中的成员函数。
- 简而言之:编译看左边,运行看右边。
class Fu{
int num = 4;
static void method(){
System.out.println("fu static method run");
}
}
class Zi extends Fu{
int num = 5;
static void method(){
System.out.println("zi static method run");
}
}
class Demo{
public static void main(String[] args) {
Fu f = new Zi();
f.method();
}
}
多态静态函数
- 多态调用时:编译和运行都参考引用类型变量所属的类中的静态函数。
- 简而言之:编译和运行看等号的左边。其实真正调用静态方法是不需要对象的,静态方法通过类直接调用。
结论:
- 对于成员变量和静态函数,编译和运行都看左边。
- 对于成员函数,编译看左边,运行看右边
简单来说,如以下代码:
public class DuoTaiDemo {
public static void main(String[] args) {
Fu fu = new Zi();
//当打印成员变量时 始终打印的是父类的成员变量
//与子类的成员变量无关
System.out.println(fu.num);
//当调用成员函数时 使用的是父类的成员函数 或者是子类重写的父类函数
fu.show();
}
}
class Fu {
int num = 5;
public void show() {
System.out.println("Fu show...");
}
}
class Zi extends Fu{
int num = 10;
public void show() {
System.out.println("Zi show....");
}
}