Java基础篇——第一部

第一章:面向对象基础

1.1 什么是面向对象?面向对象的三大特性是什么?

核心答案:

面向对象编程(OOP)是一种以"对象"为核心的编程范式,将现实世界的事物抽象为程序中的对象,对象包含数据(属性)和操作数据的方法。其三大核心特性是:封装、继承、多态

详细解析:

1.1.1 面向对象编程概念解析

  • 对象:是系统中描述客观事物的基本单位,是属性和方法的封装体。例如,"学生"对象拥有学号、姓名等属性,拥有选课、考试等方法。
  • :是创建对象的"蓝图"或"模板",定义了同类对象共有的属性和方法。对象是类的具体实例。
  • 核心思想:通过对现实世界的抽象,提高代码的可重用性、可维护性和可扩展性,更符合人类的思维模式。

1.1.2 面向对象与面向过程的对比

  • 面向过程:以"函数"为中心,关注解决问题的步骤,数据与操作分离。代表语言:C。
  • 面向对象:以"对象"为中心,关注参与解决问题的实体及其相互关系,数据与操作绑定在一起。代表语言:Java, C++。
  • 类比:建造房子。面向过程关注"打地基、砌墙、封顶"等步骤;面向对象关注"设计师、工人、材料商"等角色(对象)以及他们如何协作。

1.1.3 封装的具体含义和实现方式

  • 含义:隐藏对象的内部实现细节,仅对外提供公共的访问接口。核心是"高内聚,低耦合"。

  • 实现

    1. 使用 private 等访问修饰符将属性私有化。
    2. 提供公共的 gettersetter 方法来访问和修改属性。
    3. 对属性的合法性校验可以在 setter 方法中进行。
  • 好处:保证数据的安全性,提高代码的健壮性,使类的内部修改不影响外部调用。

1.1.4 继承的具体含义和实现方式

  • 含义:子类(派生类)可以继承父类(基类)的非私有属性和方法,并可以添加自己的新特性。核心是"is-a"关系。

  • 实现 :在Java中使用 extends 关键字。

    java

    scala 复制代码
    class Animal { // 父类
        void eat() { System.out.println("eating..."); }
    }
    class Dog extends Animal { // 子类
        void bark() { System.out.println("barking..."); }
    }
    // Dog 对象可以调用 eat() 和 bark()
  • 好处 :实现代码复用,建立类之间的层次体系。Java是单继承,一个类只能有一个直接父类。

1.1.5 多态的具体含义和实现方式

  • 含义 :同一操作作用于不同的对象,可以产生不同的执行结果。主要包括编译时多态(静态绑定)运行时多态(动态绑定)

  • 实现

    1. 方法重载(Overload) :编译时多态。在同一类中,方法名相同,参数列表不同。

    2. 方法重写(Override) :运行时多态。在继承体系中,子类重写父类的方法。

    3. 父类引用指向子类对象:这是实现运行时多态的关键。

      java

      scss 复制代码
      Animal myAnimal = new Dog(); // 向上转型
      myAnimal.eat(); // 如果Dog重写了eat(),则调用Dog的eat()方法
  • 好处 :提高程序的扩展性和灵活性。例如,新增一个 Cat 类,无需修改原有处理 Animal 的代码。

1.1.6 面向对象的优势和应用场景

  • 优势

    • 易维护:结构清晰,封装使代码更易理解和修改。
    • 易扩展:继承和多态使得系统能够以较低成本适应新需求。
    • 易复用:通过类和对象的抽象,功能模块可以被多次使用。
    • 适合大型复杂系统:能更好地进行模块化设计和团队协作。
  • 应用场景:几乎所有现代软件系统,包括Web应用、桌面软件、移动应用、游戏开发、企业级后端服务等。


1.2 抽象类和接口的区别

核心答案:

抽象类是对类的抽象,定义了一类对象的共同特征(包括属性和方法);接口是对行为的抽象,定义了一组对象必须遵守的契约(只有方法声明)。在Java 8之后,两者都可以包含有实现的方法,但设计初衷和侧重点不同。

详细解析:

