JAVA多态的机制(学习心得)

1.前言

        大家好!这一篇博文是本人在学习java多态时的心得也是本人发布第一篇博文,为什么发这篇博文呢?首先一个是受 学生为什么要在CSDN写博客?_csdn写博客有什么用_Faith_xzc的博客-CSDN博客这篇博文的启发,这篇博文让我看到作为一个学习者来说写博文的必要性,第二个是如果我的博文能够让比我起步晚的同志有所收获它就是有意义的,第三个就是如果能够通过写博文来让我们共同成长是一件非常好的事情。

2.对于面向对象——多态

        好了前言介绍完毕我们直入主题!我们知道JAVA是一门面向对象的高级程序设计语言,那么面向对象最主要的特性是什么呢?就我目前所知有三个封装、继承、多态,多态建立在封装和继承的基础上。继承使得子类能够继承父类的特征,并且加入一些新的特征(说白了就是子类默认拥有父类中可以访问的属性和方法,并且可以加入只有自己才拥有的属性和方法)。

        子类是父类的特殊化,每个子类实例都是父类的实例(但是父类实例并不是子类实例),例如一个动物类,它有一个狗类和一个猫类,我们可以说狗是动物、猫是动物但我们不能说动物是狗、动物是猫,是这样的吧?因此我们是不是就可以声明一个父类变量里面存放一个子类的实例对象呢?是的,我们确实可以这样去做,并且这就是多态中的 向上转型 。

        可能有的同志会想既然有了向上转型是不是还有对应的向下转型呢?是的有向下转型这样一个东东,不过向下转型要比向上转型要严格一点。

        首先我们说向上转型是使用父类变量指向子类实例对象的引用,那么向下转型就是使用子类变量指向父类变量中的引用,那我们知道一个大的类型转化为小的类型是不能直接转换的,我们要使用强制转换即将父类变量强制转化为子类类型并将其引用赋值给子类对象就像基本类型转换一样(子类变量 = (子类类型)(父类变量))。

        可能有同志有会想既然是使用子类变量指向父类变量中的引用那么为什么还要使用父类变量进行呢?我直接一点使用  new  关键字返回一个父类引用再强制转换不就好了?em...我们前面说过向下转型要比向上转型要复杂要严格,那么仅仅一个强制转换是不是还不够复杂不够严格啊?这里我们就引入的二个要求就是向下转型的前提要是父类变量指向的引用要是转换子类的实例对象(这个怎么理解呢?可以把它看成是一只披着羊皮的狼,它看着是一只羊但是我在不知道它本质是狼的前提下可不可以让他变成一只狼,肯定是可以的我们只要把它的羊皮扒拉下来它不就是一只狼嘛!)我们同样可以理解父类变量指向子类对象的引用就是一只披着羊皮的狼,我们通过强制转换将它的羊皮扒拉下来它就是一只狼,我们自然就知道它是一只狼可以用狼来称呼它即使用子类变量指向它。通过前面我们知道向下转型的第二个要求是父类变量必须指向一个要转换子类类型的对象(即披着父类皮的子类对象),我们要通过强制转换(把父类强制转换为子类对象用子类变量来指向这个对象),是不是可以说向下转型必然伴随着向上转型。

package douT;

class Animal{
    public void shout(){
        System.out.println("动物叫...");
    }
}

class Dog extends Animal{
    @Override
    public void shout() {
        System.out.println("汪汪汪...");
    }
}

class Cat extends Animal{
    @Override
    public void shout() {
        System.out.println("喵喵喵...");
    }
}

