首页 / Java 入门教程 / 封装与访问修饰符

Java 入门教程

封装与访问修饰符

本教程共 100 篇 · 第 36 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

JavaJava 入门教程封装访问修饰符getter-setterprivate

36. 封装与访问修饰符

本节目标:理解封装是「把数据藏起来、只留受控接口」,掌握 private、默认、protected、public 四种访问级别,并学会用 getter/setter 安全读写私有属性。

封装是面向对象第一特性,核心就一句:把内部细节藏起来,只给别人必要的入口。就像电视用遥控操作,不用伸手进电路板。

不封装会付出什么代价

先看一个把字段直接暴露出去的类:

public class Student {
    public String name;
    public int age;
}
```java

写起来是省事,用起来也直接。麻烦在于外面爱怎么改就怎么改:

```java
Student s = new Student();
s.age = -5;          // 编译通过,运行正常,数据却是荒谬的
s.age = 99999;       // 同样拦不住
s.name = null;       // 后面谁调 s.name.length() 就崩

这些代码全都能编译通过。等到线上报出 NullPointerException,你得翻遍整个项目找是谁把 name 设成了 null——因为任何一行代码都有嫌疑

更糟的是重构。哪天你想把 age 换成 birthYear,实时算年龄,结果发现全项目有两百处直接读 s.age。改不动了。

封装解决的正是这两件事:把非法数据挡在门外,把内部实现的改动圈在类里面

四种访问修饰符

Java 用修饰符控制「谁能看到这个成员」,一共四档:

修饰符同一个类同一个包不同包的子类任意位置
private可见不可见不可见不可见
默认(不写)可见可见不可见不可见
protected可见可见可见不可见
public可见可见可见可见

从上到下逐渐放宽。private 最严,只有自己类里能用;public 最松,谁都能用。

有两个细节容易记错。

默认级别不写任何关键字,它的正式名字叫「包级私有」。注意没有 default 这个修饰符可以写在成员前面——default 是接口默认方法用的关键字,两码事。

protected 比默认更宽,不是更窄。 它在包内的可见性和默认级别一样,额外多给了「不同包的子类」这一档。很多人以为 protected 只对子类开放,其实同包的非子类也能访问。

越权访问的真实报错

试着在别的类里碰 private 字段:

public class Student {
    private String name;
}

class Test {
    void demo() {
        Student s = new Student();
        s.name = "小明";      // 编译错误
    }
}
```java

编译器给出的原文是:

```text
Test.java:8: error: name has private access in Student
        s.name = "小明";
         ^
1 error

这个错误发生在编译期,代码根本生不成 class 文件。这正是访问修饰符的价值——把误用挡在运行之前

Warning

把成员变量设成 public 等于没封装。工程里成员变量几乎清一色 private,再用方法暴露受控的读写口。这不是教条,是被无数次线上事故换来的共识。

getter 与 setter

想让外面能读能改,但又保留把关权,就提供 getter(读)和 setter(写):

public class Student {
    private int age;

    public int getAge() {              // getter:只读,不做校验
        return age;
    }

    public void setAge(int age) {      // setter:写入前先把关
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException("年龄必须在 0 到 150 之间,收到:" + age);
        }
        this.age = age;
    }
}
```bash

逐行看 setter 干了什么:

- 第一步检查范围,`age < 0` 和 `age > 150` 都是非法值。
- 不合法就抛 `IllegalArgumentException`,把错误信息带上实际收到的值,方便排查。
- 通过校验才执行 `this.age = age`,用 `this` 区分成员变量和同名形参。

现在再想塞进 -5 就不可能了,程序会当场抛异常,而不是带着脏数据继续跑。**问题暴露得越早,修起来越便宜**

> [!TIP]
> 布尔类型的 getter 按约定写成 `isXxx()` 而不是 `getXxx()`,比如 `isAdult()`、`isEmpty()`。这个约定被 Spring 等框架依赖,不遵守可能导致属性识别不到。

## getter/setter 不必成对出现

新手常以为每个字段都要配一对 getter/setter,这是误解。该给什么口子,取决于业务需要什么。

**只读属性**:只给 getter,不给 setter。

```java
public class Order {
    private final String orderId;      // 订单号一旦生成就不能改

