1.多态的概念
通俗来说就是:多种形态,具体一点就是去完成某个行为,当不同的对象去完成的时候会产生出不同的效果
总的来说:同一件事情发生在不同对象的身上,就会产生不同的结果
2. 多态实现的条件
Java中想要实现多态,必须要满足以下几个条件,缺一不可:
- 必须在继承体系下
- 子类必须要对父类中的方法进行重写
- 通过父类引用调用重写的方法
多态实现:在代码运行时,当传递不同类对象时,会调用对应类中的方法
public class Animal {
String name;
int age;
public Animal(String name, int age){
this.name = name;
this.age = age;
}
public void eat(){
System.out.println(name + "吃饭");
}
}
public class Cat extends Animal{
public Cat(String name, int age){
super(name, age);
}
@Override
public void eat(){
System.out.println(name+"吃鱼~~~");
}
}
public class Dog extends Animal {
public Dog(String name, int age){
super(name, age);
}
@Override
public void eat(){
System.out.println(name+"吃骨头~~~");
}
}
public class TestAnimal {
// 编译器在编译代码时,并不知道要调用Dog 还是 Cat 中eat的方法
// 等程序运行起来后,形参a引用的具体对象确定后,才知道调用那个方法
// 注意:此处的形参类型必须时父类类型才可以
public static void eat(Animal a){
a.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
eat(cat);
eat(dog);
}
}
当类的调用者在编写eat这个方法的时候,参数类型为Animal,此时在方法内部并不关注当前的a指向的是那个类型的实例,此时a这个引用调用eat方法可能会有多种不同的表现,这种行为称为多态
3.重写
3.1概念
重写:也称为覆盖。重写就是对非静态方法,非private修饰的方法,非final修饰的方法,非构造方法的实现过程进行重新编写,返回值和形参都不能改变,即外壳不变,实现核心重写,好处就是子类可以在父类的基础上实现自己的方法,定义特定于自己的行为。
3.2 规则
- 子类在重写父类的方法时,一般情况下返回值和形参都不能改变
- 被重写的方法返回值类型有时可以不同,但是他们必须具有父子关系
- 访问权限不可以比父类中被重写的方法更低,例如:父类方法被public修饰,则子类方法中重写该方法就不能声明为private
- 父类方法不可以被static,final,private修饰,也不可以是构造方法,否者不可以被重写
- 重写的方法,编译器会用“@override”来注解,可以检查合法性
3.3重写和重载的区别
区别点 | 重写 | 重载 |
---|---|---|
参数列表 | 一定不可以修改 | 必须修改 |
返回类型 | 一定不可以修改(除非有父子关系) | 可以修改 |
访问限定符 | 一定不可以降低权限 | 可以修改 |
3.4 静态绑定与动态绑定
- 静态绑定:也称为前期绑定,在编译时根据用户传递的参数类型就可以知道调用哪个方法,典型代表为方法的重载
- 动态绑定:也称为后期绑定,在编译时,不可以确定方法的行为,无法知道调用哪个方法,需要等程序运行时才可以确定。
4. 向上转型和向下转型
4.1 向上转型
- 概念:创建一个子类对象,把他当做父类对象来使用,就是范围从小到大的转换
- 语法格式:==父类类型 对象名=new 子类类型()
Animal animal=new Cat("miaomiao",2);
我们就拿上面的代码来说明:
3. 使用场景
- 直接赋值
- 方法传参
- 方法返回
public class TestAnimal {
// 2. 方法传参:形参为父类型引用,可以接收任意子类的对象
public static void eatFood(Animal a){
a.eat();
}
// 3. 作返回值:返回任意子类对象
public static Animal buyAnimal(String var){
if("狗".equals(var) ){
return new Dog("狗狗",1);
}else if("猫" .equals(var)){
return new Cat("猫猫", 1);
}else{
return null;
}
}
public static void main(String[] args) {
Animal cat = new Cat("元宝",2); // 1. 直接赋值:子类对象赋值给父类对象
Dog dog = new Dog("小七", 1);
eatFood(cat);
eatFood(dog);
Animal animal = buyAnimal("狗");
animal.eat();
animal = buyAnimal("猫");
animal.eat();
}
}
通过上述的代码,我们不难发现向上转型的有限,他可以使得代码更加简单灵活,但是缺点也很明显,不可以调用子类特有的方法,怎么办呢,我们这里便引出了向下转型
4.2向下转型
将一个子类对象经过向上转型之后当做了父类的对象使用,无法再调用到子类的方法,但是我们有时候会想要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转型
- 语法格式:子类类型 对象名=(子类类名)经过向上转型的父类对象
从上述语法格式来看,其实就是强制类型转换 - 安全性问题
向下转型存在不安全的问题,不同于向上转型,是安全的,比如原来一个对象是狗类,向上转为了动物类,再向下转型的时候必须转回狗类,不可以转为猫类
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
// 向上转型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
// 编译失败,编译时编译器将animal当成Animal对象处理
// 而Animal类中没有bark方法,因此编译失败
// animal.bark();
// 向上转型
// 程序可以通过编程,但运行时抛出异常---因为:animal实际指向的是狗
// 现在要强制还原为猫,无法正常还原,运行时抛出:ClassCastException
cat = (Cat)animal;
cat.mew();
// animal本来指向的就是狗,因此将animal还原为狗也是安全的
dog = (Dog)animal;
dog.bark();
}
}
- 解决方案
使用instanceof判断一个对象所属于的类是否属于一个类的父类
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",2);
Dog dog = new Dog("小七", 1);
// 向上转型
Animal animal = cat;
animal.eat();
animal = dog;
animal.eat();
if(animal instanceof Cat){
cat = (Cat)animal;
cat.mew();
}
if(animal instanceof Dog){
dog = (Dog)animal;
dog.bark();
}
}
}
5.避免在构造方法中使用重写的方法
下面展示一段有坑的代码
class B {
public B() {
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
@Override
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
运行结果:D.func() 0
难道结果不应该是1吗,我们下面解释为什么不是1
在构造D对象的同时,会调用B的构造方法,在子类构造的时候,先构造父类。B的构造方法调用了func方法,此时会触发动态绑定,会调用D中的func,此时D还没有触发构造方法,D自身还没有构造,此时num处于未初始化状态,值为0,所以输出为0.
所以我们在构造方法中调用重写方法时一定要注意,在自己编程是尽量避免这种行为