文章目录
- 一、多态
- 1.1 向上转型
- 1.2 方法的重写
- 1.3 动态绑定
- 1.4 向下转型
- 1.5 理解多态
- 二、抽象类
- 2.1 语法规则
- 2.2 抽象类的作用
上一节我们学习了包,以及面向对象的基本特征之一:继承
【Java】包和继承
在此基础上,这一节,将介绍面向对象的其他的两个基本特征:
多态
和
抽象类
。
一、多态
什么是多态?
按照字面意思来说就是”多种状态“。
官方的解释是,多态指为不同数据类型的实体提供统一的接口
。多态类型可以将自身所支持的操作套用到其他类型的值上。
说到这里,友友们也许开始 emo 了,我好像看懂了,我有好像妹看懂。。。
阳间的解释是,我和我爸说:”爸!我要吃面!“然后我爸 DuangDuangDuang 的做了一碗面,做完后,”啪!“放我面前,我晓得了我吃的是小葱拌面。
又有一日,我和我爸说:”爸!我要吃面!“然后我爸 DuangDuangDuang 的做了一碗面,做完后,”啪!“放我面前,我晓得了我吃的是榨菜肉丝面。
又有一日,我和我爸说:”爸!我要吃面!“然后我爸 DuangDuangDuang 的做了一碗面,做完后,”啪!“放我面前,我晓得了我吃的是排骨面。
总结下来,不管我吃的什么面,他们都是面的一种,是面的不同的表现形式,这就是多态。
具体的解释是,有了继承
才有了多态
,多态是指各种各样的子类继承了父类,不满足于父类的某一方法,想要在原来的基础上加点新东西,于是使用了重写
的方法覆盖了父类的方法。最后的结局是父类中的某一方法,在继承该父类的各个子类当中表现出来各种各样的行为,使得同一个方法或属性在父类和子类们中具有不同的含义。另外实现多态还需要在创建子类时,用父类来 new 子类,即发生了向上转型
。
总结下来,想要实现多态,有三个必要条件:继承、重写以及向上转型。
1.1 向上转型
向上转型,实际上就是把子类对象赋值给父类对象的引用
。只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。
向上转型发生的时机:
- 直接赋值
- 方法的传参
- 方法的返回值
方法一:直接赋值
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
//直接赋值
Animal animal1 = new Bird(2,"麻雀");
//详细步骤
/*Animal animal = new Animal(2,"动物");
Bird bird = new Bird(2,"麻雀");
animal = bird;*/
}
}
💬代码解释:
这里的 animal 和 animal1 都是父类(Animal)的引用,指向一个子类 (Bird) 的实例,这就是
直接赋值
版本的向上转型。
方法二:方法的传参
📑代码示例:
public class TestDemo {
public static void func(Animal animal2) {
//......
}
public static void main(String[] args) {
Bird bird = new Bird(2,"麻雀");
func(bird);
}
}
💬代码解释:
这里调用了 func() 方法,实参是 Bird(子类) 类的引用,形参接收类型是 Animal(父类) 类,这就是
方法的传参
版本的向上转型。
方法三:方法的返回值
📑代码示例:
public class TestDemo {
public static Bird func2() {
Bird bird = new Bird(2,"麻雀");
return bird;
}
public static void main(String[] args) {
Animal animal3 = func2();
}
}
💬代码解释:
func() 方法的返回值类型是 Bird(子类),用 Animal(父类)这一类型接收,这就是
方法的返回值
版本的向上转型。
实际上,这三种方法本质上并没有什么区别。
1.2 方法的重写
实现多态,重写
也是必不可少,非常重要的
在之前介绍方法的那一节里,我们有介绍过什么是重载,重载和重写是需要进行区分的。
【Java】方法的使用
重载
是编译时
的多态性,使在同一个类中有多个同名,参数个数或类型不同的方法,调用该方法时通过传递不同的个数或类型参数来决定具体使用的方法。
重写
是运行时
的多态性,限于拥有继承关系的子父类,子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。方法重写又称方法覆盖
,方法覆写
。
重写条件:
- 方法名必须要相同
- 方法的返回值要相同(特殊情况除外)
- 方法的参数列表相同
📑代码示例:
class Animal {
public int age;
public String name;
public Animal(int age, String name) {
this.age = age;
this.name = name;
}
public void eat() {
System.out.println(this.age +"岁的" + this.name + " 会吃饭");
}
}
class Bird extends Animal{
public Bird(int age, String name) {
super(age, name);
}
@Override
public void eat() {
System.out.println("吃吃吃!!!");
}
}
💬代码解释:
- Bird 类继承了 Animal 类,并且重写了父类(Animal) 中的 eat() 方法。
- 重写的快捷键是
Ctrl + o
,选择需要重写的方法。- 针对重写的方法, 可以使用
@Override
注解来显式指定,可以对重写的方法进行一定的校验,例如当重写的方法名字写错时,有了该注解,就会报编译错误,示意该方法名字有误,并非重写的方法。
注意事项:
static
修饰的静态方法不能重写,因为没有办法达到多态的效果。当进行了我们以为的重写时,向上转型后,用父类引用变量来调用我们重写的静态方法时,会发现只会调用原来父类的静态方法,而并非我们所期待的子类的静态方法。- 被
final
修饰的方法不能被重写,重写后会产生编译错误。当一方法被 final 修饰后,该方法就被称为密封方法,该方法是不能够被重写的。- 重写的父类的方法不能被
private
关键字修饰。子类
重写父类的方法时,该方法的访问修饰限定符权限
一定要大于等于父类
的权限。构造方法是不能被重写
的。重写的方法,方法名是一样的,而父类的构造方法的方法名和父类的类名是一样的,倘使构造方法可以重写,将就意味着子类和父类同名。
虽然构造方法不能进行重写,但是构造方法中是可以调用重写的方法的,那么这其中又会有什么需要注意的呢?
📑代码示例:
class Animal {
public String name;
public Animal(String name) {
eat();
this.name = name;
}
public void eat() {
System.out.println(this.name + "会吃饭");
}
}
class Bird extends Animal{
public Bird(String name) {
super(name);
}
@Override
public void eat() {
System.out.println(this.name + "正在吃吃吃!!!");
}
}
public class TestDemo {
public static void main(String[] args) {
Bird bird = new Bird("阿花");
}
}
🏸 代码结果:
代码中,new 了一个 Bird 类型的对象,传了参数"阿花",为何最后结果中,小鸟"阿花"却不配拥有姓名。。。
渊源就在于在父类的构造方法的第一行对重写的方法 eat() 进行了调用,让我们来追踪一下。。。
过程追踪:
- 实例化了一个Bird 类型的对象,调用了有一个参数的构造方法,将“阿花”传了过去
- 到达了子类的构造方法时,需要先帮父类进行构造,又把“阿花”传给了父类的构造方法
- 父类的构造方法内第一行就调用了重写方法 eat(),于是调用了 Bird 类里面的 eat() 方法
- 然而此时父类还没构造完毕,“阿花”还没有真正的拥有姓名时,就去执行 eat() 方法
- 结果中没有“阿花”。。。
因此想要“阿花”拥有姓名,一定要把重用方法
的调用放在赋值之后
。
这个坑在选择题中可能会考到,因此还需看仔细,不要掉坑里了。
1.3 动态绑定
动态绑定
后,对象在调用方法的时候能够自己判定改调用谁的方法是自己的方法还是父类的方法。
📑代码示例:
实例一:
class Animal {
public int age;
public String name;
public void eat() {
System.out.println("Animal会吃饭");
}
}
class Bird extends Animal{
public void eatBird() {
System.out.println("小鸟吃吃吃!!!");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal = new Bird();
animal.eat();
}
}
🏸 代码结果:
实例二:
class Animal {
public int age;
public String name;
public void eat() {
System.out.println("Animal会吃饭");
}
}
class Bird extends Animal{
@Override
public void eat() {
System.out.println("小鸟吃吃吃!!!");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal = new Bird();
animal.eat();
}
}
🏸 代码结果:
💬代码解释:
- 实例一和实例二的区别就在于是否调用了
重写方法
。- 实例一中,没有重写方法,因此通过 Animal 类访问 eat() 方法,理所应当的调用父类的 eat() 方法。
- 实例二中,有调用子类和父类同名的重写方法 eat() 方法,在次通过 Animal 类访问 eat() 方法,调用的是子类的 eat() 方法。
- 实际上,实例二在编译的时候仍然调用的是父类的方法,运行的时候,实际上调用的是子类的方法,这就是动态绑定。
动态绑定的条件:
- 发生向上转型,即把子类对象赋值给父类对象的引用
- 通过父类引用来调用子类和父类同名的重写方法 eat() 方法
1.4 向下转型
既然有向上转型,那么自然也会有向下转型,即父类对象转换成子类对象,相对而言,用的范围更小,不那么常见。
📑代码示例:
class Animal {
public int age;
public String name;
public void eat() {
System.out.println("Animal会吃饭");
}
}
class Bird extends Animal{
@Override
public void eat() {
System.out.println("小鸟吃吃吃!!!");
}
}
public class TestDemo {
public static void main(String[] args) {
Animal animal = new Bird();
Bird bird = (Bird) animal;
bird.eat();
}
}
🏸 代码结果:
💬代码解释:
向下转型的前提是要先进行向上转型,如代码所示,需要保证 Animal 引用的是 Bird 类型的对象,否则根本没有办法将 Animal 进行强制类型转换。
虽然如此,向下转型使用起来仍然容易出错,如若将上面的 main 函数改成这样…
📑代码示例:
public class TestDemo {
public static void main(String[] args) {
Animal animal1 = new Animal();
Bird bird = (Bird) animal1;
bird.fly();
}
}
🏸 代码结果:
代码解释:
- 此时,animal1 这个引用指向的对象是 Animal 类型的,致使强制类型转换出错,animal1 这个引用没有办法转换成 Bird 类型。
- 为了让向下转型更加的安全,对于该代码,我们有必要先判断一下,animal1 的本质是否为 Bird 类型,如果不是,就根本不进行强制类型转换,进行向下转型。需要使用到关键字
instanceof
,该关键字就是用来判断一个引用是否是某个类的实例,是返回 true ,提高了向下转型的安全性。具体代码为: if (animal1 instanceof Bird) { //…}
1.5 理解多态
通过上面的对多态基础条件的将讲解,想必诸君心中慢慢开始了解多态的含义,在此,将对多态进行举例总结,更加深入理解多态。
📑代码示例:
利用多态来画图形
实例一:
class Shape {
public void draw() {
}
}
class Cycle extends Shape {
@Override
public void draw() {
System.out.println("画一个圆形○");
}
}
class Rect extends Shape {
@Override
public void draw() {
System.out.println("画一个矩形□");
}
}
public class TestDemo {
public static void paint(Shape shape) {
shape.draw();
}
public static void main(String[] args) {
Cycle cycle = new Cycle();
paint(cycle);
paint(new Rect());
}
}
🏸 代码结果:
💬代码解释:
- 这里用
paint
方法来接受传递进来的子类对象,形参类型是父类 Shape 类,从而实现了多态的条件之一,向上转型。通过 shape 引用来调用子类和父类都重写的方法 draw(),从而打印出各种图形,即表现出各种状态,这就是多态。- TestDemo 类以外的部分就是
类的实现者
完成的部分,TestDemo 类以内的部分就是类的调用者
完成的部分
实例二:
//类的实现者部分同上,又加了一个画三角形
public class TestDemo {
public static void paint() {
Shape[] shapes = {new Cycle(),new Rect(),new Rect(),new Triangle()};
for (Shape shape1:shapes) {
shape1.draw();
}
}
public static void main(String[] args) {
paint();
}
}
🏸 代码结果:
多态的优点:
- 多态是封装的更进一步, 让类调用者对类的使用成本进一步降低,即类的调用者只需要知道对象具有什么方法就行,不需要对对象的类型过分了解
- 通过实例二,我们发现多态可以避免使用多余的 if-else 语句,如果没有多态,我们就需要创建一个字符串数组,字符串为想要打印的图形,将他们通过一道道分支语句进行判断是否为想要打印的图形。
- 使用多态,使得代码的改动成本降低。
二、抽象类
在利用多态画图形的实例一中,父类 Shape 中的 draw 方法并没有实际工作,画图的工作都被继承这个父类的子类中的重写 draw 方法包了。向父类中 draw 方法这样没有实际用途的方法,可以将其设计成抽象方法
,而抽象类
就是包含这个抽象方法的类。
2.1 语法规则
📑代码示例:
abstract class Shape {
public abstract void draw();
}
abstract class A extends Shape {
public abstract void draw2();
}
class B extends A {
@Override
public void draw() {
}
@Override
public void draw2() {
}
}
💬代码解释:
- 在 draw 方法前加上
abstract
关键字, 表示这是一个抽象方法
. 包含抽象方法的类也要被abstract
关键字修饰, 表示这是一个抽象类
- 抽象方法没有方法体(没有
{ }
, 不能执行具体代码)- 抽象类
不可以被实例化
,其存在就是为了被继承
- 抽象类中也可以定义成员变量和成员方法,也被重写被子类调用(用 super 关键字)
- 一个类继承了抽象类,那么该类需要
重写所有的抽象方法
,如果不重写就会编译出错,重写的快捷方法处了Ctrl + o
以外,也可以将光标放置在报错代码处,选择Implement methods(Alt + Shift + Enter)
*abstract
不能final
共存 ,被final
修饰的类不能拥有子类,而抽象类就是要成为父类的;不能和private
共存,抽象方法被其修饰就没有办法被重写;不能和static
共存,static 修饰后是不依赖于对象的,直接调用类名就行,那么抽象方法就没有存在的必要- 抽象类 A 继承了抽象类 Shape,普通类 B 继承了 A ,则 B 里要将 A 和 Shape 中所有的抽象方法都重写
- 抽象类也可以发生向上转型,实现多态
2.2 抽象类的作用
- 写成抽象类,看代码时就知道这是抽象方法,而知道这个方法是在子类中实现的,所以有
提示作用
- 使用抽象类好比多了一重编译校验,防止在父类被误用,起到
警示作用
完!