public class Test1 {
    public static void main(String[] args) {
        Animal animal = new Cat();      //向上转型 猫可以说成它是动物
//        Cat cat = (Cat) new Animal();     可以通过编译,编译器分辨不出来它到底是父类对象还是子类对象,到执行它的时候它就"原形毕露"了
                                          //产生异常 ClassCastException,因为强制转换的对象不是Cat类的
        Cat cat = (Cat) animal;         //顺利通过,强制转换一个指向子类对象的父类变量为子类型,有点像Integery = 4; int x = (int)y;

    }
}

        可能有同志会想(其实就是本人自己想过)既然多态这么复杂为什么要用它,它到底有什么用呢?我们可以想象一下如果不使用多态的话当要调用一个方法时是不是要重写多个同样功能只是参数类型不同的方法?这样是不是太麻烦了?代码冗余。有了多态我们可以只写一个这样的方法使用要传入实参它们的父类作为形参自动的进行向上转型完成调用,如果你还想在这个方法里调用实参对象的方法 可以通过(指向子类的父类变量 instanceof 子类类型)来判断是否时当前类型的对象,是则进行向下转换,再调用子类中的方法(这样做会更安全)。这就是多态,可以更简化我们的代码,提高代码的通用性。

package douT;

class Animal{
    public void shout(){
        System.out.println("动物叫...");
    }

    public String info(Animal an){
        if(an instanceof Dog){
            return "Dog类对象";
        }
        else if(an instanceof Cat){
            return "Cat类对象";
        }
        return "";
    }
}

class Dog extends Animal{
    @Override
    public void shout() {
        System.out.println("汪汪汪...");
    }
}

class Cat extends Animal{
    @Override
    public void shout() {
        System.out.println("喵喵喵...");
    }
}

public class Test1 {
    public static void main(String[] args) {
        Animal animal1 = new Cat();
        System.out.println(animal1.info(animal1));
        Animal animal2 = new Dog();
        System.out.println(animal2.info(animal2));
    }
}

     

        我么们可以看到确实是提高代码的复用性。

3.动态绑定(重要

        在继承关系中可能会产生一个继承链,如有类A、B、C,B继承A,C继承B,对于C而言A、B都是它的父类这就构成一个继承链,其实还有一个Object类(所有类的父类),在这条继承链中可能有两个以上的类有相同的方法即有方法被重写了,那么在这个继承链中JVM(虚拟机)会如何调用这些方法呢?或者说调用方法的机制是什么?这就跟动态绑定有关了。

        在说动态绑定之前我们先了解一下声明类型实际类型两个概念。声明类型:声明变量的类型;实际类型:运行时被变量引用的对象的实际类型;

Animal animal = new Cat();    

//在这里animal就是声明类型的变量类型为Animal
//Cat就是实例类型,这里是将Cat对象的应用赋值给animal变量
                                

        我们知道继承机制中父类或父类变量是不可以调用子类中的特有属性和方法的,即使是指向子类对象的父类变量也不可以。有的同志可能会有疑惑(确实我也疑惑过),接下来的动态绑定将为我们答疑解惑。首先调用方法即在内存中调用时时根据声明类型来查找的,即声明类型或其父类里面没有子类特有的方法那自然就查找不到了,查找不到那就报出异常,

package douT;

class Animal{
    public void shout(){
        System.out.println("动物叫...");
    }

    public String info(Animal an){
        if(an instanceof Dog){
            return "Dog类对象";
        }
        else if(an instanceof Cat){
            return "Cat类对象";
        }
        return "";
    }
}

class Dog extends Animal{
    @Override
    public void shout() {
        System.out.println("汪汪汪...");
    }

    public void eat(){
        //Dog中特有的方法Animal父类中没有
        System.out.println("吃骨头...");
    }
}

class Cat extends Animal{
    @Override
    public void shout() {
        System.out.println("喵喵喵...");
    }
}

public class Test1 {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.eat();        
        //这样是行不通的即使animal变量的引用时Dog类,Dog中有eat这个方法,但编译报错
        //原因就在于Animal类中没有eat方法编译器去Animal类或其父类中查找查找不到
        //即调用方法时调用的方法一定要是在声明类型或其父类中有的方法
    }
}

        刚刚我们说到了一个编译器查找方法的机制,那么这个机制是什么样的呢?其实这个机制并不难理解即在一个声明类型中查找要调用的方法,我们知道有继承关系后子类默认拥有父类可访问的方法,如果当前声明类中没有就去父类中查找(默认拥有可访问的方法嘛,注意是可访问的方法,不可访问也是会报出异常的),沿着继承链中当前类的位置往上(父类)查找直到查找到一个可访问的方法(找到了,退出查找)或查找到Object类(最终的父类)中都没有则报出异常(找不到方法)。

