基础知识:继承与多态

来源:互联网 发布:安装Linux出现内部错误 编辑:程序博客网 时间:2024/06/03 18:01

继承

继承如何使用,就不多说了。
一个类(类A)继承另一个类(类B)后,类A将会获得类B的所有属性与方法,当然类B的private域的属性类A是不可以直接操作的,还有private域的方法也没法重写,如果类A中定义类B中private域的方法,是不会有问题的,因为该方法属于类A自己的。从这方面可以看出,private域的限制范围,除了自身外,连继承都无法访问的(private域虽然无法直接操作或者重写,但不代表没有继承到或者说是不存在)。
接下来,再来说一说有关继承的一些问题。

重写与重载

说起继承,总会提到重写,提到重写也会提到重载,两个都一起说一下吧。

public class Example {    public void printString(String str) {        System.out.println(str);    }    public void printString(int i) {        System.out.println("打印数字字符串" + i);    }    public void printString(String str, int i) {        System.out.println("打印数字与字符串" + str + "," + i);    }    public String printString() {        return "test";    }}

这个就是一个比较简单的重载,那么重载是如何实现的呢?
当调用方法的时候,是如何知道调用哪个方法的呢?也就是如何指定我们所要调用的方法。
在编译器中方法的名字与方法的参数列表成为方法的签名,例如printString(String)与printString(int),它们方法名相同,但是签名不同,也就是对于编译器来说,它们是两个不同的方法,有不同的方法体。重载利用的就是相同的方法名,不同的签名
返回类型并不是签名的一部分,也就是说返回类型与指定方法是无关的。所以如果返回类型不同,但方法名和参数相同,签名相同,产生了冲突,编译是不通过的。
不过,在虚拟机中,方法签名包含了返回类型,那么其实方法名,参数相同,返回类型不同,也是可以重载方法的,但如上所说,对编译器来说,签名不包含返回类型,所以编译不会通过。
至于为什么编译器和虚拟机的签名有所区别,我认为是为了降低复杂度,而且避免一些奇技淫巧的出现。

这里提一个问题,方法System.out.println()重载了多少个?其实不用去查看,可以用重载的定义稍微推理一下,方法println()所有的基本类型和String都可以作为参数传入,那么有几个基本类型,就重载了多少个,还有方法println() 可以传入各种对象,那么它应该还重载了一个println(Object),还有一个无参的。当然实际上还是有出入的。

重写,在子类中定义与超类有相同签名和返回类型,或有相同签名和子类方法中返回类型是超类方法中返回类型的子类时,就会重写超类的方法。假如返回类型不同而且不是上述提到的继承关系,但签名相同,将会报错。类的继承,子类会获取超类的非private的属性与方法,对编译器来说,相同的签名但是不同的返回类型,没有进行重写,相当于方法重复,所以会报错。
虽然,编译器确定方法时,返回类型不是判断依据,但是虚拟机中,返回类型是确定方法的依据之一,也就是说虚拟机中返回类型是签名的一部分。而重写,也叫覆盖,是发生在父类和子类有相同签名的方法中(如果签名不同,就是两者代表的方法不同,子类方法凭什么覆盖父类呢?)。
不过,上面不是说到有相同签名和子类方法中返回类型是超类方法中返回类型的子类时,也会发生重写吗?
如下示例代码。

public class SuperClazz {    public Object getData() {        return new Object();    }}public class SubClazz extends SuperClazz {    @Override    public Integer getData() {        return new Integer(1);    }}

我们用javap -verbose查看一下SubClazz.class文件。

  public java.lang.Integer getData();    flags: ACC_PUBLIC    Code:      stack=3, locals=1, args_size=1         0: new           #2                  // class java/lang/Integer         3: dup         4: iconst_1         5: invokespecial #3                  // Method java/lang/Integer."<init>":(I)V         8: areturn      LineNumberTable:        line 9: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature               0       9     0  this   Lextend/SubClazz;  public java.lang.Object getData();    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokevirtual #4                  // Method getData:()Ljava/lang/Integer;         4: areturn      LineNumberTable:        line 6: 0      LocalVariableTable:        Start  Length  Slot  Name   Signature               0       5     0  this   Lextend/SubClazz;