1.2.1 抽象类的定义和特性

  • 定义 :用 abstract 修饰的类。它不能被实例化。

  • 特性

    1. 可以包含抽象方法(abstract修饰,无方法体)和具体实现的方法。
    2. 可以定义成员变量(非常量)。
    3. 可以有构造方法(供子类初始化时调用)。
    4. 子类必须实现其所有抽象方法,除非子类也是抽象类。
  • 目的 :作为一些相关类的模板,提供共有的属性和部分实现,要求子类完成特定部分。

1.2.2 接口的定义和特性

  • 定义 :在Java 7及以前,接口是纯粹的契约,所有方法都是 public abstract(可省略),所有变量都是 public static final(常量)。

  • 特性(Java 8+)

    1. 抽象方法:核心,定义行为规范。
    2. 默认方法(Default Methods) :使用 default 关键字,提供默认实现。目的是在不破坏现有实现的情况下扩展接口功能。
    3. 静态方法(Static Methods) :使用 static 关键字,属于接口自身,可通过接口名直接调用。
    4. 私有方法(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. 需要定义子类的模板 ,且部分步骤是固定的,部分需要子类具体实现(模板方法模式的典型应用)。
  3. 需要控制子类的类型,构建清晰的类层次结构

1.2.5 接口的适用场景

  1. 定义一套行为规范或能力 ,不关心具体实现者是谁(如 Comparable, Serializable)。
  2. 希望一个类具备多种不同的能力(通过实现多个接口),突破单继承限制。
  3. 作为系统与外部模块之间的契约,实现解耦(如DAO层接口、Service层接口)。
  4. 用于函数式编程(只有一个抽象方法的函数式接口)。

1.2.6 JDK 8+中接口的新特性

  • 默认方法:允许接口提供方法默认实现,解决了接口升级时,所有实现类都必须实现新方法的问题。如果实现类不覆盖,则使用默认实现。
  • 静态方法:提供了与接口相关的工具方法,可以直接通过接口调用,无需通过实现类实例。
  • 私有方法:允许在接口内部封装公共代码,提高默认方法和静态方法的代码复用性,同时对外隐藏实现细节。

1.3 final、static和synchronized的修饰符

1.3.1 final修饰符的用法和原理

  • 修饰类 :该类不能被继承 。例如 StringInteger 等核心类都是 final 类,保证了不可变性和安全性。

  • 修饰方法 :该方法不能被重写。用于锁定方法行为,防止子类修改其核心逻辑。

  • 修饰变量

    • 基本类型变量:值一旦初始化就不能更改
    • 引用类型变量:引用指向的地址不能更改,但对象内部的状态可以改变(除非对象本身也是不可变的)。
    • 原理final 变量的写入发生在构造方法完成之前,且禁止指令重排序,保证了其他线程能正确看到初始化后的值,是实现线程安全不可变对象的关键。

1.3.2 static修饰符的用法和原理

  • 修饰变量(静态变量/类变量)

    • 属于类,在类加载的"准备"阶段分配内存并设置默认值,在"初始化"阶段赋值。
    • 所有实例共享同一份内存。ClassName.variableName 访问。
  • 修饰方法(静态方法)

    • 属于类,可通过类名直接调用。内部不能使用 thissuper,只能直接访问静态成员。
    • 通常用于工具方法或工厂方法。
  • 修饰代码块(静态代码块)

    • 在类加载时执行一次,用于初始化静态变量。
  • 原理static 成员与类本身相关联,而非与任何单个对象实例相关联,生命周期贯穿整个程序运行期。

1.3.3 synchronized修饰符的用法和原理

  • 用法

    1. 修饰实例方法 :锁定当前对象实例(this)。
    2. 修饰静态方法 :锁定当前类的 Class 对象。
    3. 修饰代码块 :需要显式指定锁对象(synchronized(obj) {...})。
  • 原理 :基于JVM内置的 monitorentermonitorexit 指令实现。每个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 重载的定义和实现规则

  • 定义 :在同一个类中,允许存在多个方法名相同 ,但参数列表不同的方法。

  • 规则

    1. 参数列表必须不同(类型、个数、顺序至少一项不同)。
    2. 返回类型、访问修饰符、抛出异常可以不同,但不能仅凭这些不同来重载
    3. 发生在编译期,编译器根据方法签名确定调用哪个方法。

    java

    javascript 复制代码
    void show(int a) {}
    void show(String s) {} // 参数类型不同 -> 重载
    void show(int a, String s) {} // 参数个数不同 -> 重载
    // int show(int a) { return a; } // 错误!仅返回类型不同,不是重载

1.4.2 重写的定义和实现规则

  • 定义:子类重新定义父类中已有的方法,以实现自己的特定行为。

  • 规则(两同两小一大)

    1. 方法名和参数列表完全相同
    2. 子类返回值类型 应小于等于父类(基本类型必须相同;引用类型可以是父类返回类型的子类,即协变返回类型)。
    3. 子类抛出的异常应小于等于父类(不能抛出更宽泛的检查异常)。
    4. 子类访问权限 应大于等于父类(不能缩小访问范围,如父类是 protected,子类不能是 privatedefault)。

1.4.3 重载与重写的详细区别对比

对比维度 重载 (Overload) 重写 (Override)
发生位置 同一个类中 继承关系的父子类之间
方法签名 必须不同 必须相同
返回类型 可以不同 子类方法 <= 父类方法(协变)
访问修饰符 可以不同 子类方法 >= 父类方法
抛出异常 可以不同 子类方法 <= 父类方法
绑定阶段 编译时(静态绑定/早期绑定) 运行时(动态绑定/晚期绑定)
设计目的 提供处理不同数据的同名方法,提高可读性 实现多态,子类定制父类行为

1.4.4 @Override注解的作用

  1. 编译器检查 :告诉编译器此方法意图重写父类方法。如果签名与父类方法不匹配(如拼写错误、参数错误),编译器会报错。这是一个非常重要的安全网
  2. 提高代码可读性:明确标识出这是一个重写的方法。

1.4.5 重写的访问权限限制

  • 子类重写方法的访问权限不能低于父类被重写方法的访问权限。
  • 例如:父类方法是 protected,子类重写时可以是 protectedpublic,但不能是 defaultprivate
  • 原因:面向对象设计中的里氏替换原则。子类对象应该能够透明地替换父类对象。如果降低了访问权限,用子类对象替换父类对象时,就可能无法访问到本应可访问的方法,破坏了多态。

1.4.6 协变返回类型

  • 从Java 5开始,子类重写方法时,可以返回父类方法返回类型的子类型

    java

    scala 复制代码
    class Animal {
        Animal get() { return new Animal(); }
    }
    class Dog extends Animal {
        @Override
        Dog get() { return new Dog(); } // 返回类型是Animal的子类Dog
    }
  • 这提供了更精确的返回类型,是类型安全的一种增强,常用于工厂方法模式


1.5 静态方法和实例方法的区别

核心答案:

静态方法属于类,与实例无关;实例方法属于对象,操作特定实例的数据。

详细解析:

1.5.1 静态方法的特点和使用方式

  • 归属:属于类本身,在类加载时即存在。
  • 调用 :通过 类名.方法名() 调用(也可以通过对象调用,但不推荐)。
  • 内部访问只能直接访问 类的静态成员(变量、方法)。不能直接访问 实例成员(因为实例成员依附于对象,此时可能还没有对象被创建)。不能使用 thissuper 关键字。
  • 生命周期:与类的生命周期相同。
  • 典型场景 :工具类方法(如 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 静态变量的特点和使用场景

  • 特点

    1. 在类加载过程中准备和初始化,内存位于方法区(JDK 8后元空间)。
    2. 只有一份拷贝,被所有实例共享。
    3. 生命周期与类相同,从类加载开始,到程序结束。
  • 使用场景

    1. 需要被所有对象共享的常量或配置(如 public static final PI)。
    2. 记录类的全局状态(如在线人数计数器)。
    3. 作为缓存(如单例对象的引用)。

1.6.2 实例变量的特点和使用场景

  • 特点

    1. 在对象实例化时new)在堆内存中分配空间并初始化。
    2. 每个对象都有自己独立的一份拷贝,互不影响。
    3. 生命周期与对象实例相同,对象被垃圾回收时释放。
  • 使用场景

    1. 描述对象个体特征的属性(如学生的姓名、年龄)。
    2. 对象的核心状态数据。

1.6.3 静态变量与实例变量的详细区别

特性 静态变量(类变量) 实例变量(成员变量)
内存分配时机 类加载时 创建对象时
内存位置 方法区 堆内存
拷贝数量 仅一份,全类共享 每对象一份,独立
访问方式 类名.变量名 (推荐) 对象引用.变量名
生命周期 从类加载到程序结束 从对象创建到被GC回收
默认值 有默认值(同实例变量) 有默认值(int为0,引用为null等)

1.6.4 静态变量的线程安全性问题

  • 问题 :静态变量被所有线程共享。如果多个线程同时对其进行非原子性的读写操作 ,会导致数据不一致

  • 示例static int counter = 0; 多个线程同时执行 counter++(非原子操作:读-改-写)会导致计数错误。

  • 解决方案

    1. 使用 synchronized 关键字修饰相关方法或代码块。
    2. 使用 java.util.concurrent.atomic 包下的原子类(如 AtomicInteger)。
    3. 如果变量是不可变对象 (如 String, BigDecimal),则天然线程安全。

1.6.5 静态变量的生命周期

  1. 加载:当JVM首次主动使用某个类时(如创建实例、访问静态成员、调用静态方法等),类加载器将其字节码加载到内存。
  2. 连接-准备 :为静态变量在方法区分配内存,并设置默认初始值 (如 int 为0,引用为 null)。
  3. 连接-初始化 :执行类的 <clinit>() 方法(由编译器自动收集类中所有静态变量的赋值动作和静态代码块 合并生成),为静态变量赋予程序中定义的初始值
  4. 使用:程序运行期间,类一直存在。
  5. 卸载 :当类的 ClassLoader 被回收,且该类的 Class 对象不再被引用时,类可能被卸载,静态变量占用的内存随之释放。在应用生命周期中,类的卸载不常发生。

1.7 final、finally、finalize的区别

核心答案:

三者毫无关联,只是拼写相似。

  • final关键字,用于修饰符。
  • finally代码块,用于异常处理。
  • finalize()方法 ,属于 Object 类,用于垃圾回收。

详细解析:

1.7.1 final修饰符的3种用法

  • 修饰类 :类不可继承。如 String

  • 修饰方法:方法不可被重写。

  • 修饰变量

    • 基本类型:值不可变。
    • 引用类型:引用地址不可变(但对象内容可变)。
    • 方法参数:在方法内部不能修改参数引用(对于基本类型则是值)。

1.7.2 finally代码块的执行机制

  • 作用 :与 try-catch 配合使用,用于存放无论是否发生异常都必须执行的代码,如释放资源(关闭文件、数据库连接、网络连接等)。
  • 执行时机 :在 trycatch 中的 returnbreakcontinue 语句之前 执行,或者在未捕获的异常抛出之前执行。
  • 唯一不执行的情况 :在 trycatch 中执行了 System.exit(int)(强制退出JVM),或者守护线程因所有非守护线程结束而终止。

1.7.3 finalize方法的作用和废弃原因

  • 原始设计目的:对象被垃圾回收器回收之前,允许它进行最后的资源清理工作(如关闭打开的文件描述符)。该方法由垃圾回收线程调用。

  • 废弃原因

    1. 执行时机不确定 :JVM不保证何时甚至是否调用 finalize()
    2. 性能开销大 :启用 finalize() 会显著增加垃圾回收的负担。
    3. 可能导致问题 :在 finalize() 中"复活"对象(将 this 赋值给某个引用)会使GC过程复杂化。
    4. 异常吞没finalize() 中抛出的异常会被忽略,且不会终止线程,难以调试。
  • 替代方案 :使用 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"))) {
        // ...
    } // 自动关闭,简洁安全
  • 优势

    1. 代码简洁 :无需显式编写 finally 块。
    2. 安全性高:自动管理多个资源的关闭,关闭顺序与声明顺序相反。
    3. 异常处理友好 :如果 try 块和资源关闭都抛出异常,try 块的异常会被抛出,关闭引发的异常会被抑制 (可通过 Throwable.getSuppressed() 获取),避免了异常信息丢失。

