【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )2

简介: 【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )2

三、多态

多态(字面意思):一种事物多种形态

多态中有三种重要的语法基础:向上转型、动态绑定、重写。缺一不可~

理解多态就需要理解:向上转型,即(父类对象引用子类对象)

(1)向上转型

public static void main(String[] args) {
        Animal animal = new Dog("haha",19);
}

什么情况下会发生向上转型

  1. 直接赋值

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_包_18

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_包_19

  1. 作为函数的参数

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_java_20

  1. 作为函数的返回值

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_java_21


(2)动态绑定

动态绑定的条件:

  1. 父类 引用 子类的对象

  2. 通过父类这个引用 调用 父类 和 子类 同名的覆盖(重写) 方法

重写条件:

  1. 方法名相同

  2. 参数的 个数 和 类型 相同

  3. 最好返回值相同(协变类型:返回值可以不同。返回值的关系为父子类关系)

  4. 父子类的关系

注:如果父类中包含的方法在子类中有对应的同名同参数的方法,就会进行动态绑定。由运行时决定调用哪个方法。

一般动态/静态分别指的是编译时/运行时,和static无关。

示例

public static void main(String[] args) {
        Animal animal = new Dog("haha",19);
        animal.eat();
    }

eat()

我们可以看到这里调用的时父类的方法

再看一个代码: ↓↓↓(当子类中也有同名的 eat()方法)

class Dog extends Animal{
    public Dog(String name, int age){
        super(name,age);
    }
    public void eat(){
        System.out.println("狼吞虎咽的eat()");
    }
}
 public static void main(String[] args) {
        Animal animal = new Dog("haha",19);
        animal.eat();
    }

狼吞虎咽的eat()

我们看到这里调用的是子类的方法,这是为什么呢

因为这里发生了动态绑定

利用 javap -c 打开文件的汇编代码,可以看到这里调用的还是 Animal 的 eat 方法,这是为什么呢?

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_抽象类_22

在编译的时候不能够确定此时到底调用谁的方法,在运行的时候才知道调用谁的方法,称其为运行时绑定--------即我们的动态绑定

动态绑定的两个前提:

  1. 父类引用 引用子类对象
  2. 通过这个父类引用调用父类和子类同名的覆盖方法

那么什么是同名的覆盖

👉 同名的覆盖又被叫做重写重写要满足以下几种情况

  1. 方法名相同
  2. 参数列表相同(个数+类型)
  3. 返回值相同
  4. 父子类的情况下

重写 与 重载的区别

  1. 方法名相同

  2. 参数的 个数 和 类型 必须有一个不同(重写:参数的类型和个数都相同)

  3. 返回值可以不同 (重写:协变类型可以返回值不同,一般情况返回值是一样)

  4. 重载涉及静态绑定(重写:动态绑定)

重写的注意事项:

  1. 方法不可以是静态的,静态的方法不可以重写
  2. 子类的访问修饰限定符范围一定要大于等于父类的访问修饰限定符
  3. private 方法不能重写
  4. 被 final 修饰的(关键字/方法)不可以被重写
  5. 协变类型也可以构成重写

协变类型(科普一下)

public Animal eat(){
        System.out.println("eat()");
        return null;
    }
    public Dog eat(){
        System.out.println("狼吞虎咽的eat()");
        return null;
    }

在父类引用子类的时候有一个注意事项:

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_接口_23

通过父类引用只可以访问父类自己的成员


(3)静态绑定

静态绑定:根据你给定的参数个数和类型,判断调用哪个方法(又被称为编译式多态)

动态绑定也称为运行时绑定。静态绑定也有编译时绑定的说法。为什么会有这种说法呢?

class Bird extends Animal{
    public Bird(String name, int age,String wing){
        super(name,age);
    }
    public String wing;
    public void fly(){
        System.out.println("fly()");
    }
    public void func(int a){
        System.out.println(a);
    }
    public void func(int a,int b){
        System.out.println(a);
    }
    public void func(int a,int b,int c){
        System.out.println(a);
    }
}


 public static void func(Animal animal){
        Bird bird = new Bird("haha",19,"fei");
        bird.func(10);
    }

