JavaComparable、Comparator、Cloneable接口解析,深拷贝浅拷贝详细解析

前言❤️❤️

hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹

Comparable、Comparator、Cloneable 都是 Java 当中十分高频的内置接口,比较接口用于实现对象之间的大小对比,Cloneable 用来完成对象拷贝;这些知识点建立在接口、继承的基础知识之上。熟练掌握之后可以灵活实现自定义排序、对象复制,方便我们日常开发。

本篇博文就从基础部分解析这几个知识点,一些前置知识点在之前接口和抽象类当中有解析,大家可以在专栏中跳转往期文章复习一下💪🐵

🎇个人主页:洋不写bug的博客
🎇所属专栏:Java学习之旅,从入门到进阶
🎇铁汁们对于Java的各种常用核心语法(不太常用的也有😆),都可以在上面的Java专栏学习,专栏正在持续更新中🐵🐵,有问题可以写在评论区或者私信我哦~

1,Comparable接口

写一个学生类,在main方法中创建两个学生对象,如果想要比较这两个学生对象,那肯定不能直接比较,需要建立一套比较规则

java 复制代码
package package7;

public class Student {
    public int id;
    public String name;
    public double score;

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

假如规则是先按照成绩比较,成绩相同的再去比较学号
要实现这个规则,Student类就要实现Java官方的Comparable接口(要在<>中指定当前要排序的实体类),重写compareTo方法

java 复制代码
public class Student implements Comparable<Student>{
    public int id;
    public String name;
    public double score;

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


    @Override
    public int compareTo(Student o) {
        return 0;
    }
}

在compareTo方法中制定规则时,this表示当前对象,o就表示另一个要比较的对象(o表示other)

那这里我们就制订这样的比较规则:

  • 如果this比o大,就返回<0的整数(这里我们返回-1)
  • 如果this比o小,就返回>0的正数(这里我们返回1)
  • 如果this与o相等,就返回0
  • 这里先比较成绩,如果成绩相同再比较id,只有成绩和id都相等,才返回0(理论上学生的id不会相等,所以是不会返回0的)

重写方法如下所示:

java 复制代码
    @Override
    public int compareTo(Student o) {
        if(this.score < o.score){
            return -1;
        }else if(this.score > o.score){
            return 1;
        }else{
            if(this.id < o.id){
                return -1;
            }else if(this.id > o.id){
                return 1;
            }
        }
        return 0;
    }

在main方法中测试一下,Mike的分数比Will低,比较出的结果就是-1(这里铁汁们也可以多创建几组对象,比较一下试一试,看是否符合约定的规则)

java 复制代码
package package7;

public class Test {
    public static void main(String[] args) {
        Student s1 = new Student(1,90,"Mike");
        Student s2 = new Student(2,98,"Will");
        System.out.println(s1.compareTo(s2));
    }
}

在C++中,运算符是可以重载的(就是让运算符可以用来比较我们自己创建的类对象),但是这个重载语法特性,比较容易被滥用,因此Java中是不支持的
运算符重载就是像下面这样进行比较


我们自己定义一个接口,用来进行比较,也不是不行,但是最好还是用这些官方标准库提供的接口

这样有一个很大的好处,就是可以无缝的和标准库其他类对接(在标准库中的很多类,会用到Comparable类中的compareTo方法,例如数组类的sort方法)
Java标准库提供了两个进行比较的接口,一个是Comparable,一个是Comparator

2,Comparator接口

前面的Comparable接口是直接让被比较的类(Student类)实现接口,重写方法
而Comparator类则是需要创建一个单独的类,来实现这个Comparator接口,在新的类中访问被比较类的属性,需要get方法,因此被比较类中也必须写上get方法
这里单独创建的类命名为StudentComparator,实现Comparator接口,重写compare方法(这里因为不是在被比较类内部写的了,参数就不是this和other了,而是o1和o2)
代码如下所示:

java 复制代码
package package7;


public class Student{
    public int id;
    public String name;
    public double score;

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

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public double getScore() {
        return score;
    }
}
java 复制代码
package package7;

import java.util.Comparator;

public class StudentComparator implements Comparator<Student> {

