Skip to content

封装概念

封装的本质是什么?

很多人会回答:「把数据和操作放在一起」「把字段设成 private,然后加 getter/setter」。这些都对,但不深刻。

换个问法:封装是为了防谁?

防的是两类人:一是使用者——不让他们看到不需要看到的东西;二是未来的自己——不让内部的改动,波及到外部依赖。

真正的封装,是给一个东西筑起边界:边界内的东西随便改,边界外的人不受影响。

访问修饰符:边界的四层闸门

Java 提供了四个级别的访问控制,从宽到严:

修饰符本类同包子类其他包
public
protected
default(无修饰符)
private

private 是最常用的边界手段。把它想象成一道单向门:内部可以出去,外面进不来。

protected 是个特殊存在:同包能访问,跨包子类也能访问。这是为了让继承体系内的协作更灵活,但实际用的时候要谨慎——protected 成员也是 API 的一部分,子类会依赖它。

default 通常是无意间产生的。如果你不写修饰符,Java 默认给你一个包内可见。这不一定是坏事:包内协作类之间不需要严格的边界,暴露给同包的代价可控。

public 是最大胆的选择。一旦 public,就是对外承诺:这个接口我负责维护,不会乱改。所以在设计 public API 时,要格外谨慎。

封装的三个层次

大多数教程把封装等同于「属性私有化 + getter/setter」,这是最浅的理解。封装实际上分三个层次:

第一层:数据隐藏

把不想让人直接操作的字段藏起来:

java
public class Person {

    private String name;
    private int age;

    // 外部不能直接改,只能通过方法
    public void setAge(int age) {
        if (age >= 0 && age <= 150) {
            this.age = age;
        } else {
            throw new IllegalArgumentException("年龄不合理");
        }
    }
}

这里的关键不是「private」,而是「验证逻辑」。如果 setter 里没有校验,private 就只是一个形式。

第二层:内部表示隐藏

数据隐藏只是开始。更重要的是:内部怎么存,不重要;对外提供什么能力,才重要

java
public class Person {

    // 内部可以用 String 存,也可以用 Date 存
    // 对外只暴露「操作日期的能力」,不暴露存储细节
    private LocalDate birthDate;

    public int getAge() {
        return Period.between(birthDate, LocalDate.now()).getYears();
    }

    public boolean isAdult() {
        return getAge() >= 18;
    }
}

调用方不需要知道 birthday 是用什么类型存的,也不需要知道年龄怎么算的。他们只需要「问一句」就能得到答案。哪天要把 LocalDate 换成 long 时间戳,调用方不需要改一行代码。

第三层:行为封装

最高层次的封装是:不暴露数据,只暴露行为

java
public class FileReader {

    private final String path;

    public FileReader(String path) {
        this.path = path;
    }

    // 不暴露文件句柄、字符流
    // 只暴露「读取」这个行为
    public String read() throws IOException {
        return Files.readString(Path.of(path));
    }
}

调用方不需要知道底层用的是 FileInputStream 还是 NIO,不需要管理资源。他们只需要调用 read(),得到结果。这就是封装带来的透明性

基本封装示例

把三个层次合在一起看:

java
public class EncapsulationConceptDemo {

    static class Person {
        // 第一层:数据隐藏
        private String name;
        private int age;

        // 构造时也做校验
        public Person(String name, int age) {
            this.name = name;
            setAge(age); // 通过 setter 校验
        }

        // 第二层:提供行为,不暴露字段
        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }

        // setter 带验证逻辑
        public void setAge(int age) {
            if (age >= 0 && age <= 150) {
                this.age = age;
            } else {
                throw new IllegalArgumentException("年龄必须在 0-150 之间");
            }
        }

        // 第三层:行为方法
        public boolean isAdult() {
            return age >= 18;
        }

        public String describe() {
            return name + "," + age + "岁,"
                    + (isAdult() ? "已成年" : "未成年");
        }
    }

    public static void main(String[] args) {
        Person person = new Person("张三", 25);
        System.out.println(person.describe()); // 张三,25岁,已成年

        person.setAge(-5); // 抛出异常,数据不会被污染
    }
}

封装的价值

封装不是写代码的硬性要求,而是软件设计的必然选择:

对使用者:只需要关心接口,不需要理解实现。越简单越好用。

对维护者:内部改,不影响外部。越独立越好改。

对系统:边界清晰,依赖关系简单。越清晰越稳定。

好封装的标志是什么?当内部实现全换掉,外部没有任何感知。

这就是封装的终极目标。

基于 VitePress 构建