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 呢?
Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用_第1张图片

我们发现 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."":()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."":(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[] args) {
     
        Animal animal = new Bird("鹦鹉");
        animal.eat("食物");
    }
}

我们让 animal 飞起来

animal.fly();
// 编译出错
java: 找不到符号
  符号:   方法 fly()
  位置: 类型为bit.basis.Gao.Animal的变量 animal

注意事项
编译过程中, animal 的类型是 Animal, 此时编译器只知道这个类中有一个 eat 方法, 没有 fly 方法. 虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以 animal 的类型来查看有哪些方法的. 对于 Animal animal = new Bird(“鹦鹉”) 这样的代码

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

因此,要想让上述代码实现刚才的效果必须进行向下转型

Bird bird = (Bird) animal;
bird.fly();

// 执行结果
Bird:eat()鹦鹉食物// 调用 eat 方法,运行的是子类的 eat 方法而不是父类的 eat 方法
Bird:fly()鹦鹉正在飞

但是这样的向下转型是不太可靠的,例如

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 + "正在飞");
    }
}

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

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

public class TestDemo {
     

    public static void main(String[] args) {
     
        Animal animal = new Cat("咪咪");
        Bird bird = (Bird) animal;
        bird.fly();
    }
}

// 运行出错
Exception in thread "main" java.lang.ClassCastException

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

    public static void main(String[] args) {
     
        Animal animal = new Cat("咪咪");
        if (animal instanceof Bird) {
     
            Bird bird = (Bird) animal;
            bird.fly();
        }
    }

// 运行不报错

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

10.super关键字

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

super 表示获取到父类实例的引用. 涉及到两种常见用法
Java技术核心卷中解释 super 为特殊用法而不是父类引用,把它当为父类引用来理解的话更容易理解 super 的运用

  1. 使用 super 来构造父类的构造器
 Bird(String name) {
     
        super(name);
    }
  1. 使用 super 来调用父类的普通方法
class Bird extends Animal {
     
    Bird(String name) {
     
        super(name);
    }

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

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

public static void main(String[] args) {
     
     Animal animal = new Bird("鹦鹉");
     Bird bird = (Bird) animal;
     bird.eat("食物");
 }

Bird:eat()鹦鹉食物//调用的 Bird 子类的 eat 方法
Animal:eat()鹦鹉食物//调用的 Animal 父类的 eat 方法【super.eat(food)】

这个代码中,如果在子类中直接调用 eat (不加super),那么此时就默认调用子类的 eat 方法(也就是重写了),而加上 super 才是调用的父类方法

注意:super 和 this 功能相似但还是有些区别

No 区别 this super
1 概念 访问本类中的属性和方法 子类访问父类中的属性和方法
2 查找范围 先查找本类如果本类没有就调用父类 不查找本类直接调用父类
3 特殊 表示当前对象

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

class B {
     
    B() {
     
        func();
    }

    void func() {
     
        System.out.println("B.func()");
    }
}

class D extends B {
     
    private int num = 1;

    D() {
     
        super();
    }

    @Override
    void func() {
     
        System.out.println("D.func()" + num);
    }
}

public class TestDemo {
     

    public static void main(String[] args) {
     
        D d = new D();
    }
}

通过 javap -c 字节码文件 命令来查看

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."":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #2                  // class bit/basis/Gao/D
       3: dup
       4: invokespecial #3                  // Method bit/basis/Gao/D."":()V
       7: astore_1
       8: return
}


我们查看 main 函数中的汇编代码如下:
首先 new 了一个对象, 后边的注释说明 new 的是 D 类型对象
再看 invokespecial 来查看最终运行的是哪一个类型的 func 函数,注释说明的是 D 的func也就是子类的 func函数

到这里读懂之后再分析代码的运行过程如下:
D d = new D();
子类 D 继承父类 B,而 B 的构造方法
B 的构造方法会触发动态绑定,会调用到 D 的 func
此时 D 自身还没有构造,所以此时 num 为未初始化状态,值为 0

