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

发布于:2025-05-09 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

1. Object类

1.1 Object的概念

1.2 Object例子

2. toString

2.1 toString的概念

2.2 为什么要重写toString

2.3 如何重写toString

3. 对象比较equals方法

3.1 equals( ) 方法的概念

3.2 Object类中的默认equals实现

3.3 如何正确重写equals方法

4. hashCode方法

4.1 hashCode的概念

4.2 对hashCode理解

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

6. Clonable接口和深浅拷贝


1. Object类

1.1 Object的概念

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

// 这两段代码实际上是完全等价的
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( ) 输出可读性差,比如说:

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

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

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

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


2.3 如何重写toString

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

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

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

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( )的默认实现如下:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}


3. 对象比较equals方法

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

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)进行比较:
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方法

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( ) 比较对象的内容(重写后)。

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 方法实现就是使用==比较:

public boolean equals(Object obj) {
    return (this == obj);
}

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


3.3 如何正确重写equals方法

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

//定义一个名为 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 ;
    }
}

其中这一部分就是重写:

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

 @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方法:

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方法源码: 

// Object类中的默认实现
public native int hashCode();

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

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

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

        那么为什么需要hashCode?

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

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

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


4.2 对hashCode理解

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

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接口) 

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

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方法

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

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

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

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

@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(表示两者顺序相等)
        }
    }

然后将两者进行比较:

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)
// 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方法,我们重写它就可以:

实际我们这样写就可以:

//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方法,我们重写它就可以:

我们这样去实现它:

//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(正数,表示"美" > "帅")

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


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

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岁的人:

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 这个类的对象允许被克隆。

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

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( )去帮助完成克隆。

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

/**
 * 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类:

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);
        System.out.println(person2.m.money);
    }
}

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

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

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,此时我们看到的这个现象就是浅拷贝。

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

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对象

深拷贝完整的代码如下:

// 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接口

class Money implements Cloneable { ... }
class Person implements Cloneable { ... }

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

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

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

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

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


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

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


网站公告

今日签到

点亮在社区的每一天
去签到