package douT;


class fina{
    public void eat(){
    //编写eat方法
        System.out.println("吃食物...");
    }
}

class Animal extends fina{
    //使Animal继承fina这个类
    public void shout(){
        System.out.println("动物叫...");
    }

    public String info(Animal an){
        if(an instanceof Dog){
            return "Dog类对象";
        }
        else if(an instanceof Cat){
            return "Cat类对象";
        }
        return "";
    }
}

class Dog extends Animal{
    @Override
    public void shout() {
        System.out.println("汪汪汪...");
    }

    public void eat(){
        System.out.println("吃骨头...");
    }
}

class Cat extends Animal{
    @Override
    public void shout() {
        System.out.println("喵喵喵...");
    }
}

public class Test1 {
    public static void main(String[] args) {
        //这里的测试类和上一个的是一样的,只不过这里是Animal类继承fina类,
        //并且在fina类中写了一个eat方法使Dog中的eat方法成为重写方法
        //其实也可以在Animal类中写一个eat方法,反正从Animal类往上查找能够找得到就可以调用
        Animal animal2 = new Dog();
        animal2.eat();
    }
}

      那么我们现在又发现一个问题,就是如果我们从声明类型中查找到要调用的方法后会调用那个方法呢?(我们本意是想调用实际类型中的方法,可是声明类型或其父类中也有这样一个方法,即我们调用的实际类型中的方法是一个重写方法),从上面的运行结果我们可以看出我们使如愿以偿的调用到了实际类型中的重写方法,这是为什么呢?我们明明查找到的使声明类型父类中的方法但调用的使实际类型中的重写方法呢?这就得说说编译器和虚拟机那点事儿了,这个编译器有点笨,无法识别声明类型变量中的引用是声明类型子类的对象,看到声明类型就从声明类型往上找;但是这个虚拟机可是有一双”火眼金睛“啊,一眼就看出声明类型变量中的引用是声明类型子类的对象,然后就从这个子类类型开始查找,如果你在看了这么长的文字头脑依旧清晰的话应该能够想的到这里的查找机制和上面声明类型的查找机制是一回事。

        如果同志看到这里依旧觉得很轻松的话那么请继续往下看,多态能够成为面向对象编程的三大特性之一当然不止这一点点东西啦!不知道你有没有想过这样一个问题就是如果调用一个方法但是在实例类型中没有这个方法虚拟机是不是就往上查找其父类啊,父类中有这个方法那么就执行这个方法,但是这个方法体的内容是调用一个当前实例类型对象和其父类都有的一个方法,即子类重写父类的方法,那么同志请想一下现在调用的这个方法体要调用的方法是那个?是父类中的方法还是实际类型对象中的重写方法?即一下代码:

package douT;


class fina{
    public void eat(){
        System.out.println("吃食物...");
    }
}

class Animal extends fina{
    public void shout(){
        System.out.println("动物叫...");
    }

    public String info(Animal an){
        if(an instanceof Dog){
            return "Dog类对象";
        }
        else if(an instanceof Cat){
            return "Cat类对象";
        }
        return "";
    }

    public void play(){
        //被从写要调用的方法
        System.out.println("玩游戏...");
    }

    public void gema(){
    //在测试类中调用的方法
        play();
        //方法体要调用的方法
    }
}

class Dog extends Animal{
    @Override
    public void shout() {
        System.out.println("汪汪汪...");
    }

    public void eat(){
        System.out.println("吃骨头...");
    }

    public void play(){
    //重写要调用的方法
        System.out.println("叼飞盘...");
    }
}

class Cat extends Animal{
    @Override
    public void shout() {
        System.out.println("喵喵喵...");
    }
}

