继承和多态
@[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
```

### Java 是单继承还是多继承的?
**在 Java 中,子类只能继承一个父类**。也就是**单继承**。

**Java 语言本着简单的原则舍弃了 C++ 中的多继承,这样也会使程序更具安全性**。
那么多继承到底带来什么坑?其实也不难理解:
- 如果一个子类拥有多个父类的话,那么当多个父类中有重复的属性或者方法时,子类的调用结果就会含糊不清,也就是存在**二义性**。
- 同时多继承也会增加代码的耦合性,导致**代码的可维护性和可扩展性降低**。因此 Java 使用了单继承。
Java 采用单继承的设计,但是通过接口(interface)可以实现多重继承的效果。接口是一种特殊的抽象类,它只定义了一组抽象方法和常量,不包含实现代码。一个类可以实现多个接口,从而具备多重继承的能力,但是本质上它仍然只能真正的继承(extends)一个类。
## 多态
### 什么是多态?
多态就是**使得同一个行为具有多个不同表现形式或形态的能力**。
**多态发生的三个必要条件**:
- 继承
- 方法重写/覆盖(Override)
- 父类引用指向子类对象(子类转成父类,向上转型):`Parent p = new Child();`
当把一个子类对象直接赋给父类引用变量时,例如 `Parent p = new Child()`,这个 `p` 引用变量的「编译时类型」是 `Parent` ,而「运行时类型」是 `Child`,**当运行时调用该引用变量的方法时,其方法行为总是表现出子类方法的行为特征,而不是父类方法的行为特征,这就可能出现:相同类型的变量、调用同一个方法时呈现出多种不同的行为特征,这就是多态**。

### 运行时多态和编译时多态
在 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);
}
}
```
总结一下方法重载和重写:
| | 方法重载 | 方法重写 |
| :--------: | :------: | :------------------------------------------------: |
| 方法名 | 相同 | 相同 |
| 参数列表 | 必须不同 | 必须相同 |
| 返回类型 | 可以不同 | 子类方法的返回类型可以是原父类方法返回类型的子类型 |
| 访问修饰符 | 可以不同 | 子类不能做更严格的限制(可以降低限制) |
再来俩张图配合理解


### 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 数组。

