CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Java 语言基础/课时/类与封装
RawBlame
课时目录14 节

Java 语言基础

  1. 01Java 与 JVM 是什么
  2. 02基本语法与 main
  3. 03基本类型与引用类型
  4. 04条件与循环
  5. 05方法与参数
  6. 06数组
  7. 07类与封装
  8. 08继承接口与多态
  9. 09集合框架入门
  10. 10字符串
  11. 11异常处理
  12. 12文件 IO 入门
  13. 13Maven 与项目结构
  14. 14综合练习与 dsa-java 分工

类与封装

字段、构造器、getter/setter。

40 分钟

类与封装

这一课你将能做到

  1. 独立设计一个实体类:私有字段、多个构造器、getter/setter、toString——与 Spring 后端的 User/Order 实体同款结构
  2. 说清 static 成员和实例成员的区别,用代码验证「static 属于类、实例属于对象」
  3. 会用 JDK 17 的 record 写不可变数据类,一行顶过去三十行

为什么要学

前 6 课你写的都是「过程式」代码:数据散落在一堆变量里,方法单独存在。真实后端不是这样——本站处理的是一个又一个「用户」「订单」,每个用户有 id、用户名、状态,还有配套的行为(校验、展示)。把数据和操作数据的方法打包在一起,就是面向对象(OOP)。

封装是 OOP 第一支柱:字段设为 private,外界只能通过方法访问。好处:数据坏了能定位到「哪个 setter/方法改的」;校验逻辑只有一处;内部实现随便改,外部不受影响。Spring 后端里实体、DTO、配置类,全是封装思想的具体化。

先建立图像

类 User = 设计图纸              对象 u1 = 按图纸造出来的实例
┌─────────────────────┐        ┌─────────────────────┐
│ - id : long          │        │ id = 1001           │
│ - username : String  │  new   │ username = "tom"    │
│ - status : int       │ ────▶  │ status = 1          │
├─────────────────────┤        ├─────────────────────┤
│ + getUsername()      │        │ (共用同一套方法)    │
│ + setStatus(int)     │        │                     │
└─────────────────────┘        └─────────────────────┘

图纸只有一份,实例可以造任意多个,各自有自己的字段值。

跟着做

步骤 1:写一个标准实体类

新建 User.java(注意:一个文件可以有多个非 public 类,本课示例就放在一个文件里,类不加 public,用 java Xxx.java 直跑):

class User {
    // 1. 私有字段:外界不能直接碰
    private long id;
    private String username;
    private int status;          // 0=正常 1=封禁

    // 2. 无参构造器(给框架反射用,如 MyBatis、Jackson)
    public User() {
    }

    // 3. 全参构造器:new User(1001, "tom", 0) 时调用
    public User(long id, String username, int status) {
        this.id = id;               // this.id 是字段,id 是参数
        this.username = username;
        this.status = status;
    }

    // 4. getter/setter:受控的读写通道
    public long getId() { return id; }
    public String getUsername() { return username; }

    public int getStatus() { return status; }
    public void setStatus(int status) {
        // 封装的价值:校验逻辑只写一次,所有赋值都会经过这里
        if (status != 0 && status != 1) {
            throw new IllegalArgumentException("非法状态: " + status);
        }
        this.status = status;
    }

    @Override
    public String toString() {      // 打印对象时自动调用
        return "User{id=%d, username=%s, status=%d}".formatted(id, username, status);
    }
}

public class OopDemo {
    public static void main(String[] args) {
        User u1 = new User(1001, "tom", 0);   // 创建实例
        User u2 = new User(1002, "jerry", 1);

        System.out.println(u1);               // 自动调 toString
        System.out.println(u2);
        // System.out.println(u1.username);   // 编译错!private 字段外部不可见
        System.out.println(u1.getUsername()); // 必须走方法

        u2.setStatus(0);                      // 走 setter,合法
        u1.setStatus(5);                      // 运行时报错:非法状态 5
    }
}

输出:

User{id=1001, username=tom, status=0}
User{id=1002, username=jerry, status=1}
tom
Exception in thread "main" java.lang.IllegalArgumentException: 非法状态: 5

步骤 2:this 的三副面孔

class Account {
    private String owner;
    private double balance;