12.总结

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

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

  • C++ 中的 “动态多态” 和 Java 的多态类似. 但是 C++ 还有一种 “静态多态”(模板), 就和继承体系没有关系了.
  • Python 中的多态体现的是 “鸭子类型”, 也和继承体系没有关系.
  • Go 语言中没有 “继承” 这样的概念, 同样也能表示多态.
    无论是哪种编程语言, 多态的核心都是让调用者不必关注对象的具体类型. 这是降低用户使用成本的一种重要方式.

2.抽象类

1.语法规则

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

JDK 1.8 之前,抽象类的方法默认权限是 protected
JDK 1.8时,抽象类的方法默认权限是default

abstract class Shape{
     
	abstract void draw();
}

注意事项

  1. 抽象类不能实例化
  2. 抽象方法不能是 private
  3. 抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写, 也可以被子类直接调用
abstract class Shape{
     
    abstract void draw();
}
public class TestDemo {
     

    public static void main(String[] args) {
     
        Shape shape = new Shape();// 抽象类的实例化
    }
}

java: bit.basis.Gao.Shape是抽象的; 无法实例化
private abstract class Shape {
     
    abstract void draw();
}

public class TestDemo {
     

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

java: 此处不允许使用修饰符private
abstract class Shape {
     
    abstract void draw();

    void func() {
     
        System.out.println("Rect: func()");
    }
}

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

public class TestDemo {
     
    public static void main(String[] args) {
     
        Shape shape = new Rect();
        shape.func();
    }
}

Rect: func()

2.抽象类的作用

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

抽象类本身不能实例化,必须创建该抽象类的子类,子类然后必须重写抽象类中的抽象方法【快速重写: ctrl+o】

普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?

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

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

3.接口

1.语法规则

JDK1.8之前,接口中的方法必须是 public
JDK1.8时,接口中的方法可以是 public 也可以是 default
JDK1.9是,接口中的方法可以是 private

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

  1. 使用 interface 定义一个接口
  2. 接口中的方法一定是抽象方法,所以 abstract 可以省略
  3. 接口中的方法一定是 public 因此可以省略 public
  4. Rect 使用的 implements 继承接口。此时表达的含义不是 “继承” 而是 “实现”
  5. 在创建的时候可以同样可以创建一个接口的引用,对应到子类的一个实例
  6. 接口同样不能被单独实例化
  7. 调用接口的类必须重写接口中的抽象方法【快速重写: ctrl+o】

扩展(extends) vs 实现(implements)
扩展指的是当前已经有一定的功能了, 进一步扩充功能.
实现指的是当前啥都没有, 需要从头构造出来.

接口中只能包含抽象方法;接口中也只能包含静态常量

方法+属性的完整代码

public interface IOperation {
     
public abstract void draw();// void draw();
public static final int a = 10;// int a = 10;
}

灰色部分代表代码可以省略
Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用_第2张图片
其中的 public, static, final 的关键字都可以省略. 省略后的 a 仍然表示 public 的静态常量

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

2.一个错误的代码

public interface IOperation {
     
    void draw();
}

class Circle implements IOperation{
     
    @Override
    void draw() {
     
        System.out.println("画一个圆");
    }
}

正在尝试分配更低的访问权限; 以前为public

完整格式

public interface IOperation {
     
    public abstract void draw();
    public static final int a = 10;
}

简化格式

public interface IOperation {
     
    void draw();
    int a = 10;
}

3.实现多个接口

有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.
然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果. 现在我们通过类来表示一组动物.

interface IFlying{
     
    void flying();
}
interface ISwimming{
     
    void swimm();
}
interface IRunning{
     
    void running();
}

class Animal{
     
    String name = "Animal";

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

// 跑
class Cat extends Animal implements IRunning{
     
    public Cat(String name) {
     
        super(name);
    }

    @Override
    public void running() {
     
        System.out.println(this.name + "Cat:running()");
    }
}

// 游
class Fish extends Animal implements ISwimming{
     
    public Fish(String name) {
     
        super(name);
    }

    @Override
    public void swimm() {
     
        System.out.println(this.name + "Fish: swimming()");
    }
}

// 跑+游
class Frog extends Animal implements IRunning, ISwimming{
     
    public Frog(String name) {
     
        super(name);
    }