1.8 访问修饰符public、private、protected和默认的区别

核心答案:

访问修饰符用于控制类、方法、变量的可见性范围,从宽到窄依次为:public > protected > 默认(包访问) > private

详细解析:

1.8.1 四种访问修饰符的作用范围

  1. public:公共的。在任何地方都可以访问。

  2. protected :受保护的。允许本类、同包中的类、其他包中的子类访问。

    • 注意:其他包中的子类通过子类对象或子类引用 可以访问父类的 protected 成员,但不能通过父类引用访问。
  3. 默认(无修饰符) :包私有。只允许本类、同包中的类访问。这是最常见的用于隐藏内部实现的方式。

  4. private :私有的。只允许本类内部访问。是封装性的最直接体现。

1.8.2 四种修饰符的访问权限对比表

修饰符 本类内部 同包子类/类 不同包子类 不同包非子类
public
protected ✅ (通过继承)
默认
private

1.8.3 实际开发中的使用建议

  1. 最小化访问原则 :优先使用最严格的访问级别。如果不确定,先从 private 开始,再根据需要放宽。
  2. public:用于对外提供的稳定API,如工具类的方法、框架的接口、常量。
  3. protected:用于设计给子类扩展的钩子方法(hook method)或受保护的字段,在框架设计中常见。
  4. 默认 :用于包内协作的类、方法或变量。这是隐藏实现细节、降低模块间耦合的有效手段。
  5. private :用于类的内部实现细节,通过 publicgetter/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

    arduino 复制代码
    public 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

    csharp 复制代码
    public Person() {
        this("Unknown", 0); // 调用双参构造
    }
    public Person(String name) {
        this(name, 0);
    }