我们看他的汇编代码:

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_接口_24

我们可以看到这里在编译的时候已经规定好了调用哪个 func 方法,这就是静态绑定


(5)向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象。相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途。

编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法。
虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的。

// (Bird) 表示强制类型转换
Bird bird = (Bird)animal; 
bird.fly(); 
// 执行结果
圆圆正在飞

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_面向对象三大特征_25

但是这样的向下转型有时是不太可靠的,也不太安全。

Animal animal = new Cat("小猫"); 
Bird bird = (Bird)animal; 
bird.fly(); 
// 执行结果, 抛出异常
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Bird at Test.main(Test.java:35)

animal 本质上引用的是一个 Cat 对象, 是不能转成 Bird 对象的,运行时就会抛出异常,所以,为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换。

Animal animal = new Cat("小猫"); 
if (animal instanceof Bird) { 
 Bird bird = (Bird)animal; 
 bird.fly(); 
}

instanceof 可以判定一个引用是否是某个类的实例,如果是, 则返回 true。这时再进行向下转型就比较安全了。


(4)使用多态的好处

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.

我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况

利用一个代码加以说明

class Shape{
    public void draw(){
        System.out.println();
    }
}
class sanjiao extends Shape{
    @Override
    public void draw() {
        System.out.println("△");
    }
}
class fangpian extends Shape{
    @Override
    public void draw() {
        System.out.println("♦");
    }
}
//分割线//
public class Test {
    public static void drawmap(Shape shape){
        shape.draw();
    }

    public static void main(String[] args) {
        drawmap(new fangpian());
        drawmap(new sanjiao());
    }
}

在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的,

当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为多态

使用多态的好处是什么?

  1. 类调用者对类的使用成本进一步降低.
    封装是让类的调用者不需要知道类的实现细节.
    多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可.
    因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低.
    这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷.

  2. 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
    例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下↓↓↓

  3. 可扩展能力更强.

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(); 
 } 
 } 
} 

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单,利用 foreach 进行打印

public static void drawShapes() { 
 // 我们创建了一个 Shape 对象的数组. 
 Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), 
 new Rect(), new Flower()}; 
 for (Shape shape : shapes) { 
 shape.draw(); 
 } 
} 

什么叫 “圈复杂度”

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需
进行重构

为什么拓展更好?

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高


※多态使用案例※

案例(使用多态,打印多种形状)

代码1:

class Shape{// 无论是 三角形,还是正方形等等,它们都是图形
    // 以此作为共性抽出
    public void draw(){
        System.out.println("Shape::draw()");
    }
}
// 矩形
class Rect extends Shape{
    @Override// 当我们在子类中,重写了父类中的方法。那么父类方法中的输出语句就显得毫无意义
    // 最后都是输出子类draw方法,如果你想的话,可以删掉父类的 输出语句
    public void draw(){
        System.out.println("♦");
    }
}
// 花
class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_java_26


代码2(实现多态)

class Shape{// 无论是 三角形,还是正方形等等,它们都是图形
    // 以此作为共性抽出
    public void draw(){
        System.out.println("Shape::draw()");
    }
}
// 矩形
class Rect extends Shape{
    @Override// 当我们在子类中,重写了父类中的方法。那么父类方法中的输出语句就显得毫无意义
    // 最后都是输出子类draw方法,如果你想的话,可以删掉父类的 输出语句
    public void draw(){
        System.out.println("♦");
    }
}
// 花
class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }
}

public class Test {
    public static void main(String[] args) {
        Rect rect = new Rect();
        rect.draw();// 这里是单纯,new对象,访问普通成员方法

        // 使用向上转型,重写,父类当中 draw 方法。
        // 使父类的draw有了另一种实现的方法,
        // 这种情况,被称为动态绑定。
        // 在不断重写父类draw方法,呈现draw方法的多态的实现
        // 这就是我们所说的多态
        Shape shape = new Rect();
        shape.draw();
        Shape shape1 = new Flower();
        shape1.draw();
    }

}

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_java_27

