类(class)是 Java 面向对象编程的基本构建单元,它既是数据抽象的载体,也是行为封装的边界。理解 class 的核心内容,本质上是理解 Java 如何用类型系统组织数据与行为。本文从现代 Java(25 LTS)视角出发,梳理 class 设计的核心要素。

类的本质:状态与行为的封装

一个 class 由两部分构成:字段(状态)和方法(行为)。封装的目的是把可变状态收敛到最小范围,对外只暴露不可变视图。

public final class User {
    private final String name;   // 不可变状态
    private final String email;

    public User(String name, String email) {
        this.name = Objects.requireNonNull(name);
        this.email = Objects.requireNonNull(email);
    }

    public String name() { return name; }   // 只读访问器
    public String email() { return email; }
}

核心要点:

  • 字段默认 private final,最大化不可变性
  • 不暴露 setter,避免外部随意改写状态
  • final 修饰类,防止意外继承破坏封装

优先用 Record 表达纯数据

当 class 只是一组数据的载体(没有复杂行为、不需要继承),优先用 record。它会自动生成构造器、访问器、equals/hashCode/toString,且天然不可变。

public record Point(int x, int y) {}

var p = new Point(1, 2);
int x = p.x();   // 访问器是方法形式,而非字段

record 适合值对象、DTO、领域事件等。一旦需要可变状态或继承层次,再回到普通 class。

构造与创建:静态工厂优于构造器

构造器容易重载混乱、无法缓存实例,静态工厂方法更灵活且语义清晰。

public final class Email {
    private final String value;

    private Email(String value) { this.value = value; }

    public static Email of(String value) {
        Objects.requireNonNull(value, "email must not be null");
        if (!value.contains("@")) {
            throw new IllegalArgumentException("invalid email: " + value);
        }
        return new Email(value);
    }

    public String value() { return value; }
}

静态工厂的优势:

  • 可校验参数、可返回缓存或子类型实例
  • 方法名能表达意图(offromvalueOf
  • 构造器保持 private,强制走受控的创建路径

继承体系:Sealed + Pattern Matching

传统继承容易失控,Java 25 推荐用 sealed 接口限定实现范围,配合 switch 模式匹配实现穷尽检查,构成代数数据类型

public sealed interface Shape permits Circle, Rectangle {}

public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}

double area = switch (shape) {
    case Circle c -> Math.PI * c.radius() * c.radius();
    case Rectangle r -> r.width() * r.height();
};
// 编译器保证所有分支被覆盖,新增实现会强制提示
Java Sealed Interfaces & Pattern Matching

设计原则:组合优于继承。继承表达 “is-a”,组合表达 “has-a”;后者耦合更低、更易测试。

// 错误:继承滥用
public class AdminUser extends User {}

// 正确:组合
public record UserWithRole(User user, Role role) {}

接口:抽象契约与行为协议

interface 定义的是能力契约而非实现,它是 Java 实现多态、解耦的核心机制。与 class 不同,interface 关注"能做什么",class 关注"是什么"。

基本形态与演进

早期 interface 只能包含抽象方法和常量。从 Java 8 起支持 defaultstatic 方法,Java 9 起支持 private 方法。现代 interface 已兼具契约定义与代码复用能力。

public interface Repository<T, ID> {
    Optional<T> findById(ID id);      // 抽象方法:实现类必须提供

    // default 方法:提供默认实现,实现类可按需覆盖
    default Optional<T> findByIdOrFail(ID id) {
        return findById(id).orElseThrow(() ->
            new NoSuchElementException("not found: " + id));
    }

    // static 方法:承载工厂与工具方法
    static <E> Repository<E, String> inMemory() {
        return new InMemoryRepository<>();
    }

    // private 方法:复用多个 default 方法之间的逻辑
    private void log(String msg) {
        System.out.println("[repo] " + msg);
    }
}

注意几个隐式约定:interface 中的方法默认 public abstract,字段默认 public static final——它们天然是常量,不能放可变状态。

函数式接口

只有一个抽象方法的 interface 可用 @FunctionalInterface 标注,作为 lambda 的目标类型。这是函数式编程在 Java 中的基石。

@FunctionalInterface
public interface PriceCalculator {
    double calculate(Order order);   // 唯一抽象方法

    // default 方法不计入抽象方法计数
    default PriceCalculator andThen(double discount) {
        return order -> calculate(order) * (1 - discount);
    }
}

PriceCalculator calc = order -> order.total() * 0.9;

JDK 内置 FunctionPredicateConsumerSupplier 等函数式接口,优先复用而非自定义。

多实现与菱形冲突

一个 class 可以 implements 多个 interface,这是 Java 解决多继承的方式。当多个 interface 提供同名 default 方法时,编译器强制要求子类覆盖以消除歧义:

interface A { default String hello() { return "A"; } }
interface B { default String hello() { return "B"; } }

