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

x33g5p2x  于2021-09-20 转载在 Java  
字(17.8k)|赞(0)|评价(0)|浏览(347)

上节介绍了 Java 的包和继承,如果这类知识有点疑惑的兄弟,可以去 万字解析 Java 的包和继承 这章看看,或许可以帮你解决一些疑惑哟!

今天这章主要介绍多态和抽象类,希望接下来的内容对你有帮助!

一、多态

在了解多态之前我们先了解以下以下的知识点

1. 向上转型

什么是向上转型呢?简单讲就是
把子类对象赋值给了父类对象的引用

这是什么意思呢,我们可以看下列代码

// 假设 Animal 是父类,Dog 是子类
public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Animal("动物");
        Dog dog=new Dog("二哈");
        animal=dog;
    }
}

其中将子类引用 dog 的对象赋值给了父类的引用,而上述代码也可以简化成

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

这个其实和上述代码一样,这种写法都叫“向上转型”,将子类对象的引用赋值给了父类的引用

其实向上转型以后可能用到的比较多,那么我们什么时候需要用它呢?

  • 直接赋值
  • 方法传参
  • 方法返回

其中直接赋值就是上述代码的样子,接下来让我们看一下方法传参的实例

// 假设 Animal 是父类,Dog 是子类
public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Dog("二哈");
        func(animal);
    }
    public static void func1(Animal animal){
        
    }
}

我们写了一个函数,形参就是父类的引用,而传递的实参就是子类引用的对象。也可以写成

public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Animal("动物");
        Dog dog=new Dog("二哈");
        func(dog);
    }
    public static void func1(Animal animal){
        
    }
}

那么方法返回又是啥样的呢?其实也很简单,如

// 假设 Animal 是父类,Dog 是子类
public class TestDemo{
    public static void main(String[] args){
        
    }
    public static Animal func2(){
        Dog dog=new Dog("二哈");
        return dog;
    }
}

其中在 func2 方法中,将子类的对象返回给父类的引用。还有一种也算是方法返回

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

方法的返回值是子类的引用,再将其赋值给父类的对象,这种写法也叫“向上转型”。

那么既然我们父类的引用指向了子类引用的对象,那么父类可以使用子类的一些方法吗?试一试

class Animal{
    public String name;
    public Animal(String name){
        this.name=name;
    }
    public void eat(){
        System.out.println(this.name+"吃东西"+"(Animal)");
    }
}
class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
    public void eatDog(){
        System.out.println(this.name+"吃东西"+"(Dog)");
    }
}
public class TestDemo{
    public static void main(String[] args){
        Animal animal1=new Animal("动物");
        Animal animal2=new Dog("二哈");
        animal1.eat();
        animal2.eatdog();
    }
}

结果是不可以

因为本质上 animal 的引用类型是 Animal,所以只能使用自己类里面的成员和方法

2. 动态绑定

那么我们的 animal2 可以使用 Dog 类中的 eatDog 方法吗?其实是可以的,只要我们将这个 eatDog 改名叫 eat 就行

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

修改后的部分代码如上,此时,我们之前的 animal2 直接调用 eat,就可以得到下面的结果

这也就是说明此时

  • animal1.eat() 实际调用的是父类的方法
  • animal2.eat() 实际调用的是子类的方法

那么为什么将 eatDog 改成 eat 之后,animal2.eat 调用的就是子类的方法呢?

这就是我们接下来要讲的重写

3. 方法重写

什么叫做重写呢?
子类实现父类的同名方法,并且

  • 方法名相同
  • 方法的返回值一般相同
  • 方法的参数列表相同

满足上述的情况就称为:重写、覆写、覆盖(Override)

注意事项:
*
重写的方法不能为密封方法(即被 final 修饰的方法)。我们之前了解过关键字 final,而被他修饰的方法就叫做密封方法,该方法则不能再被重写,如

