您的位置:首页 > 编程语言 > Java开发

[疯狂Java]面向对象:多态、编译时类型、运行时类型、向上兼容、兼容下反转、instanceof

2016-06-24 11:43 441 查看
1. 想明白多态必须先理解两个概念:编译时类型和运行时类型

    1) 这两种类型都是针对引用变量的;

    2) 编译时类型:

         i. 是声明引用变量时的类型;

         ii. 例如:String s;  // 那么s的编译时类型就是String

    3) 运行时类型:

         i. 是引用实际指向的对象的类型,和编译时类型完全没有任何关系;

         ii. 例如:String s = new Father();  // s的运行是类型是由其指向的对象决定的,因此其运行时类型是Father

!!虽然上述编译不会通过,但这里只是举个极端的例子来演示;

         iii. 再例如:String a = new Father();  Son s = a;  // 经过两层链接后s的运行是类型还是Father,一定要看s最终指向的堆中的对象的类型是什么,这里s的编译时类型是Son,而运行时类型还是Father;

!!即运行时类型和编译时类型没有任何关系;

    4) 一个引用只能访问到其编译时类型中的内容:

         i. 一个引用能访问的范围是由其编译时类型决定的!

         ii. 例如:String s = new Panel();  // 虽然编译不通过,但这里只是作为一个极端的例子来演示

             a. 使用引用s只能访问String的数据成员和方法;

             b. 即使s的运行时类型是Panel,但其运行时类型对于一个引用来说是不可见的;

             c. s不能访问器运行时类型Panel中的任何数据和方法,只能通过s调用String的数据和方法;

2. 多态:编译时类型表现出运行时类型的行为(虚函数表)

    1) 前面已经讲过了,一个引用变量只能调用其编译时类型类的方法;

    2) 其实Java底层为每个引用变量都准备了一张虚函数表,为什么说是虚函数,这些虚函数其实都是真正方法的入口地址;

    3) 虚函数表用于保存这个引用的编译时类型里所有可以访问的方法:

         i. 例如:Father类里两个public方法void f1();和void f2();

         ii. 现在Father f;定义了一个编译时类型为Father的引用f;

         iii. 那么Java就会为f创建一张虚函数表,里面存放了Father类可以访问到的方法,即f1和f2;

    4) 那多态是什么?先看一下多态的本质,多态在底层的本质就是对引用的虚函数表进行覆盖:

         i. 接着上面的例子,现在是:Father f = new Son();     其中Son extends Father,并且Son覆盖了父类的方法f1;

         ii. 按照上面讲的知识点,还是会老样子,先为f建立一张虚函数表,里面存放的是Father范围内可见的方法f1和f2,而这里的f1仍然是父类中的f1;

         iii. 接着编译器检查到f的运行时类型是Son,并且Son是Father的子类,更重要的是这个子类还覆盖了Father的f1方法,接着多态就发生了,编译器将子类重写父类的方法f1覆盖掉了虚函数表中的f1;

         iv. 而编译时类型为Father的f只能调用其虚函数表中的方法,当调用到f1时就变成了子类覆盖的f1了!!

    5) 从上面的本质来看多态的要求:

         i. 最明显的就是引用的运行时类型必须是编译时类型的子类;

         ii. 只有在子类覆盖了父类方法时才会发生多态(因为只有这样才会修改引用的虚函数表);

!!只有i.不满足的情况下会编译报错;

!!可以看到多态是无需强制类型转换的(即运行时类型是编译时类型的子类时),上面无需:Father f = (Father)new Son();,因为这是向上兼容的!

    6) 那多态有什么用呢?

         i. 设想一个父类Father,有多个子类Son1、Son2、Son3...,每个子类都覆盖了f1方法,并且f1的内容还都不一样;

         ii. 现在用一个Father的引用来指向各个不同子类的对象,然后分别调用f1,就会呈现出每个子类f1的行为;

!!每修改一次引用的指向,就会更新一次该引用所对应的虚函数表,例如f = new Xxx(),这里修改f的指向,因此就会根据新的运行时类型更新一下f的虚函数表;

         iii. 这就发生了,同一个引用,调用同一个方法却能表现出不同行为的现象,这就是多态;

         iv. 而其本质就是编译时类型的引用表现出运行时类型的行为;