发现,有两个getData方法,不过返回类型不相同。而方法java.lang.Object getData(),是编译器生成的。
invokevirtual操作指令是调用方法的指令之一,而在方法java.lang.Object getData()中,可以发现它的内部调用了java.lang.Integer getData()。
在超类中,getData方法的返回类型不就是Object吗?那就是说,编译器生成了方法java.lang.Object getData()来重写超类的方法,而这个生成的方法内部调用了java.lang.Integer getData()。就是说,这种重写,是利用桥方法来实现的。
当然,如果你这样写代码,编译器是不会通过的。

public Object getData(){...}//错误public Integer getData(){...}//错误

还有子类重写方法时,修饰符不能低于超类的控制等级。
从面向对象的角度去看,子类修饰符的控制等级降低,如将public改为protected,相当于把超类的这个方法隐藏起来,子类改写了继承于超类的特性,这是不允许的。
从代码实现的角度去看,当从外部调用一个被子类重写的方法时,虚拟机会去执行子类中的方法,而不是超类的方法。结果子类的方法是private域的,不可以调用,进而产生错误。

再来说一个比较常见的问题

Class SuperClazz {    public void f() throws IOException {        /*        操作        */    }}Class SupClazz extends SuperClazz {    //报错    public void f() throws Exception {        /*        操作        */    }}

子类重写超类的方法时,不能抛出更多的异常。
以面向对象的角度去看,记得当年上JAVA基础时,老师说的是“因为继承下,子类不能比超类做更多的事情,就像徒弟与师傅,徒弟不会比师傅更能干”什么的。
现在回想一下,其实描述得并不正确,子类是可以比超类做更多的事情,子类定义自己的方法时,其所能做的事情就比超类多了。更准确来说,应该是子类所继承的行为(方法),不能超出超类的限制范围(能力),否则就是改写了继承于超类的特性,是不被允许的。
因为封装性,方法内部的实现对外是不可见的,方法内部如何改变,都不会改变这个方法对外展现的特性。但是抛出异常这一行为并没有被封装,它是对外所展现的。
以代码实现的角度去看的话,会涉及到多态,在下面将会提及。

多态

多态的实现源于动态绑定,而JAVA的动态绑定属于后期绑定,即在运行时绑定,再通俗点讲,就是在运行时,才确定对象是哪个类的实例。
再说一下以下的问题:

public class SuperClazz {    public void simpleFunction() {        /*         * 操作         */    }}public class SubClazz extends SuperClazz {    @Override    public void simpleFunction() {        // TODO Auto-generated method stub        super.simpleFunction();    }    public void otherFunction() {        /*         * 操作         */    }}public class Main {    public static void main(String[] args) {        SuperClazz superClazz = new SubClazz();        //报错,无法找到改方法        superClazz.otherFunction();    }}

如上面所说的,在运行时,才确定对象的类型是什么。
声明类型为SuperClazz(超类),那么在运行前都不确定实际的类型,那么自然就先认为这个对象是SuperClazz,调用子类才有的方法,自然会报找不到方法的错误。

现在再从代码实现的角度说一下为什么子类不允许比超类抛出更多的异常:
JAVA的多态是后期绑定,在运行期间判断对象的类型,并分别调用适当的方法。但在运行前,我们不知道对象的实际类型,自然就先默认为是所声明类(如SuperClazz),所以所捕获的异常,也是SuperClazz的方法所声明要抛出的,而实际运行的时候,这个对象其实是它的子类(SubClazz),而这时SubClazz抛出更多的异常,多出来的异常就无法捕获了。

0 0
原创粉丝点击