Java面向对象编程·下_在main() 方法中使用父类引用指向子类对象的方式(向上转型),分别使用person类和wo(1)

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

这也贴合了 <<代码大全>> 中关于 “管理代码复杂程度” 的初衷.

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

如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单.

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

使用多态前:


使用多态后:


由上面两个图的对比我们可以看出多态使我们代码看起来更加的高级,且大大的优化了我们的代码量。

什么叫 “圈复杂度” ?

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.

因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需要考虑重构 .

不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10 .

3) 可扩展能力更强.

如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低.

class Triangle extends Shape {
    @Override
    public void draw() {
        System.out.println("△");
    }
}

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.

而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.

3.5向下转型 - 子类引用父类对象

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

// Animal.java
public class Animal {
    protected String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat(String food) {
        System.out.println("我是一只小动物");
        System.out.println(this.name + "正在吃" + food);
    }
}
// Bird.java
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    public void eat(String food) {
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
    }

    public void fly() {
        System.out.println(this.name + "
                正在飞"); 
          } 
    }

接下来是我们熟悉的操作

Animal animal = new Bird("圆圆"); 
animal.eat("谷子");
// 执行结果
//圆圆正在吃谷子

接下来我们尝试让圆圆飞起来

animal.fly();
// 编译出错
//找不到 fly 方法

注意事项

编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法.

虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的.

对于 Animal animal = new Bird("圆圆") 这样的代码,

  • 编译器检查有哪些方法存在, 看的是 Animal 这个类型
  • 执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型.

那么想实现刚才的效果, 就需要向下转型.

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

但是这样的向下转型有时是不太可靠的. 例如

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. 这时再进行向下转型就比较安全了.

3.6super 关键字

前面的代码中由于使用了重写机制, 调用到的是子类的方法. 如果需要在子类内部调用父类方法怎么办? 可以使用super 关键字.

super 表示获取到父类实例的引用. 涉及到两种常见用法.

  1. 使用了 super 来调用父类的构造器(这个代码前面已经写过了)
public Bird(String name) {
    super(name);
}

  1. 使用 super 来调用父类的普通方法
public class Bird extends Animal {
    public Bird(String name) {
        super(name);
    }
    @Override
    public void eat(String food) {
        // 修改代码, 让子调用父类的接口. 
        super.eat(food);
        System.out.println("我是一只小鸟");
        System.out.println(this.name + "正在吃" + food);
    }
}

在这个代码中, 如果在子类的 eat 方法中直接调用 eat (不加super), 那么此时就认为是调用子类自己的 eat (也就是递归了). 而加上 super 关键字, 才是调用父类的方法.

注意 super 和 this 功能有些相似, 但是还是要注意其中的区别.

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

一段有坑的代码. 我们创建两个类, 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

    • B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
  • 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.

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

3.8总结

多态是面向对象程序设计中比较难理解的部分. 我们会在后面的抽象类和接口中进一步体会多态的使用. 重点是多态带来的编码上的好处.

另一方面, 如果抛开 Java, 多态其实是一个更广泛的概念, 和 “继承” 这样的语法并没有必然的联系

  • C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
  • Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
  • Go 语言中没有 “继承” 这样的概念, 同样也能表示多态

无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式.

4.抽象类

4.1语法规则

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

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

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

注意事项

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

Shape shape = new Shape();
// 编译出错
//Error:(30, 23) java: Shape是抽象的; 无法实例化

2)抽象方法不能是 private 的

abstract class Shape {
    abstract private void draw();
}
// 编译出错
//Error:(4, 27) java: 非法的修饰符组合: abstract和private

3)抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用

    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

4)包含抽象方法的类,叫抽象类

5)什么是抽象方法,一个没有具体实现的方法,被abstract修饰。

6)因为抽象类不能被实例化,所以这个抽象类,其实只能被继承

7)抽象类当中,也可以包含和普通类一样的成员和方法

8)一个普通类,继承了一个抽象类,那么这个普通类当中,需要重写这个抽象的所有的抽象方法。

9)抽象类最大的作用就是为了被继承

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

11)结合第十点,当A类 再次被一个普通类继承后,那么A和B这两个抽象类当中的抽象方法,必须被重写。

4.2抽象类的作用

抽象类存在的最大意义就是为了被继承.

抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.

有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?

确实如此. 但是使用抽象类相当于多了一重编译器的校验

使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.

很多语法存在的意义都是为了 “预防出错”, 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.

充分利用编译器的校验, 在实际开发中是非常有意义的.

5.接口