3. 兼容下反转:

    1) 现在的问题是多态完事儿后想回来了应该怎么办?简单的来说就是,Father f = new Son();是多态,多态下的工作完成之后,我想让f恢复成Son类型(编译时类型),原因很简单,因为我现在又想使用f指向的对象的子类部分了(加入该Son对象只有一个f指向它,想要使用该对象的子类部分就只能通过f了);

    2) 总结来说,就是现在想让多态下引用的编译时类型恢复到和运行时类型相同的状态;

    3) 你可能会想,直接Son s = f;不就行了,现在s编译时类型是Son,而f指向的对象运行时类型是Son,那现在s的编译时类型不就和运行时类型想同了嘛!

         i. 虽然前面讲过,编译时类型和运行时类型没有关系,但上面的这个语句还是无法通过编译的;

         ii. 毕竟,Java是一个强类型语言,强类型语言指的是编译时对类型进行检查,而这个检查的对象就是编译时类型;

         iii. 编译器发现s的编译时类型是Son,而f的编译时类型是Father,而Father ≠ Son,因此会爆出类型转换异常!

    4) 向上转换是兼容的,向下转换是不兼容的!

         i. 前面讲过Father f = new Son(); 或者 Son s = new Son();  Father f = s;是不会发生类型转换异常的!因为等号左边的类型是左边的子类,即向上转换,即多态,因此类型是兼容的!

         ii. 而s = f之类的,即等号右边是左边的父类,即向下转换,是不兼容的,编译会直接报错,因为逻辑上就能解释,你只能说苹果是水果,但不能说水果是苹果,继承是is-a的关系;

    5) 但有时向下转换并不是错误的!就像上面的例子那样,多态下f的运行时类型是Son,把f赋值给Son引用恢复成Son编译时类型是无可厚非的,但这样做编译又会报错,此时就需要用到强制类型转换了!

         i. 强制类型转换除了可以用在数值类型的基本类型之间(int、double、char等之间的相互转换);

         ii. 其次强制类型转换还可以用在兼容情况下的引用类型反转,而这个说的就是上述的情况:

             a. 例如:s = (Son)f;

             b. 能这样使用的前提:

                 *1. 右边必须是左边的编译时类型范围更大(父类或间接父类);(兼容是必不可少的前提,而这个兼容指的是编译时类型的兼容,而这个方向是和向上转型相反看,因此叫做反转)

                 *2. 右边引用的运行时类型必须和左边引用的编译时类型相同;(这是强转的目的,即把编译时类型恢复成运行时类型)其实小于等于也行(即f的运行时类型是s编译时类型的子类,这样就又形成多态了)

         iii. 示例:Object o = "lala";  String s = (String)o;    // o的编译时类型是Object,而运行时类型是String,符合兼容反转的条件

4. instanceof运算符:

    1) 该运算符和+、-、*之类的都属于Java的基本运算符;

    2) 该运算符的作用是对引用进行运行时类型检查;

    3) 用法:引用 instanceof 类/接口

    4) 目的是判断该引用的运行时类型是否为指定类或接口的子类或者相等,简单地说就是检查该引用指向的对象是否是指定类或接口的实例(或者其子类的实例),返回值当然是boolean类型的;

    5) 使用规定:

         i. instanceof左边的引用的编译时类型必须和操作符右边的类型有继承关系才行!

         ii. 例如:String a = "lala";  a instanceof Math就是错误的,直接编译报错!而a instanceof Object就对了,因为a的编译时类型String和Object具有继承关系;

         iii. 为什么有这样的规定呢?理由其实很好解释,那就要反问,为什么要使用instanceof操作符呢?不就是在进行类型强转之前先检查运行时类型是否匹配,如果不匹配就不强转(否则会报错)?那运行时类型匹配也是有前提,你设想,如果两个引用的编译时类型都没有继承关系,那他们运行时类型怎么可能会有继承关系呢??怎么可能匹配呢?用脚趾头都可以知道两个编译时类型无继承关系的引用是根本无法强转匹配的!

!!因此Java干脆要求做操作数的编译时类型必须至少要和右操作数有继承关系才能继续检查运行时类型;

    6) 示例:

Object o = "lala";
if (o instanceof String) { // true
String s = (String)o;
}
out.println(o instanceof Math); // false

String s = "lala";
s instanceof Math; // 编译报错,s的编译时类型和Math没有继承关系
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息