Java Class
类(class)是 Java 面向对象编程的基本构建单元,它既是数据抽象的载体,也是行为封装的边界。理解 class 的核心内容,本质上是理解 Java 如何用类型系统组织数据与行为。本文从现代 Java(25 LTS)视角出发,梳理 class 设计的核心要素。
类的本质:状态与行为的封装
一个 class 由两部分构成:字段(状态)和方法(行为)。封装的目的是把可变状态收敛到最小范围,对外只暴露不可变视图。
1 | |
核心要点:
- 字段默认
private final,最大化不可变性 - 不暴露 setter,避免外部随意改写状态
final修饰类,防止意外继承破坏封装
优先用 Record 表达纯数据
当 class 只是一组数据的载体(没有复杂行为、不需要继承),优先用 record。它会在编译期自动生成构造器、访问器、equals/hashCode/toString,且组件字段隐式 private final,天然不可变。
1 | |
record 头部声明的是组件(component),头部之外可以补充实例方法、静态成员,也可以实现接口:
1 | |
限制同样是刻意的:组件之外不能追加实例字段,也不能 extends 任何类——record 隐式继承 java.lang.Record 且为 final。它的职责就是持有数据,需要可变状态请回到普通 class。
方法内部只需要临时数据结构时,用局部 record(Java 16+),避免为中间结果污染顶层命名空间:
1 | |
配合模式匹配还能直接解构组件,顺序与声明一致:
1 | |
record 适合值对象、DTO、领域事件等;需要校验不变量时用紧凑构造器(见下文)。一旦需要可变状态或继承层次,再回到普通 class。
构造与创建:静态工厂优于构造器
构造器容易重载混乱、无法缓存实例,静态工厂方法更灵活且语义清晰。
1 | |
静态工厂的优势:
- 可校验参数、可返回缓存或子类型实例
- 方法名能表达意图(
of、from、valueOf) - 构造器保持
private,强制走受控的创建路径
继承体系:Sealed + Pattern Matching
传统继承容易失控,Java 25 推荐用 sealed 接口限定实现范围,配合 switch 模式匹配实现穷尽检查,构成代数数据类型。
1 | |
设计原则:组合优于继承。继承表达 “is-a”,组合表达 “has-a”;后者耦合更低、更易测试。
1 | |
接口:抽象契约与行为协议
interface 定义的是能力契约而非实现,它是 Java 实现多态、解耦的核心机制。与 class 不同,interface 关注"能做什么",class 关注"是什么"。
基本形态与演进
早期 interface 只能包含抽象方法和常量。从 Java 8 起支持 default 和 static 方法,Java 9 起支持 private 方法。现代 interface 已兼具契约定义与代码复用能力。
1 | |
注意几个隐式约定:interface 中的方法默认 public abstract,字段默认 public static final——它们天然是常量,不能放可变状态。
函数式接口
只有一个抽象方法的 interface 可用 @FunctionalInterface 标注,作为 lambda 的目标类型。这是函数式编程在 Java 中的基石。
1 | |
JDK 内置 Function、Predicate、Consumer、Supplier 等函数式接口,优先复用而非自定义。
多实现与菱形冲突
一个 class 可以 implements 多个 interface,这是 Java 解决多继承的方式。当多个 interface 提供同名 default 方法时,编译器强制要求子类覆盖以消除歧义:
1 | |
Sealed Interface:受限的类型层次
用 sealed 限定哪些类型可以 implements,配合 permits 列表实现穷尽检查(见上文继承体系章节)。这是现代 Java 表达代数数据类型的标准方式:把所有可能取值收拢到固定集合,让编译器替你保证 switch 分支完整。
interface 与 abstract class 的取舍
| 维度 | interface | abstract class |
|---|---|---|
| 状态字段 | 只能有常量(隐式 public static final) | 可有实例字段 |
| 多继承 | 支持多 implements | 单继承 |
| 构造器 | 无 | 有 |
| 表达力 | “能做什么”(能力) | “是什么”(归类) |
经验法则:优先 interface。只有当需要共享实例状态或构造逻辑时才用 abstract class。interface 可被多个不相关的类实现,扩展性更强;而 interface 的演化靠 default 方法平滑推进,不会破坏既有实现。
可见性:最小化暴露
可见性按 private > 包私有 > protected > public 的顺序从严选择,只暴露必要的契约。
| 修饰符 | 同类 | 同包 | 子类(跨包) | 其他包 | 典型场景 |
|---|---|---|---|---|---|
private | ✅ | ❌ | ❌ | ❌ | 字段、辅助方法、内部实现细节 |
| 包私有(不写修饰符) | ✅ | ✅ | ❌ | ❌ | 包内协作的类、测试辅助方法 |
protected | ✅ | ✅ | ✅ | ❌ | 模板方法模式中供子类覆写的钩子 |
public | ✅ | ✅ | ✅ | ✅ | API 契约、接口方法、跨包复用的类型 |
从严选择原则:先给 private,不够再逐级放宽。绝大多数类和成员不需要 public。
| 维度 | 建议 |
|---|---|
| 顶层类 | 尽量包私有;只有跨包使用的才 public |
| 字段 | 一律 private + 不可变(final),通过 record 或 accessor 暴露 |
| 方法 | 内部辅助方法 private;只有契约方法按需提升 |
| 构造器 | 需要控制实例化时 private(工厂/Builder 模式),否则按需开放 |
| 嵌套类 | 仅外部类使用的嵌套类 private static |
| 接口方法 | 默认 public abstract;default 方法用于平滑演化 |
1 | |
字段、辅助方法、内部状态一律 private;只有需要跨包复用的契约才提升到 public。
方法设计:校验与 Optionality
方法入口应尽早校验参数,失败原子性保证异常后对象仍有效。
1 | |
对于"可能无值"的返回,用 Optional 而非 null。注意 Optional 只用于返回值,不要塞进参数或字段。
1 | |
不可变与并发安全
不可变对象天然线程安全,是并发编程的首选。必须共享可变状态时,用并发集合或 final 字段加锁隔离。
1 | |
高并发 I/O 场景下,配合虚拟线程可以以同步写法获得异步吞吐:
1 | |
枚举:受限的 class
enum 本质是继承自 java.lang.Enum 的 final class,实例在类加载时静态创建、全局唯一。它天然不可变、线程安全、单例语义明确,适合表达固定的取值集合。
1 | |
枚举真正的威力在于可以携带字段与行为,把相关的常量、元数据、策略集中在一处:
1 | |
要点:
- 枚举是实现单例的最佳方式(线程安全、防序列化攻击,无需手写双重检查锁)
- 配合
switch模式匹配可获得穷尽检查,新增常量时编译器强制处理 - 不要用
ordinal()做逻辑判断,它的顺序是脆弱的实现细节
嵌套类:收敛辅助类型
嵌套类把只服务于外部类的类型藏在其内部,减少包级污染。优先级:静态嵌套类 > 内部类 > 匿名类/局部类。
1 | |
经验法则:嵌套类若不需要访问外部实例状态,一律加 static。Lambda 和 record 在多数场景下已取代匿名类与局部类——能用 lambda 表达的策略,就别写匿名内部类。
泛型 class:类型安全的参数化
泛型让 class 在定义时推迟具体类型,使用时再指定,编译期即可消除类型转换与不安全访问。核心收益是把类型错误从运行期提前到编译期。
1 | |
几个易错点:
- 类型擦除:运行时
Stack<String>与Stack<Integer>是同一个类,泛型信息只保留在 class 文件的Signature属性中(见下文);不能new E()、不能new T[...],运行时也无法用instanceof Stack<String>区分 - 边界通配符:生产者用
<? extends T>(读)、消费者用<? super T>(写),即 PECS 原则
- 无边界通配符
<?>表达"任意类型但不依赖它",比原始类型Stack安全得多,原始类型仅为兼容遗留代码保留
Record 的紧凑构造器与校验
record 默认生成的构造器不做任何校验。需要保证不变量时,用紧凑构造器(compact constructor)在校验后赋值,保证对象一旦创建就处于有效状态。
1 | |
紧凑构造器在自动赋值前执行,赋值语句会覆盖默认行为。这是把"创建即合法"的值对象模式落到 record 上的标准写法——校验逻辑与类型定义同处一地,无需散落的工厂方法。
从源文件到 class 文件
语言层面的设计最终要落到可执行的形态。javac 把 .java 翻译成 .class,这是一次面向 JVM 规范的翻译:高级语法在此被"脱糖"降级为 JVM 能直接表达的指令,产出的字节流不依赖任何具体平台。
各阶段的职责:
- 解析:词法与语法分析,产出抽象语法树(AST)
- 符号填充:把类、方法、字段的声明登记进符号表
- 注解处理:处理器可以生成新源码并触发下一轮编译,Lombok、MapStruct 都插在这一环
- 数据流分析:类型检查、赋值检查、泛型推断、
switch穷尽性检查都在此完成 - 脱糖(desugar):现代语法降级为基础形态——lambda 与方法引用转成
invokedynamic指令,字符串拼接转成对StringConcatFactory的invokedynamic调用,增强 for 循环还原为迭代器遍历,装箱拆箱、变长参数全部显式化 - 生成:写出 class 文件
class 文件是一张结构紧凑的表,JVM 规范严格定义了每个区域:
| 组成部分 | 说明 |
|---|---|
| Magic Number | 0xCAFEBABE,class 文件的身份证 |
| 版本号 | minor 与 major 各 2 字节,Java 25 对应 major 69 |
| 常量池 | 全文件的枢纽:类名、方法名、描述符、字面量集中存放,其余结构按索引引用 |
| 访问标志 | ACC_PUBLIC、ACC_FINAL、ACC_SUPER 等修饰符位图 |
| 类、父类、接口索引 | 继承关系以常量池索引表达,java.lang.Object 的父类索引为 0 |
| 字段表、方法表 | 每项含访问标志、名称、描述符与属性;方法的 Code 属性就是字节码本体 |
| 属性表 | SourceFile、LineNumberTable、BootstrapMethods、Signature 等辅助信息 |
用一个最小的类做实验:
1 | |
编译后用 javap -v 观察产物(节选):
1 | |
文件开头正是魔数与版本:CA FE BA BE 00 00 00 45,0x0045 即 major 69。三个值得注意的细节:
- 泛型只保留签名:类型擦除发生在这一步,
Stack<String>与Stack<Integer>编译出相同字节码;擦除前的完整签名被写进Signature属性,getGenericType()读到的正是它——这解释了"泛型运行时不可用"与"反射却能拿到泛型信息"这对看似矛盾的事实 - 描述符是 class 文件的标准类型写法:
()Ljava/lang/String;表示无参、返回 String;引用类型一律写成L前缀加分号的路径 - invokedynamic 是脱糖的痕迹:源码里的字符串拼接没有生成
StringBuilder链,而是转成对StringConcatFactory的动态调用,JVM 在首次执行时才把调用点绑定为具体实现,JIT 可将其完全内联
class 在 JVM 中的形态
class 文件只是静态字节流,要经过类加载变成运行时结构才能使用。本节把三样东西连起来看:类元数据(Klass)、堆上的对象、以及 java.lang.Class 镜像——它们的关系正是对象头与反射机制的底层基础。
根类型 Object
所有 class 隐式继承 java.lang.Object,它定义了一组通用契约:equals/hashCode/toString/getClass/clone/finalize,以及 wait/notify/notifyAll 这套管程协同原语。其中 hashCode 和 equals 必须一致——equals 相等的对象必须有相同 hashCode,这是把对象放进 HashMap/HashSet 的前提。
1 | |
record 会自动生成与组件字段一致的 equals/hashCode,这是它适合做 Map key 的原因之一。
加载、链接与初始化
JVM 把 class 文件变成可用的类,走三步五阶段:
- 加载:按全限定名读取字节流,在元空间生成类元数据,同时在堆上创建一个
java.lang.Class实例作为访问入口 - 验证:文件格式、元数据、字节码、符号引用四类检查,确保字节流合法可执行;class 文件的
StackMapTable属性就是为加速字节码验证准备的 - 准备:为静态字段分配内存并置默认值(
0、null);带ConstantValue的static final常量在此阶段直接完成赋值 - 解析:把常量池中的符号引用替换为直接引用(内存地址、偏移量);JVM 允许惰性解析,首次真正使用才做
- 初始化:执行编译器合成的
<clinit>()——静态字段赋值与静态代码块的合并体。JVM 加锁保证<clinit>每类只执行一次,这正是"静态内部类单例"无需手写同步的根源
触发初始化的是主动使用:new、读写非编译期常量的静态字段、反射调用、初始化子类连带初始化父类。而 MyClass.class 这样的类字面量只会触发加载,不会初始化。
Klass 与 Class 镜像
HotSpot 中类的运行时表示是元空间里的 instanceKlass,它承载字段元信息与实例布局(对象大小、字段偏移量)、方法字节码与支撑虚分派的 vtable/itable、运行时常量池、父类与接口链、注解数据。
而 java.lang.Class 对象是 instanceKlass 在堆上的镜像:两者互相持有引用。堆上每个对象又通过对象头的 Klass Pointer 关联所属的 instanceKlass,obj.getClass() 走的正是这条链:对象 → Klass Pointer → instanceKlass → Class 镜像。虚方法分派、instanceof、强制转换也都沿这条链完成。
对象头的详细结构
每个堆对象都有一个对象头,64 位 HotSpot 上由 Mark Word 与 Klass Pointer 组成。Mark Word 是一个多用途的复用字段,内容随锁状态切换:
| 状态 | 64 bit 布局 | 锁标志位 |
|---|---|---|
| 无锁 | 25 位闲置、31 位 identity hash、1 位闲置、4 位 GC 年龄 | 01 |
| 轻量级锁 | 62 位指向栈上 Lock Record 的指针 | 00 |
| 重量级锁 | 62 位指向 ObjectMonitor 的指针 | 10 |
| GC 标记 | 62 位标记信息 | 11 |
几个关键点:
- 存的是 identity hash:Mark Word 里的是
System.identityHashCode的结果,与用户重写的hashCode()无关——后者只是普通的虚方法调用。identity hash 惰性计算,首次调用时写入,之后复用 - 锁状态就是状态机:synchronized 无竞争时用 CAS 把 Mark Word 替换为栈上 Lock Record 的指针(轻量级锁),竞争加剧再膨胀为重量级锁。偏向锁自 JDK 15(JEP 374)废弃后,Java 25 直接从无锁/轻量级开始升级
- GC 年龄占 4 bit:分代年龄上限为 15(
-XX:MaxTenuringThreshold),正是受限于这四位 - 指针压缩:堆小于 32 GB 时默认开启,Klass Pointer 与对象引用都压缩到 32 bit,显著降低内存占用
对象头之后才是实例字段,且 JVM 会对字段做重排序与对齐填充以优化访问——这也是 record 紧凑布局省内存的原因。
反射的基础实现
反射的一切都从 Class 镜像出发,前三小节的结构是它的地基:
- 查找有缓存:
getMethods()、getDeclaredFields()的结果缓存在 Class 对象内部的ReflectionData(软引用)中,首次查询从 instanceKlass 的声明信息构建,之后直接复用——"反射慢"主要慢在首次构建与调用链,而不是每次都重扫类 - 字段按偏移量读写:
Field.get()/set()底层按 instanceKlass 记录的字段偏移量直接访问对象内存;setAccessible(true)只是跳过 Java 语言访问检查,仍受模块系统约束 - 方法调用走 MethodHandle:JDK 18(JEP 416)起
Method.invoke()基于方法句柄重新实现,取代了旧的"JNI 原生调用 + 达到阈值后生成字节码访问器"的膨胀机制(旧实现在 JDK 22 移除);反射对象稳定时,JIT 能把调用链内联到接近直接调用的水平 - 更直接的替代:需要极致性能时用
MethodHandles.Lookup与VarHandle直接持有方法/字段句柄,绕开反射 API 的查找与装箱;lambda(LambdaMetafactory)与 record 的解构也都建立在 invokedynamic 之上
实践含义
对象头与反射机制不是纯理论,它们直接影响工程决策:
- 用
IdentityHashMap区分引用相等与equals相等,依赖的正是 Mark Word 里的 identity hash - 大量小对象的内存开销里,对象头占比可观——LRU 缓存、图结构等场景需要考虑紧凑表示
- 锁的代价与 Mark Word 状态机挂钩:无竞争时近乎免费,竞争激烈时膨胀为重量级锁开销陡增,因此优先无状态/不可变设计
- 框架大量依赖反射做注入与序列化(Spring、Jackson、MyBatis),把
Method/Field缓存为static final让 JIT 常量折叠是最有效的优化手段;AOT 场景(GraalVM native image)则在编译期直接消除反射
小结
Java class 的核心可以用一句话概括:用不可变状态 + 受控创建 + 最小可见性 + 组合优先,组织数据与行为。落到具体选择上:
- 纯数据用
record,复杂对象用final class - 创建用静态工厂,继承用
sealed限定 - 可见性从严,方法入口校验
- 默认不可变,并发场景优先无状态设计
把握这几条,写出的 class 通常是简洁、安全、易测试的。





