Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用

Posted 面向丈母娘编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用相关的知识,希望对你有一定的参考价值。

1.多态

1.多态初识

什么是多态呢?通俗地说“一种形式多种形态”,这样回答肯定不会让人满意,下面这段代码会告诉你什么是多态

class Animal{
    public String name;
    public int age;
}
class Dog extends Animal{
}

class Bird extends Animal{
}

public class TestDemo {
    private static void test(){
        Dog dog = new Dog();// 普通的创建一个 dog 对象
        Animal animal = new Dog();// 一个 Animal 类型的引用指向 Dog 的对象

		// Bird也可以
		Animal animal1 = new Bird();
    }

    public static void main(String[] args) {
        test();
    }
}
  1. 我们发现 new Dog() 对象不仅可以通过 Dog 类型的引用指向还可以使用它的父类 Animal 类型引用进行指向
  2. 父类变量存储子类变量就是开头所说的“一种形式多种形态”的意思,一个 Animal 类型变量可以存储阿猫阿狗,鸟等动物

思考:能否 Bird 指向 Dog 呢?

我们发现 IDEA 已经提示我们错误
原因是:Dog和Bird两个类不兼容,打个比方说:狗能是鸟吗?这一定是打破自然规律了吧,所以并不能指着狗说它是一只小鸟这样的错误。编译器也同样无法将 Bird 类指向 Dog 类。只能是它们的父类 Animal 指向 Dog 类,Bird 类等动物类。

2.向上转型

class Animal{
    String name = "Animal";
    int age = 1;

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

    void eat(){
        System.out.println(this.name + "Animal:eat()");
    }
}
class Dog extends Animal{
    Dog(String name) {
        super(name);
    }

    void eat(){
        System.out.println(this.name + "Dog:eat()");
    }
}

class Bird extends Animal{
    Bird(String name) {
        super(name);
    }

    void eat(){
        System.out.println(this.name + "Bird:eat()");
    }
}

public class TestDemo {
    private static void test(){
        Dog dog = new Dog("二哈");
        dog.eat();
        Animal animal = new Dog("二哈");
        animal.eat();
    }

    public static void main(String[] args) {
        test();
    }
}

二哈Dog:eat()
二哈Dog:eat()

这里我们加入了构造方法,给了每个类一个name="Animal"和age=1成员数据,我们输出eat方法看看结果
我们发现无论调用 dog 还是 animal 的eat方法,都是调用的 Dog 类的eat方法

// 生成一个 Dog 引用对象可以这样写
Dog dog = new Dog("二哈");
// 上方代码也可以这样写
Dog dog1 = new Dog("二哈");
Animal animal = dog1;// 或者简化为:Animal animal = new Dog("二哈");【推荐这样写】
  1. 此时 dog1 是一个父类 (Animal) 的引用, 指向一个子类 (Bird) 的实例. 这种写法称为向上转型.
  2. 向上转型这样的写法可以结合 is - a 语义来理解
  3. 例如:我说“今天喂 小狗 了吗?”或者说“今天喂 二哈 了吗?”。因为二哈确实是一条狗,也确实是一个动物

3.向上转型的方法传参

1.直接赋值

上述举例

  Dog dog = new Dog("二哈");
  Animal animal = new Dog("二哈");

就是直接赋值

2. 方法传参

public class TestDemo {

    private static void test_eat(Animal animal){
        animal.eat();
    }
    private static void test(){
        Dog dog = new Dog("二哈");
        test_eat(dog);
        Animal animal = new Dog("二哈");
        test_eat(animal);
    }

    public static void main(String[] args) {
        test();
    }
}

此时形参 animal 的类型是 Animal (基类), 实际上对应到 Dog(父类)的实例

3. 方法返回

public class TestDemo {
    public static Animal findMyAnimal() {
        Dog dog = new Dog("二哈");
        return dog;
    }

    private static void test_eat(Animal animal) {
        animal.eat();
    }

    private static void test() {
        test_eat(findMyAnimal());
    }

