继承和多态

@[toc] --- ## 继承 ### 什么是继承?(extends) Java 中的继承关系使用关键字 `extends` 来标识,示例代码如下: ```java public class Cat extends Animal{ // 为新类增加新的方法 public void run(){ System.out.println("I'm running"); } } ``` 这种关系可以表述为**子类是父类的一种类型**。 `Cat` 继承 `Animal` 后,自动拥有了父类 `Animal` 中的方法,并可以直接调用,另外,还可以对父类的方法进行重写/覆盖(@Override) > **如果子类的构造器没有显式地调用父类的构造器, 则将自动地调用父类默认的构造函数(无参构造函数)**。 如果超类没有无参构造函数, 并且在子类的构造器中又没有显式地调用超类的其他构造器,则 Java 编译器将报告错误。 需要注意的是:**使用 `super ` 调用父类的构造函数的语句必须是子类构造函数的第一条语句**。 ```java public class Cat extends Animal{ private String earKind; public Cat(int age, String earKind) { super(age); this.earKind = earKind; } ......... } ``` > **如果子类的构造器没有显式地调用父类的构造器, 则将自动地调用父类默认的构造函数(无参构造函数)**。 如果超类没有无参构造函数, 并且在子类的构造器中又没有显式地调用超类的其他构造器,则 Java 编译器将报告错误。 另外,**父类的构造函数总是先于子类的构造函数执行**。 ### 向上转型和向下转型 - 向上转型:子类转成父类,程序中出现父类对象的任何地方都可以用子类对象置换(事实上,这是多态的一种体现) ```java Animal cat = new Cat(...); // 向上转型 Cat->Animal ``` 需要注意的是:<u>父类引用变量指向子类对象后,只能使用父类已声明的方法</u>,但方法如果被重写会执行子类的方法,如果方法未被重写那么将执行父类的方法。 - 向下转型:父类转成子类,有时候也可能需要<u>将某个父类的对象引用转换成子类的对象引用,调用一些子类特有而父类没有的方法</u>。对象向下转型的语法与数值表达式的类型转换类似,仅需要用一对圆括号将目标类名括起来,并放置在需要转换的对象引用之前就可以了。例如: ```java Animal animal = new Cat(...); // 向上转型 Cat->Animal Cat cat = (Cat) animal; // 向下转型 Animal->Cat,animal 的实质还是指向 Cat ``` ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20210129222050.png) ### Java 是单继承还是多继承的? **在 Java 中,子类只能继承一个父类**。也就是**单继承**。 ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20210129221818.png) **Java 语言本着简单的原则舍弃了 C++ 中的多继承,这样也会使程序更具安全性**。 那么多继承到底带来什么坑?其实也不难理解: - 如果一个子类拥有多个父类的话,那么当多个父类中有重复的属性或者方法时,子类的调用结果就会含糊不清,也就是存在**二义性**。 - 同时多继承也会增加代码的耦合性,导致**代码的可维护性和可扩展性降低**。因此 Java 使用了单继承。 Java 采用单继承的设计,但是通过接口(interface)可以实现多重继承的效果。接口是一种特殊的抽象类,它只定义了一组抽象方法和常量,不包含实现代码。一个类可以实现多个接口,从而具备多重继承的能力,但是本质上它仍然只能真正的继承(extends)一个类。 ## 多态 ### 什么是多态? 多态就是**使得同一个行为具有多个不同表现形式或形态的能力**。 **多态发生的三个必要条件**: - 继承 - 方法重写/覆盖(Override) - 父类引用指向子类对象(子类转成父类,向上转型):`Parent p = new Child();` 当把一个子类对象直接赋给父类引用变量时,例如 `Parent p = new Child()`,这个 `p` 引用变量的「编译时类型」是 `Parent` ,而「运行时类型」是 `Child`,**当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态**。 ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20210203234209.png) ### 运行时多态和编译时多态 在 Java 中,多态分为两种类型:运行时多态和编译时多态。 1. **运行时多态(Runtime Polymorphism)**:也称为动态绑定或后期绑定。它是指在程序运行时,根据对象的实际类型确定使用哪个方法的过程。这种多态性是通过继承和方法重写(Overriding)实现的。 > 举个例子,假设有一个父类 Animal 和它的两个子类 Cat 和 Dog。这三个类都有一个叫做 makeSound() 的方法,但是 Cat 和 Dog 类重写了这个方法,分别实现了不同的声音。当程序执行时,如果使用父类 Animal 的引用指向 Cat 或 Dog 类的对象,那么调用 makeSound() 方法时,会根据对象的实际类型(Cat 或 Dog)来确定调用哪个方法。 2. **编译时多态(Compile-time Polymorphism)**:也称为静态绑定或早期绑定。它是指在程序编译时,根据方法的名称、参数类型和返回值类型确定使用哪个方法的过程。这种多态性是通过方法重载(Overloading)实现的。 > 举个例子,假设有一个类 Calculator,它有两个方法 add(int a, int b) 和 add(double a, double b)。在程序编译时,当调用 add() 方法时,编译器会根据参数的类型(int 或 double)确定调用哪个方法。 ### 重载(Overloading)和重写(Overriding)的区别 方法的重写 Overriding 和重载 Overloading **都是 Java 多态性的表现** - **方法重写是父类与子类之间多态性的表现**。其子类和父类方法的名字相同,参数个数相同,返回类型也相同,并且子类的访问权限不能比父类的严格,比如父类是 public,那么子类也只能是 public,不能比 public 更严格。也就是说,**方法重写,只有方法体是不一样的,访问权限可以有限制的修改**。 ```java class Shape { public void draw() {} } class Circle extends Shape { public void draw() { System.out.println("Circle.draw()"); } } ``` > 其实,上面说的返回类型完全相同并不严格正确。下面我们来解释一下。 > > 首先,我们需要知道**方法的名字和参数列表称为方法的签名**。例如,`draw()` 和 `draw(String) ` 是两个具有相同名字, 不同签名的方法。如果在子类中定义了一个与超类签名相同的方法, 那么子类中的这个方法就覆盖/重写了超类中的这个相同签名的方法。 > > 不过,**返回类型不是签名的一部分**, 因此,在覆盖/重写方法时, 一定要保证返回类的兼容性。 **允许子类将覆盖方法的返回类型定义为原返回类型的子类型**。 > > 例如, 假设 `Shape` 类有 > > ```java > class Shape { > public Shape draw() { > ...... > } > } > ``` > > 在后面的子类 `Circle` 中, 可以按照如下所示的方式覆盖这个方法 > > ```java > class Circle extends Shape { > public Circle draw() { > ...... > } > } > ``` > > 用专业术语来说,这两个 `draw` 方法具有**可协变的返回类型**。 - **方法重载并非多态的必要条件**,不过可以理解成**某个类的多态性的表现**。所谓方法重载,就是**一个类中定义了多个方法名相同,但是参数的数量或者类型不同的方法。方法的返回类型和访问权限可以任意修改,不以它俩作为方法重载的标志**。 ```java class Circle extends Shape { public void draw() { System.out.println("Circle.draw()"); } public void draw(int i) { System.out.println("Circle.draw()" + i); } } ``` 总结一下方法重载和重写: | | 方法重载 | 方法重写 | | :--------: | :------: | :------------------------------------------------: | | 方法名 | 相同 | 相同 | | 参数列表 | 必须不同 | 必须相同 | | 返回类型 | 可以不同 | 子类方法的返回类型可以是原父类方法返回类型的子类型 | | 访问修饰符 | 可以不同 | 子类不能做更严格的限制(可以降低限制) | 再来俩张图配合理解 ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20210204142236.png) ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20210203232313.png) ### main 方法是否可以重载? IBM 早些年出过这方面的题,考倒了一片人。首先,答案是肯定的,**main 既然作为一个方法,那它当然可以被重载**。 但是,**如果是作为程序的入口,那么 main 函数只有一种写法,Java 虚拟机在运行的时候只会调用带有参数是 String 数组的那个 `main()` 方法**,而其他重载的写法虚拟机是不认的,只能人为的调用。 举个例子: ```java class Test { public static void main(String[] args) { main(1); } public static void main(int i) { System.out.println("重载的 main 方法"); } } ``` 该程序运行结果如下: ``` 重载的 main 方法 ``` 可以看出第一个 main 方法正常调用了重载的第二个 main 方法,即 main 方法能够被完美重载。但是程序的入口仍然是第一个 main 方法即参数为 String 数组。 ![](https://cs-wiki.oss-cn-shanghai.aliyuncs.com/img/20210204142503.png)