    @Override
    public int compare(Student o1, Student o2) {
        if(o1.getScore() < o2.getScore()){
            return -1;
        }else if(o1.getScore() > o2.getScore()){
            return 1;
        }else{
            if(o1.getId() < o2.getId()){
                return -1;
            }else if(o1.getId() > o2.getId()){
                return 1;
            }
        }
        return 0;
    }
}

这里有个小技巧,写完后直接按alt + enter键,再按enter,就会自动生成重写方法(生成的方法的参数不是o1和o2,我们可以改一下)


使用Comparator接口在main方法中进行比较时,肯定就不能直接用对象调用比较方法了(因为比较方法并没有写到被比较类中)

就需要先创建一个比较器对象,再调用这个比较器对象的方法去进行判断

java 复制代码
package package7;

public class Test {
    public static void main(String[] args) {
        Student s1 = new Student(1,90,"Mike");
        Student s2 = new Student(2,98,"Will");
        StudentComparator comparator = new StudentComparator();
        System.out.println(comparator.compare(s1,s2));
    }
}

那在什么情况下用Comparable,什么情况下用Comparator呢,如下:

  • 如果比较规则只有一种,并且可以随意修改被比较的类的代码时,优先考虑Comparable,使用Comparable的话,比较规则的代码就包含在类的内部,内聚性更好
  • 如果比较规则有多种(例如发奖学金用成绩比较,挑选礼仪队成员按身高比较),或者被比较的类的源码不能修改(也就是别人写了一个类,让你来使用,这个类你是不能随意改动的,甚至你看不到这个类)

3,Clonable接口

当我们需要克隆一个对象时,应该怎么办,下面这种写法完成克隆了吗?

java 复制代码
public class Test {
    public static void main(String[] args) {
        Student s1 = new Student(1,90,"Mike");
        Student s2 = s1;
    }
}

这个其实在前面类和对象的博客中有提到过(如下图),并不算是真正的克隆,只是搞了个引用,指向堆空间中相同的位置,真正的克隆起码应该两个引用修改自己的属性不会相互影响,也就是克隆时在堆空间上再创建一片区域

要完成真正的克隆,就需要Student类继承Clonable接口(这个接口也是Java标准库中的),重写clone方法
继承这个接口后,就算没有重写clone方法,编译器这时候也不会报错,这是为什么呢?

按住ctrl键,再点击Cloneable,看一下源码,就会很奇怪的发现,这竟然是一个空接口


那既然是空接口,需要重写的clone方法是从哪来的呢,其实,这个clone方法不是Cloneable接口提供的,而是Object类提供的(Object类是Java中所有类直接或者间接的父类)

因此,无论是否实现Cloneable接口,我们都可以重写clone方法
那既然clone接口是个空接口,我们就先不实现这个接口,看下最后是否能正常完成克隆

在IDEA中按alt + insert键,选择Override Methods,按enter键


接着就会发现其中有个clone方法,这个克隆方法来自Object类

接下来生成的方法就是这样的,这里抛出了个异常,如果还没学习异常的铁汁们可以先不用管,后面博客会进行解析(这里使用的是父类Object类的clone方法,所以方法前面加上super)


可能有的铁汁就会说了,这里没有继承Cloneable接口,也能重写,而且Cloneable又是一个空的接口,那到底有啥用呢,这里我们先不实现Cloneable接口,看能不能正常运行
这里我们稍微改下默认生成的clone方法,改成public,返回Student类型,这个clone方法就重写好了

java 复制代码
    @Override
    public Student clone() throws CloneNotSupportedException {
        return (Student) super.clone();
    }

但是,在main方法中想要使用,需要抛出异常(后面博客会解析,还没学过的铁汁可以先忽略)

这里验证下,看s1和s2对象是不是不同的引用指向同一块空间了,有没有真的克隆成功

java 复制代码
package package7;

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Student s1 = new Student(1,90,"Mike");
        Student s2 = s1.clone();

        System.out.println(s2.getName());
        System.out.println(s2.getId());
        System.out.println(s2.getScore());

        s1.setScore(100);
        System.out.println(s2.getScore());
    }
}

运行完,却报错了,就是因为刚刚Student类没有实现Cloneable接口
Cloneable接口具有标记作用,也就是标记这个类可以进行克隆拷贝,运行时JVM会检查有无标记接口Cloneable,如果被比较类没有实现这个接口,那JVM就会报错

这里我们让Student类实现Cloneable接口后,再执行程序,就不会报错了