因为我们如果要定义一个鸭子它能飞能跑还能游泳,如果继承类的话就只能继承一个,且不是所有的动物都能飞、跑和游泳,但是如果我们继承接口的话,我们可以继承三个不同的接口(飞,跑,游泳)来实现我们不同的功能,由此便达到了我们的目的,接口便也诞生了!

1、使用interface来修饰的。interfece IA {}

2、接口当中的普通方法,不能有具体的实现。非要实现,只能通过关键字default来修饰这个方法。

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

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

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

6、接口是不可以被通过关键字new来实例化的。

7、类和接口之间的关系是通过implements来进行实现的。

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

9、接口当中的成员变量,默认是public static final修饰的。

10、当一个类实现一个接口之后,重写这个方法的时候,这个方法前面必须加上public

11、一个类可以通过关键字extends继承一个抽象类或者一个普通类,但是只能继承一个类,同时也可以通过implements实现多个接口,接口之间使用逗号隔开就好。

12、接口和接口之间 可以使用extends来 操作他们的关系,此时,这里面意为:拓展。一个接口 通过 extends来 拓展另一个接口的功能。此时当一个类D 通过implements实现这个接口B的时候,此时重写的方法不仅仅是B的抽象方法,还有他从C接口,拓展来的功能[方法]。

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

5.1语法规则

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

interface IShape {
    void draw();
}
class Cycle implements IShape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}
public class Test {
    public static void main(String[] args) {
        IShape shape = new Rect();
        shape.draw();
    }
}

  • 使用 interface 定义一个接口
  • 接口中的方法一定是抽象方法, 因此可以省略 abstract
  • 接口中的方法一定是 public, 因此可以省略 public
  • Cycle 使用 implements 继承接口. 此时表达的含义不再是 “扩展”, 而是 “实现”
  • 在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例.
  • 接口不能单独被实例化.

扩展(extends) vs 实现(implements)

扩展指的是当前已经有一定的功能了, 进一步扩充功能.

实现指的是当前啥都没有, 需要从头构造出来.

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

interface IShape {
    void draw();
    public static final int num = 10;
}

其中的 public, static, final 的关键字都可以省略. 省略后的 num 仍然表示 public 的静态常量.

提示:

  1. 我们创建接口的时候, 接口的命名一般以大写字母 I 开头.
  2. 接口的命名一般使用 “形容词” 词性的单词.
  3. 阿里编码规范中约定, 接口中的方法和属性不要加任何修饰符号, 保持代码的简洁性.

一个错误的代码:

interface IShape {
    abstract void draw() ; // 即便不写public,也是public 
}
class Rect implements IShape {
    void draw() {
        System.out.println("□") ; //权限更加严格了,所以无法覆写。
    }
}

接口可以不加public(因为会默认为public),但是子类继承接口后必须要带上public,因为如果子类权限低于接口则idea会报错。

5.2实现多个接口

有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.

然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.现在我们通过类来表示一组动物.

class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }
}

另外我们再提供一组接口, 分别表示 “会飞的”, “会跑的”, “会游泳的”.

interface IFlying {
    void fly();
}
interface IRunning {
    void run();
}
interface ISwimming {
    void swim();
}

接下来我们创建几个具体的动物

猫, 是会跑的.

class Cat extends Animal implements IRunning {
    public Cat(String name) {
        super(name);
    }
    @Override
    public void run() {
        System.out.println(this.name + "正在用四条腿跑");
    }
}

鱼, 是会游的.

class Fish extends Animal implements ISwimming {
    public Fish(String name) {
        super(name);
    }
    @Override
    public void swim() {
        System.out.println(this.name + "正在用尾巴游泳");
    }
}

青蛙, 既能跑, 又能游(两栖动物)

class Frog extends Animal implements IRunning, ISwimming {
    public Frog(String name) {
        super(name);
    }
    @Override
    public void run() {
        System.out.println(this.name + "正在往前跳");
    }
    @Override
    public void swim() {
        System.out.println(this.name + "正在蹬腿游泳");
    }
}

提示, IDEA 中使用 ctrl + i 快速实现接口

还有一种神奇的动物, 水陆空三栖, 叫做 “鸭子”

class Duck extends Animal implements IRunning, ISwimming, IFlying {
    public Duck(String name) {
        super(name);
    }
    @Override
    public void fly() {
        System.out.println(this.name + "正在用翅膀飞");
    }
    @Override
    public void run() {
        System.out.println(this.name + "正在用两条腿跑");
    }
    @Override
    public void swim() {
        System.out.println(this.name + "正在漂在水上");
    }
}

上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.

继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性

猫是一种动物, 具有会跑的特性.

青蛙也是一种动物, 既能跑, 也能游泳

鸭子也是一种动物, 既能跑, 也能游, 还能飞

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