代码三,通过方法和向上转型,来实现多态。让你们更加直观

代码如下

class Shape{// 无论是 三角形,还是正方形等等,它们都是图形
    // 以此作为共性抽出
    public void draw(){
        System.out.println("Shape::draw()");
    }
}
// 矩形
class Rect extends Shape{
    @Override// 当我们在子类中,重写了父类中的方法。那么父类方法中的输出语句就显得毫无意义
    // 最后都是输出子类draw方法,如果你想的话,可以删掉父类的 输出语句
    public void draw(){
        System.out.println("♦");
    }
}
// 花
class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("❀");
    }
}

public class Test {
    public static void paint(Shape shape){
        shape.draw();
    }
    public static void main(String[] args) {
        Rect rect = new Rect();
        paint(rect);
        paint(new Rect());
        System.out.println("=============");
        Flower flower = new Flower();
        paint(flower);
        paint(new Flower());
    }
 }

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_接口_28

从另一个方面来说:通过一个引用来调用不同的draw方法,会呈现出不同的表现形式。表现的形式取决于将来它引用那个对象。这就是动态。而且实现多态的大前提,就是一定要向上转型,且实现 父类和子类的重写方法。


总结:

在这个代码中, 上方的代码(矩形、花、继承)是 类的实现者 编写的, 下方的代码(main所在的类)是 类的调用者 编写的。
当类的调用者在编写 Paint 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现。(和 shape 对应的实例相关), 这种行为就称为 多态。
多态 顾名思义, 就是 “一个引用, 能表现出多种不同形态”。

多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处.
另一方面,
如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系.
C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
Go 语言中没有 “继承” 这样的概念, 同样也能表示多态.
无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要 方式.


重写方法中的一个大坑

在构造方法中调用重写的方法(一个坑)

一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func

