ObjectOrientedProgramming - 面向对象的编程(多态、抽象类、接口)- Java - 细节狂魔

x33g5p2x  于2021-11-22 转载在 Java  
字(6.9k)|赞(0)|评价(0)|浏览(288)

回顾上篇博客内容

继承:对共性的一个抽取,使用extends关键字来实现,
   语法 A extends B 
   A 是 子类
   B 是 父类
       A is B
    意义:为了代码的重复使用
      注意:
              1.子类继承了父类,那么子类在构造时,要先帮助父类进行构造,需要在子类的构造方法当中,
              ‘  ’使用super关键字来显示(int a =10;显示就是简单而直观)的调用父类的构造方法
              2. super 和 this 的 区别(super针对父类,this 针对当前的类)
              3. 重写 和 重载的区别 (
                   3.1重载:参数类型和个数至少有一个不同,返回值不做限制;重写:参数的类型和个数都相同,除特殊情况外,返回值相同
                   3.2 重载:针对一个类;重写:针对 子类(不限于一个类)
              4. 讲解了访问修饰限定词的访问权限范围。
                   4.1 privatae : 仅限一个类
                       4.1.1 在类中 一个成员变量 和 方法,如果被 private 所修饰,那么在该类被继承时,会不会被继承到子类当中。
                               我更偏向于不会,因为在子类中不能通过super.data 去访问它,而且编译器在 super后面加点,也点不出来。(这个更权威一些,有理有据)
                               说会的,意思是 被private修饰的成员变量 和 方法,虽然无法访问和调用,但是还是被继承下来了。
                   4.2 default(包的访问权限):限定在一个包内,就是字段/属性/成员变量,在不加任何修饰限定词的前提下,该成员变量默认时 包的访问权限
                   4.3 protected :在一个包里可以随意使用,在不同包里,必须是子类才能访问
                   4.4 public : 哪里都能用
   在了解多态之前,我需要搞懂  什么向上转型,什么是运行时绑定。
     1.向上转型:父类引用 引用 子类对象
     2、运行时绑定(动态绑定):通过父类引用  调用 父类和子类 同名的覆盖方法。(覆写 == 覆盖 == 重写)
         2.1 方法名称相同
         2.2 参数的类型和个数都相同
         2.3 返回值相同 【特殊:返回值也可以是协变类型】
         注意事项:
              1。 static 方法不能重写
              2.  private 修饰的方法不能重写
              3. final 修饰的方法不能重写
              4. 子类方法的修饰限定 访问的范围 大于等于 父类方法的修饰限定(子类方法的访问权限要大于等于父类方法的访问权限)
     3. 编译时绑定(静态绑定):通过方法的重载实现的。编译的时候,会根据你调用方法时,所给参数的类型和个数,来决定调用哪个同名方法
     4. 向下转型: 子类引用 引用父类对象,但不安全,不建议使用。

回到本文

理解多态

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

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

代码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("❀");
    }

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

}
效果图

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

代码如下

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

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

总结:

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

拓展:

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

使用多态的好处是什么?

  1. 类调用者对类的使用成本进一步降低。
           封装是让类的调用者不需要知道类的实现细节.
           多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可。因此,多态可以理解成是封装的更进一步, 让类的调用者对类的使用成本进一步降低.
     这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷.
  2. 能够降低代码的 “圈复杂度”,避免使用大量的 if - else
  3. 可扩展能力更强.:如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低

有第二个好处,引出问题,什么是圈复杂度?

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解.
 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
    因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 
 如果一个方法的圈复杂度太高, 就需要考虑重构
 
     不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10

对比图(情况2)

情况3 附图

抽象类

语法规则

在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由
Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract
method), 包含抽象方法的类我们称为 抽象类(abstract class)

abstract class Shape { 
 abstract public void draw(); 
}

在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体代码).
对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类.

代码如下:

abstract class Shape{
// 凡是 一个类中,包含抽象方法,就要改类前面加上 abstract 修饰,叫做抽象类。
    public abstract void draw();// 加上分号,代表没有不实现任何现象
    // 在 public 后面加上 abstract 这样它就变成了一个抽象方法
}

那么抽象类 和 普通类 又有区别?

1. 抽象类不能直接实例化.

另外 在抽象类内部 是可以定义成员变量和方法的

有人可能会说,又不能new,里面就算能存储方法和变量,也不能用,那我们要它干什么用,徒增脱发??

由1引出, 抽象类的用法:因为不能被实体化,所以这个抽象类,只能被继承。

另外:继承抽象类的普通类当中 需要重写所有的抽象方法,上面重写一个,只是因为 被abstract修饰的方法只有一个,

抽象类是可以 向上转型的

抽象类可以向上转型,就意味着能发生动态绑定、加上重写,就能形成多态。

而且 前面说的成员变量和方法都能继承过来,并且使用

一个抽象类A,如果继承了另一个抽象类B,那么抽象类A可以不实现抽象类B的抽象方法。