1.9.4 初始化代码块的执行顺序

在类中,初始化动作按以下顺序执行:

  1. 静态成员初始化静态代码块 :按在类中出现的顺序 执行。只执行一次
  2. 实例成员初始化实例代码块 :按在类中出现的顺序 执行。每次创建对象都执行
  3. 构造函数:最后执行。

1.9.5 静态代码块和静态变量初始化

  • 两者平级,按代码的书写顺序初始化。

    java

    ini 复制代码
    static 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构造方法

黄金法则

  1. 先静态,后实例,最后构造
  2. 先父类,后子类
  3. 静态成员和代码块在类加载时 执行,只一次
  4. 实例成员、代码块和构造方法在每次new执行。

1.9.7 初始化顺序面试题解析

  • 核心:牢记上述"黄金法则"。

  • 陷阱:如果父类构造方法中调用了可被重写的方法,而子类重写了该方法,那么在子类对象初始化时,会调用到子类重写的方法,而此时子类的实例初始化可能还未完成,可能导致访问到未初始化的变量(默认值)。

    java

    scala 复制代码
    class 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关键字的四种用法

  1. 指代当前对象:在实例方法或构造方法中,引用当前正在操作的对象。

    java

    typescript 复制代码
    public void setName(String name) {
        this.name = name; // 区分同名的局部变量和实例变量
    }
  2. 调用本类的其他构造方法this(...),必须是构造方法中的第一条语句

    java

    scss 复制代码
    public Person(String name) {
        this(name, 0); // 调用另一个构造方法
    }
  3. 作为参数传递:将当前对象作为参数传递给其他方法。

    java

    kotlin 复制代码
    someMethod(this);
  4. 作为返回值:从方法中返回当前对象,支持链式调用。

    java

    kotlin 复制代码
    public Person setAge(int age) { this.age = age; return this; }
    // 使用: person.setName("Tom").setAge(25);