// 假如这是父类中的方法
public final void eat(){
    System.out.println(this.name+"要吃东西");
}

此类方法是不能被重写的
*
子类的访问修饰限定符权限一定要大于等于父类的权限,但是父类不能是被 private修饰
*
方法不能被 static 修饰
*
一般针对重写的方法,可以使用 @Override 注解来显示指定。加了他有什么好处呢?看下面代码

// 假如下面的 eat 是被重写的方法
class Dog extends Animal{
    @Override
    private void eat(){
        // ...
    }
}

当我们如出现 eat 被写成了 ate 时候,那么编译器就会发现父类中是没有 ate 方法的,就会编译报错,提示无法构成重写
*
重写时可以修改返回值,方法名和参数类型及个数都不可以修改。仅当返回值为类类型时,重写的方法才可以修改返回值类型,且必须是父类方法返回值的子类;要么就不修改,与父类返回值类型相同

了解到这,大家对于重写肯定有了一个概念。此时我们再回忆一下之前学过的重载,可以做一个表格来进行对比

区别重载(Overload)重写(Override)
概念方法名称相同、参数列表不同、返回值无要求方法名称相同、参数列表相同、返回类型一般相同
范围一个类继承关系
限制没有权限要求被覆写的方法不能拥有比父类更严格的访问控制权限

比较结果就是,两者没啥关系呀

讲到这里,我们好像一直没有说明上一小节的标题动态绑定是啥

那么什么叫做动态绑定呢?发生的条件如下

  1. 发生向上转型(父类引用需要引用子类对象)
  2. 通过父类引用,来调用子类和父类的同名覆盖方法

那为啥是叫动态的呢?经过反汇编我们可以发现

  • 编译的时候: 调用的是父类的方法
  • 但是运行的时候: 实际上调用的是子类的方法

因此这其实是一个动态的过程,也可以叫其运行时绑定

4. 向下转型

既然介绍了向上转型,那肯定也缺不了向下转型呀!什么时向下转型呢?想想向上转型就可以猜到它就是
把父类对象赋值给了子类对象的引用

那么换成代码就是

// 假设 Animal 是父类,Dog 是子类
public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Animal("动物");
        Dog dog=animal;
    }
}

但是只是上述这样写是不行的,会报错

为什么呢?我们可以这样想一下
狗是动物,但是动物不能说是狗,这相当于是一个包含的关系。

因此可以将狗的对象直接赋值给动物,但是不能将动物的对象赋值给狗

我们就可以使用强制类型转换,这样上述代码就不会报错了

public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Animal("动物");
        Dog dog=(Dog)animal;
    }
}

我们接着用 dog 引用去运行一下 eat 方法

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

运行后出现了错误

动物不能被转换成狗!

那我们该怎么做呢?我们要记住一点:
使用向下转型的前提是:一定要发生了向上转型

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

这样就没问题啦!

像上述我们提到使用向下转型的前提是要发生向上转型。我们其实可以理解为,我们在使用向上转型的时候,有些功能无法做到,故我们再使用向下转型来完善代码(emmm,纯属个人愚见啦)。就比如

// 假设我的 Dog 类中有一个看家的方法 guard
public class TestDemo{
    public static void main(String[] args){
        Animal animal=new Dog("二哈");
        animal.guard();
    }
}

上述代码就会报错,因为 Animal 类中是没有 guard 方法的。因此我们就要借用向下转型

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

注意:

其实向下转型不常使用,使用它可能会不小心犯一些错误。如果我们上述的代码又要继续使用一些其他动物的特有方法,如果忘了它们没有发生向上转型,就会报错。

为了避免这种错误: 我们可以使用 instanceof

instanceof:可以判定一个引用是否是某个类的实例,如果是则返回 true,不是则返回 false,如

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

上述代码就是先判断 Animal 的引用是否是 Bird 的实例,我们知道它应该是 Dog 的实例,故返回 false

5. 关键字 super