class B { 
 public B() { 
 // do nothing 
 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
  • 构造 D 对象的同时, 会调用 B 的构造方法.
  • B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
  • 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.

结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题.


抽象类

(1)什么是抽象类

抽象的反义词是具体,越不具体,就越抽象。abstract修饰的类就叫做抽象类,除了不能被实例化之外!其他语法规则和普通类都一样。

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_包_29

abstract关键字存在的意义,就是让程序员明确的告诉编译器,这个类是一个抽象的类,不应该进行实例化,于是编译器就要做好相关检查工作。


(2)什么是抽象方法

  • 方法前头加上 abstract 此时这就是一个抽象方法了.
  • 抽象方法不需要方法体.
  • 抽象方法只能在抽象类中存在(也可以在接口中存在), 不能在普通的类中存在.
  • 抽象方法存在的意义就是为了让子类进行重写.

(3)抽象类中的规则及注意事项

abstract class Shape { 
 abstract public void draw(); 
}
  1. 抽象类不能够被实例化
    例如上面的Shape类,不能写为下面这种形式:但是可以向上转型。
Shape shape = new Shape(); 
// 编译出错
Error:(30, 23) java: Shape是抽象的; 无法实例化
  1. 抽象方法不能是 private 的
    如果一个方法被private修饰,则这个方法不能再被private修饰。
abstract class Shape { 
 abstract private void draw(); 
} 
// 编译出错
Error:(4, 27) java: 非法的修饰符组合: abstractprivate
  1. 抽象类中可以包含其他的非抽象方法, 也可以包含字段。这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用。
abstract class Shape { 
 abstract public void draw(); 
 void func() { 
 System.out.println("func"); 
 } 
} 
class Rect extends Shape { 
 ... 
} 
public class Test { 
 public static void main(String[] args) { 
 Shape shape = new Rect(); 
 shape.func(); 
 } 
} 
// 执行结果
func
  1. 如果一个普通类继承了一个抽象类,则这个普通类需要重写这个抽象类所有的抽象方法。除非普通类也加上abstract才不用重写抽象父类的所有抽象方法。
abstract class Shape {
    abstract public void func();
}
class Circle extends Shape{
    public void func() {
        
    }
}

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_包_30

  1. 在4的基础上,如果已经有了一个抽象类(子类)继承了一个抽象类(父类),此时还有一个普通类继承抽象类(子类)时,需要重写抽象类(子类)和抽象类(父类)的所有抽象方法。

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_抽象类_31

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_包_32

  1. 抽象方法与抽象类都不能被final修饰。

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_面向对象三大特征_33

既然知道了 抽象类不能被 final修饰的原理,那么由此推论出:抽象方法也不可以被 final修饰。.

【JAVA SE】——包、继承、多态、抽象类、接口 ( 巨细!总结 )_接口_34


总结抽象类:

  1. 包含抽象方法的类,叫做抽象类
  2. 什么是抽象方法,即没有具体实现的方法,被 abstract 修饰
  3. 抽象类不可以实例化
  4. 由于不能被实例化,所以抽象类只能被继承(最大的作用)
  5. 抽象类当中也可以包括和普通一样的成员和方法(静态的也可以)
  6. 一个普通类继承了一个抽象类,那么这个普通类需要重写抽象类的所有抽象方法
  7. 一个抽象类A如果继承一个抽象类B,那么这个抽象类A可以不实现抽象父类B的抽象方法
  8. 结合第7点当A类被一个普通类继承后,A和B这两个抽象类当中的抽象方法必须被重写
  9. 抽象类不能被 final 修饰,抽象方法也不可以被 final 修饰

目录
相关文章
|
9天前
|
JSON Java Apache
非常实用的Http应用框架,杜绝Java Http 接口对接繁琐编程
UniHttp 是一个声明式的 HTTP 接口对接框架,帮助开发者快速对接第三方 HTTP 接口。通过 @HttpApi 注解定义接口,使用 @GetHttpInterface 和 @PostHttpInterface 等注解配置请求方法和参数。支持自定义代理逻辑、全局请求参数、错误处理和连接池配置,提高代码的内聚性和可读性。
|
30天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
40 6
|
30天前
|
存储 Java 数据处理
Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位
【10月更文挑战第16天】Java Set接口凭借其独特的“不重复”特性,在集合框架中占据重要地位。本文通过快速去重和高效查找两个案例,展示了Set如何简化数据处理流程,提升代码效率。使用HashSet可轻松实现数据去重,而contains方法则提供了快速查找的功能,彰显了Set在处理大量数据时的优势。
32 2
|
10天前
|
Java
java线程接口
Thread的构造方法创建对象的时候传入了Runnable接口的对象 ,Runnable接口对象重写run方法相当于指定线程任务,创建线程的时候绑定了该线程对象要干的任务。 Runnable的对象称之为:线程任务对象 不是线程对象 必须要交给Thread线程对象。 通过Thread的构造方法, 就可以把任务对象Runnable,绑定到Thread对象中, 将来执行start方法,就会自动执行Runable实现类对象中的run里面的内容。
25 1
|
16天前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
42 4
|
22天前
|
安全 Java
在 Java 中使用实现 Runnable 接口的方式创建线程
【10月更文挑战第22天】通过以上内容的介绍,相信你已经对在 Java 中如何使用实现 Runnable 接口的方式创建线程有了更深入的了解。在实际应用中,需要根据具体的需求和场景,合理选择线程创建方式,并注意线程安全、同步、通信等相关问题,以确保程序的正确性和稳定性。
|
20天前
|
Java
Java基础(13)抽象类、接口
本文介绍了Java面向对象编程中的抽象类和接口两个核心概念。抽象类不能被实例化,通常用于定义子类的通用方法和属性;接口则是完全抽象的类,允许声明一组方法但不实现它们。文章通过代码示例详细解析了抽象类和接口的定义及实现,并讨论了它们的区别和使用场景。
|
21天前
|
Java 测试技术 API
Java零基础-接口详解
【10月更文挑战第19天】Java零基础教学篇,手把手实践教学!
18 1
|
26天前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
19 3
|
26天前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
33 2