1.10.2 super关键字的三种用法

  1. 访问父类的成员 :当子类重写了父类的方法或隐藏了父类的字段时,使用 super. 来明确调用父类的版本。

    java

    typescript 复制代码
    @Override
    void someMethod() {
        super.someMethod(); // 先执行父类逻辑
        // ... 子类扩展逻辑
    }
  2. 调用父类的构造方法super(...),必须是子类构造方法中的第一条语句 。如果子类构造方法没有显式调用 super(...)this(...),编译器会自动插入一个无参的 super()

    java

    csharp 复制代码
    public Child() {
        super("parentArg"); // 调用父类有参构造
    }
  3. 在泛型中指代上界<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的使用限制

  1. static 上下文中不能使用 :因为 thissuper 都指向对象实例,而静态成员属于类,不依赖于任何实例。
  2. 调用 this()super() 必须是构造方法的第一条语句

第二章:Java核心类

2.1 String、StringBuffer、StringBuilder的区别

2.1.1 String的特性、源码分析和使用场景

核心答案: String是不可变的字符序列,基于final char[]实现,设计为不可变类。

详细解析:

  1. 不可变性实现

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对象
  1. 使用场景

    • 字符串常量、配置信息
    • 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 字符串拼接的性能优化

  1. 编译期优化