例如, 现在实现一个方法, 叫 “散步”

public static void walk(IRunning running) {
    System.out.println("我带着伙伴去散步");
    running.run();
}

在这个 walk 方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行

Cat cat = new Cat("小猫");
walk(cat);
Frog frog = new Frog("小青蛙");
walk(frog);
// 执行结果
//我带着伙伴去散步
//小猫正在用四条腿跑
//我带着伙伴去散步
//小青蛙正在往前跳

甚至参数可以不是 “动物”, 只要会跑!

    class Robot implements IRunning {
        private String name;
        public Robot(String name) {
            this.name = name;
        }
        @Override
        public void run() {
            System.out.println(this.name + "正在用轮子跑");
        }
    }
    Robot robot = new Robot("机器人");
    walk(robot);
// 执行结果
//机器人正在用轮子跑

5.3接口使用实例

刚才的例子比较抽象, 我们再来一个更能实际的例子.

给对象数组排序

给定一个学生类

class Student {
    private String name;
    private int score;
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
        return "[" + this.name + ":" + this.score + "]";
    }
}

再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序).

Student[] students = new Student[] {
        new Student("张三", 95),
        new Student("李四", 96),
        new Student("王五", 97),
        new Student("赵六", 92),
};

按照我们之前的理解, 数组我们有一个现成的 sort 方法, 能否直接使用这个方法呢?

Arrays.sort(students);
System.out.println(Arrays.toString(students));
// 运行出错, 抛出异常. 
//Exception in thread "main" java.lang.ClassCastException: Student cannot be cast to
//java.lang.Comparable

仔细思考, 不难发现, 和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确. 而两个学生对象的大小关系怎么确定? 需要我们额外指定.

让我们的 Student 类实现 Comparable 接口, 并实现其中的 compareTo 方法

class Student implements Comparable {
    private String name;
    private int score;
    public Student(String name, int score) {
        this.name = name;
        this.score = score;
    }
    @Override public String toString() {
        return "[" + this.name + ":" + this.score + "]";
    }
    @Override
    public int compareTo(Object o) {
        Student s = (Student)o;
        if (this.score > s.score) {
            return -1;
        } else if (this.score < s.score) {
            return 1;
        } else {
            return 0;
        }
    }
}

在 sort 方法中会自动调用 compareTo 方法. compareTo 的参数是 Object , 其实传入的就是 Student 类型的对象.然后比较当前对象和参数对象的大小关系(按分数来算).

  • 如果当前对象应排在参数对象之前, 返回小于 0 的数字;
  • 如果当前对象应排在参数对象之后, 返回大于 0 的数字;
  • 如果当前对象和参数对象不分先后, 返回 0;

再次执行程序, 结果就符合预期了.

// 执行结果
//[[王五:97], [李四:96], [张三:95], [赵六:92]]

注意事项: 对于 sort 方法来说, 需要传入的数组的每个对象都是 “可比较” 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则.

为了进一步加深对接口的理解, 我们可以尝试自己实现一个 sort 方法来完成刚才的排序过程(使用冒泡排序)

public static void sort(Comparable[] array) {
    for (int bound = 0; bound < array.length; bound++) {
        for (int cur = array.length - 1; cur > bound; cur--) {
            if (array[cur - 1].compareTo(array[cur]) > 0) {
                // 说明顺序不符合要求, 交换两个变量的位置
                Comparable tmp = array[cur - 1];
                array[cur - 1] = array[cur];
                array[cur] = tmp;
            }
        }
    }
}

再次执行代码

sort(students);
System.out.println(Arrays.toString(students));
// 执行结果
//[[王五:97], [李四:96], [张三:95], [赵六:92]]

5.4接口间的继承

接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.

        interface IRunning {
            void run();
        }
        interface ISwimming {
            void swim();
        }
// 两栖的动物, 既能跑, 也能游
        interface IAmphibious extends IRunning, ISwimming {
        }
        class Frog implements IAmphibious { 
 ...
        }

通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法.

接口间的继承相当于把多个接口合并在一起.

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

   }
    }
}

}


再次执行代码



sort(students);
System.out.println(Arrays.toString(students));
// 执行结果
//[[王五:97], [李四:96], [张三:95], [赵六:92]]


### 5.4接口间的继承


接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.



    interface IRunning {
        void run();
    }
    interface ISwimming {
        void swim();
    }

// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming {
}
class Frog implements IAmphibious {

}


通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法.



> 
> 接口间的继承相当于把多个接口合并在一起.
> 
> 


[外链图片转存中...(img-huKou2iW-1715678380113)]
[外链图片转存中...(img-tIL8HTyx-1715678380114)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 11
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值