    @Override
    public void swimm() {
     
        System.out.println(this.name + "Frog: swimming()");
    }

    @Override
    public void running() {
     
        System.out.println(this.name + "Frog: swimming()");
    }
}

// 跑+游+飞
class Duck extends Animal implements IFlying, IRunning, ISwimming{
     
    public Duck(String name) {
     
        super(name);
    }

    @Override
    public void flying() {
     
        System.out.println(this.name + "Duck: swimming()");
    }

    @Override
    public void swimm() {
     
        System.out.println(this.name + "Duck: swimming()");
    }

    @Override
    public void running() {
     
        System.out.println(this.name + "Duck: swimming()");
    }
}

public class TestDemo {
     
    public static void main(String[] args) {
     
    }
}

上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口. 继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性 .

猫是一种动物, 具有会跑的特性
青蛙也是一种动物, 既能跑, 也能游泳
鸭子也是一种动物, 既能跑, 也能游, 还能飞

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

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

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

    public static void main(String[] args) {
     
        Cat cat = new Cat("小猫");
        walk(cat);
        Frog frog = new Frog("小青蛙");
        walk(frog);
    }
}

我带着伙伴去散步
小猫Cat:running()
我带着伙伴去散步
小青蛙Frog: swimming()

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

class Robot implements IRunning {
     
    String name;

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

    @Override
    public void running() {
     
        System.out.println(this.name + "Robot: running()");
    }
}

public class TestDemo {
     
    public static void main(String[] args) {
     
        IRunning roboot = new Robot("机器人");
        roboot.running();
    }
}

机器人Robot: running()

4.接口使用实例

1.compareable

class Student{
     
    String name;
    float score;

    public Student(String name, float score) {
     
        this.name = name;
        this.score = score;
    }

    @Override
    public String toString() {
     
        return "[" + this.name + ":" + this.score + "]";
    }
}
public class TestDemo {
     
    public static void main(String[] args) {
     
        Student[] students = {
     new Student("A", 1.1f), new Student("D", 4.4f), new Student("C", 3.3f), new Student("B", 2.3f)};
    }
}

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

Arrays.sort(students);

Exception in thread "main" java.lang.ClassCastException Student cannot be cast to java.lang.Comparable

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

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

class Student implements Comparable<Student> {
     
    String name;
    float score;

    public Student(String name, float score) {
     
        this.name = name;
        this.score = score;
    }

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

    @Override
    public int compareTo(Student o) {
     
        return (int) (this.score - o.score);
    }
}

public class TestDemo {
     
    public static void main(String[] args) {
     
        Student[] students = {
     new Student("A", 1.1f), new Student("D", 4.4f), new Student("C", 3.3f), new Student("B", 2.3f)};
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}


[[A:1.1], [B:2.3], [C:3.3], [D:4.4]]

分析 Comparable 源码
Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用_第3张图片

public interface Comparable: 一个泛型数据接口
public int compareTo(T o);接口内部的一个抽象方法

因此我们的 Student 类在实现 Comparable 接口的时候可以填充这个泛型的数据类型后再重写 compareTo 方法

返回值 含义
<0 当前对象排在参数对象之前
>0 当前对象排在参数对象之后
==0 当前对象与参数对象不分先后

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

2. comparator【比较器,类似于C语言中的qsort】

C语言中的 qsort 需要额外编写一个比较函数。comparator也可以通过编写函数的形式实现排序【以冒泡为例】

public class TestDemo {
     