java

ini 复制代码
String s = "a" + "b" + "c"; // 编译期优化为 "abc"
  1. 运行时优化

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不可变的底层实现

实现机制:

  1. final类:防止被继承和修改行为
  2. final字符数组:存储数据的数组引用不可变
  3. 无修改方法:所有看似修改的方法都返回新对象
  4. 私有构造函数:控制对象创建

2.2.2 安全性考虑

  1. 作为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
  1. 网络传输安全:URL、参数等不会被意外修改
  2. 类加载安全:类名、方法名等标识符的安全

2.2.3 线程安全

  • 不可变对象天然线程安全,无需同步
  • 多线程共享时无竞态条件

2.2.4 性能优化(字符串常量池)

  1. 字符串驻留(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
  1. 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 不可变对象的模式

如何创建不可变类:

  1. 类声明为final
  2. 所有字段声明为private final
  3. 不提供setter方法
  4. 通过构造函数初始化所有字段
  5. 返回可变对象时返回深拷贝

2.3 String的intern()方法的作用和原理

2.3.1 intern()方法的定义和作用

定义: 返回字符串在常量池中的规范化表示。

java

arduino 复制代码
public native String intern();

作用:

  1. 如果常量池中已存在相同字符串,返回常量池引用
  2. 如果不存在,将字符串添加到常量池并返回引用
  3. 保证相同内容的字符串在内存中只有一份

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()方法的使用场景

  1. 节省内存:大量重复字符串时

java

ini 复制代码
List<String> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++) {
    list.add(("key" + (i % 1000)).intern()); // 只有1000个不同字符串
}
  1. 快速比较

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字符串常量池(移动到堆中)

变化原因:

  1. 永久代大小难以确定
  2. 字符串常量占用大量永久代空间
  3. 永久代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方法的作用和约定

作用: 返回对象的哈希码,用于哈希表
约定(重要!):

  1. 同一对象多次调用hashCode()应返回相同值
  2. equals()相等的对象,hashCode()必须相等
  3. 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;
}

最佳实践:

  1. 使用Objects.equals()比较字段
  2. 使用Objects.hash()生成hashCode
  3. 保持一致性:equals用到的字段,hashCode也要用到
  4. 重写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);
}

哈希码合约

  1. 一致性:同一对象多次调用应返回相同值
  2. 相等性:equals()为true的对象,hashCode()必须相等
  3. 不等性:equals()为false的对象,hashCode()尽量不相等(非必须)

2.6.3 equals(Object obj)方法

作用:比较两个对象是否相等。

java

typescript 复制代码
public boolean equals(Object obj) {
    return (this == obj);
}

equals合约

  1. 自反性:x.equals(x)必须返回true
  2. 对称性:x.equals(y) == y.equals(x)
  3. 传递性:x.equals(y) && y.equals(z) ⇒ x.equals(z)
  4. 一致性:多次调用结果相同
  5. 非空性:x.equals(null)返回false

2.6.4 clone()方法

作用:创建并返回对象的副本。

java

java 复制代码
protected native Object clone() throws CloneNotSupportedException;

使用要求

  1. 类必须实现Cloneable接口(标记接口)
  2. 默认是浅拷贝
  3. 需要深拷贝需重写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;

废弃原因

  1. 执行时机不确定
  2. 性能影响大
  3. 可能导致内存泄漏
  4. Java 9标记为废弃

替代方案

  • try-with-resources(AutoCloseable)
  • 显式cleanup方法

2.6.9 Object类的设计哲学

核心原则

  1. 通用性:所有Java对象的基类
  2. 扩展性:提供可重写的方法模板
  3. 线程安全基础:wait/notify机制
  4. 对象标识: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 不同字符编码的影响

编码方案

  1. LATIN1(ISO-8859-1):1字节/字符,覆盖西欧字符
  2. 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码点和码元

