在面试中回答时,建议按照 “最常用 → 较常用 → 高级/偏门” 的顺序由浅入深地介绍。
Java 创建对象的 5 种方式
1. 使用 new 关键字 (最常用)
- 描述: 这是最常见、最标准的创建对象方式。
- 原理: JVM 会为对象分配内存,并调用构造函数进行初始化。
- 示例:
User user = new User();
2. 使用反射机制 (框架常用)
反射有两种具体方式:
- A. 使用
Class类的newInstance()方法:- 示例:
User user = (User) Class.forName("com.example.User").newInstance(); - 限制: 只能调用无参构造函数,且该构造函数必须是
public的。如果类没有无参构造或构造函数私有,会抛异常。
- 示例:
- B. 使用
Constructor类的newInstance()方法:- 示例:
Constructor<User> constructor = User.class.getConstructor(); User user = constructor.newInstance(); - 优势: 更强大。它可以调用有参构造函数,甚至可以通过
setAccessible(true)暴力破解私有构造函数。
- 示例:
3. 使用 clone() 方法 (复制对象)
- 描述: 不通过构造函数,而是直接从内存中复制一个现有的对象。
- 条件: 目标类必须实现
Cloneable接口并重写Object.clone()方法。 - 特点: 这是一个浅拷贝(除非自己手动实现深拷贝逻辑),且不会调用构造函数。
- 示例:
User copyUser = (User) originalUser.clone();
4. 使用序列化与反序列化 (深拷贝常用)
- 描述: 将对象转为字节流,再还原为新对象。
- 条件: 类必须实现
Serializable接口。 - 特点: 不会调用构造函数。这是实现深拷贝的标准手段之一。
- 示例: 通过
ObjectInputStream的readObject()方法。
5. 使用第三方库 (如 Unsafe) (极少直接使用)
面试加分总结表
| 方式 | 是否调用构造函数? | 核心特点 | 适用场景 |
|---|---|---|---|
| new 关键字 | 是 | 最简单、最标准 | 日常开发 99% 的场景 |
| 反射 (Reflection) | 是 | 动态性强 | Spring 等框架底层、工厂模式 |
| clone() | 否 | 内存级复制 (浅拷贝) | 快速复制对象副本 |
| 反序列化 | 否 | 深拷贝 | 网络传输、缓存、深拷贝 |
| Unsafe | 否 | 底层、危险 | 框架底层黑科技 |
2. New出的对象什么时候回收?
这是一个考察 JVM 垃圾回收(GC)机制 的核心问题。
面试回答逻辑
一句话直接回答: 通过 new 创建的对象,由 Java 虚拟机(JVM)的垃圾回收器自动管理。简单来说,当一个对象没有任何引用指向它(即“不可达”)时,它就有可能被回收。
1. 怎么判断对象“该死了”?(核心算法)
JVM 主要通过两种算法来判断对象是否存活:
- A. 引用计数法 (Reference Counting) [了解即可]
- 原理: 给对象添加一个引用计数器,每有一个地方引用它,计数器+1;引用失效,计数器-1。当计数器为 0 时,对象可回收。
- 致命缺陷: 无法解决 “循环引用” 问题(例如 A 引用 B,B 引用 A,但没其他人引用它俩,计数器都不为 0,导致无法回收)。
- 注: 现代主流 JVM(如 HotSpot)一般不使用这种方法。
- B. 可达性分析算法 (Reachability Analysis) [标准答案]
- 原理: 以一系列被称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索。
- 判定: 如果一个对象到 GC Roots 没有任何引用链相连(即不可达),则证明该对象是无用的,可以被回收。
2. 什么是 GC Roots?(加分点)
面试官通常会追问:“哪些对象可以作为 GC Roots?” 常见的有:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象(比如方法里定义的局部变量
User user = new User())。 - 方法区中类静态属性引用的对象 (
static变量)。 - 方法区中常量引用的对象 (
final常量)。 - 本地方法栈中 JNI(即 Native 方法)引用的对象。
3. 对象最后的“遗言”:finalize()
- 机制: 在对象被标记为不可达之后,如果该对象重写了
finalize()方法且未被执行过,JVM 会把它放入一个队列中。 - 自救: 对象可以在
finalize()中通过重新与引用链建立联系(比如把自己赋值给某个全局变量)来“复活”。 - 现状: 极不推荐使用。因为它的执行时间不确定,开销大,且容易导致性能问题。现代 Java 开发中通常忽略它的存在。
总结表格
| 判定算法 | 核心逻辑 | 缺点 | 现状 |
|---|---|---|---|
| 引用计数法 | 计数器为 0 即回收 | 无法解决循环引用 | 主流 JVM 不用 |
| 可达性分析 | 从 GC Roots 搜索不到即回收 | 实现稍繁琐 | 主流标准 |
3. 如何获取私有对象?
在面试中,这个问题通常是在考察你对 封装性(Encapsulation) 的理解以及对 反射机制(Reflection) 的掌握程度。
1. 常规方式:通过公共方法 (Getter)
这是符合面向对象设计原则的“正规军”做法。
- 原理: 类的设计者为了安全性,将属性设为
private,但同时提供public的访问器方法(如getXxx())。 - 适用场景: 正常的业务开发。
- 代码示例:
// 外部直接调用 public 方法获取内部 private 属性 String value = obj.getPrivateField();
2. 暴力方式:通过反射机制 (Reflection) —— 面试重点
这是绕过 Java 访问控制检查的“黑客”做法,也是面试官最想听到的答案。
- 原理: 利用 Java 的反射 API,在运行时动态获取类的元信息,并强制取消访问检查。
- 核心步骤(三步走):
- 获取 Class 对象:
obj.getClass() - 获取私有字段: 使用
getDeclaredField("fieldName")。- 注意: 不能用
getField(),因为它只能获取 public 字段。
- 注意: 不能用
- 暴力破解(关键点): 调用
field.setAccessible(true)。- 这一步关闭了 Java 的访问安全检查机制,允许你访问私有成员。
- 获取 Class 对象:
- 代码示例:
Java
Class<?> clazz = obj.getClass(); Field field = clazz.getDeclaredField("privateField"); // 1. 获取字段 field.setAccessible(true); // 2. 暴力破解:设为可访问 String value = (String) field.get(obj); // 3. 获取值
总结表格
| 方式 | 安全性 | 复杂度 | 是否破坏封装 | 评价 |
|---|---|---|---|---|
| Getter 方法 | 高 | 低 | 否 | 推荐的标准写法 |
| 反射机制 | 低 | 高 | 是 | 强大的非常规手段,主要用于框架底层 |