class C implements A, B {
    @Override
    public String hello() {
        return A.super.hello();   // 显式选择某一个的实现
    }
}

Sealed Interface:受限的类型层次

sealed 限定哪些类型可以 implements,配合 permits 列表实现穷尽检查(见上文继承体系章节)。这是现代 Java 表达代数数据类型的标准方式:把所有可能取值收拢到固定集合,让编译器替你保证 switch 分支完整。

interface 与 abstract class 的取舍

维度interfaceabstract class
状态字段只能有常量(隐式 public static final可有实例字段
多继承支持多 implements单继承
构造器
表达力“能做什么”(能力)“是什么”(归类)

经验法则:优先 interface。只有当需要共享实例状态或构造逻辑时才用 abstract class。interface 可被多个不相关的类实现,扩展性更强;而 interface 的演化靠 default 方法平滑推进,不会破坏既有实现。

可见性:最小化暴露

可见性按 private > 包私有 > protected > public 的顺序从严选择,只暴露必要的契约

修饰符同类同包子类(跨包)其他包典型场景
private字段、辅助方法、内部实现细节
包私有(不写修饰符)包内协作的类、测试辅助方法
protected模板方法模式中供子类覆写的钩子
publicAPI 契约、接口方法、跨包复用的类型

从严选择原则:先给 private,不够再逐级放宽。绝大多数类和成员不需要 public

维度建议
顶层类尽量包私有;只有跨包使用的才 public
字段一律 private + 不可变(final),通过 record 或 accessor 暴露
方法内部辅助方法 private;只有契约方法按需提升
构造器需要控制实例化时 private(工厂/Builder 模式),否则按需开放
嵌套类仅外部类使用的嵌套类 private static
接口方法默认 public abstractdefault 方法用于平滑演化
public interface UserService {
    Optional<User> findById(String id);
    User create(String name, String email);
}

字段、辅助方法、内部状态一律 private;只有需要跨包复用的契约才提升到 public

方法设计:校验与 Optionality

方法入口应尽早校验参数,失败原子性保证异常后对象仍有效。

public User createUser(String name, String email) {
    Objects.requireNonNull(name, "name must not be null");
    if (name.isBlank()) {
        throw new IllegalArgumentException("name must not be blank");
    }
    return new User(name, email);
}

对于"可能无值"的返回,用 Optional 而非 null。注意 Optional 只用于返回值,不要塞进参数或字段。

public Optional<User> findById(String id) {
    return Optional.ofNullable(repository.find(id));
}

不可变与并发安全

不可变对象天然线程安全,是并发编程的首选。必须共享可变状态时,用并发集合或 final 字段加锁隔离。

// 不可变、无状态:天然安全
public record Processor(Config config) {
    public Result process(Input input) {
        return new Result(transform(input));
    }
}

// 必须共享时用并发集合
private final ConcurrentHashMap<String, User> cache = new ConcurrentHashMap<>();

高并发 I/O 场景下,配合虚拟线程可以以同步写法获得异步吞吐:

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> doTask());
}

枚举:受限的 class

enum 本质是继承自 java.lang.Enum 的 final class,实例在类加载时静态创建、全局唯一。它天然不可变、线程安全、单例语义明确,适合表达固定的取值集合

public enum Role {
    ADMIN, USER, GUEST;
}

枚举真正的威力在于可以携带字段与行为,把相关的常量、元数据、策略集中在一处:

public enum OrderStatus {
    PENDING(false),
    PAID(true),
    SHIPPED(true),
    CANCELLED(false);

    private final boolean terminal;   // 每个实例自带状态

    OrderStatus(boolean terminal) { this.terminal = terminal; }

    public boolean isTerminal() { return terminal; }
}

要点:

  • 枚举是实现单例的最佳方式(线程安全、防序列化攻击,无需手写双重检查锁)
  • 配合 switch 模式匹配可获得穷尽检查,新增常量时编译器强制处理
  • 不要用 ordinal() 做逻辑判断,它的顺序是脆弱的实现细节

嵌套类:收敛辅助类型

嵌套类把只服务于外部类的类型藏在其内部,减少包级污染。优先级:静态嵌套类 > 内部类 > 匿名类/局部类

public final class Cache<K, V> {
    // 静态嵌套类:不持有外部类引用,首选
    private static final class Node<K, V> {
        K key; V value; Node<K, V> next;
    }

    // 内部类(非 static):隐式持有外部类引用,
    // 既阻碍 GC 又容易隐藏 bug,现代 Java 几乎用不到
    private class View implements Iterator<V> { /* ... */ }
}

经验法则:嵌套类若不需要访问外部实例状态,一律加 static。Lambda 和 record 在多数场景下已取代匿名类与局部类——能用 lambda 表达的策略,就别写匿名内部类。