其实上章就讲解过了 super 关键字,这里我再用一个表格比较下 this 和 super,方便理解

区别thissuper
概念访问本类中的属性和方法由子类访问父类中的属性和方法
查找范围先查找本类,如果本类没有就调用父类直接调用父类
表示表示当前对象
共性1不能被放在 static 修饰的方法中不能被放在 static 修饰的方法中
共性2要放在第一行(不能和 super 一起使用)要放在第一行(不能和 this 一起使用)

6. 在构造方法中调用重写方法(坑)

接下来我们看一段代码,大家可以猜猜结果是啥哦!

class Animal{
    public  String name;
    public Animal(String name){
        eat();
        this.name=name;
    }
    public void eat(){
        System.out.println(this.name+"在吃食物(Animal)");
    }
}
class Dog extends Animal{
    public Dog(String name){
        super(name);
    }
    public void eat(){
        System.out.println(this.name+"在吃食物(Dog)");
    }
}
public class TestDemo{
    public static void main(String[] args){
        Dog dog=new Dog("二哈");
    }
}

结果就是

如果没猜对的,一般有两个疑惑:

  • 没有调用 eat 方法,但为什么结果是这样的?
  • 为啥是 null?

解答:

  • 疑惑一: 因为子类继承父类需要帮父类构造方法,所以子类创建对象时,就构造了父类的构造方法,就执行了父类的 eat 方法
  • 疑惑二: 由于父类构造方法是先执行 eat 方法,而 name 的赋值在后面一步,多以此时的 name 是 null

结论:

构造方法中可以调用重写的方法,并且发生了动态绑定

7. 理解多态

介绍到这里,我们终于要开始正式介绍我们今天的一大重点多态了!那什么是多态呢?其实他和继承一样是一种思想,我们可以先看一段代码

class Shape{
    public void draw(){

    }
}
class Cycle extends Shape{
    @Override
    public void draw() {
        System.out.println("画一个圆⚪");
    }
}
class Rect extends Shape{
    @Override
    public void draw() {
        System.out.println("画一个方片♦");
    }
}
class Flower extends Shape{
    @Override
    public void draw() {
        System.out.println("画一朵花❀");
    }
}
public class TestDemo{
    public static void main(String[] args) {
        Cycle shape1=new Cycle();
        Rect shape2=new Rect();
        Flower shape3=new Flower();
        drawMap(shape1);
        drawMap(shape2);
        drawMap(shape3);
    }
    public static void drawMap(Shape shape){
        shape.draw();
    }
}

我们发现 drawMap 这个方法被调用者使用时,都是经过父类调用了其中的 draw 方法,并且最终的表现形式是不一样的。而这种思想就叫做多态。

更简单的说,多态就是
一个引用能表现出多种不同的形态

而多态是一种思想,实现它的前提有两点

  • 向上转型
  • 调用同名的覆盖方法

而一种思想的传承总有它独到的好处,那么使用多态有什么好处呢?

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

  • 封装是让类的调用者不需要知道类的实现细节
  • 多态能让类的调用者连这个类的类型是什么都不必知道,只需要这个对象具有某种方法即可

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

这段代码的意思就是要分别打印圆、方片、圆、方片、花,如果不使用多态的话,我们一般就会写出上面这种方法。而使用多态的话,代码就会显得很简单,如

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

我们可以通过下面这种图理解上面的代码

而整体看起来,使用了多态的代码就简单了很多

3)可扩展能力强
如上述画图的代码,如果我们要新增一种新的形状,使用多态的方式改动成本也比较低,如

// 增加三角形
class Triangle extends Shape { 
    @Override 
    public void draw() { 
    	System.out.println("△"); 
    } 
}

运用多态的话,我们扩展的代码增加一个新类就可以。而对于不使用多态的情况,就还需要对 if-else 语句进行一定的修改,故改动成本会更高

8. 小结