    public static void main(String[] args) {
        test();
    }
}
二哈Dog:eat()

此时方法 findMyAnimal 返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例.

4. 动态绑定

说到多态,就离不开向上转型,向上转型的运行结果又离不开动态绑定。

源代码如下:

class Animal {
    String name = "Animal";
    int age = 1;

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

    void eat() {
        System.out.println(this.name + "Animal:eat()");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    void eat() {
        System.out.println(this.name + "Dog:eat()");
    }
}

class Bird extends Animal {
    Bird(String name) {
        super(name);
    }

    void eat() {
        System.out.println(this.name + "Bird:eat()");
    }
}

public class TestDemo {
    public static void main(String[] args) {
        Animal animal = new Dog("二哈");
        animal.eat();
    }
}

汇编代码如下:

cxf@cxfdeMacBook-Pro Gao % javap -c TestDemo
警告: 二进制文件TestDemo包含bit.basis.Gao.TestDemo
Compiled from "TestDemo.java"
public class bit.basis.Gao.TestDemo {
  public bit.basis.Gao.TestDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class bit/basis/Gao/Dog
       3: dup
       4: ldc           #3                  // String 二哈
       6: invokespecial #4                  // Method bit/basis/Gao/Dog."<init>":(Ljava/lang/String;)V
       9: astore_1
      10: aload_1
      11: invokevirtual #5                  // Method bit/basis/Gao/Animal.eat:()V
      14: return
}
cxf@cxfdeMacBook-Pro Gao % 

  1. 其中 invokespecial 指代的是:调用无须动态绑定的实例方法【调用实例方法;对超类、私有和实例初始化方法调用进行特殊处理】,通俗易懂地说就是调用父类的构造方法
  2. 程序从 main 函数开始,所以会首先构造父类的构造方法也就是Object无参自带的构造方法【 1: invokespecial #1 // Method java/lang/Object."": ()V】
  3. 程序在接着完成内部的构造方法也就是Dog【 6: invokespecial #4 // Method bit/basis/Gao/Dog."":(Ljava/lang/String;)V】
  4. 程序调用 Animal 的。eat() 的函数【 11: invokevirtual #5 // Method bit/basis/Gao/Animal.eat:()V】

明明调用的 Animal.eat() 的函数放啊,为何打印出Dog.eat(0方法?

二哈Animal:eat()

却打印出了

二哈Dog:eat()

这就是上文中解释的动态绑定了–调用无需动态绑定的实例方法,而Dog.eat()需要动态绑定因此会调用父类 Animal.eat() 的方法,然后再运行时 JVM 会进行特殊处理掉用 子类 的方法,因此会打印出

二哈Dog:eat()

这样的意外结果。

因此, 在 Java 中, 调用某个类的方法, 究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个引 用指向的是父类对象还是子类对象. 这个过程是程序运行时决定的(而不是编译期), 因此称为 动态绑定.

5.方法重写

针对刚才的 eat 方法来说:
子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override).
关于重写的注意事项

  1. 重写和重载完全不一样. 不要混淆(思考一下, 重载的规则是啥?)
  2. 普通方法可以重写, static 修饰的静态方法不能重写.
  3. 重写中子类的方法的访问权限不能低于父类的方法访问权限.
  4. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外).

方法权限示例: 将子类的 eat 改成 private

class Animal {
    String name = "Animal";
    int age = 1;

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

    void eat() {
        System.out.println(this.name + "Animal:eat()");
    }
}

class Dog extends Animal {
    Dog(String name) {
        super(name);
    }

    private eat() {
        System.out.println(this.name + "Dog:eat()");
    }
}
Dog中的eat()无法覆盖bit.basis.Gao.Animal中的eat()正在尝试分配更低的访问权限; 以前为package

另外, 针对重写的方法, 可以使用 @Override 注解来显示指定【快捷键:command+o或者ctr+o】

class Bird extends Animal {
    Bird(String name) {
        super(name);
    }