    public Account(String owner, double balance) {
        this.owner = owner;         // 面孔 1:区分字段和同名参数
        this.balance = balance;
    }

    public Account deposit(double amount) {
        this.balance += amount;
        return this;                // 面孔 2:返回自己,支持链式调用
    }

    public Account transfer(Account other, double amount) {
        other.deposit(amount);      // 也可以直接写字段(同类内 private 互通)
        this.balance -= amount;
        return this;
    }

    public void show() {
        System.out.printf("%s 余额: %.2f%n", owner, balance);
    }
}

public class ThisDemo {
    public static void main(String[] args) {
        Account a = new Account("tom", 100);
        Account b = new Account("jerry", 50);
        a.transfer(b, 30).deposit(10).show();   // 链式调用:tom 余额 80.0
        b.show();                                // jerry 余额 80.0
    }
}

(面孔 3 是构造器间调用 this(...),下面步骤 3 出现。)

步骤 3:构造器重载与 this(...) 转发

class Order {
    private long id;
    private String product;
    private int quantity;

    public Order(long id, String product, int quantity) {   // 主构造器
        this.id = id;
        this.product = product;
        this.quantity = quantity;
    }

    public Order(String product) {
        this(System.nanoTime(), product, 1);   // this(...) 必须是构造器第一行
        // 默认数量 1,id 用时间戳凑数
    }
}

public class CtorDemo {
    public static void main(String[] args) {
        Order o1 = new Order("键盘", 2);
        Order o2 = new Order("鼠标");
        System.out.println("两个订单创建成功");
    }
}

注意:一旦你手写了任何构造器,编译器就不再自动送无参构造器。框架需要无参构造器时必须显式补一个。

步骤 4:static——属于类而不是对象

class IdGenerator {
    static long current = 1000;          // 类级别:所有实例共享同一份
    long id;                             // 实例级别:每个对象一份

    long nextId() {
        current++;                       // 改的是共享的那份
        this.id = current;
        return id;
    }
}

public class StaticDemo {
    public static void main(String[] args) {
        IdGenerator g1 = new IdGenerator();
        IdGenerator g2 = new IdGenerator();

        System.out.println(g1.nextId());   // 1001
        System.out.println(g2.nextId());   // 1002:current 是共享的
        System.out.println(IdGenerator.current);  // 1002:static 用类名访问
    }
}

记忆:static 成员在方法区/元空间只有一份,用类名访问;实例成员在堆里每个对象一份,用对象访问。 main 为什么是 static,现在完全通了——JVM 启动时没有对象,static 不需要对象。

步骤 5:record——JDK 17 的不可变数据类

// 一行 = 字段 + 构造器 + getter + equals + hashCode + toString
record Point(int x, int y) {
    // 可以加紧凑校验
    Point {
        if (x < 0 || y < 0) throw new IllegalArgumentException("坐标不能为负");
    }
}

public class RecordDemo {
    public static void main(String[] args) {
        Point p = new Point(3, 4);
        System.out.println(p);           // Point[x=3, y=4]
        System.out.println(p.x());       // 3(访问器名就叫 x,不是 getX)
        System.out.println(p.equals(new Point(3, 4)));  // true:按值比较
        // p.x = 10;                     // 编译错:record 字段 final 不可变
    }
}

适用场景:纯数据载体(DTO、坐标、配置项)。需要可变状态的业务实体(如 User)仍用传统类。

讲清楚要点

  1. 类是模板,对象是实例:new 一次造一个,字段值各存各的,方法全体共用。
  2. 封装三板斧:字段 private、提供 public getter/setter、在 setter/构造器里做校验。「所有字段加 getter/setter」不是目的,「受控访问」才是。
  3. this 指当前对象:参数与字段同名时必须用它区分;构造器第一行 this(...) 可复用另一个构造器。
  4. static 的三条纪律:static 方法不能直接访问实例成员(没有对象上下文);static 方法不能写 this;工具类(如 Arrays)全是 static。
  5. toString/equals 配套:不重写 toString 打印出来是 User@1b6d3586;record 自动生成 equals/hashCode,传统类要自己写(IDE 一键生成)。
  6. 命名空间:实体类与数据库表对应(User ↔ user 表),DTO 后缀表示传输对象——本站后端用 UserDO/UserDTO 区分层次。