    public Order(String orderId) {
        this.orderId = orderId;
    }

    public String getOrderId() {       // 只提供读
        return orderId;
    }
}

派生属性:字段根本不存在,值是算出来的。

public class Person {
    private int birthYear;

    public void setBirthYear(int year) {
        this.birthYear = year;
    }

    public int getAge() {              // 没有 age 字段,实时计算
        return 2026 - birthYear;
    }
}
```bash

外面调 `getAge()` 拿到年龄,完全不知道内部存的其实是出生年份。哪天你想改成从生日精确计算,只需要动这一个方法,调用方一行都不用改。

这就是封装给你的自由度。

## 保护可变对象

有个坑很隐蔽:getter 直接把内部的可变对象返回出去,等于把门开了。

```java
public class Team {
    private List<String> members = new ArrayList<>();

    public List<String> getMembers() {
        return members;                // 危险:外面拿到的是内部那个 List
    }
}

调用方拿到这个 List 后,直接 team.getMembers().clear() 就能把内部数据清空,你的所有校验全部绕过。

稳妥的做法是返回一份不可修改的视图:

public List<String> getMembers() {
    return List.copyOf(members);       // Java 10+,返回不可变副本
}
```bash

`List.copyOf` 是 Java 10 引入的,返回的列表任何修改操作都会抛 `UnsupportedOperationException`。

历史兼容写法是 `Collections.unmodifiableList(members)`,它包装的是原列表的视图,原列表变了视图也跟着变,语义有差别,新代码优先用 `List.copyOf`。

## 类和方法的访问控制

修饰符不只管字段,也管方法和类。

**方法**:只在类内部用的辅助逻辑,标 `private` 更干净。对外暴露的接口越少,别人误用的机会越少,你以后改起来越自由。

****:顶层类只能用 `public` 或不写(包级私有),不能用 `private` 或 `protected`。一个 `.java` 文件里 `public` 类最多一个,且文件名必须和它一致。

```java
// 文件名必须是 Student.java
public class Student { }

// 同一个文件里可以有包级私有的类,不能加 public
class Helper { }

record:Java 16 起的新选择

如果一个类纯粹用来装数据,全字段不可变,Java 16 正式引入的 record 能省掉大量样板代码。

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

这一行等价于一个带 private final 字段、全参构造、访问器方法、`equals`、`hashCode`、`toString` 的完整类。

访问器方法名是 `x()` 和 `y()`,不带 `get` 前缀:

```java
Point p = new Point(3, 4);
System.out.println(p.x());        // 3
System.out.println(p);            // Point[x=3, y=4]

record 也能加校验,写在紧凑构造器里:

public record Age(int value) {
    public Age {                            // 紧凑构造器,没有参数列表
        if (value < 0 || value > 150) {
            throw new IllegalArgumentException("年龄非法:" + value);
        }
    }
}
```bash

> [!NOTE]
> `record` 的字段全是 final,创建后不能改。需要可变对象时还得用传统类加 getter/setter。第 49 章会把 record 讲透。

## 常见疑问

**问:private 成员真的完全访问不到吗?**

正常代码访问不到,编译器会直接拦下。但通过反射调用 `setAccessible(true)` 可以强行突破。所以封装是**编译期的约定**,不是运行期的安全边界。真要防篡改得靠别的手段,日常开发中把它当作约定遵守就够了。

**问:同一个类的两个不同对象,能互相访问对方的 private 字段吗?**

能。`private` 的边界是「类」,不是「对象」。所以下面这种写法完全合法:

```java
public class Money {
    private int amount;

    public boolean richerThan(Money other) {
        return this.amount > other.amount;   // 直接读另一个对象的私有字段
    }
}

equals 方法能高效比较,靠的就是这个规则。

问:所有字段都加 getter/setter,和直接 public 有区别吗?

有,但比想象中小。无脑生成全套 getter/setter,确实只比 public 多了一层壳。真正的区别在于你保留了随时加校验、改内部结构的权力,而 public 字段一旦被大量引用就动不了了。更好的做法是按需暴露:能不给 setter 就不给,能只读就只读。