概念区分

  1. 码元(Code Unit) :编码方案的基本单位

    • UTF-8:1字节码元
    • UTF-16:2字节码元
    • String.length()返回码元数
  2. 码点(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是面向对象的语言,但基本类型不是对象。包装类型解决了三个问题:

  1. 对象化需求:让基本类型能像对象一样操作,可以调用方法
  2. 集合支持:Java集合框架只能存储对象,不能存基本类型
  3. 泛型支持 :泛型类型参数必须是类类型
    同时,Java通过自动装箱拆箱机制,减少了开发者在这两种类型间转换的代码负担。"

高频问题2:Integer缓存机制是什么?

标准答案

"Integer类内部有一个IntegerCache静态内部类,默认缓存了-128到127之间的256个Integer对象。当通过valueOf()方法或自动装箱创建在这个范围内的Integer时,会直接返回缓存的对象,而不是新建。这提高了性能,减少了内存占用,是享元模式的应用。但这也带来了一个常见陷阱:用==比较在这个范围内的Integer会返回true,超出范围则返回false,所以比较包装类型应该用equals()方法。"

高频问题3:自动装箱的性能影响?

标准答案

"自动装箱确实有性能开销,主要体现在:

  1. 对象创建开销:每次装箱都可能在堆上创建新对象
  2. 内存占用更大:Integer对象比int占用更多内存
  3. 缓存未命中时的开销 :超出缓存范围会创建新对象
    在性能敏感的代码中,比如大规模循环,应该优先使用基本类型。但在大多数业务代码中,这种开销是可以接受的,自动装箱带来的代码简洁性更重要。"

八、总结:记住这几点就够了

  1. 基本类型为效率,包装类型为功能:按需选择
  2. 比较包装类型用equals() :永远不要依赖==
  3. 性能敏感处用基本类型:循环、频繁计算
  4. 注意可能的NPE:包装类型可以为null
  5. 了解缓存范围:-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
    }
}

最佳实践

  1. 实现Cloneable接口
  2. 重写clone()方法为public
  3. 调用super.clone()开始
  4. 深拷贝所有可变引用字段
  5. 处理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);
    }
}

优点

  1. 不需要实现Cloneable接口

  2. 可以控制哪些字段被拷贝

  3. 支持final字段

  4. 更明确的API

深拷贝与浅拷贝核心对比表

对比维度 浅拷贝 深拷贝
定义 只复制对象本身,不复制内部引用字段指向的实际对象 复制对象本身及其所有引用字段指向的实际对象(递归复制整个对象图)
内存行为 原对象和拷贝对象共享引用字段的内存地址 原对象和拷贝对象独立拥有各自的引用字段内存地址
实现方式 Object.clone()(默认)、拷贝构造函数(仅复制引用) 重写clone()并递归拷贝、序列化/反序列化、手动创建新对象
对象关系 一层的对象复制 递归的深层次对象复制
修改影响 修改拷贝对象的引用字段会影响原对象 修改拷贝对象的引用字段不影响原对象
性能特点 速度快,内存消耗少 速度慢,内存消耗大
实现复杂度 简单 复杂(需处理嵌套和循环引用)
典型场景 字段主要是基本类型或不可变对象、需要共享数据 包含可变引用字段且需要完全独立副本、多线程环境、值对象传递
常见问题 意外数据共享、线程不安全 循环引用问题、性能开销大、实现复杂
注意事项 确保引用字段是线程安全的或不可变的 处理循环引用避免栈溢出、考虑性能影响
面试回答要点与记忆口诀
  1. 先说本质:"深拷贝和浅拷贝最核心的区别在于,对于对象内部的引用类型字段,是复制了引用地址(浅拷贝,导致共享),还是递归复制了整个引用对象(深拷贝,实现隔离)。"
  2. 再谈影响:"因此,修改浅拷贝对象的引用字段内容会影响原对象,而深拷贝则完全独立。"
  3. 列举方法 :"实现上,浅拷贝用默认的clone()或集合的拷贝构造。深拷贝有三种常见方式:一重写clone()(麻烦),二用序列化(通用但性能稍差),三写拷贝构造或工厂方法(最清晰推荐) 。"
  4. 不忘特例 :"对于StringInteger这类不可变对象,浅拷贝共享它们是完全安全的。"
  5. 处理难点 :"深拷贝时要注意循环引用 问题,可以通过维护一个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()
    ));