俗话说的好,躲得了一时,躲不了一世,如果 继承了抽象类Shape的抽象类A,被一个普通类继承,你需要实现 抽象类A 和 B。两者的抽象方法。

抽象类 不能被 final修饰

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

总结

  1. 包含抽象方法的类,叫做抽象类
  2. 什么是抽象方法,一个没有具体实现的方法,被 abstract 所修饰。
  3. 抽象类是不可以被实例化(不能 new)
  4. 由于抽象类不能实体化,所以,抽象类只能被继承
  5. 抽象类当中,也可以包含和普通类一样的成员和方法
  6. 一个普通类,继承了一个抽象类,那么这个普通类当中,需要重写

    所有的抽象方法。

  7. 抽象类的最大的作用,就是为了被继承。
  8. 一个抽象类A,如果继承了另一个抽象类B,那么抽象类A可以不实现

    抽象类B的抽象方法。
  9. 结合第8点,当A类 再次被一个普通类所继承后,那么 A 和 B 两个

    抽象类当中的抽象方法,必须被重写。
  10. 抽象类 不能被 final修饰,那么由此推论出:抽象方法也不可以被

    final修饰。

    11.抽象方法不能是 private 的

    12.抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象

    方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调

    用.

抽象类的作用

抽象类存在的最大意义就是为了被继承.
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.

有些朋友可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?
确实如此. 但是使用抽象类相当于多了一重编译器的校验(如果你没有重写抽象方法,编译器会报错,提醒你)

&ensp;

接口

接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含
静态常量.

语法规则

在刚才的打印图形的示例中, 我们的父类 Shape 并没有包含别的非抽象方法, 也可以设计成一个接口

接口当中,普通方法不能有具体的方法实现。

如果非要有实现具体方法,也行在方法前面加上 default修饰,表示 该方法是此接口的默认方法。

凡事都有例外:接口当中,静态方法可以有具体的实现。(静态方法也是不能重写的)

由此得出结论:接口当中的方法(静态,和 默认),只能通过 接口的引用来调用。

不知有没有细心的人发现上面程序程序中 修饰方法的关键字public都是灰色的,意味着在接口当中,所有的方法都默认是public。

上面说到接口是 抽象类的进一步抽象。那么接口可不可以实例化?

接口的使用

由上个点得出的结论:如果一个类实现了接口,那么这个类就需要 重写 接口当中抽象方法,默认 和 static 的方法,不需要重写

上个点说到, 默认 和 static 的 方法 是不需要重写,但是 默认的方法,想要重写,还是能被重写的。但是static的方法不能被重写,因为它本来就不能被重写,

接口虽然不能实例化,但是能通过实例化有关系类的对象(这个对象是来实现接口的),达到向上转型。(在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例)

效果图

既然可以被重写,那么 意味着接口也能是多态

接口中只能包含抽象方法. 对于字段来说,接口中只能包含静态常量(static final)

既然知道了,接口当中的属性和方法,写法上可以省略。那么现在我们再来实现一下,子类重写方法。

一个类可以继承多个接口

在一个类继承另一个类/抽象类的时候(但只能继承一个类,这个前篇文章讲过),还可以继承多个接口。只不过继承了抽象类,需要重写抽象类当中的抽象方法

既然 类与类之间,类与接口之间 都是能够被继承的,那么接口与接口之间,又有什么关系?能否被继承?

接口与接口之间,可以使用 extends来 操作它们的关系,此时,extends 在这里意为 拓展。(一个接口A 通过 extends 来拓展 另一个接口B的功能,此时当一个类 通过 implements 实现 接口A,此时重写的方法,不仅仅是 A 的抽象方法,还有从B接口,拓展来的功能【方法】)

实践

这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力

总结:

1.使用interface来定义一个接口,例:interface IA{},这就是一个接口

2. 接口当中的普通方法,不能有具体的实现。非要实现,只能通过关

键字 default 来修饰 这个方法。

3.接口当中,可以有static的方法。

4.里面所有方法都是public的。

5. 抽象方法,默认是 public abstract 的。

6. 接口是不可以被实例化的(不可以被new的)。

7. 类和接口之间的关系是通过 implements(工具)实现的。

8.当一个类 实现了 一个接口,就必须要重写接口当中的抽象方法。

9.在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例

10.接口当中的字段/属性/成员变量,默认是public static final 修饰的。

11. 当一个类实现接口之后,重写抽象方法的时候,重写方法的前面必

须加上public,因为接口中方法默认都是public的,而且重写方法的访

问权限,必须要大于等于父类当中方法的访问权限

12.一个类可以通过关键字extends继承一个抽象类或者普通类。但是

只能继承一个类。同时也可以通过implements实现多个接口。接口之

间使用逗号隔开。

13.接口与接口之间,可以使用 extends来 操作它们的关系,此时,

extends 在这里意为 拓展,而不是继承,(一个接口 通过 extends 来拓展 另一个接口的功能)

本文结束(还有 三个常用接口的内容,将在下篇博客中出现)

相关文章

微信公众号

最新文章

更多

目录