泛型 class:类型安全的参数化

泛型让 class 在定义时推迟具体类型,使用时再指定,编译期即可消除类型转换与不安全访问。核心收益是把类型错误从运行期提前到编译期

public final class Stack<E> {
    private final List<E> elements = new ArrayList<>();

    public void push(E e) { elements.add(e); }
    public E pop() {
        if (elements.isEmpty()) throw new NoSuchElementException();
        return elements.removeLast();
    }
}

几个易错点:

  • 类型擦除:运行时 Stack<String>Stack<Integer> 是同一个类,泛型信息只在编译期存在;不能 new E()、不能 new T[...],运行时也无法用 instanceof Stack<String> 区分

  • 边界通配符:生产者用 <? extends T>(读)、消费者用 <? super T>(写),即 PECS 原则

    PECS Principle in Java Generics
  • 无边界通配符 <?> 表达"任意类型但不依赖它",比原始类型 Stack 安全得多,原始类型仅为兼容遗留代码保留

Record 的紧凑构造器与校验

record 默认生成的构造器不做任何校验。需要保证不变量时,用紧凑构造器(compact constructor)在校验后赋值,保证对象一旦创建就处于有效状态。

public record Email(String value) {
    public Email {                       // 紧凑构造器,无参数列表
        Objects.requireNonNull(value);
        if (!value.contains("@")) {
            throw new IllegalArgumentException("invalid email: " + value);
        }
        value = value.trim().toLowerCase();   // 可规范化赋值
    }
}

紧凑构造器在自动赋值前执行,赋值语句会覆盖默认行为。这是把"创建即合法"的值对象模式落到 record 上的标准写法——校验逻辑与类型定义同处一地,无需散落的工厂方法。

Object 与对象头

前面讨论的都是语言层面的 class 抽象,但运行时每个对象在堆上都有具体布局。理解这点,才能解释 hashCode、锁、GC 的底层行为。

根类型 Object

所有 class 隐式继承 java.lang.Object,它定义了一组通用契约:equals/hashCode/toString/getClass/clone/finalize,以及 wait/notify/notifyAll 这套管程协同原语。其中 hashCodeequals 必须一致——equals 相等的对象必须有相同 hashCode,这是把对象放进 HashMap/HashSet 的前提。

@Override
public boolean equals(Object o) {
    return o instanceof User u && u.name.equals(name) && u.email.equals(email);
}

@Override
public int hashCode() {
    return Objects.hash(name, email);   // 与 equals 字段一致
}

record 会自动生成与组件字段一致的 equals/hashCode,这是它适合做 Map key 的原因之一。

对象头(Object Header)

每个堆对象在 JVM 内部都有一个对象头,主要由两部分构成:

  • Mark Word(64 位上通常 64 bit):存储运行时元数据——identity hashcode、GC 分代年龄、锁状态标志等
  • Klass Pointer:指向类元数据(Class 对象在元空间的表示),开启指针压缩时压缩为 32 bit
JVM Object Heap Layout

几个关键点:

  • hashcode 惰性计算hashCode() 首次调用时才写入 Mark Word,之后复用;这也是为什么重写 hashCode 后仍要保证一致性
  • 锁状态复用 Mark Word:synchronized 的轻量级锁、重量级锁就是通过改写 Mark Word 实现的;偏向锁自 JDK 15(JEP 374)起默认禁用并废弃,Java 25 中锁升级直接从无锁/轻量级开始
  • GC 年龄占 4 bit:分代年龄上限为 15(-XX:MaxTenuringThreshold),正是受限于这 4 位
  • 指针压缩:堆小于 32 GB 时默认开启,Klass Pointer 与对象引用都压缩到 32 bit,显著降低内存占用

对象头之后才是实例字段,且 JVM 会对字段做重排序与对齐填充以优化访问。这也是 record 和紧凑布局的价值——字段少、对齐开销小,对象更省内存。

实践含义

对象头不是纯理论,它直接影响工程决策:

  • IdentityHashMap 区分引用相等与 equals 相等,依赖的正是 identity hashcode(Mark Word 里的那个)
  • 大量小对象的内存开销里,对象头占比可观——这正是 LRU 缓存、图结构等场景需要考虑紧凑表示的原因
  • 锁的代价与 Mark Word 状态机挂钩:无竞争时近乎免费,竞争激烈时膨胀为重量级锁开销陡增,因此优先无状态/不可变设计

小结

Java class 的核心可以用一句话概括:用不可变状态 + 受控创建 + 最小可见性 + 组合优先,组织数据与行为。落到具体选择上:

  • 纯数据用 record,复杂对象用 final class
  • 创建用静态工厂,继承用 sealed 限定
  • 可见性从严,方法入口校验
  • 默认不可变,并发场景优先无状态设计

把握这几条,写出的 class 通常是简洁、安全、易测试的。