1. 概述

本文深入剖析 Java 中的 public 访问修饰符,重点讲解它在类和成员上的使用场景与最佳实践。

同时,我们会指出将字段声明为 public 带来的隐患和设计缺陷。

如果你对 Java 的访问控制机制还不熟悉,建议先阅读我们的《Java 访问修饰符详解》一文。

✅ 核心观点:public 是定义 API 的关键,但过度暴露会带来维护成本和设计僵化。


2. 何时使用 public 修饰符

public 修饰的类、接口及其成员,共同构成了我们对外暴露的 API。这是其他开发者能够看到并用于控制对象行为的部分。

⚠️ 但滥用 public 会破坏面向对象编程(OOP)的封装原则,带来以下问题:

  • API 膨胀:接口变大,使用者难以聚焦核心功能
  • 耦合加剧:外部代码依赖了内部实现,后续修改极易“牵一发而动全身”,导致兼容性问题

💡 踩坑提示:不要为了“方便测试”或“图省事”而把字段设为 public,这是典型的短期便利、长期还债。


3. public 接口与类

3.1 public 接口

public 接口定义了一套规范,可以有多个具体实现。这些实现可以由你提供,也可以由第三方完成。

典型的例子是 JDBC 中的 Connection 接口。JDK 只定义了连接数据库的行为契约,具体实现交由各数据库厂商完成:

Connection connection = DriverManager.getConnection(url);

这里的 getConnection() 返回的是某个具体技术栈的实现类实例(如 com.mysql.cj.jdbc.ConnectionImpl),但调用方无需关心。

✅ 这就是“面向接口编程”的精髓:解耦定义与实现。


3.2 public 类

public 类允许外部通过实例化或静态引用使用其成员。例如:

assertEquals(0, new BigDecimal(0).intValue()); // 调用实例方法
assertEquals(2147483647, Integer.MAX_VALUE);   // 访问静态常量

此外,我们还可以设计 可被继承的 public 类,通常配合 abstract 修饰符使用。

🔍 关键理解:abstract 类就像一个骨架,既提供了可复用的字段和方法实现,又强制子类实现抽象方法。

Java 集合框架中的 AbstractList 就是典型例子:

public class ListOfThree<E> extends AbstractList<E> {

    @Override
    public E get(int index) {
        // 自定义获取元素逻辑
    }

    @Override
    public int size() {
        // 自定义大小逻辑
    }
}

你只需实现 get()size(),其他方法如 indexOf()containsAll() 已由父类默认实现。

⚠️ 注意:设计可继承的 public 类需格外谨慎,一旦发布就很难修改,否则会破坏所有子类。


3.3 嵌套 public 类与接口

嵌套的 public 类/接口也是一种 API 设计手段,相比顶层声明有两个优势:

  • ✅ 明确表达“外层类与内层类型”之间的强关联,提升语义清晰度
  • ✅ 减少源文件数量,代码更紧凑

典型案例如 Map.Entry

for (Map.Entry<String, String> entry : mapObject.entrySet()) { }

Entry 作为 Map 的嵌套接口,直观表明它是 Map 的组成部分,避免在 java.util 下多出一个独立的 Entry 类文件。

📚 延伸阅读:更多细节可参考《Java 嵌套类详解》。


4. public 方法

public 方法用于暴露可执行的操作。例如 String 类中的:

assertEquals("alex", "ALEX".toLowerCase());

如果方法不依赖实例字段,完全可以声明为 public static,便于直接调用。典型如:

assertEquals(1, Integer.parseInt("1"));

构造器通常也是 public,以便外部创建实例。但也有例外,比如单例模式中构造器是 private:

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {} // 防止外部实例化

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

✅ 简单粗暴总结:public 方法 = 对外暴露的行为契约。


5. public 字段:能不用就不用

public 字段允许直接修改对象状态,这是非常危险的做法,应尽量避免

5.1 线程安全问题 ❌

非 final 的 public 字段,或 final 但指向可变对象的字段,天然不具备线程安全性。多个线程可随意修改其值或状态,无法控制。

📚 建议阅读:Java 线程安全实践指南


5.2 无法干预修改行为 ❌

public 字段一旦暴露,你就失去了对赋值过程的控制权。

✅ 正确做法:使用 private 字段 + public setter,加入校验逻辑:

public class Student {
    private int age;
    
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("年龄不合法");
        }
        this.age = age;
    }
}

这样既能保护数据一致性,又能灵活扩展(比如记录日志、触发事件等)。


5.3 数据类型变更困难 ❌

public 字段一旦发布,就成了客户端契约的一部分。后续想改内部表示?几乎不可能,因为外部代码可能直接依赖了该字段。

✅ 正确做法:隐藏字段,通过 getter/setter 解耦内外:

public class Student {
    private StudentGrade grade; // 内部改用对象表示
    
    public void setGrade(int grade) {        
        this.grade = new StudentGrade(grade);
    }

    public int getGrade() {
        return this.grade.getGrade().intValue();
    }
}

对外仍返回 int,内部却可以自由演进。这就是封装的价值。


5.4 唯一例外:静态不可变常量 ✅

唯一可以接受 public 字段的场景是:public static final 不可变常量

public static final String SLASH = "/";
public static final int MAX_RETRY_COUNT = 3;

这类字段值不可变,且通常用于配置或通用符号,暴露无副作用。

⚠️ 注意:必须是 final 且指向不可变对象(如 String、Integer、基本类型),否则仍有风险。


6. 总结

  • public 是定义 API 的核心手段,用于类、接口、方法和极少数常量字段
  • 过度使用 public 会导致 API 膨胀、耦合严重,限制后续重构
  • 字段尽量私有化,通过 getter/setter 控制访问,保障封装性和扩展性
  • 唯一可接受的 public 字段是 public static final 不可变常量

💡 最后提醒:API 设计是一门平衡艺术——既要易用,又要留足演进空间。别让今天的“方便”,成为明天的“技术债”。

所有示例代码已上传至 GitHub:https://github.com/eugenp/tutorials/tree/master/core-java-modules/core-java-lang-oop-modifiers


原始标题:Java ‘public’ Access Modifier | Baeldung