目录
1.多态
1.1多态概述
同一个对象,在不同时刻表现出来的不同形态
(也就是多态研究的是对象的不同形态)
举例说明:(说一个猫的例子)
我们可以说猫是猫:猫 cat = new 猫();
我们也可以说猫是动物:动物 animal = new 猫();
(这个时候猫这个对象,其实是把它认为是一只动物了)
这里猫在不同的时刻表现出来了不同的形态,这就是多态。
程序中多态的前提和体现:
- 有继承或者实现关系(此处实现关系还没有学习)
- 有方法重写
- 有父类引用指向子类对象(动物 animal = new 猫();就好比这个代码左边内容就是父类的引用,右边内容就是子类的对象,因为我们研究的是对象的多态,重点要看子类对象)
程序演示:
public class Animal {
public void eat(){
System.out.println("动物吃东西");
}
}
//1.有继承/实现关系
class Cat extends Animal {//2.有方法重写,直接输入父类方法名就可以快捷输入
@Override
public void eat() {
super.eat();
}
}
/*
多态:
同一个对象,在不同时刻表现出来的不同形态
举例:猫
我们可以说猫是猫:猫 cat = new 猫();
我们也可以说猫是动物:动物 animal = new 猫();
这里猫在不同的时刻表现出来了不同的形态:这就是多态
多态的前提和体现:
有继承/实现关系
有方法重写
有父类引用指向子类对象
*/
class AnimalDemo {
public static void main(String[] args) {
//3.父类引用指向子类对象,就是创建子类对象调用父类方法
Animal animal = new Cat();
animal.eat();
}
}
1.2多态中成员访问特点
先到代码中演示,再来小结:
public class Animal {
//1.在动物类里面定义了一个成员变量和方法
public int age = 40;
public void eat(){
System.out.println("动物吃东西");
}
}
//2.猫类继承动物类
class Cat extends Animal{
//3.在猫类里面有两个成员变量
public int age = 20;
public int weight = 10;
//4.重写eat方法
@Override
public void eat() {
System.out.println("猫吃鱼");
}
//成员方法
public void playGame(){
System.out.println("猫捉迷藏");
}
}
/*
测试类
*/
class AnimalDemo {
public static void main(String[] args) {
//5.有父类引用指向子类对象
Animal a = new Cat();
/*6.因为我们最终new的是猫,猫类里面有两个成员变量
通过子类对象输出成员变量看看输出的是谁
*/
System.out.println(a.age);//40
/*9.最终执行结果是40,说明我们通过多态的形式去访问
成员变量其实是访问父类中的成员变量。那么也就是说
多态的形式访问成员变量,它的编译要看左边的内容,
它的运行也要看左边。
*/
//7.输出weight报错了
// System.out.println(a.weight);
/*8.虽然说我们最终new的内存中的对象是Cat,
但是外界看到的其实是动物这个引用,所以说
我们通过多态的形式在访问成员变量的时候它的
编译要看左边,首先得看父类里面有没有,它里面有
才行,所以age没保存,它里面没有weight这个成员变量
所以a.weight报错了*/
//10.访问成员方法
a.eat();//猫吃鱼
/*12.执行得到猫吃鱼,猫里面的eat方法输出的是猫吃鱼
动物里面的eat方法输出的是动物吃东西,由此可见,这一次
输出的是重写后的方法中的内容,所以我们说多态的形式访问
成员方法编译看左边,运行看右边。
*/
/*11.访问子类方法也报错了,说明方法它也是看
左边的Animal有没有,Animal没有 所以它这里报错
*/
// a.playGame;
}
}
小结:
成员变量:编译看左边,执行看左边。
成员方法:编译看左边,执行看右边。
为什么成员变量和成员方法的访问不一样呢?
因为成员方法有重写,而成员变量没有。
1.3多态的好处和弊端
先到程序中演示再小结:
public class Animal {
//1.动物类里面有一个eat方法
public void eat(){
System.out.println("动物吃东西");
}
}
//2.猫继承动物
class Cat extends Animal{
//3.重写eat方法
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
//14.Dog继承Animal
class Dog extends Animal{
//15.继承之后重写方法
@Override
public void eat() {
System.out.println("狗吃骨头");
}
//36.在狗类里面添加一个方法
public void lookDoor(){
System.out.println("狗看门");
}
}
/*31.还想添加一个动物,继承Animal重写eat方法,
这一次就不需要在操作类里面再写方法了,只需要到测试类里面创建对象调用方法
*/
class Pig extends Animal{
@Override
public void eat() {
System.out.println("猪吃白菜");
}
}
/*
动物操作类
*/
class AnimalOperator {//4.在这个类里面有一个方法,参数是猫类型的
//11.就相当于把new Cat传递给这个形参c
// public void useAnimal(Cat c) {//12.这里形参就相当于Cat c = new Cat();
// c.eat();//13.这里调方法最终就是调的Cat里面的eat方法。
// }
//16.在操作类里面添加新方法
//19.就相当于把new Dog传递给了这个地方
// public void useAnimal1(Dog d){//20.相当于 Dog d = new Dog();
// d.eat();//21.这里调用方法最终就是调用的Dog里面的eat方法
//}
/*22.假如说我要添加很多的动物,都需要新建类,这样很麻烦,还要去动物操作类里面修改方法
因为我要添加一个新方法,最后到测试类里面来创建这个新添加的动物的对象,然后调用方法。我们
要添加一个动物,要建一个类,这样操作没有问题。最后测试创建这个动物类的对象也没有问题,
但是这个动物操作类我每次进来都添加一个方法,这样太麻烦了,有没有更简单的方法呢?
有!继续演示→23.
*/
//23.把操作类里面的方法先注释掉,然后写一个方法,方法名也叫useAnimal
/*24.然后思考参数写什么
不管它是猫还是狗,它们都继承了Animal,也就是说它们都可以看成是动物,
所以在里面直接传一个Animal
*/
//27.创建对象的Cat c = new Cat();先把new Cat传递给了Animal a
//29.然后看测试类里面的Dog d = new Dog();,与上面同理
public void useAnimal(Animal a){
/*28.这一次就相当于Animal a = new Cat();
然后编译看左边,Animal里面有一个eat方法,然后执行看右边
Cat里面重写了eat方法,所以控制台输出猫吃鱼
*/
//30.Animal a = new Dog,编译看左,执行看右,所以输出狗吃骨头
a.eat();
//37.然后到操作类的方法里面里面调方法
// a.lookDoor();//报错
/*38.也就是说这个多提的形式,
它不能访问我们具体的子类所特有的功能,所以这是多态形式访问的一个弊端
*/
}
}
/*
测试类
*/
class AnimalDemo {
public static void main(String[] args) {
//6.在测试类里创建动物操作类的对象,调用方法
AnimalOperator a = new AnimalOperator();
//8.注意调用方法的参数是猫类型的,所以要先创建猫类型的变量
Cat c = new Cat();//9.此处这个c就相当于new Cat
//7.对象有了之后调用方法
a.useAnimal(c);//10.然后把c传递给这个方法的参数
//17.创建动物操作类对象调用方法,这里直接使用同一个对象就好了
Dog d = new Dog();
a.useAnimal(d);//18.这个d就是new Dog
//25.然后执行得到的结果还是一样的 猫吃鱼和狗吃骨头,怎么理解呢?
//32.创建猪的对象
Pig p = new Pig();
//33.然后把p传递给这个方法的参数
a.useAnimal(p);//34.执行得到猪吃白菜
/*35.所以以后要添加新的动物类只许要添加一个类,
然后在这添加对象,然后调方法就可以了,操作类里的内容就不需要再改了
看到这里就可以明白多态的好处:提高了程序的拓展性,它的具体体现就是
定义方法的时候,使用父类型做参数,将来在使用的时候,使用的是具体的
子类型,参与操作。
接着再来说→36.
*/
}
}
小结:
多态的好处:提高了程序的扩展性。
具体体现:定义方法的时候,使用父类型作为参数,将来在使用的时候,使用具体的子类型参与操作。
多态的弊端:不饿能使用子类的特有功能。
1.4多态中的转型
多态的转型分类:
- 向上转型:
从子到父
父类引用指向子类对象
- 向下转型:
从父到子
父类引用转为子类对象
如何理解向上转型和向下转型呢?
直接程序中演示:
public class Animal {
//1.动物类里面有一个eat方法
public void eat(){
System.out.println("动物吃东西");
}
}
//2.Cat继承Animal
class Cat extends Animal{
//3.改写eat方法
@Override
public void eat() {
System.out.println("猫吃鱼");
}
//4.特有方法
public void playGame(){
System.out.println("猫捉迷藏");
}
}
/*
向上转型:
从子到父
父类引用指向子类对象
向下转型:
从父到子
父类引用转为子类对象
*/
class AnimalDemo {
public static void main(String[] args) {
//5.在测试类里面创建一个多态方式的对象
//6.new Cat创建的是子类对象,Animal a是父类引用
Animal a = new Cat();//7.父类引用指向子类对象,这个动作称为向上转型
//8.也就是把子类对象赋值给父类引用,这个叫向上转型
//9.调方法
a.eat();//猫吃鱼
/*10.再来解释一下这个操作,
编译看左边Animal a,执行看右边new Cat();
先看左边Animal里面有eat这个方法,所以编译通过
右边Cat重写了eat方法,所以在控制台输出的是猫吃鱼
*/
/*11.接下来Cat里面有一个特有方法,现在我想通过a去访问这个特有方法
就会报错,因为编译看左边Animal里面没有这个方法,所以这里报错
*/
// a.playGame();
//12.而现在我就想调用这个方法应该怎么办呢?
//13.很简单,我们创建Cat类的对象就好了
// Cat c = new Cat();
//14.然后调用方法
// c.eat();//猫吃鱼
// c.playGame();//猫捉迷藏
/*15.这一次我们就可以调用猫的特有方法了,
但是 我们这里创建的是猫的对象,它当然可以使用猫的特有方法了
我现在是在内存中有两个猫的对象了,Animal a = new Cat();有一个
Cat c = new Cat();有一个,为了一个特有的方法创建两个对象,没有必要啊
所以我们要把第二个创造对象给改一下
*/
//16.我现在还想去用这个特有方法可以怎么办呢?
//17.这里就要用到向下转型
//18.这里就是把父类对象a强转为Cat对象,然后赋值给Cat对象c
Cat c = (Cat)a;
//19.然后通过c来调用特有方法
c.eat();//猫吃鱼
c.playGame();//猫捉迷藏
//20.这样就没有问题,这样就是通过向下转型解决了多态的弊端(不能访问子类的特有功能)。
}
}
1.5多态转型内存图解
这里有一个代码块:
有一个测试类 类名:AnimalDemo
在类里面有一个main方法,然后咋在main方法里面
首先main方法会加载到栈内存里面。
进去之后执行代码
Animal a = new Cat();
使用多态式:父类引用指向子类对象创建了Animal父类对象a。也就是这里会把子类对象赋值给父类引用(向上转型)。
此时Animal a 就会出现在main方法中
new Cat();
首先要看一下Cat这个类它继承了Animal里面有两个方法一个重写Animal的eat方法一个Cat特有方法playGame。
然后再来看一下Animal这个方法里面有一个eat方法。
然后new Cat就会在堆内存就会开辟一个空间(假设地址值是001)
这里重点要记得Cat是继承了Animal
那么这个时候把这个对内存的地址给了Animal a
这是多态的形式,也叫向上转型:
执行此多态代码编译
的左边Animal a,执行看右边new Cat();
首先会进入到Animal类里面看看有没有eat方法,如果有就编译通过,没有就报错。然后执行的时候去到Cat类里面看看有没有重写方法Cat,有就执行此改写的方法,没有会执行父类Animal的eat方法。
发现Cat里面有eat方法然后eat方法就会加载到栈内存
然后在控制台输出猫吃鱼。
eat方法执行完毕就会从栈内存消失
然后往下执行Cat c = (Cat) a;
Cat c这个动作就会在main方法中出现。
然后再来看Cat a就会通过001指向堆内存这块内容
然后堆内存中这个001就是一个Cat,这个转换是可以进行的
然后把这个地址值001也给了这个c
(这样操作就不会在内存空间创建一个新的对象),这就是父类引用转换为子类对象(向下转型)。
然后通过c调用eat和playGame方法
c.eat();
c.playGame();
这个c代表的是一只猫然后我们就要调Cat里面的eat方法和playGame方法
然后执行完毕继续向下执行
a = new Dog();
这个Dog也继承了Animal,Dog类里面也有重写的Animal中的eat方法。
这个时候在堆内存就会再new一个空间出现new Dog(地址值是002)因为Dog继承了Animal所以它也是可以赋值给a的也是多态的形式(向上转型)。
这个时候就把Animal a的地址值改为了002
接着往下执行a.eat();
编译看左边,执行看右边,
然后通过父类对象a访问002调用Dog类中的eat方法,同理编译看右,执行看左。首先啊看父类对象a是Animal类的对象,所以就会进入到Animal类里看看有没有eat方法,如果有,就会编译通过,没有就报错。
然后执行右边 new Dog();会进入到Dog类里看看有没有写eat方法,如果有就执行,如果没有就执行Animal里的eat方法。
发现Dog类中有eat方法,这个时候Dog中的eat方法就会加载到栈内存中。
然后在控制台输出狗吃骨头
然后往下执行,方法执行完毕从栈内存消失。
接着往下执行
Cat cc = (Cat)a;
Cat cc就会出现在main方法中
(Cat) a;我们来看一下这一次的a通过002指向堆内存的002的空间地址
代表的是Dog而我们的Cat是只猫,虽然说我们的Dog和Cat都继承着Animal
但是它们相互之间是不等价的,所以它们相互之间是不能进行转换的
所以这个地方就会报错:
如果强制执行就会报错在控制台输出:
ClassCastException
到这报错便不会往下执行了。
到代码中演示一下:
public class Animal {
public void eat(){
System.out.println("动物吃东西");
}
}
class Cat extends Animal{
@Override
public void eat() {
System.out.println("猫吃鱼");
}
public void playGame(){
System.out.println("猫捉迷藏");
}
}
class Dog extends Animal{
@Override
public void eat() {
System.out.println("狗吃骨头");
}
public void lookDoor(){
System.out.println("狗看门");
}
}
class AnimalDemo {
public static void main(String[] args) {
//向上转型
Animal a = new Cat();
a.eat();//猫吃鱼
//向下转型
Cat c = (Cat) a;
c.eat();//猫吃鱼
c.playGame();//猫捉老鼠
//向上转型
a = new Dog();
a.eat();//狗吃骨头
//向下转型,类型转换异常
Cat cc = (Cat) a;//报错:ClassCastException
cc.eat();
cc.playGame();
}
}