4,浅拷贝和深拷贝

当类中的属性都是内置类型(基础数据类型,例如int,long,float)时,就不涉及深浅拷贝的问题

如果涉及到引用类型,那就要区分深浅拷贝了(除去基础(内置)数据类型之外,剩下所有可以 new 出来的全部都是引用类型
示例如下:

Money类,类中的属性为amount

Person类里面的属性有id,也有Money对象的引用,这里注意getMoney和setMoney方法,实际上读取和改变的是amount的值

想要对Person类进行克隆,就要在Person类中实现一下Cloneable接口,再重写下clone方法

java 复制代码
public class Money {
    private double amount = 1000;

    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }
}
java 复制代码
package package8;

public class Person implements Cloneable{
    private int id;
    private Money money = new Money();

    @Override
    public Person clone() throws CloneNotSupportedException {
        return (Person) super.clone();
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public double getMoney() {
        return money.getAmount();
    }

    public void setMoney(double amount) {
        money.setAmount(amount);
    }
}

接着在main方法中测试一下,看拷贝的效果如何

java 复制代码
package package8;

public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person p1 = new Person();
        p1.setId(1);
        Person p2 = p1.clone();

        System.out.println(p2.getId());
        System.out.println(p2.getMoney());

        p1.setId(3);
        System.out.println(p2.getId());

        p1.setMoney(2000);
        System.out.println(p1.getMoney());
        System.out.println(p2.getMoney());
    }
}

结果如下,就出现问题了,p1修改id对p2没有影响,但p1修改money的account的值,p2就受到影响了

也就是说,这里的拷贝没有进行引用类型真正的拷贝,p1和p2还是共用了一份引用内存,这种拷贝就称为浅拷贝
浅拷贝内存图如下:

虽然p1和p2指向的是两个不同的对象,但是p1.money和p2.money仍然是同一个对象

深拷贝就是不光把Person对象本身进行拷贝,还要把Person内部持有的引用类型的属性,也进行拷贝

引用类型中如果还有引用类型,就要递归的进行拷贝,保证所有内部持有的引用类型,都对应拷贝一份新的
Money对象也要进行拷贝,那Money类就需要实现Cloneable接口,重写clone方法,Person类里面重写的clone方法要加上对Money对象的拷贝,代码如下:

java 复制代码
package package8;

public class Money implements Cloneable{
    private double amount = 1000;

    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }

    @Override
    public Money clone() throws CloneNotSupportedException {
        return (Money) super.clone();
    }
}
java 复制代码
package package8;

public class Person implements Cloneable{
    private int id;
    private Money money = new Money();

    @Override
    public Person clone() throws CloneNotSupportedException {
        Person p = (Person) super.clone();
        p.money = p.money.clone();
        return p;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public double getMoney() {
        return money.getAmount();
    }

    public void setMoney(double amount) {
        money.setAmount(amount);
    }
}

3再执行下main方法,这时候更改Money对象的amount值就不会相互影响了

总结

到这里,接口和抽象类的基础知识已经基本上解析完了,总结图如下所示:

到这里JavaSE的内容的基础语法部分大部分已经解析完了,下篇博客会解析下Java内部类和Object类,学完后就可以写基础的项目了,下下篇博客会详细解析一个基础的图书管理系统,初学Java的铁汁可以拿来练手🐵

以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

相关推荐
吴声子夜歌1 小时前
正则指引——Golang
开发语言·golang·正则表达式
坐吃山猪1 小时前
WebFlux_学习Subscriber总结
java·开发语言·学习·webflux
白露与泡影1 小时前
Java面试题及答案整理(2026年金九银十最新版,持续更新)
java·开发语言
少控科技1 小时前
农场设备管理代码(1)
开发语言·c#
❀搜不到1 小时前
isat-sam分割导出掩码图
开发语言·python
geovindu2 小时前
java: Memento Pattern
java·开发语言·后端·备忘录模式·行为模式
xcLeigh2 小时前
Unity基础:Start与Update方法——Unity脚本生命周期初探
java·unity·游戏引擎·教程
zephyr052 小时前
链表实现O(n log n)时间复杂度的排序——归并排序详解
java·数据结构·算法
曾阿伦2 小时前
Trae CN Python环境调试debug指南
开发语言·python