【JAVA】十三、基础知识“接口”精细讲解!(三)(新手友好版~)

目录

[1. Object类](#1. Object类)

[1.1 Object的概念](#1.1 Object的概念)

[1.2 Object例子](#1.2 Object例子)

[2. toString](#2. toString)

[2.1 toString的概念](#2.1 toString的概念)

[2.2 为什么要重写toString](#2.2 为什么要重写toString)

[2.3 如何重写toString](#2.3 如何重写toString)

[3. 对象比较equals方法](#3. 对象比较equals方法)

[3.1 equals( ) 方法的概念](#3.1 equals( ) 方法的概念)

[3.2 Object类中的默认equals实现](#3.2 Object类中的默认equals实现)

[3.3 如何正确重写equals方法](#3.3 如何正确重写equals方法)

[4. hashCode方法](#4. hashCode方法)

[4.1 hashCode的概念](#4.1 hashCode的概念)

[4.2 对hashCode理解](#4.2 对hashCode理解)

[5. 接口使用实例(comparable接口)](#5. 接口使用实例(comparable接口))

[6. Clonable接口和深浅拷贝](#6. Clonable接口和深浅拷贝)


1. Object类

1.1 Object的概念

在Java中,Object类位于java.lang包中,是java默认提供的一个类,是所有类的父类。即使你没有显式地写extends Object去继承,Java也会默认让你的类继承Object父类,即所有类的对象都可以使用Object的引用进行接受。

java 复制代码
// 这两段代码实际上是完全等价的
class MyClass { /*...*/ }

class MyClass extends Object { /*...*/ }

1.2 Object例子

我们可以看一个例子:

1. Object 类

  • 是Java中所有类的父类
  • 任何对象都可以用Object类型接收
  • 提供默认方法比如:toString(),equals(),hashCode()(这三个知识点后面会进行详细的讲解啦)以下图片是Object提供的方法:

2. 多态特性

function(Object obj)可以接受:

  • Person对象(new Person())
  • Student对象(new Student())
  • 甚至String等其他所有的对象

3. 默认输出说明

  • 直接打印对象时,默认调用 toString()
  • 默认格式:类名@哈希码(比如Person@1b6d3586)

2. toString


2.1 toString的概念

toString( ) 是Java中所有类都继承Object类里面的一个方法,用于返回对象的字符串表示形式。默认实现返回的是类名@哈希码 ,但通常我们会重写它。


2.2 为什么要重写toString

默认的 to String( ) 输出可读性差,比如说:

java 复制代码
Person person = new Person("小美", 21);
System.out.println(person); // 输出结果是:Person@1b6d3586

可以看到输出的结果是类名@哈希码,而不是我们想要的名字小美,年龄21。我们再看下一种重写了to String( ) 方法的情况:

java 复制代码
System.out.println(person); // 输出的结果是:Person{name='小美', age=21}

这样就能够更加直观地查看对象内容啦~


2.3 如何重写toString

如何重写toString方法,可以参考这个:

点击重写后,就会出现下面框起来的一段代码,那就表示重写了toString方法:

此时我们实例化一个对象并且输出结果:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Person person = new Person(21,"小美");
        System.out.println(person);
    }
}

结果是:

按照之前的,若是没有那段重写toString的代码,输出结果就是:

我们已经反复说过了,当类没有重写to String( )方法时,直接打印对象或调用to String( )会得到类似类名@哈希码的结果,这是因为所有的Java类都隐式继承自Object类,Object.toString( )的默认实现如下:

java 复制代码
public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

3. 对象比较equals方法

接下来我们看下面的代码,person1和person2都叫做小美,年龄21,我们比较person1和person2:

java 复制代码
public class Test {
    public static void main(String[] args) {
        Person person1 = new Person(21,"小美");
        System.out.println(person1);
        Person person2 = new Person(21,"小美");
        System.out.println(person2);
        System.out.println("---------------------------");//分割线
        //比较person1和person2是否是同一个对象
        System.out.println(person1 == person2);
    }
}

输出结果为:

我们会发现结果是不一样的,结果是false;

我们可以通过结果看到person1和person2的地址是不一样的,也就意味着person1 == person2 比较里面存的值是不一样的,person1和person2里面存的是地址,地址都不一样,结果返回的肯定是false啦~

总结说一下就是,此时我们可以理解成此时比较的都是变量中的值,可以认为是地址。

假设这个时候我们加上equals方法~

  • 使用person1. equals(person2)进行比较:
java 复制代码
public class Test {
    public static void main(String[] args) {
        Person person1 = new Person(21,"小美");
        System.out.println(person1);
        Person person2 = new Person(21,"小美");
        System.out.println(person2);
        System.out.println("---------------------------");//分割线
        //比较person1和person2是否是同一个对象
        //System.out.println(person1 == person2);

        System.out.println(person1.equals(person2));
    }
}

输出结果:

结果仍然是false。这又是为什么呢,我们找到equals这个方法细看:

在这里,谁调用equals方法谁就是this,很明显person1就是this。这种情况下面两种写法是没有区别的。

那么这种情况我们该怎么办呢❓

此时我们不仅要重写我们的toString方法,还要重写我们的equals方法❗

java 复制代码
package demo1;
import javax.xml.namespace.QName;
//定义一个名为 Person 的类
class Person {
    //String是字符串类型,用于存储文本
    public String name;
    //int是整数类型,用于存储数字
    public int age;
    // 构造方法,用于创建Person对象时初始化对象
    public Person(int age,String name) {
        this.name = name;
        this.age = age;
    }
    //重写toString方法,用于打印对象信
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
    //重写equals方法
    @Override
    public boolean equals(Object obj) {
        return true;
    }
}
public class Test {
    public static void main(String[] args) {
        Person person1 = new Person(21,"小美");
        System.out.println(person1);
        Person person2 = new Person(21,"小美");
        System.out.println(person2);
        System.out.println("---------------------------");//分割线
        //比较person1和person2是否是同一个对象
        System.out.println(person1 == person2);
        //调用equals方法比较
        System.out.println(person1.equals(person2));
    }
}

结果:

我们可以看到上面重写equals方法中,return true;equals( )被重写总是返回true,所以无论System.out.println(person1.equals(person2));输出什么,返回的永远都是true;若是改成return false;不管输出什么,最后返回的永远都是false。

总结一下,若是我们要比较内容上的不同,则需要调用equals方法:

  • 使用person1. equals(person2)进行比较
  • 重写我们的equals方法

📌现在,我们正式介绍一下java中的equals方法~

3.1 equals( ) 方法的概念

equals( ) 是 Java 中用于比较两个对象是否"相等"的方法,定义在Object 类中。所有 Java 类都继承自Object,因此所有对象都有equals( ) 方法。

基本要点:

  • 默认行为 :Object 类中的 equals( ) 方法实现是 == 比较,比较两个对象的内存地址是否相同~

  • 重写目的:我们通常重写 equals( ) 来定义对象内容的比较逻辑,而不是内存地址比较~

  • 与 == 的区别== 比较引用类型变量(内存地址)或基本类型变量的值,equals( ) 比较对象的内容(重写后)。

java 复制代码
Person person1 = new Person(21,"小美");
Person person2 = new Person(21,"小美");

System.out.println(person1 == person2);      // false - 比较引用
System.out.println(person1.equals(person2)); // true  - 比较内容

3.2 Object类中的默认equals实现

所有Java类都继承自 Object 类,其默认的 equals 方法实现就是使用==比较:

java 复制代码
public boolean equals(Object obj) {
    return (this == obj);
}

这显然不能满足我们比较对象内容的需求,因此需要重写equals方法。


3.3 如何正确重写equals方法

让我们通过一个完整的Person类示例来理解如何正确重写equals方法:

java 复制代码
//定义一个名为 Person 的类
class Person {
    //String是字符串类型,用于存储文本
    public String name;
    //int是整数类型,用于存储数字
    public int age;
    // 构造方法,用于创建Person对象时初始化对象
    public Person(int age,String name) {
        this.name = name;
        this.age = age;
    }
    //重写toString方法,用于打印对象信
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +  // 输出name属性
                ", age=" + age +          //输出1age属性
                '}';
    }
    //重写equals方法,用于比较两个Person对象是否相等
    @Override
    public boolean equals(Object obj) {
        // 1.检查传入的对象是否为null
        if (obj == null) {
            return false ;// null不等于任何对象
        }
        // 2.检查是否是同一个对象(内存地址是否相同)
        if(this == obj) {
            return true ;//同一个对象当然相等
        }
        // 3.检查对象类型是否匹配
        // instanceof用于检查对象是否是某个类的实例
        // 如果不是Person类对象
        if (!(obj instanceof Person)) {
            return false ;//类型不同肯定不相等
        }
        // 4.向下转型,比较属性值
        Person person = (Person) obj ;
        // 5.比较关键属性值
        // 对于基本类型age用==比较
        // 对于引用类型name用equals比较(注意name不能为null)
        return this.name.equals(person.name) && this.age==person.age ;
    }
}

其中这一部分就是重写:

可以像我们上述的那样写,也可以用电脑生成的这样写:

java 复制代码
 @Override
    public boolean equals(Object obj) {
        //检查是否是同一个对象
        if (this == obj) return true;
        //检查是否是null或者类型不同
        if (obj == null || getClass() != obj.getClass()) return false;
        //类型转换
        Person person = (Person) obj;
        //比较年龄和名字是否相同
        return age == person.age && Objects.equals(name, person.name);
    }

接下来测试一下我们的equals方法:

java 复制代码
public class Test {
    public static void main(String[] args) {
        //创建两个内容相同但内存地址不同的Person对象
        Person person1 = new Person(21, "小美");
        System.out.println(person1);
        Person person2 = new Person(21, "小美");
        System.out.println(person2);
        System.out.println("---------------------------");//分割线
        //比较person1和person2是否是同一个对象,== 比较的是内存地址
        System.out.println(person1 == person2);
        //调用equals方法比较,equals比较的是内容(因为我们重写了equals方法)
        System.out.println(person1.equals(person2));
    }
}

结果为:

总结一下:

  • 如果是以后自定义的类型,那么一定要记住重写equals方法
  • 比较对象中的内容是否相同的时候,一定要重写equals方法

4. hashCode方法

4.1 hashCode的概念

hashCode也是Java中Object类提供的一个方法,它返回对象的哈希码值(一个32位整数)。哈希码的主要用途是提高哈希表的性能,让对象能够快速被查找。

hashcode方法源码:

java 复制代码
// Object类中的默认实现
public native int hashCode();

📌 简单理解:可以把hashCode想象成对象的"身份证号",虽然不能完全唯一标识一个对象,但可以用来快速区分大多数对象。

可以看刚刚toString方法的源码:

hashCode( )这个方法,帮我们算了一个具体的对象位置,这个位置是经过处理之后的一个哈希值~

那么为什么需要hashCode?

假设我们有10000个Person对象要存储:

  • 没有hashCode:每次查找都需要遍历所有对象,调用equals比较,时间复杂度O(n)。

  • 有hashCode:先比较hashCode快速定位大致范围,再精细比较,时间复杂度接近O(1)。


4.2 对hashCode理解

我们输出一下两个person的哈希值:

java 复制代码
public class Test {
    public static void main(String[] args) {
        //创建两个内容相同但内存地址不同的Person对象
        Person person1 = new Person(21, "小美");
        System.out.println(person1);
        Person person2 = new Person(21, "小美");
        System.out.println(person2);
        System.out.println("---------------------------");//分割线
        System.out.println(person1.hashCode());
        System.out.println(person2.hashCode());
    }
}

结果显然是不同的:

这个时候我们有一个需求,我们认为这两个person就是同一个对象,将来往哈希表里存放的时候,不用多存一份,我们认为这两个就是一份,放在同一个位置,这个时候逻辑就不一样了,这个时候我们重写hashCode:

再观察结果会发现一样了:

这儿也就说明重写hashCode后,逻辑上将这两个person的属性name和age传给这个方法,它在逻辑上算出了同一个位置。

如不重写hashCode,原来它们该出现在同一个位置,但是不会了。

后面讲到哈希表的时候,就知道两个一样的对象我们想放在同一个位置,此时就可以利用重写这个方法来实现 啦~


5. 接口使用实例(comparable接口)

现在,假设我们有两位学生,我们想比较一下两位学生年龄谁大谁小:

java 复制代码
class Student {
    public String name;
    public int age;
    //构造方法,用于创建Student对象时初始化属性
    public Student(String name,int age) {
        this.name = name;
        this.age = age;
    }
    //重写toString方法,用于打印对象信息
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class Test2 {
     Student student1 = new Student("小美",21);
     Student student2 = new Student("小帅",22);
}

或许有的友友们想直接:System.out.println(student1 > student2);但是不可以的,这两个不是基本数据类型。目前比不了两个学生的年龄,这两个学生是引用类型,这个时候就需要implements了。

  • Student类实现了Comparable接口,表示学生对象可以互相比较。
  • 泛型<Student>表示这是Student类之间的比较。
  • Comparable是Java中的一个接口,位于java.lang包中。它定义了对象的自然排序方式,任何实现了Comparable接口的类都可以和同类对象进行比较和排序

当我们实现这个接口后,我们需要重写Comparable里的一个compareTo方法:

java 复制代码
 @Override
    public int compareTo(Student o) {
        return 0;
    }

我们要在这里面填写用什么比较,比如说拿名字比或者拿年龄进行比较,这里我们用年龄进行比较,这样实现:

java 复制代码
 @Override
    public int compareTo(Student o) {
        //比较年龄,return返回比较结果:
        //负数:当前对象"小于"参数对象
        //零:两对象"相等"
        //正数:当前对象"大于"参数对象
        return this.age - o.age;
    }

如果这里看不懂,我们也可以这样写:

java 复制代码
@Override
    public int compareTo(Student o) {//参数o是要比较的另一个学生对象
        if(this.age > o.age) {
            return 1;// 如果当前学生年龄较大,返回1(表示this应该排在o后面)
        } else if (this.age < o.age) {
            return -1;// 如果当前学生年龄较小,返回-1(表示this应该排在o前面)
        }else {
            return 0;// 如果两人年龄相同,返回0(表示两者顺序相等)
        }
    }

然后将两者进行比较:

java 复制代码
public class Test2 {
    public static void main(String[] args) {
        Student student1 = new Student("小美", 21);
        Student student2 = new Student("小帅", 22);
        //student1这个对象和student2这个对象进行比较
        System.out.println(student1.compareTo(student2));
    }
}

System.out.println(student1.compareTo(student2)); 这就将student1和student2进行了比较。我们看结果是一个负数:

比较出student1的年龄比student2的年龄小。

总结一下: 自定义类型想要比较大小,要实现comparable这个接口,一定要重写里面的compareTo方法


comparable和equals都用来比较对象,那么二者有什么区别?

Comparable equals()
目的 判断两个对象的大小 判断两个对象是否逻辑相等
所属接口/类 java.lang.Comparable要重写compareTo Object类方法(可重写)
返回值 int(负数/0/正数) boolean(true/false)
java 复制代码
// Comparable接口
public interface Comparable<T> {
    int compareTo(T o);  // 返回:-1(小于)、0(等于)、1(大于)
}

// Object.equals()
public boolean equals(Object obj);  // 返回:true/false

再说一说这个接口的不好之处:对类的侵入性比较强,比如说我们根据姓名去比较大小,可能就会出现问题。当然我们有另一种解决方式。

这时候我们Student不实现Comparable接口,我们再定义一个AgeComparator类去实现另一个接口Comparator,在里面指定是Student。

在这个接口里面有一个compare方法,我们重写它就可以:

实际我们这样写就可以:

java 复制代码
//AgeComparator类:专门用来比较Student对象的年龄
//实现了Comparator<Student>接口表示这是一个Student的比较
class AgeComparator implements Comparator<Student> {
  //实现compare方法 - 定义两个学生的年龄比较规则
  //o1 第一个学生对象
  //o2 第二个学生对象
  //return 比较结果:
  //结果负数:o1的年龄 < o2的年龄
  //结果为零:o1的年龄 == o2的年龄
  //结果为正数:o1的年龄 > o2的年龄
    @Override
    public int compare(Student o1, Student o2) {
        //用o1的年龄减去o2的年龄得到比较结果
        //例如:21岁 vs 22岁 → 21-22 = -1(表示o1比o2小)
        return o1.age - o2.age;
    }
}
public class Test2 {
    public static void main(String[] args) {
        Student student1 = new Student("小美", 21);
        Student student2 = new Student("小帅", 22);
        //创建年龄比较器的实例
        AgeComparator ageComparator = new AgeComparator();
        //使用比较器比较两个学生,并且输出结果。
        System.out.println(ageComparator.compare(student1, student2));
    }
}

我们通过创建一个对象,通过对象的引用去调用compare,我们传的是student,则把student1和student2传进去就好了,我们的返回值是一个整数,通过sout输出就好。结果为:

说明student1的年龄比studen2的年龄小一岁。

同样我们可以根据姓名去比较

再定义一个NameComparator类去实现另一个接口Comparator,在里面指定是Student。在这个接口里面有一个compare方法,我们重写它就可以:

我们这样去实现它:

java 复制代码
//NameComparator类:专门用来按姓名比较Student对象
class NameComparator implements Comparator<Student> {
//实现compare方法 - 定义两个学生姓名的比较规则
//o1 第一个学生对象
//o2 第二个学生对象
//return 比较结果:
//负数:o1的姓名在字典序中小于o2的姓名
//零:两个姓名相同
//正数:o1的姓名在字典序中大于o2的姓名
    @Override
    public int compare(Student o1, Student o2) {
        //使用String类的compareTo方法进行字符串比较
        //compareTo会逐个比较字符的Unicode值
        return o1.name.compareTo(o2.name);
    }
}
public class Test2 {
    public static void main(String[] args) {
        Student student1 = new Student("小美", 21);
        Student student2 = new Student("小帅", 22);
        //创建姓名比较器的实例
        NameComparator nameComparator = new NameComparator();
        //使用比较器比较两个学生的姓名,并且输出结果。
        System.out.println(nameComparator.compare(student1,student2));
    }
}

输出的结果为:

Java中字符串比较是通过 String.compareTo( )方法实现的,规则如下:

逐个字符比较 :从第一个字符开始,比较对应位置的 Unicode 值,有三种可能结果

  • 如果字符不同:返回当前字符的 Unicode 差值

  • 如果字符相同:继续比较下一个字符

  • 如果全部字符相同但长度不同:最后return返回长度差值

中文比较是基于 Unicode 编码的:每个汉字都有对应的 Unicode 值,比较的时候会转换成 Unicode 数值进行比较啦~

比如说博主这里的"小美" vs "小帅":

  • "小"和"小"相同(Unicode: 23567)
  • 比较第二个字:"美"(32654)和 "帅"(24069)
  • 32654 - 24069 = 8585(正数,表示"美" > "帅")

以上是比较器的使用,这种方法对类的侵入性不强~


下面是完整的详细代码,希望能对友友们有帮助:

java 复制代码
class Student{
    public String name;
    public int age;
    //构造方法,用于创建Student对象时初始化属性
    public Student(String name,int age) {
        this.name = name;
        this.age = age;
    }
    //重写toString方法,用于打印对象信息
    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
//AgeComparator类:专门用来比较Student对象的年龄
//实现了Comparator<Student>接口表示这是一个Student的比较
class AgeComparator implements Comparator<Student> {
  //实现compare方法 - 定义两个学生的年龄比较规则
  //o1 第一个学生对象
  //o2 第二个学生对象
  //return 比较结果:
  //结果负数:o1的年龄 < o2的年龄
  //结果为零:o1的年龄 == o2的年龄
  //结果为正数:o1的年龄 > o2的年龄
    @Override
    public int compare(Student o1, Student o2) {
        //用o1的年龄减去o2的年龄得到比较结果
        //例如:21岁 vs 22岁 → 21-22 = -1(表示o1比o2小)
        return o1.age - o2.age;
    }
}
//NameComparator类:专门用来按姓名比较Student对象
class NameComparator implements Comparator<Student> {
//实现compare方法 - 定义两个学生姓名的比较规则
//o1 第一个学生对象
//o2 第二个学生对象
//return 比较结果:
//负数:o1的姓名在字典序中小于o2的姓名
//零:两个姓名相同
//正数:o1的姓名在字典序中大于o2的姓名
    @Override
    public int compare(Student o1, Student o2) {
        //使用String类的compareTo方法进行字符串比较
        //compareTo会逐个比较字符的Unicode值
        return o1.name.compareTo(o2.name);
    }
}
public class Test2 {
    public static void main(String[] args) {
        Student student1 = new Student("小美", 21);
        Student student2 = new Student("小帅", 22);
        //创建年龄比较器的实例
        AgeComparator ageComparator = new AgeComparator();
        //使用比较器比较两个学生,并且输出结果。
        System.out.println(ageComparator.compare(student1, student2));
        //创建姓名比较器的实例
        NameComparator nameComparator = new NameComparator();
        //使用比较器比较两个学生的姓名,并且输出结果。
        System.out.println(nameComparator.compare(student1,student2));
    }
}

6. Clonable接口和深浅拷贝

Cloneable 是 Java 中的一个标记接口 ,位于 java.lang 包中。它没有任何方法,只是用来标记一个类可以被克隆

下面通过具体代码对这个知识点进行讲解:

我们实例化了一位age是6岁的人:

java 复制代码
class Person {
    public int age;
    public Person(int age) {
        this.age = age;
    }
    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                '}';
    }
}
public class Test3 {
    public static void main(String[] args) {
        Person person1 = new Person(6);
    }
}

这个时候我们观察对应的内存结构:

我们现在有一个要求,将左边引用所指的对象克隆一份,也就是将来还会有person2,这个时候就要调用 clone 方法了。

但是这个时候不能成功的调用,会出现报错:

1. 没有实现 Cloneable 接口

Java中有规定,要使用 clone( ) 方法,类必须显式实现 Cloneable 接口。Cloneable是一个标记接口(没有方法的接口),它告诉 JVM 这个类允许被克隆。不实现它调用 clone( ) 会抛CloneNotSupportedException异常。

2. 没有重写 clone( ) 方法

即使实现了 Cloneable 接口,还需要重写 Object 类的 clone( ) 方法并将其访问修饰符改为public ,因为 Object中的 clone( ) 是 protected 的。


我们重写clone( ) 方法

这个时候我们如果看到CloneNotSupportedException异常,我们要处理掉它,将鼠标光标点在红线clone( )处,按住Alt和Enter,再点击第一个。

代码就会变为:

但这个时候又会出现另一个错误,这个时候我们要向下转型

为什么要向下转型呢?我们之前讲过在Java 中,clone( )方法是在 Object 类中定义的。

注意它的返回类型是Object,而不是具体的子类类型。 虽然我们重写了 clone( ) 方法,但是方法签名中的返回类型仍然是 Object ,编译器只知道返回的是 Object ,不知道实际是 Person。所以我们要向下转型,强制类型转换,显式告诉编译器:这个对象实际上是 Person 类型。


现在我们重写了clone( )方法,也进行了向下转型,但是我们还没有实现接口!接下来实现接口Cloneable

我们来观察这个接口的源码:

会发现接口内部全部都是空的,没有定义任何一个方法,是空接口,也被称为**标记接口。**仅仅作为一个"标记",告诉 JVM 这个类的对象允许被克隆。

现在我们也实现了接口,我们输出观察结果:

java 复制代码
public class Test3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(6);
        Person person2 = (Person)person1.clone();
        System.out.println(person1);
        System.out.println(person2);
    }
}

成功实现了克隆,这个时候我们观察内部结构:

克隆的时候,会克隆出一模一样的部分(右边上面蓝色框),也有个地址0x88,此时我们的person2就存储了克隆出来新的对象的内容,整体的克隆就完成了,是通过调用super.clone( )去帮助完成克隆。

下面是完整的代码,希望能对友友们有帮助:

java 复制代码
/**
 * Person类实现了Cloneable接口,表示这个类的对象可以被克隆
 * Cloneable是一个标记接口,内部没有方法,只是用来表示允许克隆
 */
class Person implements Cloneable {
    public int age;
    //public Money m;
    //构造方法 - 创建Person对象时初始化属性
    public Person(int age) {
        this.age = age;
    }
    //重写toString方法 - 定义对象打印时的显示格式
    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                '}';
    }
    /**
     * 重写clone方法 - 实现对象克隆功能
     * @return 克隆后的新对象
     * @throws CloneNotSupportedException 如果不支持克隆则抛出异常
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 调用Object类的clone()方法实现浅拷贝
        return super.clone();
    }
}
public class Test3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(6);
        // 克隆person1创建person2
        // 需要强制转型,因为clone()返回的是Object类型
        Person person2 = (Person)person1.clone();
        System.out.println(person1);
        System.out.println(person2);
    }
}

😼现在我们对这个代码做出小小的改动,我们添加一个Money类:

java 复制代码
class Money {
    public double money = 66.6;
}
class Person implements Cloneable {
    // 定义两个属性:age(年龄)和m(钱)
    public int age;// 基本数据类型,直接存储值
    public Money m;// 引用类型,存储的是对象的地址
    //构造方法 - 创建Person对象时初始化属性
    public Person(int age) {
        this.age = age;
        this.m = new Money();
    }
@Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                '}';
    }
@Override
    protected Object clone() throws CloneNotSupportedException {
        // 调用Object类的clone()方法实现浅拷贝
        return super.clone();
    }
}

然后我们在测试类中写如下输出:

java 复制代码
public class Test3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(6);
        // 克隆person1创建person2
        // 需要强制转型,因为clone()返回的是Object类型
        Person person2 = (Person)person1.clone();
        //打印两个Person对象中的money值
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
    }
}

这个时候观察结果money都是66.6:

这个时候我们再做一些改变,将克隆后的person2的money改为99.9,再来输出一下改完的钱:

java 复制代码
public class Test3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(6);
        // 克隆person1创建person2
        // 需要强制转型,因为clone()返回的是Object类型
        Person person2 = (Person)person1.clone();
        // 打印两个Person对象中的money值
        System.out.println(person1.m.money);// 输出person1的钱
        System.out.println(person2.m.money);// 输出person2的钱
        System.out.println("----------------------");//分割线
        // 修改person2的money值为99.9
        person2.m.money = 99.9;
        // 再次打印两个对象的money值
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
    }
}

这个时候观察结果,发现都是99.9:

原来我们期望的是person2的money更改,person1的不更改,但是现在两个都更改了。我们看到的这种情况叫做浅拷贝

我们接下来分析一下这种情况的原因,我们观察内部结构:

我们 new person 的时候,现在里面不仅有age,也有money,m是一个引用,也会占据一块内存(堆上下面的蓝色方框)。我们在构造方法中实例化了一块对象,也就意味着我们 new Money( )里面存了一个0x65(堆上右边橙色方框)。此时第一行代码结束。

接下来我们克隆对象,我们将person1所指的对象(堆上下面的蓝色方框)克隆一份,也就是有一样的一份占了一块内存,person2所指向克隆出来的这份对象(堆上上面的蓝色区域),地址是0x981,person2里面存的是0x981(栈上红色方框区域)。我们要注意,我们克隆的是person1所指向的对象,但是没有拷贝对象里面的对象,那么意味着m还是指向原来的66.6的money(绿色箭头)。age还是10,m还是0x65。此时不管是输出person1里面money的值还是person2里面的money的值,都是66.6。此时分割线上面的代码讲解完毕。


接下来讲解分割线下面的代码,修改了person2里面的money内容,改为99.9。

现在我们修改了,改为99.9,我们通过person1去拿的时候,也是99.9,通过person2去拿的时候,也是99.9,此时我们看到的这个现象就是浅拷贝。

完整代码如下:希望能帮助大家理解浅拷贝:

java 复制代码
class Money {
    public double money = 66.6;
}
class Person implements Cloneable {
    // 定义两个属性:age(年龄)和m(钱)
    public int age;// 基本数据类型,直接存储值
    public Money m;// 引用类型,存储的是对象的地址
    //构造方法 - 创建Person对象时初始化属性
    public Person(int age) {
        this.age = age;
        this.m = new Money();
    }
@Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                '}';
    }
@Override
    protected Object clone() throws CloneNotSupportedException {
        // 调用Object类的clone()方法实现浅拷贝
        return super.clone();
    }
}
public class Test3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(6);
        // 克隆person1创建person2
        // 需要强制转型,因为clone()返回的是Object类型
        Person person2 = (Person)person1.clone();
        // 打印两个Person对象中的money值
        System.out.println(person1.m.money);// 输出person1的钱
        System.out.println(person2.m.money);// 输出person2的钱
        System.out.println("----------------------");//分割线
        // 修改person2的money值为99.9
        person2.m.money = 99.9;
        // 再次打印两个对象的money值
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
    }
}

那么什么是深拷贝呢,就是我们希望将对象里面的对象也克隆一份。接下来我们回到代码上进行深拷贝。我们希望改动person2的money,让它变为99.9,而person1的不变还是66.6。

具体步骤:

  1. 让Money类也实现Cloneable接口

只有实现了这个接口的Money类才能被克隆啦~

  1. 在Person类重写的clone方法中
  • 先调用super.clone()完成基本类型的拷贝
  • 然后对引用类型字段m手动调用clone()方法
  • 这样person1和person2就会有各自独立的Money对象

深拷贝完整的代码如下:

java 复制代码
// Money类实现Cloneable接口才能被克隆
class Money implements Cloneable{
    public double money = 66.6;
    // 重写clone方法,允许Money对象被克隆
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 调用父类(Object)的clone方法实现克隆
        return super.clone();
    }
}
/**
 * Person类实现了Cloneable接口,表示这个类的对象可以被克隆
 * Cloneable是一个标记接口,内部没有方法,只是用来表示允许克隆
 */
class Person implements Cloneable {
    // 定义两个属性:age(年龄)和m(钱)
    public int age;// 基本数据类型,直接存储值
    public Money m;// 引用类型,存储的是对象的地址
    //构造方法 - 创建Person对象时初始化属性
    public Person(int age) {
        this.age = age;
        this.m = new Money();// 创建一个新的Money对象
    }
    //重写toString方法 - 定义对象打印时的显示格式
    @Override
    public String toString() {
        return "Person{" +
                " age=" + age +
                '}';
    }
    /**
     * 重写clone方法 - 实现对象克隆功能
     * @return 克隆后的新对象
     * @throws CloneNotSupportedException 如果不支持克隆则抛出异常
     */
    @Override
    protected Object clone() throws CloneNotSupportedException {
        // 1. 先调用父类的clone方法完成基本类型的拷贝(浅拷贝)
        //这会复制age基本类型,并复制m引用(此时两个对象的m指向同一个Money对象)
       Person tmp = (Person) super.clone();
        // 2. 对引用类型m进行克隆,创建新的Money对象(深拷贝关键步骤)
        //将tmp的m指向克隆出来的新Money对象,而不是原来的那个
       tmp.m = (Money) this.m.clone();
        // 3. 返回深拷贝后的对象
       return tmp;
    }
}
public class Test3 {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person person1 = new Person(6);
        // 克隆person1创建person2
        // 需要强制转型,因为clone()返回的是Object类型
        Person person2 = (Person)person1.clone();
        // 打印两个Person对象中的money值
        System.out.println(person1.m.money);// 输出person1的钱
        System.out.println(person2.m.money);// 输出person2的钱
        System.out.println("----------------------");//分割线
        // 修改person2的money值为99.9
        person2.m.money = 99.9;
        // 再次打印两个对象的money值
        // 如果是浅拷贝,person1的money也会变成99.9
        // 但这里是深拷贝,所以只有person2的money改变
        System.out.println(person1.m.money);
        System.out.println(person2.m.money);
    }
}

输出结果为:

这里再对深拷贝的知识点讲解一下:

深拷贝是指创建一个新对象,并递归地复制原对象及其引用的所有对象 。与浅拷贝的区别在于:

  • 浅拷贝 :只复制基本类型,引用类型只复制引用地址(新旧对象共享引用对象)

  • 深拷贝:完全复制整个对象结构,包括所有引用对象(新旧对象完全不共享任何引用)

对于上述代码实现深拷贝的过程再进行总结一下:

1.让所有相关类实现Cloneable接口

java 复制代码
class Money implements Cloneable { ... }
class Person implements Cloneable { ... }

2.在每个类中重写clone()方法

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

3.在包含引用类型的类中,手动克隆引用对象

java 复制代码
Person tmp = (Person) super.clone();  // 先浅拷贝
tmp.m = (Money) this.m.clone();      // 再手动克隆引用对象
return tmp;

我们要知道,深拷贝和浅拷贝看的就是代码的实现过程,不能单纯的认为克隆这个方法就是且拷贝或者是深拷贝,不是说某个方法是深拷贝或者浅拷贝,而是这整个代码的实现过程是深拷贝或者浅拷贝~


制作不易,更多内容加载中~感谢友友们的点赞收藏关注~~

如有问题欢迎批评指正,祝友友们生活愉快,学习工作顺顺利利!

相关推荐
零千叶13 分钟前
【面试】AI大模型应用原理面试题
java·设计模式·面试
坐吃山猪5 小时前
SpringBoot01-配置文件
java·开发语言
我叫汪枫5 小时前
《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
java·开发语言·nio
yaoxtao5 小时前
java.nio.file.InvalidPathException异常
java·linux·ubuntu
Swift社区7 小时前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT7 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy8 小时前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss9 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续9 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升