文章目录
- 🚀文章导读
- 1.1 多态的概念
- 1.2 多态的实现条件
- 1.3 向上转型和向下转型
- 1.4 重写
- **面试问题:重写和重载的区别**
- 多态的实现
🚀文章导读
在本篇文章中,将会有很多的干货和知识点掌握,希望读者慢慢耐心阅读
在本篇文章中,将详细的讲解面向对象的第三大特性,多态,与前两种特性不同,也比前两种特性稍难,前两种特性都是通过某些关键字对类进行操作,对对象进行描述,而多态是一种非常抽象的思想。在理解多态之前,需要先了解学习多态前必备的知识点:
1、在继承体系下,向上转型和向下转型
2、父类和子类方法构成重写
3、通过父类的引用调用子类的重写方法
4、动态绑定和静态绑定
理解以上四点,就能理解什么是多态!!!
1.1 多态的概念
多态概念:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。
简单点来说就是:当完成某个行为时,不同的对象去完成时会产生不同的效果!
在Java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)
1.2 多态的实现条件
1、继承:在多态中必须存在继承关系的子类和父类
2、重写:子类对父类中的某些方法进行重新定义,在调用这些方法时,就会调用子类的重写方法
3、向上转型:在多态中,需要用父类引用指向子类对象,只有这样才能够具备通过父类调用子类中重写父类的方法
以上就是多态的实现条件,现在看不懂没关系,下面将会针对这些条件一一讲解
1.3 向上转型和向下转型
1、向上转型
向上转型其实就是把数据类型小的引用转换成数据类型大的引用,进行数据类型之间的一个转换,从而能够访问到大的数据类型中的成员方法,但是不能访问到子类特有的方法。下面用代码解释:
//父类Animal
public class Animal {
public String name;
public int age;
public void eat() {
System.out.println(name+"吃饭");
}
}
//子类Cat
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
//定义一个cat独有的新方法miMi
public void miMi() {
System.out.println("喵喵");
}
}
//TestAnimal类
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("小黑", 12);
Animal animal = new Animal("animal",12);
cat.miMi();//cat可以调用属于自己的miMi方法
cat.eat();//cat能调用eat方法,虽然子类中没有,但是它继承了父类,所以调用的是父类中的eat方法
animal.eat();//animal可以调用属于自己的eat方法
animal.miMi();//但animal不能调用cat引用的对象里的miMi方法,因为animal引用的是父类对象,父类对象里没有miMi这个方法
}
}
以上是没有经过向上转型时的代码 ,下面用代码演示三种向上转型的的三种方式;
1、直接赋值
2、方法传参
3、通过返回类型
1、直接赋值
将子类的对象直接赋值给父类的引用
public class TestAnimal {
public static void main(String[] args) {
//父类引用 - 引用了子类对象
Animal animal2 = new Cat("小猫",11);
animal2.eat();
animal2.miMi();//错误,没办法引用
//经过向上转型后,变成了父类引用 引用了子类对象,但是animal2还不能访问Cat类中的miMi()方法,那向上转型有什么用呢,不要着急,等一下讲过重写就知道了就知道了
}
}
2、方法传参
public class TestAnimal {
//形参用Animal类型接受
public static void eat(Animal animal) {
animal.eat();
}
public static void main(String[] args) {
//父类引用 - 引用了子类对象
Cat cat = new Cat("小猫",12);
//实参是Cat类型
eat(cat);
}
}
在main方法中,实例化了三个对象,然后调用了TestAnimal中的eat()方法,而传参时,传入的实参是对象的引用,而实参的类型是子类,形参用的是父类来接受的,这里就相当与进行了类型转化,因为,类也是一种引用数据类型嘛!所以,在这种场景下,发生了向上转型!
当然这里会出现一个疑问,不是参数类型不同不能进行传参么,对的,类型不同不能进行传参,但是,如果是父子类关系的话,就可以进行传参!
3、通过返回类型
public class TestAnimal {
//返回类型设置成父类Animal
public static Animal animalMethod() {
return new Cat("小猫",11);
}
public static void main(String[] args) {
//返回的类型是Animal,所以用Animal来接受
Animal animal = animalMethod();
animal.eat();
}
}
向上转型的优点:让代码的实现更加灵活;
向上转型的缺点:不能调用到子类特有的方法,只能调用到发生重写的方法;
以上就是向上转型的三种方式,请读者朋友慢慢品会。
2、向下转型
将类型大的向类型小的进行转换(不安全)
//父类
public class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
public void eat() {
System.out.println("吃饭");
}
}
//子类
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
public void miMi() {
System.out.println("喵喵");
}
}
//子类
public class Dog extends Animal{
public Dog(String name, int age) {
super(name, age);
}
}
//测试
public class TestAnimal {
public static void main(String[] args) {
//发生向上转型
Animal animal = new Cat("小猫",11);;
animal.miMi();//无法进行调用
//向下转型
Cat cat = (Cat)animal;
cat.miMi();
//对狗类进行向上转型
Animal animal1 = new Dog("小狗",12);
//对animal向下转型
Cat cat1 = (Cat)animal1;//运行时会抛出错误
cat1.miMi();
}
}
第一种情况:
当进行Cat进行向上转型后,通过父类引用仍然是没办法访问miMi方法,因为在Animal类中根本就没有miMi方法,所以,可以将animal进行向下转型,变成Cat类型的,因为Cat类里面有miMi方法,所以可以进行调用;
第二种情况:
当对狗类进行向上转型后,有对animal进行了向下转型,转化成了Cat类,并没有报错,但是运行时抛出了异常,因为animal本来指向的是狗类,但是非要强转成猫类,这样驴头不对马嘴,当然是不行的,所以就会报出类转换异常ClassCastException;而第一种情况是,因为animal本来指向的就是猫类,向下转型转换成猫类也是没问题的;所以向下转型是不安全的。
如果要进行向下转型,需要利用instanceof 作出检查,如果表达式为真,则可以安全转化;下面代码演示:
public class TestAnimal {
public static void main(String[] args) {
//发生向上转型
Animal animal = new Cat("小猫",11);;
//向下转型
Cat cat = (Cat)animal;
cat.miMi();
//对狗类进行向上转型
Animal animal1 = new Dog("小狗",12);
//假如if语句进行检查
if(animal1 instanceof Cat) {
Cat cat1 = (Cat)animal1;
cat1.miMi();
}else {
System.out.println("转换异常");
}
}
}
1.4 重写
概念:重写也称为覆盖,在子类中,对父类中的方法进行重新编写,等于把父类中方法复制黏贴到子类里面。
现在注意力回到刚才向上转型那里,刚刚经过向上转型后,父类引用指向了子类对象,虽然能够调用eat方法,但是调用的仍然是父类中的eat方法,那么现在,在子类中也定义一个和父类一模一样的eat方法,经过向上转型之后会发生什么变化呢?
//父类Animal
public class Animal {
public String name;
public int age;
public void eat() {
System.out.println(name+"吃饭");
}
}
//子类Cat
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
//定义一个cat独有的新方法miMi
public void miMi() {
System.out.println("喵喵");
}
//定义一个和父类中一模一样的eat方法
@Override
public void eat() {
System.out.println(name+"吃猫粮");
}
}
//TestAnimal类
public class TestAnimal {
//进行向上转型
public static void method(Animal animal) {
animal.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("小猫",11);
method(cat);
}
}
运行结果显示,虽然是父类引用,引用了子类对象,但是在调用eat方法时,并没有调用父类中的eat方法,而是通过父类引用去调用了子类中的重写了父类的eat方法,所以得出结论,因为在子类中,对父类的eat方法进行了重写,所以在调用eat方法是,会调用子类的eat方法。在下面多态的实现中还会结合例子为大家讲解;但是,在这里,还要再引出两个新的名词:动态绑定和静态绑定,刚刚在通过父类引用去调用子类中重写了父类的eat方法的这个过程就是动态绑定。
动态绑定(运行时绑定)概念:即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用了哪个类中的方法,方法的重写就是动态绑定;
下面通过汇编代码讲解为什么称为运行时绑定
静态绑定概念:在编译时,根据用户所传递的实参的类型及顺序确定了具体调用哪个方法。函数的重载就是静态绑定;
重写规则:
1、在子类中被重写的方法必须与父类中的方法外壳一模一样,但可以修改方法体中的内容,外壳即修饰符、返回值、方法名、参数列表
2、重写方法的返回值可以不同,但必须是父子类关系
3、注意:父类中,被private、final、修饰的方法不能被重写,静态方法和构造方法不能被重写
4、如果父类方法被public修饰,则在子类中重写该方法时,就不能被剩余的三个修饰符修饰,所以:在重写时,访问权限不能比父类中被重写的方法的访问权限更低。
5、重写方法可以用@Overrride注解显示描述,有了这个注解能进行一些合法的校验,例如把方法名字拼错了,此时编译器就会报错,提示无法构成重写。
6、构造器不能被重写,但可以被重载
面试问题:重写和重载的区别
1、方法的重载和重写都是实现多态的方式,区别在与前者实现的是编译时的多态性,而后者实现的是运行时的多态性
2、重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
3、重写:发生在父子类中,方法名、参数列表必须相同,返回在可以不同,但必须是父子类关系,在子类中,重写方法的访问修饰符要大于等于父类中的访问修饰符,如果父类方法访问修饰符是private,则子类中就不是重写
多态的实现
在代码中,写了一个父类Animal ,和子类Dog、子类Cat、子类Bird;它们分别都继承了父类Animal;所以继承方式是多个类继承同一个类;多说无益,下面将一步一步的讲解->
//父类Animal
public class Animal {
public String name;
public int age;
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
//成员方法
public void eat() {
System.out.println(name+"吃饭");
}
public void eat() {
System.out.println(name+"吃饭");
}
}
//子类cat
public class Cat extends Animal {
public Cat(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name+"吃猫粮");
}
}
//子类Dog
public class Dog extends Animal{
public Dog(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name+"吃狗粮");
}
}
//子类Bird
public class Bird extends Animal{
public Bird(String name, int age) {
super(name, age);
}
@Override
public void eat() {
System.out.println(name+"吃鸟粮");
}
}
//类TestAnimal用于测试
public class TestAnimal {
public static void eat(Animal animal) {
animal.eat();
}
public static void main(String[] args) {
Cat cat = new Cat("小猫", 12);
Dog dog = new Dog("小狗", 13);
Bird bird = new Bird("小鸟", 14);
//因为eat()方法是用static修饰的,所以不用通过引用来调用eat()方法
eat(cat);//传入对象的引用
eat(dog);
eat(bird);
}
}
当传入的对象的引用不同时,通过animal所调用的eat方法就会发生不一样的效果,从而就验证了:当要完成一个行为时,不同的对象去完成时,产生的效果也不同!
你品,你细品!!!
再来看这张图,实例化了三个子类对象,分别调用了三次eat方法,将三个不同的引用分别作为实参传给了型参,而形参的类型是父类Animal类型,所以发生了向上转型,所以当animal引用的是Cat对象时,调用的就是子类Cat里面的eat()方法,当animal引用的是Dog对象时,调用的就是Dog里面的eat()方法,当animal引用的是Bird对象时,调用的就是Bird里面的eat()方法;所以这样就完成了通过父类去调用子类中的重写方法;当然,这是其中一种理解方式;还有第二种理解方式:当在main方法中调用eat方法时,传入一个cat引用,因为animal的类型是Animal,所以进行了向上转型,又因为,父类中的eat和子类Cat中的eat方法发生了重写,所以子类Cat中的eat方法覆盖了父类中的eat方法,所以在调用时,调用了被覆盖的父类中的eat方法!!!
所以只要能在继承的条件下发生向上转型,就能发生动态绑定,能发生动态绑定,就可以实现多态!
以上就是关于多态的讲解,多态是一种思想,需要我们慢慢的去品汇,去理解,如果觉得本篇文章不错,还望留下一个小小小的👍哟