⚠️ 重要注意事项

  1. 不可变对象的特殊性:String、Integer等不可变对象在浅拷贝中是安全的
  2. 嵌套集合的深拷贝:如果集合中的元素本身也是集合,需要递归深拷贝
  3. 循环引用问题:对象图中有循环引用时,手动深拷贝可能导致栈溢出
  4. 性能考虑:深拷贝大对象或复杂对象图时性能影响显著
  5. 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的完整执行流程

执行流程总结

  1. 执行try块代码
  2. 如果异常,跳转到匹配的catch块
  3. 无论是否异常,finally都会执行(特殊情况除外)
  4. 继续执行后续代码

执行顺序示例

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代码块的重要特性

特性总结

  1. 总是执行:无论是否发生异常(特殊情况除外)
  2. 资源清理:用于释放资源(文件、连接等)
  3. 异常传播:finally中的异常会传递给调用者
  4. 覆盖返回值: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中的执行

执行规则总结

  1. return值暂存:执行return时,返回值被计算并暂存

  2. finally执行:执行finally块代码

  3. 返回值确定

    • 如果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("执行清理工作");
}));

最佳实践

  1. 避免在finally中使用return(掩盖异常)
  2. 避免在finally中抛出异常(掩盖原始异常)
  3. 优先使用try-with-resources进行资源管理
  4. 使用关闭钩子处理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 多个资源的关闭顺序

关闭规则

  1. 后声明的资源先关闭
  2. 按声明顺序的逆序关闭
  3. 每个资源的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());
    }
}

最佳实践总结

  1. 优先使用try-with-resources:减少代码复杂度,避免资源泄漏
  2. 支持多个资源:自动按正确顺序关闭
  3. 异常抑制机制:确保不丢失任何异常信息
  4. 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", "数字格式错误");
    }
}

性能最佳实践

  1. 避免在循环中使用异常
  2. 高频API考虑使用错误码
  3. 使用isDebugEnabled()避免日志性能开销
  4. 批量操作收集错误而非立即抛出

3.5 自定义异常如何实现?

3.5.1 自定义检查异常

实现要点

  1. 继承Exception类
  2. 提供多个构造方法
  3. 添加有用的字段和方法
  4. 实现序列化

示例

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 自定义运行时异常

实现要点

  1. 继承RuntimeException类
  2. 提供领域特定的字段
  3. 保持不可变性

示例

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
信息完整 包含足够上下文 包含足够上下文

常见错误

  1. 过度设计异常类
  2. 忽略序列化兼容性
  3. 缺少有意义的构造方法
  4. 不遵循命名规范

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;
    }
}

自定义异常最佳实践总结

  1. 选择合适的父类

    • 检查异常:需要调用者处理的业务错误
    • 运行时异常:编程错误,参数验证失败
  2. 提供有用的信息

    • 包含错误码、详细消息、上下文信息
    • 支持异常链,保留原始异常
  3. 遵循设计原则

    • 不可变性
    • 序列化支持
    • 命名规范
  4. 考虑国际化

    • 支持多语言错误消息
    • 使用ResourceBundle管理消息
  5. 合理的错误码设计

    • 分层编码,易于分类
    • 包含足够信息,易于排查

完整示例

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:如何设计好的错误码系统?

  • :分层编码(系统_模块_编号),语义清晰,易于扩展和分类。
相关推荐
留简2 小时前
从零搭建一个现代化后台管理系统:基于 React 19 + Vite + Ant Design Pro 的最佳实践
前端·react.js
小满zs2 小时前
Next.js第十八章(静态导出SSG)
前端·next.js
CAN11772 小时前
快速还原设计稿之工作流集成方案
前端·人工智能
A24207349302 小时前
深入浅出JS事件:从基础原理到实战进阶全解析
开发语言·前端·javascript
疯狂踩坑人2 小时前
【Nodejs】Http异步编程从EventEmitter到AsyncIterator和Stream
前端·javascript·node.js
软弹2 小时前
Vue2 - Dep到底是什么?如何简单快速理解Dep组件
前端·javascript·vue.js
晴虹2 小时前
lecen:一个更好的开源可视化系统搭建项目--介绍、搭建、访问与基本配置--全低代码|所见即所得|利用可视化设计器构建你的应用系统-做一个懂你的人
前端·后端·低代码
城东米粉儿2 小时前
Android 插件 笔记
android
WangHappy2 小时前
面试官:如何优化批量图片上传?队列机制+分片处理+断点续传三连击!
前端·node.js