    private static void comparable_BubbleSort(){
     
        Student[] students = {
     new Student("A", 1.1f), new Student("D", 4.4f), new Student("C", 3.3f), new Student("B", 2.2f)};
        for (int i = 0; i < students.length-1; i++) {
     
            boolean flg = true;
            for (int j = 0; j < students.length-i-1; j++) {
     
                if(students[j].compareTo(students[j+1])>0){
     
                    Student tmp = students[j];
                    students[j] = students[j+1];
                    students[j+1] = tmp;
                    flg = false;
                }
            }
            if (flg){
     
                break;
            }
        }
        System.out.println(Arrays.toString(students));
    }
    public static void main(String[] args) {
     
        comparable_BubbleSort();
    }
}

[[A:1.1], [B:2.2], [C:3.3], [D:4.4]]

细心的朋友可能会发现上述代码中我还重写了 comparator 函数,这个函数是根据分数排名的

@Override
    public int compareTo(Student o) {
     
        return (int) (this.score - o.score);
    }

有了 comparable 为什么还有 comparator呢?

我们发现 comparable 需要在 数据类型的类中修改 return 返回值,所以不方便于后续的调用,如果想要多种方案的排序,岂不是每次都要修改很麻烦。因此 comparator 诞生了,他可以当作一个 “比较器” 来使用
Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用_第4张图片
源码中我们得知返回的是整数int: -1,0,1这样的关系。标号1,2,3说明了它们不同数值所代表的关系

class ScoreRank implements Comparator<Student> {
     
    @Override
    public int compare(Student o1, Student o2) {
     
        return (int) (o1.score-o2.score);
    }
}
class NameRank implements Comparator<Student>{
     
    @Override
    public int compare(Student o1, Student o2) {
     
        return o1.name.compareTo(o2.name);
    }
}

    public static void main(String[] args) {
     
        Student[] students = {
     new Student("A", 1.1f), new Student("D", 4.4f), new Student("C", 3.3f), new Student("B", 2.2f)};
        Arrays.sort(students, new ScoreRank());
        System.out.println("Score rank: " + Arrays.toString(students));
        Arrays.sort(students, new NameRank());
        System.out.println("Name rank: " + Arrays.toString(students));
    }

Score rank: [[A:1.1], [B:2.2], [C:3.3], [D:4.4]]
Name rank: [[A:1.1], [B:2.2], [C:3.3], [D:4.4]]

我们发现,comparator 确实在其中起到了 “比较器” 的作用。以后按照方案排序只需要写一个类 class,然后 Arrays.sort(datas, new class()) 即可排序任何数据类型的元素

3. Cloneable 接口和深拷贝

Java内置了很多接口, Cloneable 就是其中之一
Object 类就存在一个 Clone 方法, 调用这个方法即可实现对象的 “拷贝”, 但是要想合法调用 clone 方法必须要实现 cloneable 接口. 否则就会出现 CloneNotSupportedException 错误

没有实现接口的错误代码:

class Person{
     
    String name = "abc";
}
public class TestDemo {
     
    public static void main(String[] args) {
     
        Person p1 = new Person();
        Person p_clone = (Person) p1.clone();
    }
}

java: clone()java.lang.Object 中是 protected 访问控制

Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用_第5张图片
根据提示,我们修改一下成员权限再看看

class Person{
     
    protected String name = "abc";
}
public class TestDemo {
     
    public static void main(String[] args) {
     
        Person p1 = new Person();
        Person p_clone = (Person) p1.clone();
    }
}

java: 未报告的异常错误java.lang.CloneNotSupportedException; 必须对其进行捕获或声明以便抛出

我们发现 p1.clone() 被编译器提示错误,我们 option+enter 试试异常捕获

public static void main(String[] args) {
     
        Person p1 = new Person();
        try {
     
            Person p_clone = (Person) p1.clone();
            System.out.println("src: " + p1 + "hash: " + p1.hashCode());
            System.out.println("clone: " + p_clone + "hash: " + p_clone.hashCode());
        } catch (CloneNotSupportedException e) {
     
            e.printStackTrace();
        }
    }

src: Person{
     name='abc'}hash: 1846274136
clone: Person{
     name='abc'}hash: 1639705018

打印出的 hash 值不同,说明是一个深拷贝。

Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用_第6张图片
思考

上诉代码只是拷贝的一个 String 类型的数据,如果拷贝的是一个对象呢?
我们给 Person 增加一个对象数据元素:Money对象
Cloneable 拷贝出的对象是一份 "浅拷贝"

class Money implements Cloneable{
     
    protected float money = 12.5f;

    @Override
    protected Object clone() throws CloneNotSupportedException {
     
        return super.clone();
    }
}
class Person extends Money implements Cloneable {
     