到此为止,面向对象的三大特点:封装、继承、多态已经全部介绍完了。由于我个人的理解也有限,所以讲的可能不好、不足,希望大家多多理解呀。

接下来将会介绍抽象类和接口,其中也会进一步运用到多态,大家可以多多练习,加深思想的理解。

二、抽象类

1. 概念

我们上面刚写过一个画图型的代码,其中父类的定义是这样的

class Shape{
    public void draw(){

    }
}

我们发现,父类中的 draw 方法里面没有内容,而绘图都是通过各种子类的 draw 方法完成的。

像上述代码,这种没有实际工作的方法,我们可以通过 abstract 来设计设计成一个抽象方法,而包含抽象方法的类就是抽象类

设计之后的代码就是这样的

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

2. 注意事项

方法和类都要由 abstract 修饰
*
抽象类中可以定义其他数据成员和成员方法,如

abstract class Shape{
    public int a;
    public void b(){
        // ...
    }
    public abstract void draw();
}

但要使用这些成员和方法,需要靠子类通过 super 才能使用
*
抽象类不可以被实例化
*
抽象方法不能是被 private 修饰的
*
抽象方法不能是被 final 修饰的,它与 abstract 不能被共存
*
如果子类继承了抽象类,但不需要重写父类的抽象方法,则可以将子类用 abstract 修饰,如

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

此时该子类中既可以定义普通方法也可以定义抽象方法
*
一个抽象类 A 可以被另外的抽象类 B 继承,但是如果有其他的普通类继承了抽象类 B,则该普通类需要重写 A 和 B 中的所有抽象方法

3. 抽象类的意义

我们要知道抽象类的意义就是为了被继承

从注意事项中就知道抽象类本身是不能被实例化的,要想使用它,只能创建子类去继承,就比如

abstract class Shape{
    public int a;
    public void b(){
        // ...
    }
    public abstract void draw();
}
class Cycle extends Shape{
    @Override
    public void draw(){
        System.out.println("画一个⚪");
    }
}
public class TestDemo{
    public static void main(String[] args){
        Shape shape=new Cycle();
    }
}

要注意子类需要重写父类的所有抽象方法,不然代码就会报错

3. 抽象类的作用

那么抽象类既然不能被实例化,那为什么要用它呢?
使用了抽象类就相当于多了一重编译器的效验

啥意思呢?就比如按照上述画图的代码,实际工作其实是由子类完成的,如果不小心误用了父类,父类不是抽象类的话是不会报错的,因此将父类设计成抽象类,它会在父类被实例化的时候报错,让我们尽早地发现错误

三、接口

我们上面介绍了抽象类,抽象类中除了抽象方法还可以包含普通的方法和成员。

而接口中也可包含方法和字段,但只能是抽象方法和静态常量。

1. 语法规则

我们可以将上述 Shape 改写成一个 接口,代码如下

interface IShape{
    public static void draw();
}

具体的语法规则如下:

接口是使用 interface 定义的
*
接口的命名一般以大写字母 I 开头
*
接口中的方法一定是抽象的、被 public 修饰的方法,因此其中抽象方法可以简化代码为

interface IShape{
    void draw();
}

这样写默认是 public abstract
*
接口中也可以包含被 public 修饰的静态常量,并且可以省略 public static final,如

interface IShape{
    public static final int a=10;
    public static int b=10;
    public int c=10;
    int d=10;
}

接口不能被单独实例化,和抽象类一样需要被子类继承使用,但是接口中使用 implements 继承,如

interface IShape{
    public static void draw();
}
class Cycle implements IShape{
    @Override
    public void draw(){
        System.out.println("画一个圆预⚪");
    }
}

和 extends 表达含义是”扩展“不同,implements 表达的是”实现“,即表示当前什么都没有,一切需要从头构造
*
基础接口的类需要重写接口中的全部抽象方法
*
一个类可以使用 implements 实现多个接口,每个接口之间使用逗号分隔开就可以,如

