目录
[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。
具体步骤:
- 让Money类也实现Cloneable接口:
只有实现了这个接口的Money类才能被克隆啦~

- 在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;
我们要知道,深拷贝和浅拷贝看的就是代码的实现过程,不能单纯的认为克隆这个方法就是且拷贝或者是深拷贝,不是说某个方法是深拷贝或者浅拷贝,而是这整个代码的实现过程是深拷贝或者浅拷贝~
制作不易,更多内容加载中~感谢友友们的点赞收藏关注~~
如有问题欢迎批评指正,祝友友们生活愉快,学习工作顺顺利利!