    protected String name = "abc";
    Money money = new Money();

    @Override
    protected Object clone() throws CloneNotSupportedException {
     
        return super.clone();
    }

    @Override
    public String toString() {
     
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }

}

public class TestDemo {
     
    public static void main(String[] args) {
     
        Person p1 = new Person();
        try {
     
            Person p_clone = (Person) p1.clone();
            System.out.println("p1_hash: " + p1.hashCode() + "; p1_hash_money: " + p1.money.hashCode());
            System.out.println("p_clone_hash: " + p_clone.hashCode() + "; p_clone_hash_money: " + p_clone.money.hashCode());
        } catch (CloneNotSupportedException e) {
     
            e.printStackTrace();
        }
    }
}

p1_hash: 1846274136; p1_hash_money: 1639705018
p_clone_hash: 1627674070; p_clone_hash_money: 1639705018

我们发现 p1 和 p_clone 地址不同,说明内存中拷贝了两份【深拷贝】;而 money 这个对象地址相同,说明 p1 和 p_clone 一起共用内存中同一块地址【浅拷贝】
Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用_第7张图片

如何使对象数据类型也进行 深拷贝 呢?

需要重写 Person 类中的 clone 方法

  1. 首先克隆一个副本
  2. 然后副本中的 money 对象在进行克隆
  3. 最后返回克隆好的副本
    缺点: 每次都要手动编写克隆的数据
    优点: 实现了深拷贝
@Override
    protected Object clone() throws CloneNotSupportedException {
     
        Person personClone = (Person) super.clone();
        personClone.money = (Money) personClone.money().clone();
        return personClone;
    }

完整代码如下:

class Money implements Cloneable{
     
    protected float money = 12.5f;

    @Override
    protected Object clone() throws CloneNotSupportedException {
     
        return super.clone();
    }
}
class Person extends Money implements Cloneable {
     
    protected String name = "abc";
    Money money = new Money();

    @Override
    protected Object clone() throws CloneNotSupportedException {
     
        Person personClone = (Person) super.clone();
        personClone.money = (Money) personClone.money.clone();
        return personClone;
    }

    @Override
    public String toString() {
     
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }

}

public class TestDemo {
     
    public static void main(String[] args) {
     
        Person p1 = new Person();
        try {
     
            Person p_clone = (Person) p1.clone();
            System.out.println("p1_hash: " + p1.hashCode() + "; p1_hash_money: " + p1.money.hashCode());
            System.out.println("p_clone_hash: " + p_clone.hashCode() + "; p_clone_hash_money: " + p_clone.money.hashCode());
        } catch (CloneNotSupportedException e) {
     
            e.printStackTrace();
        }
    }
}

p1_hash: 1846274136; p1_hash_money: 1639705018
p_clone_hash: 1627674070; p_clone_hash_money: 1360875712

Java多态,抽象类和接口还不熟悉?comparable和comparator,Cloneable使用_第8张图片
此时就实现了深拷贝,new Money 的地址也不一样

5.接口间的继承

interface IRunning {
     
    void run();
}

interface ISwimming {
     
    void swim();
}

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

}

class Frog implements IAmphibious {
     

}

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

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

6.总结

抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不 能包含普通方法, 子类必须重写所有的抽象方法.
如之前写的 Animal 例子. 此处的 Animal 中包含一个 name 这样的属性, 这个属性在任何子类中都是存在的. 因此此 处的 Animal 只能作为一个抽象类, 而不应该成为一个接口.

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

再次提醒:

抽象类存在的意义是为了让编译器更好的校验, 像 Animal 这样的类我们
并不会直接使用, 而是使用它的子类. 万一不小心创建了 Animal 的实例, 编译器会及时报错提醒我们.

No 区别 抽象类 接口
1 结构组成 普通类+抽象方法 抽象方法+全局变量
2 权限 各种权限 仅能public
3 子类使用 extends implements
4 关系 一个抽象类可以实现若干接口 接口不能继承抽象类,但可以使用 extends 扩展接口功能
5 子类限制 一个子类只能继承一个抽象类 一个子类可以实现多个接口

你可能感兴趣的:(Java基础,java)