多态是同一个行为具有多个不同表现形式或形态的能力。
多态的分类
- 编译时多态(设计时多态):方法重载
- 运行时多态:Java 运行时系统根据调用该方法的实例的类型来决定选择调用哪个方法则被称为运行时多态
我们平时说的多态,多指运行时多态
多态的实现方法
重写
这个内容写过了,可以访问 Java基础六 中的重写 & 重载。
接口
- 生活中的接口最具代表性的就是插座,例如一个三接头的插头都能接在三孔插座中,因为这个是每个国家都有各自规定的接口规则,有可能到国外就不行,那是因为国外自己定义的接口类型。
- java中的接口类似于生活中的接口,就是一些方法特征的集合,但没有方法的实现。具体可以看 Java基础七-接口 这一章节的内容
抽象类和抽象方法
详情请看Java基础七-抽象类 。
向上转型 & 向下转型
要转型,首先要有继承。
向上类型转换(Upcast):将子类对象转换为父类,父类可以是接口
隐式/自动类型转换,是小类型到大类型的转换。
向下类型转换(Downcast):将父类型转换为子类型
强制类型转换,是大类型到小类型。
通过 instanceof 运算符,来解决引用对象的类型,避免类型转换的安全性问题,提高代码的健壮性。
向上转型
举一个大家都知道的例子:
1 | public class Animal { |
1 | public class Cat extends Animal{ |
1 | public class Dog extends Animal { |
1 | public class Main { |
执行结果:
猫吃鱼
狗吃肉
这就是向上转型,向上转型是安全的,因为任何子类都继承并接受了父类的方法。从例子中也可以看出猫和狗都属于他们的父类——Animal,这是可行的。但是向下转型就不行,若所所有的动物都是猫或者都是狗就不成立的。(所以向下转型要通过强制类型转换)
Animal animal = new Cat(); 将子类对象 Cat 转换为父类对象 Animal 。这个时候 animal 这个引用调用的方法是子类方法。
转型过程中需要注意的问题
- 向上转型时,子类单独定义的方法会丢失。比如上面 Dog 类中定义的 run 方法,当 animal 引用指向 Dog 类实例时是访问不到 run 方法的,
animal.run();会报错。 - 子类引用引用不能指向父类对象。
Cat c = (Cat)new Animal()这样是不行的。
向上转型的应用
当一个子类对象向上转型父类类型后,就被当成了父类的对象,所能调用的方法会减少,只能调用子类重写了父类的方法以及父类派生的方法(如 set(),get()方法),而不能调用子类独有的方法。
继续用上面的例子:
1
2
3
4
5
6
7
8
9
10public class Main {
public static void main(String[] args) {
Animal animal = new Cat();//向上转型
animal.eat();
animal = new Dog();
animal.eat();
//如果调用 Dog 类中的 run() 方法。
animal.run();//这个会报错,编译不通过
}
}可以调用子类重写父类的方法 eat(),但调用子类独有的方法 run() 时就是无效的
父类中的静态方法是不允许被子类重写的
如父类 Animal 中含有静态方法 sleep()
1
2
3
4
5
6
7
8public class Animal {
public void eat(){
System.out.println("animal eating...");
}
public static void sleep(){
System.out.println("animal sleep...");
}
}当子类 Cat 中也定义同名方法时,此时 sleep() 算 Cat 类自己独有的方法
1
2
3
4
5
6
7
8public class Cat extends Animal{
public void eat(){
System.out.println("猫吃鱼");
}
public static void sleep(){
System.out.println("猫要睡大觉!");
}
}1
2
3
4
5
6
7public class Main {
public static void main(String[] args) {
Animal animal = new Cat();
animal.eat();
animal.sleep();
}
}执行结果:
猫吃鱼
animal sleep…实际上调用的是父类的静态方法 sleep()
这是因为父类的静态方法可以被子类继承,但是不能重写。
向上转型的好处
- 减少重复代码,使代码变得简介
- 提高系统扩展性
举个例子:比如,我现在有很多种类的动物,要喂它们吃东西。如果不用向上转型,那我需要这样写:
1 | public void eat(Cat c){ |
一种动物写一个方法,如果我有一万种动物,我不得写一万个方法?假设你很厉害,耐着性子写完了,突然又来一个新的动物,你是不是又要单独为它写一个 eat() 方法?
那如果我用向上转型呢?
1 | public void eat(Animal a){ |
这样代码是不是简洁了许多?而且这个时候,如果我又有一种新的动物加进来,我只需要实现它自己的类,让他继承Animal就可以了,而不需要为它单独写一个eat方法。是不是提高了扩展性?
扩展
多态的实现可以通过向上转型和动态绑定机制来完成,向上转型实现了将子类对象向上转型为父类类型,而动态绑定机制能识别出对象转型前的类型,从而自动调用该类的方法,两者相辅相成。
绑定就是将一个方法调用同一个方法所在的类连接到一起就是绑定。绑定分为静态绑定和动态绑定两种。
静态绑定
在程序运行之前进行绑定(由编译器和链接程序完成的),也叫作前期绑定。
动态绑定
在程序运行期间由 JVM 根据对象的类型自动的判断应该调用那个方法,也叫做后期绑定。
静态绑定的例子
如有一个父类 Human,它派生出来三个字类 Chinese 类、American 类、British类,三个子类中都重写了父类中的 speak() 方法,在测试类中用静态绑定的方式调用方法 speak()。
1 | public class Human { |
1 | public class Chinese extends Human{ |
1 | public class American extends Human{ |
1 | public class British extends Human{ |
1 | public class Main { |
这种调用方式是在代码里指定的,编译时编译器就知道 c 调动的是 Chinese 中的 speak() 方法,a 调用的是 American 的 speak() 方法。
动态绑定的例子
如果我们在测试类中做如下改动
1 | public class Main { |
运行结果:
speak English.
speak chinese.
speak English.
speak American English.
speak English.
此时,Human 类中随机生成 Chinese 类、American 类和 British 类的对象,编译器不能根据代码直接确定调用那个类中的 speak() 方法,直到运行时才能根据产生的随机数 n 的值来确定 humans[i]到底代表哪一个子类的对象,这样才能最终确定调用的是哪个类中的 speak() 方法,这就是动态绑定。
向下转型
与向上转型相对应的就是向下转型了。向下转型是把父类对象转为子类对象。(请注意!这里是有坑的。)它是用子类引用指向父类实例。
下图,在进行转换是会报错
1 | Animal a = new Cat(); |
这就告诉我们向下转型不能自动转换,我们需要强转,所以乡下转型又叫做强制类型转换。
正确的语句是:
1 | //还是上面的animal和cat dog |
输出结果:
猫吃鱼
1 | Dog d = (Dog) a; |
报错:java.lang.ClassCastException: Cat cannot be cast to Dog
1 | Animal a1 = new Animal(); |
报错:java.lang.ClassCastException: Animal cannot be cast to Cat
为什么第一段代码不报错呢?相比你也知道了,因为a本身就是 Cat 对象,所以它理所当然的可以向下转型为Cat,也理所当然的不能转为 Dog,你见过一条狗突然就变成一只猫这种现象?
而a1为 Animal 对象,它也不能被向下转型为任何子类对象。比如你去考古,发现了一个新生物,知道它是一种动物,但是你不能直接说,啊,它是猫,或者说它是狗。
向下转型注意事项
- 向下转型的前提是父类对象指向的是子类对象(也就是说,在向下转型之前,它得先向上转型)
- 向下转型只能转型为本类对象,兄弟类之间不能进行强制类型转换。(猫是不能变成狗的)。
大概你会说,我特么有病啊,我先向上转型再向下转型??
声明上转型对象是为了可以直接调用子类中重写的方法,但是不能调用子类新增的方法。而下转型对象可以调用子类新增的方法
我们回到上面的问题:喂动物吃饭,吃了饭做点什么呢?不同的动物肯定做不同的事,怎么做呢?
1 | public void eat(Animal a){ |
现在,你懂了么?这就是向下转型的简单应用,可能举得例子不恰当,但是也可以说明一些问题。
敲黑板,划重点!看到那个 instanceof 了么?
instanceof 运算符
instanceof 运算符用来判断对象是否可满足某个特定类型实例特征,简单的来说,就是判断其左边对象是否为其右边类的实例,返回boolean类型的数据。可以用来判断继承中的子类的实例是否为父类的实现。
1 | public class Main { |
运行结果:
true
true
false
true
true
false
true
true
抽象类 & 抽象方法
应用场景
某个父类只是限定其子类应该包括怎样的方法,但不需要准确知道这些子类如何实现这些方法。
抽象类
Java 中使用抽象类,限制实例化
1 | public abstract class Animal{ |
抽象方法
abstract 也可用于方法——抽象方法
1 | public abstract void eat(); |
注意
- 抽象类不能直接实例化,必须借助子类完成相应的实例化操作
- 子类如果没有重写父类中所有的抽象方法,则也要定义为抽象类
- 抽象方法所在的类一定是抽象类
- 抽象类中可以没有抽象方法
接口
- 接口定义了某一批类所需要遵守的规范
- 接口不关心这些类的内部数据,也不关心这些类里方法的实现细节,它只规定这些类里必须提供某些方法
语法
1 | [修饰符] interface 接口[extends 父接口1,父接口2..]{ |
注意
- 接口可以实现多继承,即一个子接口可以同时继承多个父接口
- 实现接口的类如果不能实现接口中所有待重写的方法,则必须设置为抽象类
- 一个类可以继承自一个父类,同时实现多个接口
内部类
在 Java 中,可以将一个类定义在另一个类里面或者一个方法里面,这样的类称为内部类
与之对应,包含内部类的类被称为外部类