类相关知识点
什么是类?什么是对象?
Java 中最核心、最有趣的两个概念:类 和 对象。这就像是“造物主”的工作,理解了它们,你就拿到了进入 Java 世界大门的钥匙!🗝️
我会用一个超可爱的比喻,让你一秒就懂!🐱
什么是【类】(Class)?📋
类,就是一张【设计蓝图】或者一个【物种模板】。 🧾
它只负责描述和定义,但本身并不是一个具体的东西。
比如,你想造一只【猫】,你得先有一份《猫的设计蓝图》:
-
🐾 属性 (是什么):这只猫应该有【颜色】、【品种】、【年龄】...(这些叫 成员变量)
-
🤹 行为 (能做什么):这只猫会【喵喵叫】、【会卖萌】、【会跑】...(这些叫 成员方法)
这份蓝图写得清清楚楚,但它本身并不是一只真猫。你没法撸这份蓝图,对吧?
用代码表示这个“类”(蓝图):
// 这就是一个‘Cat’类,它是一份蓝图
public class Cat {
// 👉 属性/状态 (成员变量)
String color; // 颜色
String breed; // 品种
int age; // 年龄
// 👉 行为/功能 (成员方法)
public void meow() {
System.out.println("喵喵喵!");
}
public void eat() {
System.out.println("咔嚓咔嚓吃猫粮!");
}
}
你看,我们定义了一只“猫”应该有的特征和行为,但这里并没有一只真正的猫出现。
什么是【对象】(Object)?🐈
对象,就是根据【蓝图】真正制造出来的【具体实物】。 �️
这个过程叫做 “实例化”,听起来很高大上,但其实就像用模具扣小饼干一样简单!🍪
根据上面那份《猫的设计蓝图》,我们现在来造几只真猫:
public class Main {
public static void main(String[] args) {
// 根据‘Cat’蓝图,造第一只具体的猫 -> 对象1号
Cat cat1 = new Cat();
cat1.color = "橙色"; // 给它涂上橙色
cat1.breed = "橘猫"; // 设定品种为橘猫
cat1.age = 2; // 设定年龄2岁
cat1.meow(); // 让这只猫喵喵叫 -> 输出:喵喵喵!
// 根据同一份蓝图,再造第二只具体的猫 -> 对象2号
Cat cat2 = new Cat();
cat2.color = "灰色";
cat2.breed = "英短";
cat2.age = 1;
cat2.eat(); // 让这只猫吃东西 -> 输出:咔嚓咔嚓吃猫粮!
// 看!它们都是猫,但有自己独特的属性!
}
}
你看!
-
cat1 和 cat2 就是两个对象(也叫实例)。
-
它们都源自同一份蓝图 Cat 类。
-
它们都有喵喵叫和吃东西的能力(方法)。
-
但它们可以有不同的状态(属性):一只是大橘,一只是英短。
🎯核心关系总结
概念 比喻 角色 关键词
类 (Class) 设计蓝图 📋、饼干模具 🍪、物种概念 抽象模板 class Cat
对象 (Object) 真正的房子 🏠、具体的饼干 🍪、一只真猫 🐱 具体实例 new Cat()
一句话记住: 类是模板,对象是根据模板创造出的具体实例。 先有类,再有对象。 没有“猫”的概念(类),就不存在“我家的这只橘猫”(对象)。
💡 为什么这么设计?(面向对象编程 OOP)
这种“类-对象”的思维模式,就是 Java 的核心哲学——面向对象编程。
它非常符合我们现实世界的思维方式:
- 模块化:把代码像乐高一样拆分和组装,结构清晰。🧩
- 复用性:一份蓝图(类)可以造出无数个对象,代码不用重复写。
- 易维护:想修改所有猫的行为?直接改蓝图(类)就行了,所有猫(对象)都会自动变!
所以,当你下次看到 new 这个关键字时,就在心里大喊:“变!给我根据这个类,实例化一个对象出来!” 🔮
Object类
Object类是所有类的超类,所有对象都继承自Object类。
Object类提供了一些方法,如equals()、toString()、hashCode()和getClass()。
| 方法 | 描述 |
| equals() | 指示某个其他对象是否与此对象“相等” |
| hashCode() | 返回该对象的哈希码值 |
| toString() | 返回该对象的字符串表示 |
| clone() | 创建并返回此对象的一个副本 |
| wait() | 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法 |
| notify() | 唤醒在此对象监视器上等待的单个线程 |
| notifyAll() | 唤醒在此对象监视器上等待的所有线程 |
| finalize() | 当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法 |
| getClass() | 返回一个对象的运行时类 |
hashCode() 与 equals()
面试官可能会问你:“你重写过 hashCode() 和 equals()么?为什么重写 equals() 时必须重写 hashCode() 方法?”
hashCode() 有什么用?
hashCode() 的作用是获取哈希码(int 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode()定义在 JDK 的 Object 类中,这就意味着 Java 中的任何类都包含有 hashCode() 函数。另外需要注意的是: Object 的 hashCode() 方法是本地方法,也就是用 C 语言或 C++ 实现的,该方法通常用来将对象的内存地址转换为整数之后返回。
为什么要有 hashCode?
我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode?
下面这段内容摘自我的 Java 启蒙书《Head First Java》:
当你把对象加入
HashSet时,HashSet会先计算对象的hashCode值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode值作比较,如果没有相符的hashCode,HashSet会假设对象没有重复出现。但是如果发现有相同hashCode值的对象,这时会调用equals()方法来检查hashCode相等的对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals的次数,相应就大大提高了执行速度。
其实, hashCode() 和 equals()都是用于比较两个对象是否相等。
那为什么 JDK 还要同时提供这两个方法呢?
这是因为在一些容器(比如 HashMap、HashSet)中,有了 hashCode() 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进HastSet的过程)!
我们在前面也提到了添加元素进HastSet的过程,如果 HashSet 在对比的时候,同样的 hashCode 有多个对象,它会继续使用 equals() 来判断是否真的相同。也就是说 hashCode 帮助我们大大缩小了查找成本。
那为什么不只提供 hashCode() 方法呢?
这是因为两个对象的hashCode 值相等并不代表两个对象就相等。
那为什么两个对象有相同的 hashCode 值,它们也不一定是相等的?
因为 hashCode() 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 hashCode )。
总结下来就是 :
- 如果两个对象的
hashCode值相等,那这两个对象不一定相等(哈希碰撞)。 - 如果两个对象的
hashCode值相等并且equals()方法也返回true,我们才认为这两个对象相等。 - 如果两个对象的
hashCode值不相等,我们就可以直接认为这两个对象不相等。
相信大家看了我前面对 hashCode() 和 equals() 的介绍之后,下面这个问题已经难不倒你们了。
为什么重写 equals() 时必须重写 hashCode() 方法?
因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。
思考 :重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。
总结 :
equals方法判断两个对象是相等的,那这两个对象的hashCode值也要相等。- 两个对象有相同的
hashCode值,他们也不一定是相等的(哈希碰撞)。
toString()
默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
clone()
clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
Cloneable接口
clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
① 实现Cloneable接口,这是一个标记接口,自身没有方法。
② 覆盖clone()方法,可见性提升为public。
浅拷贝:被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象。 深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象。
wait() notify() notifyAll()
在多线程进行解释
finalize()
finalize()方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法,一般开发中我们不需要太过深入的关注。
finalize()方法中一般用于释放非Java 资源(如打开的文件资源、数据库连接等),或是调用非Java方法(native方法)时分配的内存(比如C语言的malloc()系列函数)。
避免使用
由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间这段时间是任意长的。我们并不能依赖finalize()方法能及时的回收占用的资源,可能出现的情况是在我们耗尽资源之前,gc却仍未触发,因而通常的做法是提供显示的close()方法供客户端手动调用。
另外,重写finalize()方法意味着延长了回收对象时需要进行更多的操作,从而延长了对象回收的时间。
延长周期
利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期。
wait() notify() notifyAll()
在多线程进行解释
finalize()
finalize()方法是Object类中提供的一个方法,在GC准备释放对象所占用的内存空间之前,它将首先调用finalize()方法,一般开发中我们不需要太过深入的关注。
finalize()方法中一般用于释放非Java 资源(如打开的文件资源、数据库连接等),或是调用非Java方法(native方法)时分配的内存(比如C语言的malloc()系列函数)。
避免使用
由于finalize()方法的调用时机具有不确定性,从一个对象变得不可到达开始,到finalize()方法被执行,所花费的时间这段时间是任意长的。我们并不能依赖finalize()方法能及时的回收占用的资源,可能出现的情况是在我们耗尽资源之前,gc却仍未触发,因而通常的做法是提供显示的close()方法供客户端手动调用。
另外,重写finalize()方法意味着延长了回收对象时需要进行更多的操作,从而延长了对象回收的时间。
延长周期
利用finalize()方法最多只会被调用一次的特性,我们可以实现延长对象的生命周期。
访问权限
Java中有四种访问权限,分别是「public、protected、包访问权限(默认)、private」,如果省略了访问修饰符,那默认访问权限为「包访问权限」。 这四种权限从「最大权限」到「最小权限」分别是:
public > protected > 包访问权限> private
可以对类或类中的成员(字段和方法)加上访问修饰符。
| 访问权限 | 本类 | 本包的类 | 子类 | 非子类的外包类 |
|---|---|---|---|---|
| public | 是 | 是 | 是 | 是 |
| protected | 是 | 是 | 是 | 否 |
| default | 是 | 是 | 否 | 否 |
| private | 是 | 否 | 否 | 否 |
- 「包访问权限:」 没有任何修饰符的权限就是「包访问权限」,意味着当前包的所有类都可以访问这个成员,如表中所示,对于本包之外的类,这个成员就变成了「private」,访问不了
- 「public:」 被public修饰的成员对任意一个类都是可用的,任何一个类都可以访问到,通过操作该类的对象随意访问「public」成员
- 「protected:」 在相同的class内部,同一个包内和其他包的子类中能被访问。要理解「protected」权限,就需要了解「继承」,因为这个权限处理的就是继承相关的概念,继承而来的子类可以访问「public、protected」
- 「private:」 除了包含这个成员的类之外,所有类都无法访问这个成员,相当于自我封闭,防止其他类改变或删除这个方法
重写与重载
1. 重写(Override)
存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。
为了满足里式替换原则,重写有以下三个限制:
- 子类方法的访问权限必须大于等于父类方法;
- 子类方法的返回类型必须是父类方法返回类型或为其子类型。
- 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
使用 @Override 注解,可以让编译器帮忙检查是否满足上面的三个限制条件。
在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有再到父类中查看,看是否从父类继承来。否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为:
- this.func(this)
- super.func(this)
- this.func(super)
- super.func(super)
2. 重载(Overload)
存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。
继承(抽象类与接口)
继承的概念
继承是java面向对象编程技术的一块基石,因为它允许创建分等级层次的类。
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
继承的特性
- 子类拥有父类非 private 的属性、方法。
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
- Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
- 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。
在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。
super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。
this关键字:指向自己的引用。
接口和抽象类的基本语法区别
Java中接口和抽象类的定义语法分别为interface与abstract关键字。
抽象类
在Java中被abstract关键字修饰的类称为抽象类,被abstract关键字修饰的方法称为抽象方法,抽象方法只有方法的声明,没有方法体。抽象类的特点:
a、抽象类不能被实例化只能被继承;
b、包含抽象方法的一定是抽象类,但是抽象类不一定含有抽象方法;
c、抽象类中的抽象方法的修饰符只能为public或者protected,默认为public;
d、一个子类继承一个抽象类,则子类必须实现父类抽象方法,否则子类也必须定义为抽象类;
e、抽象类可以包含属性、方法、构造方法,但是构造方法不能用于实例化,主要用途是被子类调用。
接口
Java中接口使用interface关键字修饰,特点为:
a、接口可以包含变量、方法;变量被隐式指定为public static final,方法被隐式的指定为public abstract(JDK1.8之前);
b、接口支持多继承,即一个接口可以extends多个接口,间接的解决了Java中类的单继承问题;
c、一个类可以实现多个接口;
d、JDK1.8中对接口增加了新的特性:(1)、默认方法(default method):JDK 1.8允许给接口添加非抽象的方法实现,但必须使用default关键字修饰;定义了default的方法可以不被实现子类所实现,但只能被实现子类的对象调用;如果子类实现了多个接口,并且这些接口包含一样的默认方法,则子类必须重写默认方法;(2)、静态方法(static method):JDK 1.8中允许使用static关键字修饰一个方法,并提供实现,称为接口静态方法。接口静态方法只能通过接口调用(接口名.静态方法名)
相同点
(1)都不能被实例化 (2)接口的实现类或抽象类的子类都只有实现了接口或抽象类中的方法后才能实例化。
不同点
(1)接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
(2)实现接口的关键字为implements,继承抽象类的关键字为extends。一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
(3)接口强调特定功能的实现,而抽象类强调所属关系。
(4)接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public、abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
| 参数 | 抽象类 | 接口 |
|---|---|---|
| 实现 | 子类使用 extends 关键字来继承抽象类,如果子类不是抽象类,则需要提供抽象类中所有声明的方法的实现。 | 子类使用 implements 关键字来实现接口,需要提供接口中所有声明的方法的实现。 |
| 访问修饰符 | 可以用 public、protected 和 default 修饰 | 默认修饰符是 public,不能使用其它修饰符 |
| 方法 | 完全可以包含普通方法 | 只能包含抽象方法、静态方法、默认方法和私有方法,不能为普通方法提供方法实现 |
| 变量 | 既可以定义普通成员变量,也可以定义静态常量 | 只能定义静态常量,不能定义普通成员变量 |
| 构造方法 | 抽象类里的构造方法并不是用于创建对象,而是让其子类调用这些构造方法来完成属于抽象类的初始化操作 | 没有构造方法 |
| 初始化块 | 可以包含初始化块 | 不能包含初始化块 |
| main 方法 | 可以有 main 方法,并且能运行 | 没有 main 方法 |
| 与普通Java类的区别 | 抽象类不能实例化,除此之外和普通 Java 类没有任何区别 | 是完全不同的类型 |
| 运行速度 | 比接口运行速度要快 | 需要时间去寻找在类种实现的方法,所以运行速度稍微有点慢 |
抽象类和接口的应用场景
抽象类的应用场景:
- 父类只知道其子类应该包含怎样的方法,不能准确知道这些子类如何实现这些方法的情况下,使用抽象类。
- 从多个具有相同特征的类中抽象出一个抽象类,以这个抽象类作为子类的模板,从而避免了子类设计的随意性。
接口的应用场景:
- 一般情况下,实现类和它的抽象类之前具有 "is-a" 的关系,但是如果我们想达到同样的目的,但是又不存在这种关系时,使用接口。
- 由于 Java 中单继承的特性,导致一个类只能继承一个类,但是可以实现一个或多个接口,此时可以使用接口。
什么时候使用抽象类和接口:
- 如果拥有一些方法并且想让它们有默认实现,则使用抽象类。
- 如果想实现多重继承,那么必须使用接口。因为 Java 不支持多继承,子类不能继承多个类,但可以实现多个接口,因此可以使用接口。
- 如果基本功能在不断改变,那么就需要使用抽象类。如果使用接口并不断需要改变基本功能,那么就需要改变所有实现了该接口的类。
内部类和外部类
1.什么是内部类?
内部类:定义在类当中的一个类
2.什么是外部类?
最普通的,我们平时见到的那种类,就是在一个后缀为.java的文件中,直接定义的类
3.为什么要使用内部类?
1.增强封装,把内部类隐藏在外部类当中,不允许其他类访问这个内部类
2.增加了代码一个维护性
3.内部类可以直接访问外部类当中的成员
4.内部类可以分为哪几种?
内部类可以分为四种:
1.实例内部类:直接定义在类当中的一个类,在类前面没有任何一个修饰符
2.静态内部类:在内部类前面加上一个static
3.局部内部类:定义在方法的内部类
4.匿名内部类:属于局部内部的一种特殊情况
5.内外部类的修饰符
外部类的修饰符只能有两个public 或者是 默认修饰
内部类可以使用很多个修饰符
实例内部类
- 实例内部类是 属于对象的内部类,是 不属于类的,不使用static修饰的内部类
意思就是把这玩意儿看成是一个对象,别把他当类看
- 想要使用内部类 必须得要 先创建外部类
- 在实例内部类中存在对外部类的引用
意思就是 实例内部类在堆中开辟的空间 里面不仅存放着自己的字段方法和this地址,还有外部类的地址
- 内部类 可以 访问 外部类 当中的成员
- 外部类 不能 直接访问 内部类 当中的成员
意思是 我们要把‘实例内部类’看作对象,而不是类,因此内部类中的成员变量不能使用static修饰,因为static是属于‘类’的 字段和方法,而实例内部类是对象。如果强行写上,编译器会报错。
class Circle {
double radius = 0;
public Circle(double radius) {
this.radius = radius;
}
class Draw { //内部类
public void drawSahpe() {
System.out.println("drawshape");
}
}
}
不过要注意的是,当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
外部类.this.成员变量
外部类.this.成员方法
class Circle {
private double radius = 0;
public Circle(double radius) {
this.radius = radius;
getDrawInstance().drawSahpe(); //必须先创建成员内部类的对象,再进行访问
}
private Draw getDrawInstance() {
return new Draw();
}
class Draw { //内部类
public void drawSahpe() {
System.out.println(radius); //外部类的private成员
}
}
}
成员内部类是依附外部类而存在的,也就是说,如果要创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:
public class Test {
public static void main(String[] args) {
//第一种方式:
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner(); //必须通过Outter对象来创建
//第二种方式:
Outter.Inner inner1 = outter.getInnerInstance();
}
}
class Outter {
private Inner inner = null;
public Outter() {
}
public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}
class Inner {
public Inner() {
}
}
}
局部内部类
局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
class People{
public People() {
}
}
class Man{
public Man(){
}
public People getWoman(){
class Woman extends People{ //局部内部类
int age =0;
}
return new Woman();
}
}
匿名内部类
最经典的就是线程实现匿名内部类
public static void main(String[] args) {
Thread thread=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("hello word");
}
});
}
静态内部类
静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。
public class Test {
public static void main(String[] args) {
Outter.Inner inner = new Outter.Inner();
}
}
class Outter {
public Outter() {
}
static class Inner {
public Inner() {
}
}
}
深拷贝和浅拷贝
我们来聊聊 Java 里一个非常经典的问题:深拷贝 和 浅拷贝。这就像是“复制粘贴”里的大学问!📋➡️📋
我会用一个超形象的比喻,让你轻松理解它们的区别和用途。
🧠 核心概念:拷贝的是什么?
在 Java 中,变量有两种类型:
- 基本类型(如 int, double, char):变量直接存储数据值。
- 引用类型(如 Object, 数组, String):变量存储的是对象的内存地址(就像一把钥匙 🗝️),而不是对象本身。
拷贝操作的核心区别,就在于如何处理这些“钥匙”。
- 什么是【浅拷贝】(Shallow Copy)? 📷
浅拷贝就像是:你配了一把新钥匙,但打开的是同一个房间!
• 操作:创建一个新对象,然后将原对象的非静态字段直接复制到新对象。
• 结果:
◦ 如果字段是基本类型,则拷贝其值。
◦ 如果字段是引用类型,则拷贝其地址引用(也就是配了一把新钥匙),而不是它所引用的对象本身。因此,原对象和拷贝对象里的引用类型字段会指向同一个子对象。
举个例子: 我们有一个 Person 类,他拥有一只 Pet(宠物)。
class Pet {
public String name;
Pet(String name) { this.name = name; }
}
class Person implements Cloneable {
public int age;
public Pet pet; // 引用类型字段
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 默认的clone()方法实现的就是浅拷贝
}
}
现在进行浅拷贝:
Person original = new Person();
original.age = 20;
original.pet = new Pet("Tom"); // 他有一只叫Tom的宠物
Person shallowCopy = (Person) original.clone(); // 浅拷贝
内存发生了什么? 🤔
如图所示,age 是基本类型,值被直接复制了。但 pet 是引用类型,复制的是地址。所以现在,original 和 shallowCopy 的两把“钥匙”都能打开同一个宠物房间。
会产生什么效果? 如果你通过 shallowCopy.pet.name = "Jerry"; 修改了宠物的名字,那么 original.pet.name 也会变成 "Jerry"!😱 因为它们操作的是同一个宠物对象。
- 什么是【深拷贝】(Deep Copy)? 🏗️
深拷贝就像是:你不仅配了新钥匙,还按照原房间的格局,完全重新建了一个新房间和新家具!
• 操作:创建一个新对象,然后递归地复制原对象中的所有字段以及这些字段所引用的所有对象。直到整个对象树都被复制。
• 结果:原对象和拷贝对象完全独立,没有任何共享的引用类型数据。修改任何一个对象,都不会影响另一个。
如何实现深拷贝? Java 默认的 clone() 方法做不到深拷贝,需要我们自己手动实现。
class Person implements Cloneable {
public int age;
public Pet pet;
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone(); // 1. 先浅拷贝Person本身
cloned.pet = (Pet) this.pet.clone(); // 2. 再手动拷贝Pet对象!
return cloned;
}
}
// Pet类也必须实现Cloneable接口和clone方法
class Pet implements Cloneable {
public String name;
Pet(String name) { this.name = name; }
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
现在进行深拷贝:
Person original = new Person();
original.age = 20;
original.pet = new Pet("Tom");
Person deepCopy = (Person) original.clone(); // 深拷贝
内存发生了什么? 🤔
如图所示,deepCopy 不仅自己是一个新对象,连它内部的 pet 也是一个全新的、独立的对象。只是初始状态和原对象一样。
现在你再修改: deepCopy.pet.name = "Jerry"; original.pet.name 依然还是 "Tom",丝毫未变!✅ 因为它们是完全独立的两个宠物。
🎯 总结与对比
特性 浅拷贝 (Shallow Copy) 深拷贝 (Deep Copy)
比喻 配一把新钥匙 🔑 -> 同一个房间 建一个新房间 🏗️ -> 全套新家具
复制内容 基本类型值,引用类型地址 整个对象树,包括所有子对象
独立性 原对象和拷贝对象共享引用对象 原对象和拷贝对象完全独立
性能 速度快,开销小 ⚡ 速度慢,开销大 🐢
实现难度 简单(通常用 Object.clone()) 复杂(需要递归实现 Cloneable)
💡 如何选择?
• 用浅拷贝:当对象只包含基本类型,或者你希望、允许原对象和拷贝对象共享内部对象时。
• 用深拷贝:当对象包含引用类型,并且你不希望原对象和拷贝对象的修改互相影响时。这在需要完全隔离的场景下至关重要。
其他实现深拷贝的方法(更推荐): 除了手动实现 clone(),你还可以通过: • 序列化再反序列化 🧾
• 使用第三方库(如 Apache Commons Lang 的 SerializationUtils)
这些方法可以避免繁琐的递归克隆,但前提是涉及的对象都必须可序列化。