【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 修饰

目录
相关文章
|
16天前
|
安全 Java API
JAVA并发编程JUC包之CAS原理
在JDK 1.5之后,Java API引入了`java.util.concurrent`包(简称JUC包),提供了多种并发工具类,如原子类`AtomicXX`、线程池`Executors`、信号量`Semaphore`、阻塞队列等。这些工具类简化了并发编程的复杂度。原子类`Atomic`尤其重要,它提供了线程安全的变量更新方法,支持整型、长整型、布尔型、数组及对象属性的原子修改。结合`volatile`关键字,可以实现多线程环境下共享变量的安全修改。
|
6天前
|
Java 编译器
封装,继承,多态【Java面向对象知识回顾①】
本文回顾了Java面向对象编程的三大特性:封装、继承和多态。封装通过将数据和方法结合在类中并隐藏实现细节来保护对象状态,继承允许新类扩展现有类的功能,而多态则允许对象在不同情况下表现出不同的行为,这些特性共同提高了代码的复用性、扩展性和灵活性。
封装,继承,多态【Java面向对象知识回顾①】
|
20天前
|
Java 编译器
Java——类与对象(继承和多态)
本文介绍了面向对象编程中的继承概念,包括如何避免重复代码、构造方法的调用规则、成员变量的访问以及权限修饰符的使用。文中详细解释了继承与组合的区别,并探讨了多态的概念,包括向上转型、向下转型和方法的重写。此外,还讨论了静态绑定和动态绑定的区别,以及多态带来的优势和弊端。
22 9
Java——类与对象(继承和多态)
|
4天前
|
Java API 数据处理
Java 包(package)的作用详解
在 Java 中,包(package)用于组织和管理类与接口,具有多项关键作用:1)系统化组织代码,便于理解和维护;2)提供命名空间,避免类名冲突;3)支持访问控制,如 public、protected、默认和 private,增强封装性;4)提升代码可维护性,实现模块化开发;5)简化导入机制,使代码更简洁;6)促进模块化编程,提高代码重用率;7)管理第三方库,避免命名冲突;8)支持 API 设计,便于功能调用;9)配合自动化构建工具,优化项目管理;10)促进团队协作,明确模块归属。合理运用包能显著提升代码质量和开发效率。
|
4天前
|
Java 数据安全/隐私保护
Java 包(package)的使用详解
Java中的包(`package`)用于组织类和接口,避免类名冲突并控制访问权限,提升代码的可维护性和可重用性。通过`package`关键字定义包,创建相应目录结构即可实现。包可通过`import`语句导入,支持导入具体类或整个包。Java提供多种访问权限修饰符(`public`、`protected`、`default`、`private`),以及丰富的标准库包(如`java.lang`、`java.util`等)。合理的包命名和使用对大型项目的开发至关重要。
|
8天前
|
Java
Java 多态趣解
在一个阳光明媚的午后,森林中的动物们举办了一场别开生面的音乐会。它们组成了一支乐队,每种动物都有独特的演奏方式。通过多态的魅力,狗、猫和青蛙分别展示了“汪汪”、“喵喵”和“呱呱”的叫声,赢得了观众的阵阵掌声。熊指挥着整个演出,每次调用 `perform()` 方法都能根据不同的动物对象唤起对应的 `makeSound()` 方法,展现了 Java 多态性的强大功能,让整场音乐会既有趣又充满表现力。
|
8天前
|
Java
Java 的继承
在一个森林中,各种动物共存,如狗和猫。为了管理和组织这些动物,我们采用面向对象的方法设计模型。首先创建 `Animal` 超类,包含 `name` 和 `age` 属性及 `makeSound()` 和 `displayInfo()` 方法。接着,通过继承 `Animal` 创建子类 `Dog` 和 `Cat`,重写 `makeSound()` 方法以发出不同的声音。实例化这些子类并使用它们,展示了继承带来的代码重用、扩展性和多态性等优点。这种方式不仅简化了代码,还体现了现实世界的层次结构。
|
3月前
|
搜索推荐 Java 编译器
【Java探索之旅】多态:重写、动静态绑定
【Java探索之旅】多态:重写、动静态绑定
24 0
|
Java 程序员 C++
java面向对象编程_包_继承_多态_重载和重写_抽象类_接口_this和super(3)
java面向对象编程_包_继承_多态_重载和重写_抽象类_接口_this和super(3)
199 0
java面向对象编程_包_继承_多态_重载和重写_抽象类_接口_this和super(3)
|
Java 编译器
java面向对象编程_包_继承_多态_重载和重写_抽象类_接口_this和super(2)
java面向对象编程_包_继承_多态_重载和重写_抽象类_接口_this和super(2)
141 0
java面向对象编程_包_继承_多态_重载和重写_抽象类_接口_this和super(2)
下一篇
无影云桌面