    @Override
    void eat() {
        System.out.println(this.name + "Bird:eat()");
    }
}

有了这个注解能帮我们进行合法性检验。列入不小心把 eat 写成了 ate ,那么此时编译器就会发现父类中没有 ate 方法,就会报错提示无法完成重写
推荐在代码中进行重写方法时显式加上 @Override 注解

6.重载和重写区别

No区别重载(overload)重写(override)
1概念方法名相同,参数个数和类型不同,返回值不做要求方法名相同,参数个数和类型相同,返回值尽量相同
2范围一个类继承关系中
3限制没有权限要求被覆写的方法不能拥有比父类更严格的访问控制权限

事实上, 方法重写是 Java 语法层次上的规则, 而动态绑定是方法重写这个语法规则的底层实现. 两者本质上描述 的是相同的事情, 只是侧重点不同

7.理解多态

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.
我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况.
代码示例: 打印多种形状

class Shape{
    void draw(){
    }
}

class Cycle extends Shape{
    @Override
    void draw() {
        System.out.println("画一个圆");
    }
}

class Rect extends Shape{
    @Override
    void draw() {
        System.out.println("画一个矩形");
    }
}

class Flower extends Shape{
    @Override
    void draw() {
        System.out.println("画一个花");
    }
}
public class TestDemo {
    private static void func(Shape s){
        s.draw();
    }
    private static void test(){
        Shape[] shapes = new Shape[]{new Cycle(), new Rect(), new Flower()};
        for (Shape s: shapes) {
            func(s);
        }
    }
    public static void main(String[] args) {
        test();
    }
}

画一个圆
画一个矩形
画一个花
  1. 创建1个父类 Shape,写一个 draw 方法,什么都不用干
  2. 创建一些图案类,继承父类 Shape,则每个子类都会有 draw 方法
  3. 每个子类重写 draw 方法
  4. func 函数形参是 Shape 类型,在 test 函数中的foreach循环的s都会进入 Shape 中进行向上转型
  5. 每个子类重写 draw 方法在运行 s.draw() 的时候进行动态绑定,不用考虑类型是否兼容

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

多态顾名思义, 就是 “一个引用, 能表现出多种不同形态”

8.多态的好处

1.类调用者对类的使用成本进一步降低

  • 封装是为了让类的调用者不需要知道累的实现细节
  • 多态能让类的调用者无需考虑这个类的类型,只需要知道对象的某个方法即可
    因此可以理解多态是封装的更进一步,让类的调用者对类的使用成本进一步降低

2.能够降低代码的 “圈复杂度”, 避免使用大量的 if - else

如下打印的形状需要进行if-else匹配才行

    private static void test() {
        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();
            }
        }
    }

如果换用多态,只需要一行代码解决

private static void func(Shape s) {
        s.draw();
    }

    private static void test() {
        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();
            }
        }
    }

什么叫 “圈复杂度” ?

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

3.可扩展能力更强.

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

class Triangel extends Shape{
    @Override
    void draw() {
        System.out.println("画一个三角");
    }
}
  • 对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
  • 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高.

9.向下转型

向上转型就是子类对象转为父类对象;向下转型就是父类对象转为字类对象。相较于向上转型,向下转型用的不多但是也有一定用途

package bit.basis.Gao;

class Animal {
    String name;

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

    void eat(String food) {
        System.out.println("Animal:eat()" + this.name + food);
    }
}

class Bird extends Animal {
    Bird(String name) {
        super(name);
    }

    void eat(String food) {
        System.out.println("Bird:eat()" + this.name + food);
    }

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

public class TestDemo {

    public static void main(String[]以上是关于Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用的主要内容,如果未能解决你的问题,请参考以下文章

Java 基础语法爆肝两万字解析 Java 的多态抽象类和接口

day21接口类和抽象类,隔离原则,开放封闭原则,多态

JAVA面向对象的多态性及抽象类和接口

Java中的抽象类和接口

Dart9(九)抽象类、多态、 接口

python 接口类抽象类多态