interface A{
    void func1();
}
interface B{
    void func2();
}
class C implements A,B{
    @Override
    public void func1(){
        
    }
    @Override
    public void func2{
        
    }
}

注意这个类要重写所有继承的接口的所有抽象方法,在 IDEA 中使用 ctrl + i ,快速实现接口
*
接口和接口之间的关系可以使用 extends 来维护,这是意味着”扩展“,即某个接口扩展了其他接口的功能,如

interface A{
    void func1();
}
interface B{
    void func2();
}
interface D implements A,B{
    @Override
    public void func1(){
          
    }
    @Override
    public void func2{
          
    }
    void func3();
}

注意:

在 JDK1.8 开始,接口当中的方法可以是普通方法,但前提是:这个方法是由 default 修饰的(即是这个接口的默认方法),如

interface IShape{
    void draw();
    default public void func(){
        System.out.println("默认方法");
    }
}

2. 实现多个接口

我们之前介绍过,Java 中的继承是单继承,即一个类只能继承一个父类

但是可以同时实现多个接口,故我们可以通过多接口去达到多继承类似的效果

接下来通过代码来理解吧!

class Animal{
    public String name;
    public Animal(String name){
        this.name=name;
    }
}
class Bird extends Animal{
    public Bird(String name){
        super(name);
    }
}

此时子类 Bird 继承了父类 Animal,但是不能再继承其他类了,但是还可以继续实现其他的接口,如

class Animal{
    public String name;
    public Animal(String name){
        this.name=name;
    }
}
interface ISwing{
    void swing();
}
interface IFly{
    void fly();
}
class Bird extends Animal implements ISwing,IFly{
    public Bird(String name){
        super(name);
    }
    @Override
    public void swing(){
        System.out.println(this.name+"在游");
    }
    @Override
    public void fly(){
        System.out.println(this.name+"在飞");
    }
}

上述代码就相当于实现了多继承,因此接口的出现很好的解决了 Java 单继承的问题

并且我们可以感受到,接口表达的好像是具有了某种属性,因此有了接口以后,类的使用者就不必关注具体的类型了,而只要关注该类是否具备某个能力,比如

public class TestDemo {
    public static void fly(IFly flying){
        flying.fly();
    }
    public static void main(String[] args) {
        IFly iFly=new Bird("飞鸟");
        fly(iFly);
    }
}

因为飞鸟本身具有飞的属性,所以我们不必关注具体的类型,因为只要会飞的都可以实现飞的属性,如超人也会飞,就可以定义一个超人的类

class SuperMan implements IFly{
    @Override
    public void fly(){
        System.out.println("超人在飞");
    }
}
public class TestDemo {
    public static void fly(IFly flying){
        flying.fly();
    }
    public static void main(String[] args) {
        fly(new SuperMan());
    }
}

注意:

子类先继承父类再实现接口

3. 接口的继承

语法规则里面就介绍了,接口和接口之间可以使用 extends 来维护,可以使某个接口扩展其他接口的功能

这里就不再重述了

下面我们再学习一些接口,来加深对于接口的理解

4. Comparable 接口

我们之前介绍过 Arrays 类中的 sort 方法,它可以帮我们进行排序,比如

public class TestDemo {
    public static void main(String[] args) {
        int[] array={2,9,4,1,7};
        System.out.println("排序前:"+Arrays.toString(array));
        Arrays.sort(array);
        System.out.println("排序后:"+Arrays.toString(array));

    }
}

而接下来我想要对一个学生的属性进行排序。

首先我实现一个 Student 类,并对 toString 方法进行了重写

class Student{
    private String name;
    private int age;
    private double score;

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}

接下来我写了一个数组,并赋予了学生数组一些属性

public class TestDemo {
    public static void main(String[] args) {
        Student[] student=new Student[3];
        student[0]=new Student("张三",18,96.5);
        student[0]=new Student("李四",19,99.5);
        student[0]=new Student("王五",17,92.0);
    }
}