public class Test1 {
    public static void main(String[] args) {
        Animal animal2 = new Dog();
        animal2.gema();
    }
}

        从运行结果我们可以看到输出的是实际类型对象中的重写方法(pleay),有的同志可能会产生疑惑(我就是其中之一)为什么在实际类型对象的父类中查找到的方法不是依靠就近原则调用父类中的方法,反而调用其子类(实际类型对象)中的方法呢?其实我觉得这并没有违反就近原则,这个可以从继承的角度去理解它,实际类型不是继承其父类吗?那么其父类中的可访问方法是不是就默认被实际类型对象继承了?实际类型对象调用它继承来的方法其本质不还是在自己类中进行调用吗?在自己类中依靠就近原则调用自己类中有的方法好像也没有什么问题吧!看到这里不知道同志有没有注意到本段开始用的是查找,确实是查找,查找到父类有这样一个可访问的方法那聪明的虚拟机自然也知道其子类(实际类型对象)也有这样一个方法(继承),那么就调用实际类型对象中的这个方法(我自己的猜想)。注意这一切的前提是我们测试调用的方法必须是子类可访问的否则报出异常(可能连查找都查找不到,查找的方法是当前类和父类中可访问的方法),多态一定要注意父类中的方法是可访问的即访问权限 ,这个已经提示好多次了。

        不知道同志有没有发现我们上面说的都是方法,我们知道一个类有其属性和方法构成。对于类中的属性是不支持动态绑定的。

package douT;


class fina{
    public void eat(){
        System.out.println("吃食物...");
    }
}

class Animal extends fina{
    private int age;
    //定义一个age属性值默认为0

    public int getAge() {
    //定义一个getAge方法,获取age属性
        return age;
    }

    public void shout(){
        System.out.println("动物叫...");
    }

    public String info(Animal an){
        if(an instanceof Dog){
            return "Dog类对象";
        }
        else if(an instanceof Cat){
            return "Cat类对象";
        }
        return "";
    }

    public void play(){
        System.out.println("玩游戏...");
    }

    public void gema(){
        play();
    }
}

class Dog extends Animal{
    private int age = 3;
    //同样定义一个age值为3

    @Override
    public void shout() {
        System.out.println("汪汪汪...");
    }

    public void eat(){
        System.out.println("吃骨头...");
    }

    public void play(){
        System.out.println("叼飞盘...");
    }
}

class Cat extends Animal{
    @Override
    public void shout() {
        System.out.println("喵喵喵...");
    }
}

public class Test1 {
    public static void main(String[] args) {
        Animal animal2 = new Dog();
        System.out.println("age : " + animal2.getAge());
    }
}

        如果按照我们上面讲的动态绑定机制,是不是实例类型对象继承其父类的getAge方法,然后调用这个继承的方法返回实例类型对象的age值,如果类中的属性支持动态绑定的话这个分析应该没有错误(我觉得),我们看一下运行结果:

 

从结果上我们可以看出并没有按照我们上面分析的输出实例对象的3而是输出其父类的age默认值0其原理可能是:当实例类和去父类加载到虚拟机(本例中:先调用实例对象默认无参构造方法再调用父类默认无参构造方法创建对象)中时其各自的属性都生成并赋值,虽然实例类型对象和其父类对象中的属性名称相同但属于不同对象虚拟机可以区分,他们都存储在同一个堆内存中,调用属性时不知道怎么就调用了父类对象的属性(如以后明白了会补上),但从上面已经可以看出类中的属性并不具备动态绑定。

4.致谢

        好了,罗里吧嗦的写了这么多一路写下来我也不知道自己写了些什么,想到什么写什么(可能有单杂乱),如果同志看到这里觉得写的不好或有错误还请恕罪!1041842330@qq.com 这是本人的邮箱可以及时与我沟通或评论留言,以及时更正避免误导其他同志何其过错。如果同志有什么想要对我说的恳请留言,本人渴望在这方面与同志交流!如果有好的建议或意见本人将洗耳恭听!

         本人写这篇博文的时候受韩老师的java课程益处良多,在此致谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值