多态概述
一种思想方法,多态落实到代码上具体的体现形式,就是通过以下三点来体现的:
- 1.向上转型
- 2.动态绑定
- 3.方法重写
接下来都以下代码举例:
//Animal.java
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public void eat(String food) {
System.out.println(name + "正在吃"+food);
}
}
//Bird.java
public class Bird extends Animal {
public Bird(String name) {
super(name);
}
public void fly() {
System.out.println(name + "正在飞");
}
@Override
public void eat(String food) {
System.out.println("我是一只小鸟");
System.out.println(name + "正在吃" + food);
}
}
//Cat.java
public class Cat extends Animal {
public Cat(String name) {
super(name);
}
}
向上转型
Bird bird = new Bird("鸟鸟");
Animal bird2 = bird;
// 或者写成下面的方式
Animal bird2 = new Bird("鸟鸟");
此时 bird2 是一个父类 (Animal) 的引用,指向一个子类 (Bird) 的实例。这种写法称为向上转型。
程序员会画一种 UML 图的方式来表示类之间的关系. 此时父类通常画在子类的上方. 所以我们就称为 “向上转型” ,表示往父类的方向转。
方法传参向上转型
public class Test {
public static void main(String[] args) {
Bird bird = new Bird("鸟鸟");
feed(bird);
}
public static void feed(Animal animal) {
animal.eat("谷子");
}
}
// 执行结果
鸟鸟正在吃谷子
方法返回向上转型
public class Test {
public static void main(String[] args) {
Animal animal = findMyAnimal();
}
public static Animal findMyAnimal() {
Bird bird = new Bird("鸟鸟");
return bird;
}
}
此时方法 findMyAnimal 返回的是一个 Animal 类型的引用,但是实际上对应到 Bird 的实例。
动态绑定
// 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 中, 调用某个类的方法,究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) ,要看究竟这个引用指向的是父类对象还是子类对象. 这个过程是程序(运行时决定)的(而不是编译期),因此称为 动态绑定。
方法重写
针对上面的 eat ()方法来说:
子类实现父类的同名方法,并且 参数的类型和个数完全相同 ,这种情况称为 覆写/重写/覆盖(Override)
关于重写的注意事项:
- 重写和重载完全不一样. 不要混淆
- 普通方法可以重写, static 修饰的静态方法不能重写.
- 重写中子类的方法的访问权限不能低于父类的方法访问权限
- 针对重写的方法,可以使用 @Override 注解来显式指定。(有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet),那么此时编译器就会发现父类中没有 aet 方法,就会编译报错,提示无法构成重写)
示例:将子类的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)
理解多态
多态可概括为, 就是 “一个引用,能表现出多种不同形态”。
使用多态的好处是什么?
1)类调用者对类的使用成本进一步降低
- 封装是让类的调用者不需要知道类的实现细节
- 多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可
因此,多态可以理解成是封装的更进一步,让类调用者对类的使用成本进一步降低
2)能够降低代码的 “圈复杂度”,避免使用大量的 if - else
3)可扩展能力更强。
向下转型(instanceof)
向上转型是子类对象转成父类对象,向下转型就是父类对象转成子类对象. 相比于向上转型来说,向下转型没那么常见。
示例如下:
Animal animal = new Bird("鸟鸟");
animal.eat("谷子");
// (Bird) 表示强制类型转换
Bird bird = (Bird)animal;
bird.fly();
但是这样转不一定可靠,如下代码:
Animal animal = new Cat("小猫");
Bird bird = (Bird)animal;
bird.fly();
animal 本质上引用的是一个 Cat 对象,是不能转成 Bird 对象的. 运行时就会抛出异常。为此,我们用instanceof关键字判定一个引用是否是某个类的实例。如果是,则返回 true,这时再进行向下转型就比较安全了。
Animal animal = new Cat("小猫");
if (animal instanceof Bird) {
Bird bird = (Bird)animal;
bird.fly();
}
抽象类
如果父类没有什么实际工作,我们可以把它设计成一个 抽象方法(abstract method),包含抽象方法的类我们称为 抽象类(abstract class)。
abstract class Shape {
abstract public void draw();
}
- 在 draw 方法前加上 abstract 关键字,表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { },不能执行具体代码)。
- 对于包含抽象方法的类,必须加上 abstract 关键字表示这是一个抽象类。
注意事项:
- 1.抽象类不能直接实例化
- 2.抽象方法不能是 private 的
- 3.抽象类中 可以包含其他的非抽象方法,也可以包含字段。这个非抽象方法和普通方法的规则都是一样的,可以被重写,也可以被子类直接调用。
抽象类示例:
abstract class Shape {
abstract public void draw();
void func() {
System.out.println("func");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("□");
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("○");
}
}
public class Test {
public static void main(String[] args) {
Shape shape = new Rect();
shape.draw();
shape.func();
}
}
抽象类的作用:
抽象类存在的最大意义就是为了被继承。抽象类本身不能被实例化,要想使用,只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。
普通的类也可以被继承呀,普通的方法也可以被重写呀,为什么还要有抽象类和抽象方法呢?
回答:使用抽象类相当于多了一重编译器的校验,实际工作不应该由父类完成,而应由子类完成。那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题。很多语法存在的意义都是为了 “预防出错”。
接口
接口是抽象类的更进一步。抽象类中还可以包含非抽象方法和字段,而接口中包含的方法都是抽象方法,,字段只能包含静态常量。
上面打印图形的代码设计成一个接口:
interface IShape {
void draw();
public static final int num = 10;
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("○");
}
}
public class Test {
public static void main(String[] args) {
IShape shape = new Rect();
shape.draw();
}
}
- 使用 interface 定义一个接口
- 接口中的方法一定是抽象方法, 因此可以省略 abstract
- 接口中的方法一定是 public, 因此可以省略 public
- Cycle 使用 implements 继承接口. 此时表达的含义不再是 “扩展”, 而是 “实现”
- 在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.
- 接口不能单独被实例化.
- IShape中字段的 public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量
实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的。然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果。
下列代码,鸭子水陆空三栖:
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
class Duck extends Animal implements IRunning, ISwimming, IFlying {
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + "正在用翅膀飞");
}
@Override
public void run() {
System.out.println(this.name + "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在漂在水上");
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法:一个类继承一个父类,同时实现多种接口。
继承表达的含义是 is - a 语义,而接口表达的含义是 具有 xxx 特性
接口间的继承
接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
//两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {
...
}
应用接口的实例
class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
@Override
public int compareTo(Object o) {
Student s = (Student)o;
if (this.score > s.score) {
return -1;
} else if (this.score < s.score) {
return 1;
} else {
return 0;
}
}
}
public class Test {
public static void main(String[] args) {
Student[] students = new Student[] {
new Student("张三", 95),
new Student("李四", 96),
new Student("王五", 97),
new Student("赵六", 92),
};
Arrays.sort(students);
System.out.println(Arrays.toString(students));
}
}
在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象。
然后比较当前对象和参数对象的大小关系(按分数来算)。
- 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
- 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
- 如果当前对象和参数对象不分先后, 返回 0;
注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则。