那么我们可以直接通过 sort 函数进行排序吗?我们先写如下代码

public class TestDemo {
    public static void main(String[] args) {
        Student[] student=new Student[3];
        student[0]=new Student("张三",18,96.5);
        student[1]=new Student("李四",19,99.5);
        student[2]=new Student("王五",17,92.0);
        System.out.println("排序前:"+student);
        Arrays.sort(student);
        System.out.println("排序后:"+student);
    }
}

最终结果却是

我们来分析一下

ClassCastException:类型转换异常,说 Student 不能被转换为 java.lang.Comparable

这是什么意思呢?我们思考由于 Student 是我们自定义的类型,里面包含了多个类型,那么 sort 方法怎么对它进行排序呢?好像没有一个依据。

此时我通过报错找到了 Comparable

可以知道这个应该是一个接口,那我们就可以尝试将我们的 Student 类继承这个接口,其中后面的 < T > 其实是泛型的意思,这里改成 < Student > 就行

class Student implements Comparable<Student>{
    public String name;
    public int age;
    public double score;

    public Student(String name, int age, double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}

但此时还不行,因为继承需要重写接口的抽象方法,所以经过查找,我们找到了

增加的重写方法就是

@Override
public int compareTo(Student o) {
    // 新的比较的规则
}

这里应该就是比较规则的设定地方了,我们再看看 sort 方法中的交换

也就是说如果此时的左值大于右值,则进行交换

那么如果我想对学生的年龄进行排序,重写后的方法应该就是

@Override
public int compareTo(Student o) {
    // 新的比较的规则
    return this.age-o.age;
}

此时再运行代码,结果就是

而到这里我们可以更深刻的感受到,接口其实就是某种属性或者能力,而上述 Student 这个类继承了这个比较的接口,就拥有了比较的能力

缺点:
*
当我们比较上述代码的姓名时,就要将重写的方法改为

@Override
public int compareTo(Student o) {
    // 新的比较的规则
    return this.name.compareTo(o.name);
}

当我们比较上述代码的分数时,就要将重写的方法改为

@Override
public int compareTo(Student o) {
    // 新的比较的规则
    return int(this.score-o.score);
}

我们发现当我们要修改比较的东西时,就可能要重新修改重写的方法。这个局限性就比较大

为了解决这个缺陷,就出现了下面的接口 Comparator

4. Comparator 接口

我们进入 sort 方法的定义中还可以看到一个比较方法,其中有两个参数数组与 Comparator 的对象

这里就用到了 Comparator 接口

这个接口啥嘞?我们可以先定义一个年龄比较类 AgeComparator,就是专门用来比较年龄,并让他继承这个类

class AgeCompartor implements Comparator<Student>{

}

再通过按住 ctrl 并点击它,我们可以跳转到它的定义,此时我们可以发现它里面有一个方法是

这个与上述 Comparable 中的 compareTo 不同,那我先对它进行重写

class AgeCompartor implements Comparator<Student>{
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age-o2.age;
    }
}

我们再按照 sort 方法的描述,写如下代码

public class TestDemo {
    public static void main(String[] args) {
        Student[] student=new Student[3];
        student[0]=new Student("张三",18,96.5);
        student[1]=new Student("李四",19,99.5);
        student[2]=new Student("王五",17,92.0);
        
        System.out.println("排序前:"+Arrays.toString(student));
        AgeComparator ageComparator=new AgeComparator();
        Arrays.sort(student,ageComparator);
        System.out.println("排序后:"+Arrays.toString(student));
    }
}

这样就可以正常的对学生的年龄进行比较了,而此时我们要再对姓名进行排序,我们就可以创建一个姓名比较类 NameComparator

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

而我们也只需要将 sort 方法的参数 ageComparator 改成 nameComparator 就可以了

