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

Java基础之方法的重载与覆盖

2017-12-03 00:52 246 查看
1、重载
    Java中的方法定义包括了方法头和方法体两部分,其中方法头用来唯一(标识)一个方法,方法体代表了方法的功能。
    方法头具体包含了修饰符、返回类型、方法名称、参数列表四个部分。修饰符不影响方法的定义,先忽略,重点关注后三个部分。
    地球人都知道,一个类中无法定义两个方法头完全相同的方法,不然调用该方法时,到底执行的是哪个呢?所以,必须要有一个标志来区别两个不同的方法。最直观的,莫过于方法名称。然而,方法名称不是唯一能够区别两个方法的标志,我们还有参数列表。也就是说,一个类中,可以定义两个方法名称相同的方法,只要它们的参数列表不同就可以了,起个高大上的名字,就叫方法重载。Jvm可以根据参数列表的不同自动识别出该调用哪个方法。
    参数列表不同表现为:参数个数不同;或者参数个数相同,但是对应位置上的类型不同。第二种情况比如:
void set(int a, String b);
void set(String b, int a);

    这样的两个方法其实只是两个参数对掉了一下位置,但也是可以被JVM区别的。然而实际中应该尽量避免这么做,因为根本没什么卵用还容易引起混淆。
    方法名称和参数列表合称为方法签名,那么java中,唯一标识一个方法的标志就是方法签名。只要两个方法的方法签名不同,它们就会被JVM识别为两个不同的方法。只有能够被JVM唯一定位到的方法,编译器才会放行通过。
    方法签名不包括返回值,也就是说,一个类中无法定义两个方法签名相同但是返类型不同的方法。
    难道不同通过返回值区分两个方法吗?理论上是可以的,至少编译器是可以识别出来的。但问题在于JVM不会去识别返回类型,所以编译器还是不让你过。根源在于JVM,也就是运行时对方法的定位。假设我们定义了两个如下的 方法,它们具有相同的方法签名,但是返回类型不同:
String get() {return "a";}
int get() {return 3;}

现在调用其中一个:
int c = get();
    看,我们已经指明了返回类型了啊,为什么还不能区别?注意这个语句的执行顺序,是先调用方法获取到结果,再把结果赋值给变量。这意味着,调用方法的时候,JVM根本不关心返回类型是什么,先拿到结果再说。至于JVM为什么要这么设计暂时忽略(其实是我不知道、不明白),只要知道,是因为JVM无法识别返回类型,才导致了定义两个签名相同但是返回类型不同的两个方法无法编译通过。

2、覆盖
    覆盖发生在继承时,具体指的是在子类中定义一个父类中已经存在的方法。“已经存在”的判断标准是方法签名,不包括返回类型。其实,在两个类中分别定义一个签名甚至包括返回类型都一样的方法没什么奇怪的,毕竟不在同一个类。然而当两个类具有继承关系的时候,这就被称为覆盖了。
    有什么不同吗?有的,当子类继承父类的时候,父类的变量和方法在子类中也是有效的(暂不考虑访问权限),相当于父类的代码在子类中也出现了(语法上,可以直接调用父类的方法)。于是,重载遇到的难题再次在继承中遇到:现在子类和父类中有两个签名相同的方法,那么调用的时候,到底调用哪个呢?重载的情景下(同一个类中),不允许两个方法的签名完全相同,因为JVM真的无法分辨;但是在继承的情景下,却允许子类定义一个与父类签名相同的方法,这是因为JVM有办法分辨出这两个方法,毕竟二者并不是真正的在同一个类中。
    但是这么做却是危险的:子类中存在两个签名完全一样的方法可以被调用。那么发生调用的时候,到底调用的是哪一个呢?大家也都知道java的方法调用是后期绑定,也就是运行时根据对象的实际类型做选择,而不是根据对象的声明类型。所以,这种情况下,调用的其实是子类的方法。也就是说,父类的重名方法被隐藏了——无法通过子类调用。在子类内部,要调用父类的这个重名方法,只能通过super加以区别;而在其他类中,通过子类的对象,调用的永远是子类的方法。所以当不小心覆盖父类的某个方法,而在代码中通过子类对象调用父类的这个方法,那么程序的运行结果就可能不是你想要的。
    于是,我们需要@Override这个注解,来标识某方法覆盖了父类的方法。如果子类方法并没有覆盖父类的方法,添加这个注解编译时会报错。然而,除了特殊情况,没人会在写代码时为每个方法添加这个注解,只为了试探是不是不小心覆盖了父类的方法。况且,就算覆盖了父类的方法,不添加这个注解,编译仍然可以通过——@Override并不是强制性的。所以,这个注解更多是规范性质的,用来提醒自己或者别人。
    覆盖在语法上还需要满足几条规则:

    (1)当你试图覆盖父类的static方法时,子类的方法也必须是static的。假如你在子类定一个非static方法,而父类中恰好存在一个签名相同的static方法,那么编译时会报错;

    (2)同样的道理,覆盖父类的非static方法时,子类的方法也必须是非static的。这两条规则总结起来就是:不能交叉覆盖。

    (3)被final修饰的方法无法被覆盖,final就是干这个的

    (4)当你确实想要覆盖父类的某个方法时,子类中的方法签名必须和父类方法完全一致,否则覆盖就算失败,达不到覆盖的目的;
    (5)覆盖方法时,子类方法的返回值必须是父类方法返回值类型或其子类,也就是覆盖对返回值也做了规定;

    (6)覆盖方法时,子类方法的抛出异常必须是父类方法抛出异常类型或其子类,这句话经过我的实验,是这样运行:子类方法要么不抛异常,或者抛出的异常跟父类异常不相关,但是不能抛出父类异常的父类。。否则编译器报错。以后给出例子。

    (7)我遇到的一个现象,没想到还有这种操作。例子如下:
public class Parent {
public void printAll() {
print();
}

public void print() {
System.out.println("parent");
}
}

public class Child extends Parent {
public void print() {
System.out.println("child");
}

public static void main(String[] args) {
printAll();
}
}
执行的结果会使啥?打印“parent”还是”child“?反正我一开始是懵逼的。。我以为会打印”parent“,结果打印的是”child“,也就是说,子类调用了父类的方法一,方法一调用了父类的方法二,而子类恰好用方法三覆盖了方法二,那么方法一在子类中还是要调用方法三。。绕了一圈,最后还是回到了子类的覆盖方法。

3、隐藏
    子类中定义了和父类重名的变量或方法,那么通过子类只能调用到子类的变量和方法,这就是隐藏。显然,覆盖算是隐藏的一种特殊情况。为什么这么说呢?因为子类被强制类型转换成父类后,又可以访问到父类的被隐藏的变量和方法,但是方法只包括静态方法,实例方法不行。
    变量部分,static和非static可以交叉覆盖。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: