Java 基础语法爆肝两万字解析 Java 的多态抽象类和接口
Posted 吞吞吐吐大魔王
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 基础语法爆肝两万字解析 Java 的多态抽象类和接口相关的知识,希望对你有一定的参考价值。
文章目录
上节介绍了 Java 的包和继承,如果这类知识有点疑惑的兄弟,可以去 万字解析 Java 的包和继承 这章看看,或许可以帮你解决一些疑惑哟!
今天这章主要介绍多态和抽象类,希望接下来的内容对你有帮助!
一、多态
在了解多态之前我们先了解以下以下的知识点
1. 向上转型
什么是向上转型呢?简单讲就是
把子类对象赋值给了父类对象的引用
这是什么意思呢,我们可以看下列代码
// 假设 Animal 是父类,Dog 是子类
public class TestDemo{
public static void main(String[] args){
Animal animal=new Animal("动物");
Dog dog=new Dog("二哈");
animal=dog;
}
}
其中将子类引用 dog 的对象赋值给了父类的引用,而上述代码也可以简化成
public class TestDemo{
public static void main(String[] args){
Animal animal=new Dog("二哈");
}
}
这个其实和上述代码一样,这种写法都叫“向上转型”,将子类对象的引用赋值给了父类的引用
其实向上转型以后可能用到的比较多,那么我们什么时候需要用它呢?
- 直接赋值
- 方法传参
- 方法返回
其中直接赋值就是上述代码的样子,接下来让我们看一下方法传参的实例
// 假设 Animal 是父类,Dog 是子类
public class TestDemo{
public static void main(String[] args){
Animal animal=new Dog("二哈");
func(animal);
}
public static void func1(Animal animal){
}
}
我们写了一个函数,形参就是父类的引用,而传递的实参就是子类引用的对象。也可以写成
public class TestDemo{
public static void main(String[] args){
Animal animal=new Animal("动物");
Dog dog=new Dog("二哈");
func(dog);
}
public static void func1(Animal animal){
}
}
那么方法返回又是啥样的呢?其实也很简单,如
// 假设 Animal 是父类,Dog 是子类
public class TestDemo{
public static void main(String[] args){
}
public static Animal func2(){
Dog dog=new Dog("二哈");
return dog;
}
}
其中在 func2 方法中,将子类的对象返回给父类的引用。还有一种也算是方法返回
public class TestDemo{
public static void main(String[] args){
Animal animal=func2();
}
public static Dog func2(){
Dog dog=new Dog("二哈");
return dog;
}
}
方法的返回值是子类的引用,再将其赋值给父类的对象,这种写法也叫“向上转型”。
那么既然我们父类的引用指向了子类引用的对象,那么父类可以使用子类的一些方法吗?试一试
class Animal{
public String name;
public Animal(String name){
this.name=name;
}
public void eat(){
System.out.println(this.name+"吃东西"+"(Animal)");
}
}
class Dog extends Animal{
public Dog(String name){
super(name);
}
public void eatDog(){
System.out.println(this.name+"吃东西"+"(Dog)");
}
}
public class TestDemo{
public static void main(String[] args){
Animal animal1=new Animal("动物");
Animal animal2=new Dog("二哈");
animal1.eat();
animal2.eatdog();
}
}
结果是不可以
因为本质上 animal 的引用类型是 Animal,所以只能使用自己类里面的成员和方法
2. 动态绑定
那么我们的 animal2 可以使用 Dog 类中的 eatDog 方法吗?其实是可以的,只要我们将这个 eatDog 改名叫 eat 就行
class Dog extends Animal{
public Dog(String name){
super(name);
}
public void eat(){
System.out.println(this.name+"吃东西"+"(Dog)");
}
}
修改后的部分代码如上,此时,我们之前的 animal2 直接调用 eat,就可以得到下面的结果
这也就是说明此时
animal1.eat()
实际调用的是父类的方法animal2.eat()
实际调用的是子类的方法
那么为什么将 eatDog 改成 eat 之后,animal2.eat 调用的就是子类的方法呢?
这就是我们接下来要讲的重写
3. 方法重写
什么叫做重写呢?
子类实现父类的同名方法,并且
- 方法名相同
- 方法的返回值一般相同
- 方法的参数列表相同
满足上述的情况就称为:重写、覆写、覆盖(Override)
注意事项:
重写的方法不能为密封方法(即被 final 修饰的方法)。我们之前了解过关键字 final,而被他修饰的方法就叫做密封方法,该方法则不能再被重写,如
// 假如这是父类中的方法 public final void eat(){ System.out.println(this.name+"要吃东西"); }
此类方法是不能被重写的
子类的访问修饰限定符权限一定要大于等于父类的权限,但是父类不能是被 private修饰
方法不能被 static 修饰
一般针对重写的方法,可以使用
@Override
注解来显示指定。加了他有什么好处呢?看下面代码// 假如下面的 eat 是被重写的方法 class Dog extends Animal{ @Override private void eat(){ // ... } }
当我们如出现 eat 被写成了 ate 时候,那么编译器就会发现父类中是没有 ate 方法的,就会编译报错,提示无法构成重写
重写时可以修改返回值,方法名和参数类型及个数都不可以修改。仅当返回值为类类型时,重写的方法才可以修改返回值类型,且必须是父类方法返回值的子类;要么就不修改,与父类返回值类型相同
了解到这,大家对于重写肯定有了一个概念。此时我们再回忆一下之前学过的重载,可以做一个表格来进行对比
区别 | 重载(Overload) | 重写(Override) |
---|---|---|
概念 | 方法名称相同、参数列表不同、返回值无要求 | 方法名称相同、参数列表相同、返回类型一般相同 |
范围 | 重载不是必须在一个类当中(继承) | 继承关系 |
限制 | 没有权限要求 | 被覆写的方法不能拥有比父类更严格的访问控制权限 |
比较结果就是,两者没啥关系呀
讲到这里,我们好像一直没有说明上一小节的标题动态绑定是啥
那么什么叫做动态绑定呢?发生的条件如下
- 发生向上转型(父类引用需要引用子类对象)
- 通过父类引用,来调用子类和父类的同名覆盖方法
那为啥是叫动态的呢?经过反汇编我们可以发现
- 编译的时候: 调用的是父类的方法
- 但是运行的时候: 实际上调用的是子类的方法
因此这其实是一个动态的过程,也可以叫其运行时绑定
4. 向下转型
既然介绍了向上转型,那肯定也缺不了向下转型呀!什么时向下转型呢?想想向上转型就可以猜到它就是
把父类对象赋值给了子类对象的引用
那么换成代码就是
// 假设 Animal 是父类,Dog 是子类
public class TestDemo{
public static void main(String[] args){
Animal animal=new Animal("动物");
Dog dog=animal;
}
}
但是只是上述这样写是不行的,会报错
为什么呢?我们可以这样想一下
狗是动物,但是动物不能说是狗,这相当于是一个包含的关系。
因此可以将狗的对象直接赋值给动物,但是不能将动物的对象赋值给狗
我们就可以使用强制类型转换,这样上述代码就不会报错了
public class TestDemo{
public static void main(String[] args){
Animal animal=new Animal("动物");
Dog dog=(Dog)animal;
}
}
我们接着用 dog 引用去运行一下 eat 方法
public class TestDemo{
public static void main(String[] args){
Animal animal=new Animal("动物");
Dog dog=(Dog)animal;
dog.eat();
}
}
运行后出现了错误
动物不能被转换成狗!
那我们该怎么做呢?我们要记住一点:
使用向下转型的前提是:一定要发生了向上转型
public class TestDemo{
public static void main(String[] args){
Animal animal=new Dog("二哈");
Dog dog=(Dog)animal;
dog.eat();
}
}
这样就没问题啦!
像上述我们提到使用向下转型的前提是要发生向上转型。我们其实可以理解为,我们在使用向上转型的时候,有些功能无法做到,故我们再使用向下转型来完善代码(emmm,纯属个人愚见啦)。就比如
// 假设我的 Dog 类中有一个看家的方法 guard
public class TestDemo{
public static void main(String[] args){
Animal animal=new Dog("二哈");
animal.guard();
}
}
上述代码就会报错,因为 Animal 类中是没有 guard 方法的。因此我们就要借用向下转型
public class TestDemo{
public static void main(String[] args){
Animal animal=new Dog("二哈");
Dog dog =animal;
dog.guard();
}
}
注意:
其实向下转型不常使用,使用它可能会不小心犯一些错误。如果我们上述的代码又要继续使用一些其他动物的特有方法,如果忘了它们没有发生向上转型,就会报错。
为了避免这种错误: 我们可以使用 instanceof
instanceof
:可以判定一个引用是否是某个类的实例,如果是则返回 true,不是则返回 false,如public class TestDemo{ public static void main(String[] args){ Animal animal=new Dog("二哈"); if(animal instanceof Bird){ Bird bird=(Bird)animal; bird.fly(); } } }
上述代码就是先判断 Animal 的引用是否是 Bird 的实例,我们知道它应该是 Dog 的实例,故返回 false
5. 关键字 super
其实上章就讲解过了 super 关键字,这里我再用一个表格比较下 this 和 super,方便理解
区别 | this | super |
---|---|---|
概念 | 访问本类中的属性和方法 | 由子类访问父类中的属性和方法 |
查找范围 | 先查找本类,如果本类没有就调用父类 | 直接调用父类 |
表示 | 表示当前对象 | 无 |
共性1 | 不能被放在 static 修饰的方法中 | 不能被放在 static 修饰的方法中 |
共性2 | 要放在第一行(不能和 super 一起使用) | 要放在第一行(不能和 this 一起使用) |
6. 在构造方法中调用重写方法(坑)
接下来我们看一段代码,大家可以猜猜结果是啥哦!
class Animal{
public String name;
public Animal(String name){
eat();
this.name=name;
}
public void eat(){
System.out.println(this.name+"在吃食物(Animal)");
}
}
class Dog extends Animal{
public Dog(String name){
super(name);
}
public void eat(){
System.out.println(this.name+"在吃食物(Dog)");
}
}
public class TestDemo{
public static void main(String[] args){
Dog dog=new Dog("二哈");
}
}
结果就是
如果没猜对的,一般有两个疑惑:
- 没有调用 eat 方法,但为什么结果是这样的?
- 为啥是 null?
解答:
- 疑惑一: 因为子类继承父类需要帮父类构造方法,所以子类创建对象时,就构造了父类的构造方法,就执行了父类的 eat 方法
- 疑惑二: 由于父类构造方法是先执行 eat 方法,而 name 的赋值在后面一步,多以此时的 name 是 null
结论:
构造方法中可以调用重写的方法,并且发生了动态绑定
7. 理解多态
介绍到这里,我们终于要开始正式介绍我们今天的一大重点多态了!那什么是多态呢?其实他和继承一样是一种思想,我们可以先看一段代码
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("画一个方片♦");
}
}
class Flower extends Shape{
@Override
public void draw() {
System.out.println("画一朵花❀");
}
}
public class TestDemo{
public static void main(String[] args) {
Cycle shape1=new Cycle();
Rect shape2=new Rect();
Flower shape3=new Flower();
drawMap(shape1);
drawMap(shape2);
drawMap(shape3);
}
public static void drawMap(Shape shape){
shape.draw();
}
}
我们发现 drawMap 这个方法被调用者使用时,都是经过父类调用了其中的 draw 方法,并且最终的表现形式是不一样的。而这种思想就叫做多态。
更简单的说,多态就是
一个引用能表现出多种不同的形态
而多态是一种思想,实现它的前提有两点
- 向上转型
- 调用同名的覆盖方法
而一种思想的传承总有它独到的好处,那么使用多态有什么好处呢?
1)类调用者对类的使用成本进一步降低
- 封装是让类的调用者不需要知道类的实现细节
- 多态能让类的调用者连这个类的类型是什么都不必知道,只需要这个对象具有某种方法即可
2)能够降低代码的“圈复杂度”,避免使用大量的 if-else 语句
圈复杂度:
是一种描述一段代码复杂程度的方式。可以将一段代码中条件语句和循环语句出现的个数看作是“圈复杂度”,这个个数越多,就认为理解起来更复杂。
我们可以看一段代码
public static void drawShapes(){
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
这段代码的意思就是要分别打印圆、方片、圆、方片、花,如果不使用多态的话,我们一般就会写出上面这种方法。而使用多态的话,代码就会显得很简单,如
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
我们可以通过下面这种图理解上面的代码
而整体看起来,使用了多态的代码就简单了很多
3)可扩展能力强
如上述画图的代码,如果我们要新增一种新的形状,使用多态的方式改动成本也比较低,如
// 增加三角形 class Triangle extends Shape { @Override public void draw() { System.out.println("△"); } }
运用多态的话,我们扩展的代码增加一个新类就可以。而对于不使用多态的情况,就还需要对 if-else 语句进行一定的修改,故改动成本会更高
8. 小结
到此为止,面向对象的三大特点:封装、继承、多态已经全部介绍完了。由于我个人的理解也有限,所以讲的可能不好、不足,希望大家多多理解呀。
接下来将会介绍抽象类和接口,其中也会进一步运用到多态,大家可以多多练习,加深思想的理解。
二、抽象类
1. 概念
我们上面刚写过一个画图型的代码,其中父类的定义是这样的
class Shape{
public void draw(){
}
}
我们发现,父类中的 draw 方法里面没有内容,而绘图都是通过各种子类的 draw 方法完成的。
像上述代码,这种没有实际工作的方法,我们可以通过 abstract
来设计设计成一个抽象方法,而包含抽象方法的类就是抽象类
设计之后的代码就是这样的
abstract class Shape{
public abstract void draw();
}
2. 注意事项
方法和类都要由 abstract 修饰
抽象类中可以定义
以上是关于Java 基础语法爆肝两万字解析 Java 的多态抽象类和接口的主要内容,如果未能解决你的问题,请参考以下文章
❤️ 爆肝3天!两万字图文 SQL 零基础入门,不怕你学不会,就怕你不收藏!❤️
❤️ 爆肝3天!两万字图文 SQL 零基础入门,不怕你学不会,就怕你不收藏!❤️