我们可以将上述 AgeComparator 和 NameComparator 理解成比较器 ,而使用 Comparator 这个接口比 Comparable 的局限性小很多,我们如果要对某个属性进行比较只要增加它的比较器即可

5. Cloneable 接口和深拷贝

首先我们可以看这样的代码

class Person{
    public String name ="LiXiaobo";

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class TestDemo {
    public static void main(String[] args) {
        Person person=new Person();
    }
}

那什么是克隆呢?应该就是搞一个副本出来,比如

那么既然这次讲 Cloneable 接口,我就对其进行继承呗!

class Person implements Cloneable{
    public String name ="LiXiaobo";

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

但是我们发现就算继承之后,我们也不能通过创建的引用去找到一个克隆的方法。此时我们可以点到 Cloneable的定义看看

太牛了,啥都没有!

  • 我们发现,Cloneable 这个接口是一个空接口(也叫标记接口),而这个接口的作用就是:如果一个类实现了这个接口,就证明它是可以被克隆的
  • 而在使用它之前,我们还需要重写 Object 的克隆方法(所有的类默认继承于 Object 类)

怎样重写克隆方法呢?通过 ctrl + o,就可以看到

再选择 clone 就🆗,重写后的代码就变成

class Person implements Cloneable{
    public String name ="LiXiaobo";

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

而此时我们就可以看到一个 clone 方法

点击之后,我们发现居然还是报错

原因是由于重写的 clone 方法会抛出异常,针对这个就有两种方式,今天介绍简单一点的方式
*
方式一: 将鼠标放到 clone 上,按住 Alt + enter,你就会看到

点击红框框就行,但是你会发现点击后还是报错,这是由于重写的方法的返回值是 Object,而编译器会认为这是不安全的,因此将它强制转换成 Person 就可以了。此时我们再将克隆的副本输出发现结果没问题

并且通过地址的打印,副本和原来的地址是不一样的

介绍到这里,简单的克隆流程就已经介绍完了。但是接下来我们再深入一点思考,在 Person 类原有代码的基础上增加整形 a

class Person implements Cloneable{
    public String name ="LiXiaobo";
    public int a=10;
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

此时我们再通过 person 和 person 分别打印,代码如下

public class TestDemo3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person=new Person();
        Person person1=(Person)person.clone();
        System.out.println(person.a);
        System.out.println(person1.a);
        System.out.println("#############");
        person1.a=50;
        System.out.println(person.a);
        System.out.println(person1.a);
    }
}

结果如下

我们发现这种情况 person1 就完全是一个副本,对它进行修改是与 person 无关的。

但是我们再看下面这种情况,我们定义一个 Money 类,并在 Person 创建它

class Money{
    public int money=10;
}
class Person implements Cloneable{
    public String name ="LiXiaobo";
    public Money money=new Money();
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

然后我们再修改 person1 中的 money 的值,代码如下

public class TestDemo3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person=new Person();
        Person person1=(Person)person.clone();
        System.out.println(person.money.money);
        System.out.println(person1.money.money);
        System.out.println("#############");
        person.money.money=50;
        System.out.println(person.money.money);
        System.out.println(person1.money.money);
    }
}

这次的结果是

这是为什么呢?我们可以分析下面的图片

由于克隆的是 person 的对象,所以只克隆了(0x123)的 money,而(0x456)的 money 没有被克隆,所以就算前面的 money 被克隆的副本也指向它,所以改变副本的 money,它也会被改变

而上述这种情况其实叫做浅拷贝,那么怎么将其变成深拷贝呢?
我们只要将 money 引用所指向的对象也克隆一份

步骤:

将 Money 类也实现 Cloneable 接口,并重写克隆方法

class Money implements Cloneable{
    public int money=10;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

修改 Person 中的克隆方法

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

此时便是深拷贝了!

四、总结

以上便是个人对于多态、抽象类和接口的认知了,可能讲的不是很好,但是我已经尽力去诠释了,希望对大家有所帮助!

相关文章