第一章:面向对象基础
1.1 什么是面向对象?面向对象的三大特性是什么?
核心答案:
面向对象编程(OOP)是一种以"对象"为核心的编程范式,将现实世界的事物抽象为程序中的对象,对象包含数据(属性)和操作数据的方法。其三大核心特性是:封装、继承、多态。
详细解析:
1.1.1 面向对象编程概念解析
- 对象:是系统中描述客观事物的基本单位,是属性和方法的封装体。例如,"学生"对象拥有学号、姓名等属性,拥有选课、考试等方法。
- 类:是创建对象的"蓝图"或"模板",定义了同类对象共有的属性和方法。对象是类的具体实例。
- 核心思想:通过对现实世界的抽象,提高代码的可重用性、可维护性和可扩展性,更符合人类的思维模式。
1.1.2 面向对象与面向过程的对比
- 面向过程:以"函数"为中心,关注解决问题的步骤,数据与操作分离。代表语言:C。
- 面向对象:以"对象"为中心,关注参与解决问题的实体及其相互关系,数据与操作绑定在一起。代表语言:Java, C++。
- 类比:建造房子。面向过程关注"打地基、砌墙、封顶"等步骤;面向对象关注"设计师、工人、材料商"等角色(对象)以及他们如何协作。
1.1.3 封装的具体含义和实现方式
-
含义:隐藏对象的内部实现细节,仅对外提供公共的访问接口。核心是"高内聚,低耦合"。
-
实现:
- 使用
private等访问修饰符将属性私有化。 - 提供公共的
getter和setter方法来访问和修改属性。 - 对属性的合法性校验可以在
setter方法中进行。
- 使用
-
好处:保证数据的安全性,提高代码的健壮性,使类的内部修改不影响外部调用。
1.1.4 继承的具体含义和实现方式
-
含义:子类(派生类)可以继承父类(基类)的非私有属性和方法,并可以添加自己的新特性。核心是"is-a"关系。
-
实现 :在Java中使用
extends关键字。java
scalaclass Animal { // 父类 void eat() { System.out.println("eating..."); } } class Dog extends Animal { // 子类 void bark() { System.out.println("barking..."); } } // Dog 对象可以调用 eat() 和 bark() -
好处 :实现代码复用,建立类之间的层次体系。Java是单继承,一个类只能有一个直接父类。
1.1.5 多态的具体含义和实现方式
-
含义 :同一操作作用于不同的对象,可以产生不同的执行结果。主要包括编译时多态(静态绑定) 和运行时多态(动态绑定) 。
-
实现:
-
方法重载(Overload) :编译时多态。在同一类中,方法名相同,参数列表不同。
-
方法重写(Override) :运行时多态。在继承体系中,子类重写父类的方法。
-
父类引用指向子类对象:这是实现运行时多态的关键。
java
scssAnimal myAnimal = new Dog(); // 向上转型 myAnimal.eat(); // 如果Dog重写了eat(),则调用Dog的eat()方法
-
-
好处 :提高程序的扩展性和灵活性。例如,新增一个
Cat类,无需修改原有处理Animal的代码。
1.1.6 面向对象的优势和应用场景
-
优势:
- 易维护:结构清晰,封装使代码更易理解和修改。
- 易扩展:继承和多态使得系统能够以较低成本适应新需求。
- 易复用:通过类和对象的抽象,功能模块可以被多次使用。
- 适合大型复杂系统:能更好地进行模块化设计和团队协作。
-
应用场景:几乎所有现代软件系统,包括Web应用、桌面软件、移动应用、游戏开发、企业级后端服务等。
1.2 抽象类和接口的区别
核心答案:
抽象类是对类的抽象,定义了一类对象的共同特征(包括属性和方法);接口是对行为的抽象,定义了一组对象必须遵守的契约(只有方法声明)。在Java 8之后,两者都可以包含有实现的方法,但设计初衷和侧重点不同。
详细解析:
1.2.1 抽象类的定义和特性
-
定义 :用
abstract修饰的类。它不能被实例化。 -
特性:
- 可以包含抽象方法(
abstract修饰,无方法体)和具体实现的方法。 - 可以定义成员变量(非常量)。
- 可以有构造方法(供子类初始化时调用)。
- 子类必须实现其所有抽象方法,除非子类也是抽象类。
- 可以包含抽象方法(
-
目的 :作为一些相关类的模板,提供共有的属性和部分实现,要求子类完成特定部分。
1.2.2 接口的定义和特性
-
定义 :在Java 7及以前,接口是纯粹的契约,所有方法都是
public abstract(可省略),所有变量都是public static final(常量)。 -
特性(Java 8+) :
- 抽象方法:核心,定义行为规范。
- 默认方法(Default Methods) :使用
default关键字,提供默认实现。目的是在不破坏现有实现的情况下扩展接口功能。 - 静态方法(Static Methods) :使用
static关键字,属于接口自身,可通过接口名直接调用。 - 私有方法(Java 9+) :使用
private关键字,服务于接口内部的默认方法或静态方法,用于提取公共代码。
1.2.3 抽象类与接口的详细区别对比
| 特性 | 抽象类 | 接口 (Java 8+) |
|---|---|---|
| 设计目的 | 表示"是什么"(is-a),是类的抽象。 | 表示"能做什么"(has-a/can-do),是行为的抽象。 |
| 成员变量 | 无限制,可以是各种类型。 | 默认且只能是 public static final 常量。 |
| 构造方法 | 可以有。 | 不能有。 |
| 方法实现 | 可以有抽象方法和具体方法。 | 可以有抽象方法、默认方法、静态方法、私有方法。 |
| 继承 | 单继承,一个类只能继承一个抽象类。 | 多实现,一个类可以实现多个接口。 |
| 访问修饰符 | 方法可以用任意访问修饰符。 | 抽象方法默认 public,不能是 private(Java 9私有方法除外)。 |
1.2.4 抽象类的适用场景
- 多个类具有高度相似的代码逻辑和属性,需要提取公共部分以避免重复。
- 需要定义子类的模板 ,且部分步骤是固定的,部分需要子类具体实现(模板方法模式的典型应用)。
- 需要控制子类的类型,构建清晰的类层次结构。
1.2.5 接口的适用场景
- 定义一套行为规范或能力 ,不关心具体实现者是谁(如
Comparable,Serializable)。 - 希望一个类具备多种不同的能力(通过实现多个接口),突破单继承限制。
- 作为系统与外部模块之间的契约,实现解耦(如DAO层接口、Service层接口)。
- 用于函数式编程(只有一个抽象方法的函数式接口)。
1.2.6 JDK 8+中接口的新特性
- 默认方法:允许接口提供方法默认实现,解决了接口升级时,所有实现类都必须实现新方法的问题。如果实现类不覆盖,则使用默认实现。
- 静态方法:提供了与接口相关的工具方法,可以直接通过接口调用,无需通过实现类实例。
- 私有方法:允许在接口内部封装公共代码,提高默认方法和静态方法的代码复用性,同时对外隐藏实现细节。
1.3 final、static和synchronized的修饰符
1.3.1 final修饰符的用法和原理
-
修饰类 :该类不能被继承 。例如
String、Integer等核心类都是final类,保证了不可变性和安全性。 -
修饰方法 :该方法不能被重写。用于锁定方法行为,防止子类修改其核心逻辑。
-
修饰变量:
- 基本类型变量:值一旦初始化就不能更改。
- 引用类型变量:引用指向的地址不能更改,但对象内部的状态可以改变(除非对象本身也是不可变的)。
- 原理 :
final变量的写入发生在构造方法完成之前,且禁止指令重排序,保证了其他线程能正确看到初始化后的值,是实现线程安全不可变对象的关键。
1.3.2 static修饰符的用法和原理
-
修饰变量(静态变量/类变量) :
- 属于类,在类加载的"准备"阶段分配内存并设置默认值,在"初始化"阶段赋值。
- 所有实例共享同一份内存。
ClassName.variableName访问。
-
修饰方法(静态方法) :
- 属于类,可通过类名直接调用。内部不能使用
this或super,只能直接访问静态成员。 - 通常用于工具方法或工厂方法。
- 属于类,可通过类名直接调用。内部不能使用
-
修饰代码块(静态代码块) :
- 在类加载时执行一次,用于初始化静态变量。
-
原理 :
static成员与类本身相关联,而非与任何单个对象实例相关联,生命周期贯穿整个程序运行期。
1.3.3 synchronized修饰符的用法和原理
-
用法:
- 修饰实例方法 :锁定当前对象实例(
this)。 - 修饰静态方法 :锁定当前类的
Class对象。 - 修饰代码块 :需要显式指定锁对象(
synchronized(obj) {...})。
- 修饰实例方法 :锁定当前对象实例(
-
原理 :基于JVM内置的
monitorenter和monitorexit指令实现。每个Java对象都有一个关联的监视器锁(Monitor) 。线程进入同步块前需获得锁,退出时释放锁。它保证了原子性 和可见性(遵循 happens-before 原则)。
1.3.4 三个修饰符的区别和联系
-
关注点不同:
final关注不变性(变量值、方法行为、类结构)。static关注归属与生命周期(属于类,与实例无关)。synchronized关注线程安全(并发访问的同步)。
-
联系:
- 一个
static final的变量是全局常量 (如Math.PI)。 synchronized static方法用于保护类的静态资源,锁是类对象,与实例锁不同。
- 一个
1.3.5 静态代码块和实例代码块的执行顺序
java
csharp
public class Test {
static { System.out.println("静态代码块 - 1"); }
{ System.out.println("实例代码块 - 每次创建对象都执行"); }
static { System.out.println("静态代码块 - 2"); }
public static void main(String[] args) {
new Test();
new Test();
}
}
// 输出:
// 静态代码块 - 1
// 静态代码块 - 2
// 实例代码块 - 每次创建对象都执行
// 实例代码块 - 每次创建对象都执行
- 静态代码块 :在类加载时 执行,只执行一次,按代码顺序执行。
- 实例代码块 :在每次创建对象时 执行,在构造方法中的代码之前执行。
1.4 重载(Overload)和重写(Override)的区别
核心答案:
重载发生在同一个类中 ,是编译时多态;重写发生在父子类之间,是运行时多态。它们是完全不同的概念。
详细解析:
1.4.1 重载的定义和实现规则
-
定义 :在同一个类中,允许存在多个方法名相同 ,但参数列表不同的方法。
-
规则:
- 参数列表必须不同(类型、个数、顺序至少一项不同)。
- 返回类型、访问修饰符、抛出异常可以不同,但不能仅凭这些不同来重载。
- 发生在编译期,编译器根据方法签名确定调用哪个方法。
java
javascriptvoid show(int a) {} void show(String s) {} // 参数类型不同 -> 重载 void show(int a, String s) {} // 参数个数不同 -> 重载 // int show(int a) { return a; } // 错误!仅返回类型不同,不是重载
1.4.2 重写的定义和实现规则
-
定义:子类重新定义父类中已有的方法,以实现自己的特定行为。
-
规则(两同两小一大) :
- 方法名和参数列表完全相同。
- 子类返回值类型 应小于等于父类(基本类型必须相同;引用类型可以是父类返回类型的子类,即协变返回类型)。
- 子类抛出的异常应小于等于父类(不能抛出更宽泛的检查异常)。
- 子类访问权限 应大于等于父类(不能缩小访问范围,如父类是
protected,子类不能是private或default)。
1.4.3 重载与重写的详细区别对比
| 对比维度 | 重载 (Overload) | 重写 (Override) |
|---|---|---|
| 发生位置 | 同一个类中 | 继承关系的父子类之间 |
| 方法签名 | 必须不同 | 必须相同 |
| 返回类型 | 可以不同 | 子类方法 <= 父类方法(协变) |
| 访问修饰符 | 可以不同 | 子类方法 >= 父类方法 |
| 抛出异常 | 可以不同 | 子类方法 <= 父类方法 |
| 绑定阶段 | 编译时(静态绑定/早期绑定) | 运行时(动态绑定/晚期绑定) |
| 设计目的 | 提供处理不同数据的同名方法,提高可读性 | 实现多态,子类定制父类行为 |
1.4.4 @Override注解的作用
- 编译器检查 :告诉编译器此方法意图重写父类方法。如果签名与父类方法不匹配(如拼写错误、参数错误),编译器会报错。这是一个非常重要的安全网。
- 提高代码可读性:明确标识出这是一个重写的方法。
1.4.5 重写的访问权限限制
- 子类重写方法的访问权限不能低于父类被重写方法的访问权限。
- 例如:父类方法是
protected,子类重写时可以是protected或public,但不能是default或private。 - 原因:面向对象设计中的里氏替换原则。子类对象应该能够透明地替换父类对象。如果降低了访问权限,用子类对象替换父类对象时,就可能无法访问到本应可访问的方法,破坏了多态。
1.4.6 协变返回类型
-
从Java 5开始,子类重写方法时,可以返回父类方法返回类型的子类型。
java
scalaclass Animal { Animal get() { return new Animal(); } } class Dog extends Animal { @Override Dog get() { return new Dog(); } // 返回类型是Animal的子类Dog } -
这提供了更精确的返回类型,是类型安全的一种增强,常用于工厂方法模式。
1.5 静态方法和实例方法的区别
核心答案:
静态方法属于类,与实例无关;实例方法属于对象,操作特定实例的数据。
详细解析:
1.5.1 静态方法的特点和使用方式
- 归属:属于类本身,在类加载时即存在。
- 调用 :通过
类名.方法名()调用(也可以通过对象调用,但不推荐)。 - 内部访问 :只能直接访问 类的静态成员(变量、方法)。不能直接访问 实例成员(因为实例成员依附于对象,此时可能还没有对象被创建)。不能使用
this和super关键字。 - 生命周期:与类的生命周期相同。
- 典型场景 :工具类方法(如
Math.sqrt())、工厂方法、单例模式的获取实例方法。
1.5.2 实例方法的特点和使用方式
- 归属:属于类的某个具体实例(对象)。
- 调用 :必须通过
对象引用.方法名()调用。 - 内部访问 :可以直接访问类的静态成员和本实例的所有成员(包括私有成员)。
- 生命周期:与对象实例的生命周期相同,对象被回收后方法调用入口不再可用。
- 典型场景:实现对象的行为和业务逻辑,操作对象的属性。
1.5.3 静态方法与实例方法的详细区别
| 特性 | 静态方法 | 实例方法 |
|---|---|---|
| 归属 | 类 | 对象实例 |
| 调用依赖 | 无需创建对象 | 必须创建对象 |
| 访问权限 | 仅能直接访问静态成员 | 可访问静态和实例成员 |
| 关键字 | 不能使用 this, super |
可以使用 this, super |
| 内存 | 在方法区,只有一份 | 随对象在堆中,每对象有独立引用 |
| 多态 | 不支持重写,无动态绑定 | 支持重写,有动态绑定 |
1.5.4 静态方法能否被重写
- 不能 。静态方法在编译期就通过类名确定了调用关系(静态绑定),与对象无关。子类可以定义与父类签名相同的静态方法 ,但这只是隐藏(Hide) 了父类方法,并非重写。
- 重要结论 :应该始终使用类名调用静态方法,避免通过对象引用调用,以消除歧义,明确表达调用的是静态方法。
1.5.5 方法调用的绑定机制
- 早期绑定(静态绑定) :在编译期 就能确定具体调用哪个方法。适用于:
private方法、final方法、static方法、构造方法、重载的方法。 - 晚期绑定(动态绑定) :在运行期 根据对象的实际类型来确定调用哪个方法。适用于:重写 的方法。这是多态的基础,JVM通过方法表(
vtable)来实现。
1.6 静态变量和实例变量的区别
核心答案:
静态变量是类的状态,所有实例共享;实例变量是对象的状态,每个实例独有。
详细解析:
1.6.1 静态变量的特点和使用场景
-
特点:
- 在类加载过程中准备和初始化,内存位于方法区(JDK 8后元空间)。
- 只有一份拷贝,被所有实例共享。
- 生命周期与类相同,从类加载开始,到程序结束。
-
使用场景:
- 需要被所有对象共享的常量或配置(如
public static final PI)。 - 记录类的全局状态(如在线人数计数器)。
- 作为缓存(如单例对象的引用)。
- 需要被所有对象共享的常量或配置(如
1.6.2 实例变量的特点和使用场景
-
特点:
- 在对象实例化时 (
new)在堆内存中分配空间并初始化。 - 每个对象都有自己独立的一份拷贝,互不影响。
- 生命周期与对象实例相同,对象被垃圾回收时释放。
- 在对象实例化时 (
-
使用场景:
- 描述对象个体特征的属性(如学生的姓名、年龄)。
- 对象的核心状态数据。
1.6.3 静态变量与实例变量的详细区别
| 特性 | 静态变量(类变量) | 实例变量(成员变量) |
|---|---|---|
| 内存分配时机 | 类加载时 | 创建对象时 |
| 内存位置 | 方法区 | 堆内存 |
| 拷贝数量 | 仅一份,全类共享 | 每对象一份,独立 |
| 访问方式 | 类名.变量名 (推荐) |
对象引用.变量名 |
| 生命周期 | 从类加载到程序结束 | 从对象创建到被GC回收 |
| 默认值 | 有默认值(同实例变量) | 有默认值(int为0,引用为null等) |
1.6.4 静态变量的线程安全性问题
-
问题 :静态变量被所有线程共享。如果多个线程同时对其进行非原子性的读写操作 ,会导致数据不一致。
-
示例 :
static int counter = 0;多个线程同时执行counter++(非原子操作:读-改-写)会导致计数错误。 -
解决方案:
- 使用
synchronized关键字修饰相关方法或代码块。 - 使用
java.util.concurrent.atomic包下的原子类(如AtomicInteger)。 - 如果变量是不可变对象 (如
String,BigDecimal),则天然线程安全。
- 使用
1.6.5 静态变量的生命周期
- 加载:当JVM首次主动使用某个类时(如创建实例、访问静态成员、调用静态方法等),类加载器将其字节码加载到内存。
- 连接-准备 :为静态变量在方法区分配内存,并设置默认初始值 (如
int为0,引用为null)。 - 连接-初始化 :执行类的
<clinit>()方法(由编译器自动收集类中所有静态变量的赋值动作和静态代码块 合并生成),为静态变量赋予程序中定义的初始值。 - 使用:程序运行期间,类一直存在。
- 卸载 :当类的
ClassLoader被回收,且该类的Class对象不再被引用时,类可能被卸载,静态变量占用的内存随之释放。在应用生命周期中,类的卸载不常发生。
1.7 final、finally、finalize的区别
核心答案:
三者毫无关联,只是拼写相似。
final是关键字,用于修饰符。finally是代码块,用于异常处理。finalize()是方法 ,属于Object类,用于垃圾回收。
详细解析:
1.7.1 final修饰符的3种用法
-
修饰类 :类不可继承。如
String。 -
修饰方法:方法不可被重写。
-
修饰变量:
- 基本类型:值不可变。
- 引用类型:引用地址不可变(但对象内容可变)。
- 方法参数:在方法内部不能修改参数引用(对于基本类型则是值)。
1.7.2 finally代码块的执行机制
- 作用 :与
try-catch配合使用,用于存放无论是否发生异常都必须执行的代码,如释放资源(关闭文件、数据库连接、网络连接等)。 - 执行时机 :在
try或catch中的return、break、continue语句之前 执行,或者在未捕获的异常抛出之前执行。 - 唯一不执行的情况 :在
try或catch中执行了System.exit(int)(强制退出JVM),或者守护线程因所有非守护线程结束而终止。
1.7.3 finalize方法的作用和废弃原因
-
原始设计目的:对象被垃圾回收器回收之前,允许它进行最后的资源清理工作(如关闭打开的文件描述符)。该方法由垃圾回收线程调用。
-
废弃原因:
- 执行时机不确定 :JVM不保证何时甚至是否调用
finalize()。 - 性能开销大 :启用
finalize()会显著增加垃圾回收的负担。 - 可能导致问题 :在
finalize()中"复活"对象(将this赋值给某个引用)会使GC过程复杂化。 - 异常吞没 :
finalize()中抛出的异常会被忽略,且不会终止线程,难以调试。
- 执行时机不确定 :JVM不保证何时甚至是否调用
-
替代方案 :使用
try-with-resources(Java 7+)或显式地在finally块中调用close()方法来管理资源。
1.7.4 三者区别总结表
| 特性 | final | finally | finalize |
|---|---|---|---|
| 性质 | 修饰符(关键字) | 异常处理代码块关键字 | Object类的一个方法名 |
| 作用 | 定义不可变性 | 确保清理代码一定执行 | 对象被回收前的"遗言" |
| 关联 | 类/方法/变量 | try-catch | 垃圾回收 |
| 当前状态 | 广泛使用,核心特性 | 广泛使用,但部分被 try-with-resources 替代 | 已废弃(Java 9标记,未来版本移除) |
1.7.5 try-with-resources替代finally
-
语法 :在
try后跟一对圆括号,括号内声明并初始化一个或多个实现了java.lang.AutoCloseable接口的资源。资源会在try块结束后自动关闭,无论是否发生异常。java
java// 传统方式 BufferedReader br = null; try { br = new BufferedReader(new FileReader("file.txt")); // ... } finally { if (br != null) br.close(); // 手动关闭,代码冗长 } // try-with-resources (Java 7+) try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) { // ... } // 自动关闭,简洁安全 -
优势:
- 代码简洁 :无需显式编写
finally块。 - 安全性高:自动管理多个资源的关闭,关闭顺序与声明顺序相反。
- 异常处理友好 :如果
try块和资源关闭都抛出异常,try块的异常会被抛出,关闭引发的异常会被抑制 (可通过Throwable.getSuppressed()获取),避免了异常信息丢失。
- 代码简洁 :无需显式编写
1.8 访问修饰符public、private、protected和默认的区别
核心答案:
访问修饰符用于控制类、方法、变量的可见性范围,从宽到窄依次为:public > protected > 默认(包访问) > private。
详细解析:
1.8.1 四种访问修饰符的作用范围
-
public:公共的。在任何地方都可以访问。 -
protected:受保护的。允许本类、同包中的类、其他包中的子类访问。- 注意:其他包中的子类通过子类对象或子类引用 可以访问父类的
protected成员,但不能通过父类引用访问。
- 注意:其他包中的子类通过子类对象或子类引用 可以访问父类的
-
默认(无修饰符) :包私有。只允许本类、同包中的类访问。这是最常见的用于隐藏内部实现的方式。 -
private:私有的。只允许本类内部访问。是封装性的最直接体现。
1.8.2 四种修饰符的访问权限对比表
| 修饰符 | 本类内部 | 同包子类/类 | 不同包子类 | 不同包非子类 |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
protected |
✅ | ✅ | ✅ (通过继承) | ❌ |
默认 |
✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
1.8.3 实际开发中的使用建议
- 最小化访问原则 :优先使用最严格的访问级别。如果不确定,先从
private开始,再根据需要放宽。 public:用于对外提供的稳定API,如工具类的方法、框架的接口、常量。protected:用于设计给子类扩展的钩子方法(hook method)或受保护的字段,在框架设计中常见。默认:用于包内协作的类、方法或变量。这是隐藏实现细节、降低模块间耦合的有效手段。private:用于类的内部实现细节,通过public的getter/setter方法间接暴露。
1.8.4 包访问权限的细节
- 如果类A和类B在同一个包下,即使它们没有继承关系,B也可以访问A的默认访问权限的成员。
- 这是Java组织代码、实现模块化的一种简单方式。将功能相关的类放在同一个包内,它们可以方便地共享一些内部实现,同时对外部包隐藏。
1.8.5 访问控制与继承的关系
-
子类继承 父类的所有非私有成员(无论访问修饰符是什么),但不一定能直接访问。
-
子类内部能否直接使用父类的成员,取决于该成员的访问修饰符:
public/protected:子类可以直接访问。默认:只有同包子类可以直接访问。private:子类不能直接访问,只能通过父类提供的公共或受保护的方法间接访问。
-
子类重写父类方法时,不能降低方法的访问权限(见1.4.5),这是面向对象设计的重要规则。
1.9 构造函数和初始化顺序
1.9.1 构造函数的定义和使用
- 定义 :一种特殊的成员方法,名称必须与类名完全相同 ,没有返回类型(连
void都没有)。用于在创建对象(new)时初始化对象的状态。 - 使用 :通过
new关键字调用。如果没有显式定义,编译器会提供一个无参的默认构造函数。一旦定义了任何构造函数,默认构造函数就不再自动提供。
1.9.2 默认构造函数和自定义构造函数
-
默认构造函数 :
public ClassName() {}。由编译器自动生成。 -
自定义构造函数:根据需求定义带参数的构造函数,以便用不同的初始值创建对象。
java
arduinopublic class Person { private String name; private int age; // 自定义构造函数 public Person(String name, int age) { this.name = name; this.age = age; } }
1.9.3 构造函数重载
-
一个类可以有多个构造函数,它们参数列表不同,这就是构造函数的重载。
-
可以使用
this(...)在一个构造函数中调用另一个构造函数,但必须位于第一行。java
csharppublic Person() { this("Unknown", 0); // 调用双参构造 } public Person(String name) { this(name, 0); }
1.9.4 初始化代码块的执行顺序
在类中,初始化动作按以下顺序执行:
- 静态成员初始化 和静态代码块 :按在类中出现的顺序 执行。只执行一次。
- 实例成员初始化 和实例代码块 :按在类中出现的顺序 执行。每次创建对象都执行。
- 构造函数:最后执行。
1.9.5 静态代码块和静态变量初始化
-
两者平级,按代码的书写顺序初始化。
java
inistatic int a = 1; // (1) static { a = 2; } // (2) 最终a=2 static { b = 3; } // (3) static int b; // (4) 声明可以放在后面,但准备阶段已分配内存 // 顺序: (1)准备a=0 -> (1)初始化a=1 -> (2)执行a=2 -> (3)执行b=3 -> (4)声明b
1.9.6 父子类初始化顺序详解
这是非常重要的面试考点。
java
scala
class Parent {
static { System.out.println("Parent静态代码块"); }
{ System.out.println("Parent实例代码块"); }
Parent() { System.out.println("Parent构造方法"); }
}
class Child extends Parent {
static { System.out.println("Child静态代码块"); }
{ System.out.println("Child实例代码块"); }
Child() { System.out.println("Child构造方法"); }
}
// new Child(); 的输出顺序:
// 1. Parent静态代码块
// 2. Child静态代码块
// 3. Parent实例代码块
// 4. Parent构造方法
// 5. Child实例代码块
// 6. Child构造方法
黄金法则:
- 先静态,后实例,最后构造。
- 先父类,后子类。
- 静态成员和代码块在类加载时 执行,只一次。
- 实例成员、代码块和构造方法在每次
new时执行。
1.9.7 初始化顺序面试题解析
-
核心:牢记上述"黄金法则"。
-
陷阱:如果父类构造方法中调用了可被重写的方法,而子类重写了该方法,那么在子类对象初始化时,会调用到子类重写的方法,而此时子类的实例初始化可能还未完成,可能导致访问到未初始化的变量(默认值)。
java
scalaclass Base { Base() { print(); } void print() { System.out.println("Base"); } } class Derived extends Base { int value = 10; @Override void print() { System.out.println("Derived: " + value); } } // new Derived(); 输出: Derived: 0 (因为value还未被初始化为10)- 应避免在构造方法中调用可重写的方法。
1.10 this和super关键字
1.10.1 this关键字的四种用法
-
指代当前对象:在实例方法或构造方法中,引用当前正在操作的对象。
java
typescriptpublic void setName(String name) { this.name = name; // 区分同名的局部变量和实例变量 } -
调用本类的其他构造方法 :
this(...),必须是构造方法中的第一条语句。java
scsspublic Person(String name) { this(name, 0); // 调用另一个构造方法 } -
作为参数传递:将当前对象作为参数传递给其他方法。
java
kotlinsomeMethod(this); -
作为返回值:从方法中返回当前对象,支持链式调用。
java
kotlinpublic Person setAge(int age) { this.age = age; return this; } // 使用: person.setName("Tom").setAge(25);
1.10.2 super关键字的三种用法
-
访问父类的成员 :当子类重写了父类的方法或隐藏了父类的字段时,使用
super.来明确调用父类的版本。java
typescript@Override void someMethod() { super.someMethod(); // 先执行父类逻辑 // ... 子类扩展逻辑 } -
调用父类的构造方法 :
super(...),必须是子类构造方法中的第一条语句 。如果子类构造方法没有显式调用super(...)或this(...),编译器会自动插入一个无参的super()。java
csharppublic Child() { super("parentArg"); // 调用父类有参构造 } -
在泛型中指代上界 :
<T extends SuperClass>,表示类型参数T必须是SuperClass或其子类。
1.10.3 this()和super()调用构造方法
this()和super()都必须放在构造方法的第一行 ,因此它们不能同时出现在一个构造方法中。- 设计逻辑:初始化时必须先完成父类的初始化(通过
super),或者先完成本类其他构造方法的初始化(通过this),最终都会追溯到父类构造方法。
1.10.4 this和super在继承中的实际应用
- 主要用于解决命名冲突 和实现构造方法链。
- 典型模式 :在子类构造方法中,用
super初始化父类部分,用this初始化本类特有部分,或者在多个构造方法间复用代码。
1.10.5 this和super的使用限制
static上下文中不能使用 :因为this和super都指向对象实例,而静态成员属于类,不依赖于任何实例。- 调用
this()或super()必须是构造方法的第一条语句。
第二章:Java核心类
2.1 String、StringBuffer、StringBuilder的区别
2.1.1 String的特性、源码分析和使用场景
核心答案: String是不可变的字符序列,基于final char[]实现,设计为不可变类。
详细解析:
- 不可变性实现:
java
arduino
public final class String implements Serializable, Comparable<String>, CharSequence {
private final char value[]; // JDK8及以前
private final byte value[]; // JDK9及以后(为节省内存)
private int hash; // 缓存的hashCode
}
final class:禁止继承,防止子类破坏不可变性final value[]:字符数组引用不可变(但数组内容理论上可通过反射修改)- 所有修改字符串的方法都返回新String对象
-
使用场景:
- 字符串常量、配置信息
- HashMap的键(依赖不可变性保证hashCode稳定)
- 类加载器中的类名、方法名
- 网络传输、文件路径等需要安全性的场景
2.1.2 StringBuffer的特性、源码分析和使用场景
特性: 线程安全的可变字符序列,方法使用synchronized修饰。
java
scala
public final class StringBuffer extends AbstractStringBuilder
implements Serializable, CharSequence {
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
}
实现原理:
- 继承自AbstractStringBuilder,内部使用char[] value(非final)
- 初始容量16字符,自动扩容(新容量 = 原容量×2 + 2)
- 所有公开方法都添加synchronized关键字
使用场景: 多线程环境下的字符串拼接操作。
2.1.3 StringBuilder的特性、源码分析和使用场景
特性: 非线程安全的可变字符序列,性能最优。
java
scala
public final class StringBuilder extends AbstractStringBuilder
implements Serializable, CharSequence {
@Override
public StringBuilder append(String str) {
super.append(str); // 无同步开销
return this;
}
}
与StringBuffer的区别:
- 方法没有synchronized修饰
- 单线程下性能比StringBuffer高10%-15%
- 不是线程安全的
使用场景: 单线程环境下的字符串拼接。
2.1.4 三者性能对比测试
java
ini
public class PerformanceTest {
public static void main(String[] args) {
int iterations = 100000;
// String拼接 - 最慢
long start = System.currentTimeMillis();
String s = "";
for (int i = 0; i < iterations; i++) {
s += "a"; // 每次循环创建新String对象
}
System.out.println("String耗时: " + (System.currentTimeMillis() - start) + "ms");
// StringBuilder - 最快
start = System.currentTimeMillis();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < iterations; i++) {
sb.append("a");
}
System.out.println("StringBuilder耗时: " + (System.currentTimeMillis() - start) + "ms");
// StringBuffer - 稍慢于StringBuilder
start = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int i = 0; i < iterations; i++) {
sbf.append("a");
}
System.out.println("StringBuffer耗时: " + (System.currentTimeMillis() - start) + "ms");
}
}
性能排序: StringBuilder > StringBuffer > String
2.1.5 三者线程安全性深度分析
String: 天然线程安全(不可变对象)
StringBuffer: 通过synchronized实现线程安全,但在复合操作下仍需外部同步:
java
arduino
// 即使单个方法线程安全,复合操作也可能有问题
if (buffer.indexOf("test") != -1) {
buffer.append("found"); // 这两行不是原子操作
}
// 正确做法:外部加锁
synchronized(buffer) {
if (buffer.indexOf("test") != -1) {
buffer.append("found");
}
}
StringBuilder: 非线程安全,多线程操作会导致数据不一致。
2.1.6 字符串拼接的性能优化
- 编译期优化:
java
ini
String s = "a" + "b" + "c"; // 编译期优化为 "abc"
- 运行时优化:
java
ini
// 推荐
StringBuilder sb = new StringBuilder(initialCapacity); // 预分配容量
sb.append(str1).append(str2).append(str3);
String result = sb.toString();
// 不推荐 - 每次循环都创建StringBuilder
String result = "";
for (String str : list) {
result += str; // 等价于 result = new StringBuilder(result).append(str).toString()
}
2.2 String为什么要设计成不可变的?
2.2.1 String不可变的底层实现
实现机制:
- final类:防止被继承和修改行为
- final字符数组:存储数据的数组引用不可变
- 无修改方法:所有看似修改的方法都返回新对象
- 私有构造函数:控制对象创建
2.2.2 安全性考虑
- 作为HashMap键:如果String可变,修改后hashCode变化,会导致在HashMap中找不到
java
vbnet
Map<String, Object> map = new HashMap<>();
String key = "key";
map.put(key, "value");
key.toUpperCase(); // 如果String可变,hashCode改变
System.out.println(map.get("key")); // 可能返回null
- 网络传输安全:URL、参数等不会被意外修改
- 类加载安全:类名、方法名等标识符的安全
2.2.3 线程安全
- 不可变对象天然线程安全,无需同步
- 多线程共享时无竞态条件
2.2.4 性能优化(字符串常量池)
- 字符串驻留(String Interning) :
java
ini
String s1 = "hello";
String s2 = "hello"; // 指向常量池同一对象
String s3 = new String("hello"); // 创建新对象
String s4 = s3.intern(); // 返回常量池引用
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1 == s4); // true
- hashCode缓存:
java
ini
public int hashCode() {
int h = hash; // 默认0
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i]; // 计算公式
}
hash = h; // 计算后缓存
}
return h;
}
2.2.5 String在内存中的存储
JDK8及以前:
text
rust
栈 堆
引用1 ----------> String对象
|- hash: 缓存值
|- value: char[]引用 ----> char数组
JDK9及以后:
- 为减少内存,使用byte[]存储,配合编码标志位
- 拉丁字符用ISO-8859-1编码(1字节/字符)
- 非拉丁字符用UTF-16编码(2字节/字符)
2.2.6 不可变对象的模式
如何创建不可变类:
- 类声明为final
- 所有字段声明为private final
- 不提供setter方法
- 通过构造函数初始化所有字段
- 返回可变对象时返回深拷贝
2.3 String的intern()方法的作用和原理
2.3.1 intern()方法的定义和作用
定义: 返回字符串在常量池中的规范化表示。
java
arduino
public native String intern();
作用:
- 如果常量池中已存在相同字符串,返回常量池引用
- 如果不存在,将字符串添加到常量池并返回引用
- 保证相同内容的字符串在内存中只有一份
2.3.2 intern()方法在不同JDK版本中的实现差异
JDK6:
- 常量池在永久代
- 调用intern()时,如果常量池不存在,会复制字符串到永久代
- 可能导致永久代内存溢出
JDK7+:
- 常量池移到堆中
- 调用intern()时,如果常量池不存在,会将堆中字符串的引用记录到常量池
- 不会复制字符串,节省内存
示例:
java
go
// JDK6 vs JDK7+ 区别
String s1 = new StringBuilder("ja").append("va").toString();
System.out.println(s1.intern() == s1);
// JDK6: false(常量池中已有"java")
// JDK7+: true(常量池记录堆中引用)
String s2 = new StringBuilder("计算机").append("科学").toString();
System.out.println(s2.intern() == s2);
// JDK6: false(复制到常量池)
// JDK7+: true(记录引用)
2.3.3 intern()方法的使用场景
- 节省内存:大量重复字符串时
java
ini
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
list.add(("key" + (i % 1000)).intern()); // 只有1000个不同字符串
}
- 快速比较:
java
less
// 使用==代替equals,性能更高
if (str1.intern() == str2.intern()) {
// 内容相同
}
2.3.4 intern()方法的性能影响
优点:
- 减少内存占用
- 加快字符串比较速度
缺点:
- intern()本身有性能开销(哈希查找)
- 过度使用可能导致常量池过大
- 需谨慎使用,适合大量重复的长字符串
2.3.5 字符串常量池的位置变化
- JDK6:永久代(PermGen),大小固定,易OutOfMemoryError
- JDK7:移到堆中,可以像普通对象一样被GC
- JDK8+ :元空间(Metaspace),但字符串常量池仍在堆中
2.4 字符串常量池在JDK不同版本中的变化
2.4.1 JDK6字符串常量池(位于永久代)
特点:
- 大小固定,通过-XX:MaxPermSize设置
- 不会被垃圾回收
- 字符串通过字面量创建时,先在常量池查找
java
ini
String s1 = "abc"; // 在永久代创建
String s2 = new String("abc"); // 在堆创建,永久代也有"abc"
2.4.2 JDK7字符串常量池(移动到堆中)
变化原因:
- 永久代大小难以确定
- 字符串常量占用大量永久代空间
- 永久代GC效率低
新特性:
- 字符串常量池在堆中
- intern()方法不再复制字符串
- 常量池中的字符串可以被GC回收
2.4.3 JDK8字符串常量池(元空间)
元空间 vs 永久代:
| 特性 | 永久代 | 元空间 |
|---|---|---|
| 位置 | JVM内存 | 本地内存 |
| 大小 | 固定 | 可自动扩展 |
| GC | Full GC时回收 | 类卸载时回收 |
| OOM | PermGen OOM | Metaspace OOM |
注意: 字符串常量池仍在堆中,不在元空间
2.4.4 各版本性能对比
内存占用: JDK7+ < JDK6
GC效率: JDK7+ > JDK6
intern()性能: JDK7+ > JDK6(不复制字符串)
2.4.5 字符串常量池的调优参数
bash
ini
# JDK6
-XX:PermSize=64m -XX:MaxPermSize=256m
# JDK7+
-XX:StringTableSize=60013 # 设置常量池哈希表大小(质数)
-Xms512m -Xmx1024m # 堆大小影响常量池
# JDK8+
-XX:MetaspaceSize=64m -XX:MaxMetaspaceSize=256m
2.5 equals与==、hashCode的区别
2.5.1 ==操作符的比较规则
基本类型: 比较值是否相等
引用类型: 比较内存地址是否相同(是否同一个对象)
java
ini
int a = 10, b = 10;
System.out.println(a == b); // true,值比较
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2); // false,不同对象
2.5.2 equals方法的作用和实现
作用: 比较对象内容是否相等
Object默认实现:
java
typescript
public boolean equals(Object obj) {
return (this == obj); // 默认比较地址
}
String的equals实现:
java
ini
public boolean equals(Object anObject) {
if (this == anObject) return true;
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i]) return false;
i++;
}
return true;
}
}
return false;
}
2.5.3 hashCode方法的作用和约定
作用: 返回对象的哈希码,用于哈希表
约定(重要!):
- 同一对象多次调用hashCode()应返回相同值
- equals()相等的对象,hashCode()必须相等
- equals()不相等的对象,hashCode()尽量不等(减少哈希冲突)
String的hashCode:
java
css
// 计算公式:s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
// 31是质数,有较好的分布性,且31* i = (i << 5) - i(优化性能)
2.5.4 三者关系和区别
| 操作符/方法 | 作用 | 比较内容 | 重写要求 |
|---|---|---|---|
| == | 比较引用 | 内存地址 | 不能重写 |
| equals() | 比较对象 | 内容相等 | 可重写 |
| hashCode() | 哈希值 | 整数哈希码 | 可重写 |
2.5.5 equals和hashCode的契约关系
为什么重写equals必须重写hashCode?
java
ini
class Person {
String name;
int age;
@Override
public boolean equals(Object o) {
// 只重写equals,未重写hashCode
Person p = (Person) o;
return name.equals(p.name) && age == p.age;
}
}
// 问题示例
Person p1 = new Person("Tom", 20);
Person p2 = new Person("Tom", 20);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // false
Map<Person, String> map = new HashMap<>();
map.put(p1, "value1");
System.out.println(map.get(p2)); // 可能为null,因为hashCode不同
2.5.6 如何正确重写equals和hashCode
正确实现:
java
java
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age); // 使用工具类
}
// 或者手动实现
@Override
public int hashCode() {
int result = name != null ? name.hashCode() : 0;
result = 31 * result + age; // 31是质数,减少哈希冲突
return result;
}
最佳实践:
- 使用Objects.equals()比较字段
- 使用Objects.hash()生成hashCode
- 保持一致性:equals用到的字段,hashCode也要用到
- 重写toString()方法便于调试
2.6 Object类中有哪些方法?分别有什么作用?
2.6.1 getClass()方法
作用:返回对象的运行时类(Class对象)。
java
ini
public final native Class<?> getClass();
// 使用示例
Object obj = "Hello";
Class<?> clazz = obj.getClass(); // 返回java.lang.String的Class对象
System.out.println(clazz.getName()); // java.lang.String
关键特性:
final方法,不能被子类重写- 返回的Class对象是反射API的入口
- 同一类的所有实例调用getClass()返回相同的Class对象
2.6.2 hashCode()方法
作用:返回对象的哈希码值,支持哈希表数据结构。
java
csharp
public native int hashCode();
// 默认实现(通常基于内存地址)
public int hashCode() {
return System.identityHashCode(this);
}
哈希码合约:
- 一致性:同一对象多次调用应返回相同值
- 相等性:equals()为true的对象,hashCode()必须相等
- 不等性:equals()为false的对象,hashCode()尽量不相等(非必须)
2.6.3 equals(Object obj)方法
作用:比较两个对象是否相等。
java
typescript
public boolean equals(Object obj) {
return (this == obj);
}
equals合约:
- 自反性:x.equals(x)必须返回true
- 对称性:x.equals(y) == y.equals(x)
- 传递性:x.equals(y) && y.equals(z) ⇒ x.equals(z)
- 一致性:多次调用结果相同
- 非空性:x.equals(null)返回false
2.6.4 clone()方法
作用:创建并返回对象的副本。
java
java
protected native Object clone() throws CloneNotSupportedException;
使用要求:
- 类必须实现Cloneable接口(标记接口)
- 默认是浅拷贝
- 需要深拷贝需重写clone()方法
java
java
class DeepCopyExample implements Cloneable {
private int[] data;
@Override
public DeepCopyExample clone() {
try {
DeepCopyExample copy = (DeepCopyExample) super.clone();
copy.data = data.clone(); // 深拷贝数组
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
2.6.5 toString()方法
作用:返回对象的字符串表示。
java
typescript
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
// 重写示例
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
最佳实践:
- 总是重写toString()以便调试
- 包含所有重要字段
- 格式保持一致
2.6.6 notify()/notifyAll()方法
作用:线程间通信,用于对象监视器。
java
java
public final native void notify();
public final native void notifyAll();
区别:
notify():随机唤醒一个等待线程notifyAll():唤醒所有等待线程
java
csharp
synchronized (lock) {
lock.notify(); // 唤醒一个
// lock.notifyAll(); // 唤醒所有
}
2.6.7 wait()系列方法
作用:使当前线程等待,直到其他线程调用notify()或notifyAll()。
java
java
public final void wait() throws InterruptedException;
public final void wait(long timeout) throws InterruptedException;
public final native void wait(long timeout, int nanos) throws InterruptedException;
使用模式:
java
scss
synchronized (obj) {
while (!condition) {
obj.wait(); // 释放锁并等待
}
// 条件满足,执行操作
}
2.6.8 finalize()方法
作用:对象被垃圾回收前的清理钩子。
java
csharp
protected void finalize() throws Throwable;
废弃原因:
- 执行时机不确定
- 性能影响大
- 可能导致内存泄漏
- Java 9标记为废弃
替代方案:
- try-with-resources(AutoCloseable)
- 显式cleanup方法
2.6.9 Object类的设计哲学
核心原则:
- 通用性:所有Java对象的基类
- 扩展性:提供可重写的方法模板
- 线程安全基础:wait/notify机制
- 对象标识:hashCode和equals定义对象相等性
2.7 Java中String.length()的运作原理
2.7.1 String.length()的实现原理
JDK 8及以前实现:
java
arduino
public final class String {
private final char value[];
private int hash; // Default to 0
public int length() {
return value.length; // 直接返回char数组长度
}
}
JDK 9及以后实现:
java
arduino
public final class String {
private final byte[] value;
private final byte coder; // 0 = LATIN1, 1 = UTF16
private int hash;
public int length() {
return value.length >> coder; // 根据编码调整
// LATIN1: length = value.length / 1
// UTF16: length = value.length / 2
}
}
2.7.2 不同字符编码的影响
编码方案:
- LATIN1(ISO-8859-1):1字节/字符,覆盖西欧字符
- UTF-16:2字节/字符,支持所有Unicode字符
自动检测逻辑:
java
ini
static final byte LATIN1 = 0;
static final byte UTF16 = 1;
// 字符串创建时检测编码
String latin1 = "Hello"; // 使用LATIN1编码
String utf16 = "你好"; // 使用UTF-16编码
String mixed = "Hello 你好"; // 包含非LATIN1字符,使用UTF-16
2.7.3 性能考虑
内存优化:
java
arduino
// JDK 8: 10个英文字符占用内存
// char[10] = 10 * 2字节 = 20字节
// 对象头 + 引用等 ≈ 40-56字节
// JDK 9: 同样10个英文字符
// byte[10] = 10字节
// 对象头 + coder等 ≈ 32-48字节
// 内存节省约30-40%
性能影响:
- 纯英文文本:内存减少约50%,访问速度略有提升
- 混合文本:根据字符比例自动选择最优编码
2.7.4 中文字符的长度计算
示例代码:
java
ini
String str1 = "Hello"; // 5个字符
String str2 = "你好世界"; // 4个字符
String str3 = "Hello🌍"; // 6个字符(包含表情符号)
System.out.println(str1.length()); // 5
System.out.println(str2.length()); // 4
System.out.println(str3.length()); // 6(实际存储:Hello + 代理对)
代理对处理:
java
ini
// 表情符号🌍是Unicode补充字符,需要代理对表示
String emoji = "🌍";
System.out.println(emoji.length()); // 2(代码单元数)
System.out.println(emoji.codePointCount(0, emoji.length())); // 1(实际字符数)
// 正确遍历包含代理对的字符串
for (int i = 0; i < str3.length(); ) {
int codePoint = str3.codePointAt(i);
System.out.println(Character.getName(codePoint));
i += Character.charCount(codePoint);
}
2.7.5 Unicode码点和码元
概念区分:
-
码元(Code Unit) :编码方案的基本单位
- UTF-8:1字节码元
- UTF-16:2字节码元
- String.length()返回码元数
-
码点(Code Point) :Unicode字符的唯一编号
- 基本多文种平面(U+0000到U+FFFF):1个码点 = 1个码元
- 补充字符(U+10000到U+10FFFF):1个码点 = 2个码元(代理对)
相关API:
java
rust
String str = "Hello🌍世界";
// 获取码点数量
int codePointCount = str.codePointCount(0, str.length());
System.out.println("码点数量: " + codePointCount); // 8
// 获取指定位置的码点
int codePoint = str.codePointAt(5); // 返回🌍的码点
System.out.printf("U+%04X%n", codePoint); // U+1F30D
// 遍历所有码点
str.codePoints().forEach(cp ->
System.out.printf("U+%04X ", cp));
// 输出: U+0048 U+0065 U+006C U+006C U+006F U+1F30D U+4E16 U+754C
2.8 基本数据类型和包装类型:从原理到实战的完整指南
一、核心概念:为什么需要两种类型?
Java设计这两种类型是为了兼顾效率与功能:
- 基本类型(Primitive Types) :性能高,但功能有限
- 包装类型(Wrapper Classes) :功能丰富,但有一定开销
快速对比表
| 对比维度 | 基本类型 | 包装类型 |
|---|---|---|
| 类型 | 关键字(如int, char) | 类(如Integer, Character) |
| 存储位置 | 栈(局部变量时) | 堆 |
| 默认值 | 有(int为0,boolean为false) | null |
| 功能方法 | 无 | 丰富(如转换、比较、常量) |
| 集合支持 | 不能直接存入集合 | 可以存入集合 |
| 内存占用 | 较小(int:4字节) | 较大(Integer:16字节左右) |
二、8种基本数据类型及其包装类
| 基本类型 | 大小 | 取值范围 | 包装类 | 常用常量/方法 |
|---|---|---|---|---|
| byte | 1字节 | -128 ~ 127 | Byte | BYTES(字节数)、parseByte() |
| short | 2字节 | -32768 ~ 32767 | Short | SIZE(位数)、reverseBytes() |
| int | 4字节 | -2³¹ ~ 2³¹-1 | Integer | MAX_VALUE、MIN_VALUE、parseInt() |
| long | 8字节 | -2⁶³ ~ 2⁶³-1 | Long | BYTES、toHexString() |
| float | 4字节 | ±1.4E-45 ~ ±3.4E38 | Float | NaN、POSITIVE_INFINITY、isNaN() |
| double | 8字节 | ±4.9E-324 ~ ±1.7E308 | Double | MAX_EXPONENT、isInfinite() |
| char | 2字节 | 0 ~ 65535 | Character | isDigit()、isLetter()、toUpperCase() |
| boolean | 1位 | true/false | Boolean | TRUE、FALSE、logicalAnd() |
三、自动装箱与拆箱:编译器在帮你做什么?
什么是装箱(Boxing)和拆箱(Unboxing)?
- 装箱:基本类型 → 包装类型
- 拆箱:包装类型 → 基本类型
java
ini
// 手动装箱(Java 5之前)
Integer manualBoxed = Integer.valueOf(100);
int manualUnboxed = manualBoxed.intValue();
// 自动装箱拆箱(Java 5+)
Integer autoBoxed = 100; // 编译器实际生成:Integer.valueOf(100)
int autoUnboxed = autoBoxed; // 编译器实际生成:autoBoxed.intValue()
编译器转换的真相
查看字节码可以清楚地看到转换过程:
java
ini
// 源代码
public void example() {
Integer i = 100; // 自动装箱
int j = i; // 自动拆箱
Integer k = i + 1; // 先拆箱(i.intValue()),计算,再装箱(Integer.valueOf())
}
// 等效的手写代码
public void exampleEquivalent() {
Integer i = Integer.valueOf(100);
int j = i.intValue();
Integer k = Integer.valueOf(i.intValue() + 1);
}
四、包装类型的缓存机制:提升性能的巧妙设计
Integer缓存:面试中最常问的考点
java
arduino
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
各包装类的缓存范围
| 包装类 | 缓存范围 | 是否可配置 | 特别说明 |
|---|---|---|---|
| Byte | -128 ~ 127 | 否 | 全部缓存 |
| Short | -128 ~ 127 | 否 | 全部缓存 |
| Integer | -128 ~ 127 | 是(-XX:AutoBoxCacheMax) | 最常考 |
| Long | -128 ~ 127 | 否 | 全部缓存 |
| Character | 0 ~ 127 | 否 | ASCII范围 |
| Boolean | true, false | 否 | 两个值都缓存 |
| Float | 无缓存 | - | 浮点数不适合缓存 |
| Double | 无缓存 | - | 浮点数不适合缓存 |
缓存带来的陷阱:经典面试题
java
ini
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true,使用缓存中的同一对象
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false,超出缓存范围,创建新对象
// 正确比较方法
System.out.println(c.equals(d)); // true,比较值
System.out.println(c.intValue() == d.intValue()); // true
五、使用场景与最佳实践
何时用基本类型?
java
arduino
// 场景1:性能敏感的循环
int sum = 0; // ✅ 推荐:基本类型
for (int i = 0; i < 1000000; i++) {
sum += i;
}
// 场景2:局部临时计算
double calculateArea(double radius) {
return Math.PI * radius * radius; // 基本类型运算
}
何时用包装类型?
java
csharp
// 场景1:集合中的元素
List<Integer> scores = new ArrayList<>(); // ✅ 必须用包装类型
scores.add(95);
scores.add(87);
// 场景2:可能为null的返回值
public Integer findScore(String studentId) {
// 如果没找到,返回null是合理的
return scoresMap.get(studentId);
}
// 场景3:泛型类
public class Box<T> {
private T value; // T不能是基本类型
// ...
}
六、常见陷阱与解决方案
陷阱1:空指针异常(NullPointerException)
java
ini
Integer count = null;
// 危险:自动拆箱时会抛出NPE
// int total = count + 1; // NullPointerException!
// 安全做法:先判空
int total = (count != null) ? count + 1 : 0;
陷阱2:性能黑洞
java
ini
// ❌ 性能极差:每次循环都发生装箱拆箱
Integer sum = 0;
for (int i = 0; i < 1000000; i++) {
sum += i; // sum拆箱 → 计算 → 结果装箱
}
// ✅ 性能好:使用基本类型
int sumFast = 0;
for (int i = 0; i < 1000000; i++) {
sumFast += i; // 纯基本类型运算
}
陷阱3:比较的混乱
java
scss
Integer x = 127;
Integer y = 127;
// 正确做法
if (x.equals(y)) { /* ... */ } // ✅ 比较值
if (x.intValue() == y.intValue()) { /* ... */ } // ✅
// 危险做法(有时对有时错)
if (x == y) { /* ... */ } // ❌ 只在缓存范围内有效
陷阱4:三元运算符的类型提升
java
sql
// 编译错误:类型不匹配
// int result = flag ? null : 0;
// 编译通过,但运行时可能NPE
Integer result = flag ? null : 0; // 如果flag为true,result为null
// 后续使用时可能NPE
// int value = result; // 如果result为null,这里NPE
七、实战面试问答
高频问题1:为什么要有包装类型?
标准答案 :
"Java是面向对象的语言,但基本类型不是对象。包装类型解决了三个问题:
- 对象化需求:让基本类型能像对象一样操作,可以调用方法
- 集合支持:Java集合框架只能存储对象,不能存基本类型
- 泛型支持 :泛型类型参数必须是类类型
同时,Java通过自动装箱拆箱机制,减少了开发者在这两种类型间转换的代码负担。"
高频问题2:Integer缓存机制是什么?
标准答案 :
"Integer类内部有一个IntegerCache静态内部类,默认缓存了-128到127之间的256个Integer对象。当通过valueOf()方法或自动装箱创建在这个范围内的Integer时,会直接返回缓存的对象,而不是新建。这提高了性能,减少了内存占用,是享元模式的应用。但这也带来了一个常见陷阱:用==比较在这个范围内的Integer会返回true,超出范围则返回false,所以比较包装类型应该用equals()方法。"
高频问题3:自动装箱的性能影响?
标准答案 :
"自动装箱确实有性能开销,主要体现在:
- 对象创建开销:每次装箱都可能在堆上创建新对象
- 内存占用更大:Integer对象比int占用更多内存
- 缓存未命中时的开销 :超出缓存范围会创建新对象
在性能敏感的代码中,比如大规模循环,应该优先使用基本类型。但在大多数业务代码中,这种开销是可以接受的,自动装箱带来的代码简洁性更重要。"
八、总结:记住这几点就够了
- 基本类型为效率,包装类型为功能:按需选择
- 比较包装类型用equals() :永远不要依赖==
- 性能敏感处用基本类型:循环、频繁计算
- 注意可能的NPE:包装类型可以为null
- 了解缓存范围:-128到127,Character是0到127
2.9 Java中深拷贝与浅拷贝的区别
2.9.1 浅拷贝的概念和实现
定义 :仅复制对象本身(包括其内部的基本类型字段),对于引用类型字段,复制的是其内存地址(引用),因此原对象和拷贝对象会共享这些引用指向的实际对象。
- 对于基本类型:拷贝值
- 对于引用类型:拷贝引用(指向同一对象)
实现方式:
java
java
class ShallowCopyExample implements Cloneable {
private int id;
private String name;
private List<String> hobbies; // 引用类型
@Override
public ShallowCopyExample clone() {
try {
return (ShallowCopyExample) super.clone(); // 默认浅拷贝
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
// 问题:两个对象共享同一个hobbies列表
ShallowCopyExample obj1 = new ShallowCopyExample();
obj1.setHobbies(new ArrayList<>(Arrays.asList("reading", "coding")));
ShallowCopyExample obj2 = obj1.clone();
obj2.getHobbies().add("gaming"); // 同时修改了obj1的hobbies
2.9.2 深拷贝的概念和实现
定义 :不仅复制对象本身,还递归地复制其所有引用类型字段指向的整个对象图。结果是创建出一个完全独立的新对象,修改其中任何部分都不会影响原对象。
方法1:重写clone()方法实现深拷贝
java
typescript
class DeepCopyExample implements Cloneable {
private int id;
private String name; // String是不可变对象,浅拷贝即可
private List<String> hobbies;
private Address address; // 自定义对象
@Override
public DeepCopyExample clone() {
try {
DeepCopyExample copy = (DeepCopyExample) super.clone();
// 深拷贝可变引用类型
if (this.hobbies != null) {
copy.hobbies = new ArrayList<>(this.hobbies);
}
if (this.address != null) {
copy.address = this.address.clone(); // Address也需实现Cloneable
}
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
static class Address implements Cloneable {
private String city;
@Override
public Address clone() {
try {
return (Address) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
}
2.9.3 Cloneable接口的使用
Cloneable接口的作用:
java
kotlin
public interface Cloneable {
// 标记接口,没有方法
}
Object.clone()的保护机制:
java
java
protected native Object clone() throws CloneNotSupportedException;
// 如果没有实现Cloneable接口,调用clone()会抛出异常
class NotCloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 抛出CloneNotSupportedException
}
}
最佳实践:
- 实现Cloneable接口
- 重写clone()方法为public
- 调用super.clone()开始
- 深拷贝所有可变引用字段
- 处理final字段的限制
2.9.4 序列化实现深拷贝
使用场景:对象图复杂,或不想实现Cloneable接口
java
java
import java.io.*;
class SerializationDeepCopy {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepCopy(T obj) {
try {
// 序列化到字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(obj);
oos.close();
// 从字节数组反序列化
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (T) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Deep copy failed", e);
}
}
}
// 使用要求:
// 1. 所有相关类必须实现Serializable
// 2. 性能较低,适合不频繁调用的场景
2.9.5 使用构造方法实现拷贝
拷贝构造函数模式:
java
arduino
class CopyConstructorExample {
private final int id;
private final List<String> items;
// 拷贝构造函数
public CopyConstructorExample(CopyConstructorExample other) {
this.id = other.id;
// 深拷贝列表
this.items = new ArrayList<>(other.items);
}
// 或使用工厂方法
public static CopyConstructorExample newInstance(CopyConstructorExample other) {
return new CopyConstructorExample(other);
}
}
优点:
-
不需要实现Cloneable接口
-
可以控制哪些字段被拷贝
-
支持final字段
-
更明确的API
深拷贝与浅拷贝核心对比表
| 对比维度 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 定义 | 只复制对象本身,不复制内部引用字段指向的实际对象 | 复制对象本身及其所有引用字段指向的实际对象(递归复制整个对象图) |
| 内存行为 | 原对象和拷贝对象共享引用字段的内存地址 | 原对象和拷贝对象独立拥有各自的引用字段内存地址 |
| 实现方式 | Object.clone()(默认)、拷贝构造函数(仅复制引用) |
重写clone()并递归拷贝、序列化/反序列化、手动创建新对象 |
| 对象关系 | 一层的对象复制 | 递归的深层次对象复制 |
| 修改影响 | 修改拷贝对象的引用字段会影响原对象 | 修改拷贝对象的引用字段不影响原对象 |
| 性能特点 | 速度快,内存消耗少 | 速度慢,内存消耗大 |
| 实现复杂度 | 简单 | 复杂(需处理嵌套和循环引用) |
| 典型场景 | 字段主要是基本类型或不可变对象、需要共享数据 | 包含可变引用字段且需要完全独立副本、多线程环境、值对象传递 |
| 常见问题 | 意外数据共享、线程不安全 | 循环引用问题、性能开销大、实现复杂 |
| 注意事项 | 确保引用字段是线程安全的或不可变的 | 处理循环引用避免栈溢出、考虑性能影响 |
面试回答要点与记忆口诀
- 先说本质:"深拷贝和浅拷贝最核心的区别在于,对于对象内部的引用类型字段,是复制了引用地址(浅拷贝,导致共享),还是递归复制了整个引用对象(深拷贝,实现隔离)。"
- 再谈影响:"因此,修改浅拷贝对象的引用字段内容会影响原对象,而深拷贝则完全独立。"
- 列举方法 :"实现上,浅拷贝用默认的
clone()或集合的拷贝构造。深拷贝有三种常见方式:一重写clone()(麻烦),二用序列化(通用但性能稍差),三写拷贝构造或工厂方法(最清晰推荐) 。" - 不忘特例 :"对于
String、Integer这类不可变对象,浅拷贝共享它们是完全安全的。" - 处理难点 :"深拷贝时要注意循环引用 问题,可以通过维护一个
Map记录已拷贝对象来避免无限递归。序列化方式可以天然处理这个问题。"
口诀 : "浅共享,深隔离;默认clone是浅的,要深得手动(拷贝构造)、递归(clone)或过河(序列化)。"
🔧 数组和集合的深拷贝实现
1. 数组的深拷贝
数组本身是引用类型,拷贝数组时需要注意深浅拷贝的区别。
示例:包含引用类型元素的数组
java
scss
class Person {
String name;
// 构造函数、getter/setter省略
}
// 原始数组
Person[] originalArray = new Person[2];
originalArray[0] = new Person("Alice");
originalArray[1] = new Person("Bob");
方法一:System.arraycopy() - 浅拷贝
java
ini
// 浅拷贝:只复制数组引用,不复制数组元素
Person[] shallowCopy = new Person[originalArray.length];
System.arraycopy(originalArray, 0, shallowCopy, 0, originalArray.length);
// 修改会影响原数组
shallowCopy[0].setName("Charlie");
System.out.println(originalArray[0].getName()); // 输出: Charlie(被影响了!)
方法二:Arrays.copyOf() - 浅拷贝
java
ini
// 同样也是浅拷贝
Person[] anotherShallowCopy = Arrays.copyOf(originalArray, originalArray.length);
方法三:手动深拷贝
java
ini
// 深拷贝:复制数组并复制每个元素
Person[] deepCopy = new Person[originalArray.length];
for (int i = 0; i < originalArray.length; i++) {
// 假设Person实现了深拷贝
deepCopy[i] = originalArray[i].clone(); // 或 new Person(originalArray[i])
}
// 修改不会影响原数组
deepCopy[0].setName("David");
System.out.println(originalArray[0].getName()); // 输出: Alice(不受影响)
方法四:使用序列化深拷贝(通用方法)
java
ini
import java.io.*;
public static <T extends Serializable> T[] deepCopyArray(T[] array) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(array);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (T[]) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Deep copy failed", e);
}
}
// 使用
Person[] serializedCopy = deepCopyArray(originalArray);
2. 集合的深拷贝
集合类(List、Set、Map等)的拷贝同样需要注意深浅问题。
示例:包含自定义对象的List
java
csharp
List<Person> originalList = new ArrayList<>();
originalList.add(new Person("Alice"));
originalList.add(new Person("Bob"));
方法一:构造函数 - 浅拷贝
java
csharp
// 浅拷贝:新集合,但元素引用相同
List<Person> shallowList = new ArrayList<>(originalList);
shallowList.get(0).setName("Charlie");
System.out.println(originalList.get(0).getName()); // 输出: Charlie
方法二:addAll() - 浅拷贝
java
ini
// 同样是浅拷贝
List<Person> anotherShallow = new ArrayList<>();
anotherShallow.addAll(originalList);
方法三:手动深拷贝List
java
scss
// 深拷贝List
List<Person> deepList = new ArrayList<>();
for (Person person : originalList) {
deepList.add(person.clone()); // 需要Person支持深拷贝
}
// 或者使用Stream API(Java 8+)
List<Person> deepList2 = originalList.stream()
.map(Person::clone) // 或 p -> new Person(p)
.collect(Collectors.toList());
方法四:序列化深拷贝集合
java
scss
public static <T extends Serializable> List<T> deepCopyList(List<T> list) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(list);
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
return (List<T>) ois.readObject();
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("Deep copy failed", e);
}
}
// 使用
List<Person> serializedList = deepCopyList(originalList);
方法五:Map的深拷贝
java
javascript
Map<String, Person> originalMap = new HashMap<>();
originalMap.put("a", new Person("Alice"));
originalMap.put("b", new Person("Bob"));
// 浅拷贝
Map<String, Person> shallowMap = new HashMap<>(originalMap);
// 深拷贝
Map<String, Person> deepMap = new HashMap<>();
for (Map.Entry<String, Person> entry : originalMap.entrySet()) {
deepMap.put(entry.getKey(), entry.getValue().clone());
}
// 使用Stream API深拷贝
Map<String, Person> deepMap2 = originalMap.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
e -> e.getValue().clone()
));
⚠️ 重要注意事项
- 不可变对象的特殊性:String、Integer等不可变对象在浅拷贝中是安全的
- 嵌套集合的深拷贝:如果集合中的元素本身也是集合,需要递归深拷贝
- 循环引用问题:对象图中有循环引用时,手动深拷贝可能导致栈溢出
- 性能考虑:深拷贝大对象或复杂对象图时性能影响显著
- final字段限制:深拷贝无法修改final字段的值(构造函数除外)
第三章:异常处理
3.1 Error和Exception的区别
3.1.1 Error的概念和特点
概念:Error是Throwable的子类,表示Java虚拟机无法处理的严重系统级问题。
特点总结:
- 严重性高,通常导致程序崩溃
- 无法通过代码恢复,不建议捕获
- 由JVM抛出(如内存不足、栈溢出)
- 示例:OutOfMemoryError、StackOverflowError
示例:
java
csharp
// 人为制造StackOverflowError
public static void infiniteRecursion() {
infiniteRecursion(); // 无限递归导致栈溢出
}
3.1.2 Exception的概念和特点
概念:Exception是Throwable的子类,表示程序运行时可能遇到的异常情况。
特点总结:
- 可恢复性,可以通过代码处理
- 必须捕获或声明抛出
- 分为检查异常和非检查异常
- 示例:IOException、SQLException、NullPointerException
示例:
java
csharp
try {
int result = 10 / 0; // 抛出ArithmeticException
} catch (ArithmeticException e) {
System.out.println("除数不能为零");
}
3.1.3 RuntimeException的特点
概念:Exception的子类,属于非检查异常。
特点总结:
- 编译器不强制要求处理
- 通常由程序逻辑错误引起
- 可通过编码避免,而非运行时捕获
- 应在代码层面预防
常见RuntimeException:
java
ini
// 空指针异常
String str = null;
str.length(); // NullPointerException
// 数组越界
int[] arr = {1, 2, 3};
arr[5]; // ArrayIndexOutOfBoundsException
// 类型转换异常
Object obj = "hello";
Integer num = (Integer) obj; // ClassCastException
3.1.4 检查异常和非检查异常的区别
对比表格:
| 特性 | 检查异常 | 非检查异常 |
|---|---|---|
| 继承关系 | Exception(除RuntimeException) | RuntimeException或Error |
| 编译检查 | 必须处理 | 不强制处理 |
| 设计理念 | 可恢复的外部错误 | 程序内部逻辑错误 |
| 处理方式 | 捕获或声明抛出 | 可处理可不处理 |
| 示例 | IOException、SQLException | NullPointerException、IllegalArgumentException |
代码对比:
java
csharp
// 检查异常:必须处理
public void readFile() throws IOException { // 或try-catch
FileInputStream fis = new FileInputStream("file.txt");
}
// 非检查异常:可选处理
public void process() {
String str = null;
// 可能抛出NullPointerException,但不强制处理
System.out.println(str.length());
}
3.1.5 常见Error类型
主要Error类型对比:
| Error类型 | 触发条件 | 解决方案 |
|---|---|---|
| OutOfMemoryError | 堆内存不足 | 调整JVM参数,优化代码 |
| StackOverflowError | 栈空间耗尽(无限递归) | 检查递归终止条件 |
| NoClassDefFoundError | 类定义未找到 | 检查类路径 |
| VirtualMachineError | JVM资源耗尽 | 系统级调整 |
示例:
java
csharp
// OutOfMemoryError示例
List<byte[]> list = new ArrayList<>();
while(true) {
list.add(new byte[1024*1024]); // 持续分配1MB内存
}
// StackOverflowError示例
public static void recursive() {
recursive(); // 无限递归
}
3.1.6 常见Exception类型
常见Exception分类:
| 类别 | 异常类型 | 触发场景 |
|---|---|---|
| IO异常 | IOException | 文件读写失败 |
| FileNotFoundException | 文件不存在 | |
| SQL异常 | SQLException | 数据库操作失败 |
| 运行时异常 | NullPointerException | 空指针操作 |
| ArrayIndexOutOfBoundsException | 数组越界 | |
| IllegalArgumentException | 非法参数 | |
| ClassCastException | 类型转换错误 | |
| 其他 | InterruptedException | 线程被中断 |
总结:
- Error:严重系统问题,无法恢复,不应捕获
- Exception:程序异常,可恢复,必须处理
- 检查异常:外部因素导致,必须显式处理
- 非检查异常:程序逻辑错误,应在编码时避免
3.2 try-catch-finally的执行顺序
3.2.1 try-catch-finally的完整执行流程
执行流程总结:
- 执行try块代码
- 如果异常,跳转到匹配的catch块
- 无论是否异常,finally都会执行(特殊情况除外)
- 继续执行后续代码
执行顺序示例:
java
csharp
try {
System.out.println("1. try开始");
int result = 10 / 0; // 抛出异常
} catch (ArithmeticException e) {
System.out.println("2. 捕获异常");
} finally {
System.out.println("3. finally执行");
}
System.out.println("4. 继续执行");
// 输出:1, 2, 3, 4
3.2.2 finally代码块的重要特性
特性总结:
- 总是执行:无论是否发生异常(特殊情况除外)
- 资源清理:用于释放资源(文件、连接等)
- 异常传播:finally中的异常会传递给调用者
- 覆盖返回值:finally中的return会覆盖之前的返回值
finally使用场景:
java
java
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
// 读取文件
} catch (IOException e) {
// 处理异常
} finally {
if (reader != null) {
try {
reader.close(); // 确保资源关闭
} catch (IOException e) {
// 处理关闭异常
}
}
}
3.2.3 finally不执行的特殊情况
特殊情况总结:
| 情况 | 描述 | 示例 |
|---|---|---|
| System.exit() | 立即终止JVM | System.exit(0); |
| JVM崩溃 | 操作系统强制终止 | kill -9 进程ID |
| 无限循环 | 程序无法继续执行 | while(true) {} |
| 守护线程终止 | 所有非守护线程结束 | 守护线程被JVM终止 |
示例:
java
csharp
try {
System.out.println("try执行");
System.exit(0); // finally不会执行
} finally {
System.out.println("finally执行"); // 不会输出
}
3.2.4 return在try-catch-finally中的执行
执行规则总结:
-
return值暂存:执行return时,返回值被计算并暂存
-
finally执行:执行finally块代码
-
返回值确定:
- 如果finally有return,覆盖之前的值
- 如果finally没有return,返回之前暂存的值
各种情况对比:
java
csharp
// 情况1: try有return,finally无return
public int test1() {
try { return 10; } // 返回值10暂存
finally { /* 无return */ }
// 返回:10
}
// 情况2: try有return,finally也有return
public int test2() {
try { return 10; } // 返回值10暂存
finally { return 20; } // 覆盖为20
// 返回:20
}
// 情况3: 修改基本类型返回值
public int test3() {
int i = 10;
try { return i; } // 返回10(值复制)
finally { i = 20; } // 修改i,但不影响返回值
// 返回:10
}
// 情况4: 修改引用类型返回值
public StringBuilder test4() {
StringBuilder sb = new StringBuilder("Hello");
try { return sb; } // 返回引用
finally { sb.append(" World"); } // 修改对象内容
// 返回:"Hello World"
}
3.2.5 System.exit()对finally的影响
影响总结:
System.exit()立即终止JVM,finally不会执行- 参数0表示正常退出,非0表示异常退出
- 替代方案:使用
Runtime.addShutdownHook()
对比示例:
java
csharp
// 直接使用System.exit()
try {
System.out.println("程序运行");
System.exit(0); // 立即退出
} finally {
System.out.println("不会执行"); // 不会输出
}
// 使用关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("执行清理工作");
}));
最佳实践:
- 避免在finally中使用return(掩盖异常)
- 避免在finally中抛出异常(掩盖原始异常)
- 优先使用try-with-resources进行资源管理
- 使用关闭钩子处理System.exit()时的清理工作
3.3 try-with-resources
3.3.1 try-with-resources语法
基本语法:
java
java
try (ResourceType resource = new ResourceType()) {
// 使用资源
} catch (Exception e) {
// 异常处理
}
// 资源自动关闭
示例对比:
传统方式:
java
java
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
// 使用reader
} catch (IOException e) {
// 处理异常
} finally {
if (reader != null) {
try {
reader.close(); // 手动关闭
} catch (IOException e) {
// 处理关闭异常
}
}
}
try-with-resources方式:
java
java
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
// 使用reader
// 资源自动关闭,无需finally
} catch (IOException e) {
// 处理异常
}
3.3.2 AutoCloseable接口
接口对比:
| 特性 | AutoCloseable | Closeable |
|---|---|---|
| 包位置 | java.lang | java.io |
| 继承关系 | 父接口 | 子接口 |
| close()异常 | 抛出Exception | 抛出IOException |
| 引入版本 | Java 7 | Java 5 |
自定义资源实现:
java
java
class MyResource implements AutoCloseable {
private String name;
public MyResource(String name) {
this.name = name;
System.out.println("创建: " + name);
}
@Override
public void close() throws Exception {
System.out.println("关闭: " + name);
}
}
// 使用
try (MyResource r = new MyResource("资源1")) {
// 使用资源
}
3.3.3 与传统try-catch-finally的对比
详细对比表:
| 对比方面 | try-with-resources | 传统try-catch-finally |
|---|---|---|
| 代码简洁性 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| 资源泄漏风险 | 极低 | 较高 |
| 异常处理 | 支持异常抑制 | 容易丢失原始异常 |
| 可读性 | 高 | 低 |
| 关闭顺序 | 自动逆序关闭 | 需手动保证 |
| 开发效率 | 高 | 低 |
| 错误倾向 | 低 | 高 |
3.3.4 多个资源的关闭顺序
关闭规则:
- 后声明的资源先关闭
- 按声明顺序的逆序关闭
- 每个资源的close()都会被调用
示例:
java
java
try (
Resource r1 = new Resource("1"); // 最后关闭
Resource r2 = new Resource("2"); // 第二个关闭
Resource r3 = new Resource("3") // 最先关闭
) {
// 使用资源
}
// 关闭顺序:3 → 2 → 1
3.3.5 异常抑制机制
机制总结:
- 当try块和close()都抛出异常时
- try块的异常为主要异常
- close()的异常被抑制
- 可通过getSuppressed()获取被抑制的异常
异常抑制示例:
java
java
class ProblemResource implements AutoCloseable {
public void use() throws Exception {
throw new Exception("使用异常");
}
@Override
public void close() throws Exception {
throw new Exception("关闭异常");
}
}
try (ProblemResource r = new ProblemResource()) {
r.use(); // 抛出"使用异常"
} catch (Exception e) {
System.out.println("主要异常: " + e.getMessage());
// 获取被抑制的异常
for (Throwable t : e.getSuppressed()) {
System.out.println("被抑制: " + t.getMessage());
}
}
最佳实践总结:
- 优先使用try-with-resources:减少代码复杂度,避免资源泄漏
- 支持多个资源:自动按正确顺序关闭
- 异常抑制机制:确保不丢失任何异常信息
- Java 7+特性:显著改进资源管理方式
3.4 异常处理的原则和最佳实践
3.4.1 异常处理的基本原则
四大原则总结:
| 原则 | 描述 | 示例 |
|---|---|---|
| 早抛出,晚捕获 | 发现问题立即抛出,在合适层级处理 | 在方法开始验证参数,在业务层处理异常 |
| 具体化异常 | 捕获具体异常类型,而非通用的Exception | 捕获IOException而非Exception |
| 异常用途单一化 | 仅用于异常情况,不用于控制流程 | 使用Optional而非异常表示空值 |
| 提供有意义的信息 | 包含足够上下文帮助问题诊断 | "文件'xxx.txt'不存在"而非"文件不存在" |
原则对比示例:
反例:
java
kotlin
// 捕获太笼统
try {
// 各种操作
} catch (Exception e) { // 太笼统!
// 处理
}
// 异常控制流程
try {
return array[index];
} catch (ArrayIndexOutOfBoundsException e) {
return defaultValue; // 用异常控制流程
}
正例:
java
kotlin
// 捕获具体异常
try {
// 操作
} catch (IOException e) { // 具体异常
// 处理IO异常
} catch (SQLException e) {
// 处理SQL异常
}
// 预检查而非异常
if (index >= 0 && index < array.length) {
return array[index]; // 预检查
}
return defaultValue;
3.4.2 异常链的使用
异常链的重要性:
- 保留原始异常信息
- 便于问题追踪和调试
- 支持异常的包装和转换
示例对比:
反例:丢失原始异常:
java
csharp
try {
readFile();
} catch (IOException e) {
throw new BusinessException("处理失败"); // 丢失原始异常
}
正例:保留异常链:
java
csharp
try {
readFile();
} catch (IOException e) {
throw new BusinessException("处理失败", e); // 保留异常链
}
异常链工具方法:
java
typescript
public class ExceptionUtils {
// 获取根原因
public static Throwable getRootCause(Throwable throwable) {
Throwable cause = throwable;
while (cause.getCause() != null) {
cause = cause.getCause();
}
return cause;
}
// 查找特定类型异常
public static <T> T findException(Throwable throwable, Class<T> type) {
while (throwable != null) {
if (type.isInstance(throwable)) {
return type.cast(throwable);
}
throwable = throwable.getCause();
}
return null;
}
}
3.4.3 日志记录的最佳实践
日志记录原则总结:
| 场景 | 日志级别 | 记录内容 |
|---|---|---|
| 程序入口 | INFO | 请求ID、参数等上下文信息 |
| 业务成功 | INFO | 关键业务结果 |
| 可恢复错误 | WARN | 错误详情、恢复措施 |
| 系统错误 | ERROR | 完整异常堆栈、上下文 |
| 调试信息 | DEBUG | 详细执行过程(性能敏感) |
日志记录最佳实践:
java
typescript
// 1. 在合适位置记录
public void process(Request request) {
logger.info("开始处理: {}", request.getId());
try {
// 业务逻辑
logger.info("处理成功: {}", request.getId());
} catch (BusinessException e) {
logger.warn("业务异常: {}", e.getMessage(), e);
throw e;
} catch (Exception e) {
logger.error("系统异常: {}", request.getId(), e);
throw new SystemException("系统错误", e);
}
}
// 2. 避免重复记录
// 反例:每层都记录
// 正例:在最外层统一记录
// 3. 性能考虑
if (logger.isDebugEnabled()) { // 避免不必要的字符串拼接
logger.debug("处理数据: {}", expensiveToString(data));
}
3.4.4 异常包装和转换
包装转换策略:
| 策略 | 适用场景 | 示例 |
|---|---|---|
| 直接传递 | 同层异常,语义明确 | 参数验证失败直接抛出 |
| 简单包装 | 转换异常类型,保留原因 | IOException → BusinessException |
| 添加上下文 | 补充额外信息 | 添加时间戳、请求ID等 |
| 异常转换 | 统一异常类型 | 各种异常 → ServiceException |
示例:
java
java
// 1. 基础包装
public void processFile() throws BusinessException {
try {
readFile();
} catch (IOException e) {
// 包装为业务异常
throw new BusinessException("文件处理失败", e);
}
}
// 2. 异常转换器
public class ExceptionTranslator {
public BusinessException translate(Throwable t) {
if (t instanceof SQLException) {
return new DatabaseException("数据库错误", t);
} else if (t instanceof IOException) {
return new FileSystemException("文件系统错误", t);
}
return new SystemException("系统错误", t);
}
}
3.4.5 不要忽略异常
忽略异常的危害:
- 错误被掩盖,难以调试
- 程序状态不一致
- 资源泄漏风险
正确处理方式对比:
| 处理方式 | 描述 | 适用场景 |
|---|---|---|
| 记录并抛出 | 记录日志后重新抛出 | 需要上层处理 |
| 转换为业务异常 | 包装后抛出业务异常 | 业务层统一处理 |
| 静默处理 | 仅记录日志,不抛出 | 非关键操作失败 |
| 恢复操作 | 执行替代方案 | 可恢复的错误 |
示例:
java
php
// 反例:忽略异常
try {
process();
} catch (Exception e) {
// 完全忽略!❌
}
// 正例:适当处理
try {
process();
} catch (FileNotFoundException e) {
createNewFile(); // 恢复操作
logger.info("文件不存在,已创建");
} catch (BusinessException e) {
logger.error("业务异常", e);
throw e; // 重新抛出
} catch (Exception e) {
logger.error("未知异常", e);
throw new SystemException("系统错误", e);
}
3.4.6 异常的性能考虑
性能影响对比:
| 操作 | 耗时 | 建议 |
|---|---|---|
| 创建异常对象 | 较高(需填充堆栈) | 避免在频繁调用的代码中抛异常 |
| 预检查 | 很低 | 优先使用预检查替代异常 |
| 异常控制流程 | 非常高 | 绝对避免 |
| 返回错误码 | 低 | 高频API可考虑使用 |
性能优化示例:
java
typescript
// 反例:异常控制流程(性能差)
public int getValue(int index) {
try {
return array[index];
} catch (ArrayIndexOutOfBoundsException e) {
return -1; // 异常控制流程
}
}
// 正例:预检查(性能好)
public int getValue(int index) {
if (index >= 0 && index < array.length) {
return array[index]; // 预检查
}
return -1;
}
// 高频API设计:返回结果对象
public Result<Integer> parseNumber(String str) {
try {
return Result.success(Integer.parseInt(str));
} catch (NumberFormatException e) {
return Result.error("INVALID_NUMBER", "数字格式错误");
}
}
性能最佳实践:
- 避免在循环中使用异常
- 高频API考虑使用错误码
- 使用isDebugEnabled()避免日志性能开销
- 批量操作收集错误而非立即抛出
3.5 自定义异常如何实现?
3.5.1 自定义检查异常
实现要点:
- 继承Exception类
- 提供多个构造方法
- 添加有用的字段和方法
- 实现序列化
示例:
java
arduino
public class BusinessException extends Exception {
private final String errorCode;
// 基础构造
public BusinessException(String message) {
this("BUSINESS_ERROR", message);
}
// 带错误码构造
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
// 带原因构造
public BusinessException(String message, Throwable cause) {
this("BUSINESS_ERROR", message, cause);
}
// 完整构造
public BusinessException(String errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
3.5.2 自定义运行时异常
实现要点:
- 继承RuntimeException类
- 提供领域特定的字段
- 保持不可变性
示例:
java
typescript
public class ValidationException extends RuntimeException {
private final String fieldName;
private final Object invalidValue;
public ValidationException(String fieldName, Object value, String message) {
super(String.format("字段'%s'的值'%s'无效: %s", fieldName, value, message));
this.fieldName = fieldName;
this.invalidValue = value;
}
public String getFieldName() { return fieldName; }
public Object getInvalidValue() { return invalidValue; }
}
3.5.3 自定义异常的注意事项
设计原则对比:
| 原则 | 检查异常 | 运行时异常 |
|---|---|---|
| 命名规范 | 以Exception结尾 | 以Exception结尾 |
| 继承关系 | 继承Exception | 继承RuntimeException |
| 构造方法 | 提供多个重载 | 提供多个重载 |
| 不可变性 | 字段尽量final | 字段尽量final |
| 序列化 | 实现Serializable | 实现Serializable |
| 信息完整 | 包含足够上下文 | 包含足够上下文 |
常见错误:
- 过度设计异常类
- 忽略序列化兼容性
- 缺少有意义的构造方法
- 不遵循命名规范
3.5.4 异常信息的国际化
国际化实现方案:
| 方案 | 优点 | 缺点 |
|---|---|---|
| ResourceBundle | 标准Java支持,简单 | 需要多个属性文件 |
| MessageFormat | 支持参数化消息 | 需要手动格式化 |
| 第三方库 | 功能强大 | 增加依赖 |
示例:
java
scala
public class InternationalizedException extends RuntimeException {
private final String messageKey;
private final Object[] args;
public InternationalizedException(String messageKey, Object... args) {
super(getDefaultMessage(messageKey, args));
this.messageKey = messageKey;
this.args = args;
}
public String getLocalizedMessage(Locale locale) {
ResourceBundle bundle = ResourceBundle.getBundle("messages", locale);
String pattern = bundle.getString(messageKey);
return MessageFormat.format(pattern, args);
}
}
3.5.5 异常的错误码设计
错误码设计模式对比:
| 设计模式 | 示例 | 优点 | 缺点 |
|---|---|---|---|
| 纯数字 | 1001, 1002 | 简单,有序 | 含义不明确 |
| 字母+数字 | ERR001, SYS002 | 分类明确 | 较长 |
| 分层编码 | SYS.DB.001 | 层次清晰 | 复杂 |
| HTTP风格 | 40001, 50001 | 包含状态码 | 混合含义 |
推荐方案:分层编码
java
scala
public enum ErrorCode {
// 系统错误
SYSTEM_INTERNAL_ERROR("SYS_001", "系统内部错误"),
SYSTEM_TIMEOUT("SYS_002", "系统超时"),
// 业务错误
BUSINESS_VALIDATION_FAILED("BIZ_001", "业务验证失败"),
BUSINESS_INSUFFICIENT_BALANCE("BIZ_002", "余额不足"),
// 用户错误
USER_NOT_FOUND("USR_001", "用户不存在");
private final String code;
private final String defaultMessage;
// getters...
}
// 使用
public class CodedException extends RuntimeException {
private final ErrorCode errorCode;
public CodedException(ErrorCode errorCode, String message) {
super(String.format("[%s] %s", errorCode.getCode(), message));
this.errorCode = errorCode;
}
}
自定义异常最佳实践总结:
-
选择合适的父类
- 检查异常:需要调用者处理的业务错误
- 运行时异常:编程错误,参数验证失败
-
提供有用的信息
- 包含错误码、详细消息、上下文信息
- 支持异常链,保留原始异常
-
遵循设计原则
- 不可变性
- 序列化支持
- 命名规范
-
考虑国际化
- 支持多语言错误消息
- 使用ResourceBundle管理消息
-
合理的错误码设计
- 分层编码,易于分类
- 包含足够信息,易于排查
完整示例:
java
scala
// 推荐的完整自定义异常示例
public class ApplicationException extends Exception {
private static final long serialVersionUID = 1L;
private final ErrorCode errorCode;
private final Map<String, Object> context;
public ApplicationException(ErrorCode errorCode, String message) {
this(errorCode, message, null);
}
public ApplicationException(ErrorCode errorCode, String message, Throwable cause) {
super(message, cause);
this.errorCode = errorCode;
this.context = new HashMap<>();
}
public ApplicationException addContext(String key, Object value) {
context.put(key, value);
return this;
}
// getters...
}
使用建议:
- 优先使用标准异常,必要时再自定义
- 保持异常类简单,避免过度设计
- 提供清晰的文档和使用示例
- 在团队中统一异常设计规范
本章总结
关键知识点回顾
| 主题 | 核心要点 | 最佳实践 |
|---|---|---|
| Error vs Exception | Error是系统级问题,Exception是程序异常 | Error不捕获,Exception合理处理 |
| 异常类型 | 检查异常必须处理,运行时异常可选 | 具体化异常捕获,避免通用Exception |
| try-catch-finally | finally总是执行(除特殊情况) | 避免finally中的return和异常 |
| try-with-resources | 自动资源管理,支持异常抑制 | 优先使用,替代传统方式 |
| 异常处理原则 | 早抛出晚捕获,不忽略异常 | 提供有意义信息,保留异常链 |
| 自定义异常 | 选择合适的父类,设计错误码 | 保持简单,支持序列化和国际化 |
异常处理决策树
text
javascript
遇到问题
├── 是系统资源问题(内存、线程等)?
│ └── Error → 不捕获,优化代码或环境
│
├── 是程序逻辑错误(空指针、越界等)?
│ └── 运行时异常 → 编码时避免,可捕获处理
│
├── 是外部因素(IO、网络等)?
│ └── 检查异常 → 必须处理(捕获或声明)
│
└── 是否需要自定义异常?
├── 需要调用者处理 → 检查异常
├── 程序内部错误 → 运行时异常
└── 标准异常满足需求 → 优先使用标准异常
常见问题解答
Q1:应该捕获Exception还是具体的异常类型?
- 答:优先捕获具体异常类型,最后使用Exception作为兜底。
Q2:什么时候应该使用自定义异常?
- 答:当标准异常无法准确表达业务含义,或需要添加额外信息时。
Q3:finally中抛出异常会怎样?
- 答:会覆盖try/catch中的异常,应避免在finally中抛出异常。
Q4:try-with-resources比传统方式好在哪里?
- 答:代码更简洁,自动关闭资源,支持异常抑制,减少资源泄漏。
Q5:如何设计好的错误码系统?
- 答:分层编码(系统_模块_编号),语义清晰,易于扩展和分类。