你可能会懵的地方

  • getter/setter 这么啰嗦,直接 public 字段不行吗? 现在不行,明天就出事:一旦外部直接赋值,你加校验、加日志、改内部表示都要动所有调用方。 setter 是唯一改动成本为零的通道,且 Java 生态(MyBatis、Jackson、Spring)全按 getter/setter 反射读写。

  • static 变量线程安全吗? 不安全。多个线程同时 current++ 会丢失更新(读-改-写三步会被打断)。本站后端是多线程服务,计数要用 AtomicLong 或加锁。单机学习阶段先记住结论。

  • 什么时候用 static 方法,什么时候用实例方法? 方法逻辑不依赖对象状态(输入全从参数来)→ static 工具方法;要读写实例字段或被 Spring 管理 → 实例方法。Spring Bean 全是实例方法,因为字段是注入的依赖。

  • record 能替代所有实体类吗? 不能。record 不可变、无 setter,MyBatis/Jackson 反序列化对 record 支持已完善(JDK 16+ 的主流版本),但需要可变状态、继承、懒加载的字段仍用传统类。

  • 一个对象没有被引用了会怎样? JVM 的垃圾回收器会在合适时机回收它的堆内存。局部变量出方法作用域后即「不可达」。你不用手动释放——这是相对 C/C++ 的最大解放。

错法对照

错法问题正解
字段声明成 public封装失效,校验无处安放private + getter/setter
构造器里写 id = id;参数赋给参数,字段还是 0this.id = id;
写了有参构造器后 new User() 报错编译器不再送无参构造器显式补 public User() {}
static 方法里访问实例字段编译错误:无对象上下文创建对象后访问,或把字段也改 static
new User().setId(1); 然后想再拿这个对象引用没保存,对象已不可达先存变量:User u = new User(); u.setId(1);
record 里写 setterrecord 字段是 final需要可变就用传统类

卡点急救

  1. 打印对象显示 User@1b6d3586:没重写 toString。IDEA 中 Alt+Insert → toString 一键生成,或类改用 record。
  2. 编译报「无法将类中的构造器应用到给定类型」:参数个数/类型与任何构造器都不匹配。检查你写的构造器签名,或补无参构造器。
  3. 运行报 NullPointerException(对象版):引用是 null 却调了方法。定位报错行号,检查 new 语句是否真的执行了。
  4. IDE 提示「Field injection ... access can be private」:你在同类外直接访问了 private 字段。改走 getter/setter。
  5. this(...) 报「必须是构造器中的第一个语句」:把 this(...) 挪到构造器第一行,且一个构造器最多一个 this(...) 调用。

面试常问

  • 什么是封装?为什么需要? 隐藏内部状态,仅暴露受控访问方法。收益:校验集中、实现可替换、降低耦合。举例 setter 校验非法状态。

  • static 成员和实例成员的区别? 归属(类 vs 对象)、内存(一份 vs 每对象一份)、访问方式(类名 vs 对象)、生命周期(类加载到卸载 vs 对象创建到 GC)。static 方法无 this、不能直接访问实例成员。

  • 构造器可以被继承/重写吗? 不能继承、不能重写,但可以重载;子类构造器会通过 super(...) 链式调用父类构造器(下一课)。

  • record 和普通类的区别? 不可变、自动生成构造器/访问器/equals/hashCode/toString、不能继承其他类。适合纯数据载体。

  • == 和 equals 在对象上的区别? == 比地址(是否同一对象);equals 默认等同 ==,业务类应重写为按字段比较(record 自动完成)。重写 equals 必须同时重写 hashCode。

自测三问

  1. 为什么 new User(1,"tom") 在你补写有参构造器后突然编译失败?
  2. IdGenerator.current++ 在多线程下会出什么问题?生产代码怎么写?
  3. record Point(int x, int y) 自动提供了哪些成员?p.getX() 能编译通过吗?

小结一句话

类是模板、对象是实例、封装是「private 字段 + 受控方法」;this 区分成员与参数、static 属于类不属于对象、record 一行搞定不可变数据类——Spring 后端每个实体/DTO 都长这个样子。

延伸

  • Oracle:类与对象
  • 下一课:继承、接口与多态——Spring 依赖注入的根基,Service 接口 + 实现类的标准姿势。
加载本节练习…
← 上一课数组下一课 →继承接口与多态