多态
向上转型
在刚才的例子中, 我们写了形如下面的代码
Bird bird = new Bird("圆圆");
这个代码也可以写成这个样子
Bird bird = new Bird("圆圆");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("圆圆");
此时 bird2 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为 向上转型.
向上转型这样的写法可以结合 is - a 语义来理解.
例如, 我让我媳妇去喂圆圆, 我就可以说, “媳妇你喂小鸟了没?”, 或者 “媳妇你喂鹦鹉了没?”因为圆圆确实是一只鹦鹉, 也确实是一只小鸟
为啥叫 “向上转型”?
在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序猿会画一种 UML 图的方式来表示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 “向上转型” , 表示往父类的方向转.
向上转型发生的时机:
- 直接赋值
- 方法传参
- 方法返回
直接赋值的方式我们已经演示了. 另外两种方式和直接赋值没有本质区别.
方法传参
public class Main {
public static void main(String[] args) {
Bird bird = new Bird("圆圆");
feed(bird);
}
public static void feed(Animal animal) {
animal.eat("谷子");
}
}
此时形参 animal 的类型是 Animal (基类), 实际上对应到 Bird (父类) 的实例.
方法返回
public class Main {
public static void main(String[] args) {
Animal animal = findMyAnimal();
}
public static Animal findMyAnimal() {
Bird bird = new Bird("圆圆");
return bird;
}
}
此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例.
动态绑定
当子类和父类中出现同名方法的时候, 再去调用会出现什么情况呢?
对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法, 并且在两个 eat 中分别加上不同的日志.
// Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println("我是一只小动物");
System.out.println(this.name + "正在吃" + food);
}
}
// Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void eat(String food) {
System.out.println("我是一只小鸟");
System.out.println(this.name + "正在吃" + food);
}
}
// Test.java
public class Test {
public static void main(String[] args) {
Animal animal1 = new Animal("圆圆");
animal1.eat("谷子");
Animal animal2 = new Bird("扁扁");
animal2.eat("谷子");
}
}
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子
此时, 我们发现:
animal1
和 animal2
虽然都是Animal
类型的引用, 但是animal1
指向Animal
类型的实例, animal2
指向Bird
类型的实例.
针对 animal1
和animal2
分别调用 eat
方法, 发现 animal1.eat()
实际调用了父类的方法, animal2.eat()
实际调用了子类的方法.
因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.
动态绑定的条件
1、父类引用引用子类的对象
2、通过这个父类应用调用父类和子类的同名覆盖函数
方法重写
针对刚才的 eat 方法来说:
子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).
关于重写的注意事项
- 重写和重载完全不一样. 不要混淆(思考一下, 重载的规则是啥?)
- 普通方法可以重写, static 修饰的静态方法不能重写.
- 重写中子类的方法的访问权限不能低于父类的方法访问权限.
- 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).
方法权限示例: 将子类的 eat 改成 private
// Animal.java
public class Animal {
public void eat(String food) {
...
}
}
// Bird.java
public class Bird extends Animal {
// 将子类的 eat 改成 private
private void eat(String food) {
...
}
}
// 编译出错
Error:(8, 10) java: com.bit.Bird中的eat(java.lang.String)无法覆盖com.bit.Animal中的
eat(java.lang.String)
正在尝试分配更低的访问权限; 以前为public
另外, 针对重写的方法, 可以使用 @Override 注解来显式指定.
// Bird.java
public class Bird extends Animal {
@Override
private void eat(String food) {
...
}
}
有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 eat
方法, 就会编译报错, 提示无法构成重写.
我们推荐在代码中进行重写方法时显式加上@Override
注解.
理解多态
public class Shape {
public void draw(){
}
}
class Cycle extends Shape{
@Override
public void draw() {
System.out.println("这是一个圆>>⚪");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("这是一朵花>>❀");
}
}
public class Main {
public void draw(Shape a){
a.draw();
}
public static void main(String[] args) {
Shape a=new Cycle();
Shape b=new Flower();
Main main=new Main();
main.draw(a);
main.draw(b);
}
}
抽象类
父类 Shape
中的 draw
方法好像并没有什么实际工作, 主要的绘制图形都是由Shape
的各种子类的 draw
方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class)。
abstract class Shape {
abstract public void draw();
}
- 在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体代码).
- 对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类.
1、抽象类不能实例化被对象
Shape shape=new Shape();
Shape是抽象的; 无法实例化
2、抽象方法不能是 private 的
abstract private void draw2();
java: 非法的修饰符组合: abstract和private
3、抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用
4、唯一的作用是被继承,继承后普通类必须重写抽象类所有抽象方法
5、一个抽象类A,如果继承了一个抽象类B,可以不用实现父类B的抽象方法
6、但是如果有普通类继承A,还是需要重写抽象方法,A和B的都有重写。
7、抽象类不能被final修饰,抽象方法也不能被final修饰
接口
在抽象类中,还可以包含非抽象方法, 和字段。而接口中包含的方法都是抽象方法, 字段只能包含静态常量(final static)
1、使用interfa来修饰
2、接口里面的普通不能有具体的方法实现。如果非要实现可以加一个default来修饰。
3、接口里面可以有static方法
4、里面所有的方法都是public,可以省略public
5、方法一定是抽象方法,可以省略abstract
6、接口不能通过new来实例化
实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.