Skip to content

类与对象 ​

S1-005必学入门约 90 分钟JDK 21编程语言基础 · 面向对象

1. 本节定位 ​

这是面向对象编程的第一课,也是整个 Java 学习路线的分水岭。

在此之前,你写的代码都是"一堆变量 + 一堆方法"。从本节开始,你写的每个程序都会由对象组成——对象把"数据"和"操作数据的方法"绑在一起,这是 Java 组织代码的基本单位。后面的封装、继承、多态,全部建立在本节的概念之上。

2. 学习目标 ​

完成本节后,你应该能够:

  • 分清"类"和"对象":类是模板,对象是按模板造出来的实例;
  • 写出带构造方法、成员变量、成员方法的类,并用 new 创建对象;
  • 解释 this 在构造方法中解决的歧义问题;
  • 用变量的方式理解对象引用(栈里存地址,堆里存数据);
  • 说出 == 比较对象时到底比较了什么。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

假设要管理一个银行账户,用你目前学过的知识会写成这样:

java
String owner = "张三";
long balance = 1000;

// 存款
balance += 500;

// 取款
if (500 <= balance) {
    balance -= 500;
}

只有一个账户时没问题。但当你有 1000 个账户时怎么办?定义 1000 组变量?更麻烦的是:余额和操作余额的规则被拆散了。"取款不能超过余额"这条规则写在业务代码里,任何一处忘了判断,就会出现负数余额。

类与对象解决的正是这个问题:把数据和操作数据的规则放在一起,让规则只有一处。

5. 核心概念 ​

5.1 类与对象的关系 ​

text
类(class)      = 图纸 / 模板      —— 描述"有哪些数据、能做什么操作"
对象(object)    = 按图纸造出的实例 —— 实际占用内存、有具体数值
java
BankAccount account = new BankAccount("张三", 1000);
//  ↑ 类型        ↑ 变量名(引用)    ↑ 调用构造方法创建对象

一句话:类是设计,对象是产物。一个类可以创建无数个对象。

5.2 类的三个组成部分 ​

java
public class BankAccount {
    private final String owner;   // 1. 成员变量(对象的状态)
    private long balance;

    public BankAccount(String owner, long balance) {  // 2. 构造方法(如何造对象)
        this.owner = owner;
        this.balance = balance;
    }

    public void deposit(long amount) {  // 3. 成员方法(对象的行为)
        balance += amount;
    }
}

5.3 构造方法与 this ​

构造方法有两个特征:名字与类名相同、没有返回值类型(不是 void,是什么都不写)。

this 解决的是"参数名与成员变量名相同"时的歧义:

java
public BankAccount(String owner, long balance) {
    this.owner = owner;      // this.owner 是成员变量,owner 是参数
    this.balance = balance;
}

this 的准确含义是"当前对象"。构造方法里的 this 就是"正在被创建的那个对象"。

默认构造方法

如果你不写任何构造方法,Java 会送你一个无参的空构造方法。但只要你自己写了任何一个构造方法,这个赠品就消失了——这是导致 new Foo() 编译失败的常见原因。

5.4 对象引用:栈与堆的直觉 ​

text
栈(Stack)                      堆(Heap)
┌──────────────┐               ┌────────────────────────┐
│ account ─────┼──────────────▶│ owner = "张三"          │
└──────────────┘               │ balance = 1000         │
                               └────────────────────────┘

account 这个变量里存的不是对象本身,而是对象的地址。这就是"引用类型"的含义。

由此推出一个重要结论:

java
BankAccount a = new BankAccount("张三", 1000);
BankAccount b = a;          // 复制的是地址,不是对象
b.deposit(500);
System.out.println(a.getBalance());   // 1500,a 看到了 b 的修改

5.5 == 比较的是什么 ​

java
BankAccount a = new BankAccount("张三", 1000);
BankAccount b = new BankAccount("张三", 1000);
BankAccount c = a;

System.out.println(a == b);   // false:两个不同的对象,地址不同
System.out.println(a == c);   // true :同一个对象,地址相同

== 对引用类型比较的是地址。要比较内容是否相等,需要重写 equals,这是 《equals、hashCode 与对象比较》 的主题,也是面试高频题"为什么重写 equals 还要重写 hashCode"的来源。

6. 递进示例 ​

6.1 最小示例:一个坐标点 ​

java
public class Point {
    int x;
    int y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public String describe() {
        return "(" + x + ", " + y + ")";
    }

    public static void main(String[] args) {
        Point origin = new Point(0, 0);
        Point target = new Point(3, 4);
        System.out.println("起点 " + origin.describe() + ",终点 " + target.describe());
    }
}

6.2 业务示例:账户与规则绑定 ​

java
public class BankAccount {
    private final String owner;
    private long balance;

    public BankAccount(String owner, long initialBalance) {
        if (initialBalance < 0) {
            throw new IllegalArgumentException("初始余额不能为负");
        }
        this.owner = owner;
        this.balance = initialBalance;
    }

    public void deposit(long amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("存款金额必须大于 0");
        }
        balance += amount;
    }

    public void withdraw(long amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("取款金额必须大于 0");
        }
        if (amount > balance) {
            throw new IllegalStateException("余额不足:当前 " + balance + ",尝试取 " + amount);
        }
        balance -= amount;
    }

    public String getOwner() {
        return owner;
    }

    public long getBalance() {
        return balance;
    }
}

注意"规则"写在哪里:金额校验和余额校验都在类内部。外部代码再也无法绕过规则修改余额——这就是把数据和方法放在一起的价值。

6.3 反例:字段公开会怎样 ​

java
public class BadAccount {
    public String owner;
    public long balance;      // 谁都能改,规则形同虚设
}

// 别处代码
BadAccount account = new BadAccount();
account.balance = -99999;     // 编译通过,业务规则被绕过

如何防止这种情况、为什么不直接写 private 就完事,是 《封装》 的主题。

7. 完整可运行代码 ​

文件:labs/oop-basics/src/main/java/cn/kb/learn/oop/LessonRunner.javaJava 版本:21(javac --release 21) 依赖:无 运行命令:

powershell
javac --release 21 -d out LessonRunner.java BankAccount.java
java -cp out cn.kb.learn.oop.LessonRunner
java
package cn.kb.learn.oop;

public class LessonRunner {
    public static void main(String[] args) {
        BankAccount account = new BankAccount("张三", 1000);

        account.deposit(500);
        System.out.println("存款后余额:" + account.getBalance());

        account.withdraw(200);
        System.out.println("取款后余额:" + account.getBalance());

        BankAccount sameAccount = account;
        sameAccount.deposit(100);
        System.out.println("通过另一个引用操作后,account 看到:" + account.getBalance());

        BankAccount another = new BankAccount("张三", 1400);
        System.out.println("内容相同但不是同一个对象:" + (account == another));

        try {
            account.withdraw(999999);
        } catch (IllegalStateException exception) {
            System.out.println("预期内的失败:" + exception.getMessage());
        }
    }
}

预期输出:

text
存款后余额:1500
取款后余额:1300
通过另一个引用操作后,account 看到:1400
内容相同但不是同一个对象:false
预期内的失败:余额不足:当前 1400,尝试取 999999

8. Debug 观察点 ​

在 main 里做四件事:

  1. 在 BankAccount account = new BankAccount("张三", 1000); 打断点,按 Step Into(F7)进入构造方法,观察 this 面板里 owner、balance 的赋值过程,理解 this.owner = owner 左右两边分别是谁;
  2. 在 sameAccount.deposit(100); 处打断点,展开 Variables 面板里的 account 和 sameAccount,看 id 是否相同——它们指向同一个对象;
  3. 对比 another,看它的 id 与其他两个不同,直观感受"内容相同 ≠ 同一个对象";
  4. 单步走到 account.withdraw(999999),观察异常抛出时程序跳到 catch 块,getBalance() 返回值没有变化(失败的操作不会改状态)。

9. 常见错误 ​

错误 1:NullPointerException ​

java
BankAccount account = null;
account.deposit(100);   // NPE:没有对象,调不了方法

修复:确认对象是否真的被 new 出来了;方法入口处对参数做非空校验。

错误 2:用 == 比较对象内容 ​

java
if (new String("abc") == new String("abc")) { ... }   // false

修复:比较内容用 equals:"abc".equals(value)。字符串常量池会让 "abc" == "abc" 恰好为 true,这更容易掩盖错误。

错误 3:写了构造方法后,无参构造消失 ​

java
public class User {
    private String name;
    public User(String name) { this.name = name; }
}

User user = new User();   // 编译错误:找不到无参构造方法

修复:如果需要无参构造,显式补上一个(很多框架如 MyBatis、Jackson 也需要它)。

10. 动手实验 ​

目标:设计一个符合规则的类,并用两个引用验证对象的内存行为。

步骤:

  1. 设计 Book 类:字段 title、priceInCents、stock;构造方法校验价格不能为负;
  2. 提供 buy(int count) 方法:库存不足时抛异常,成功时扣减库存;
  3. 在 main 里创建一本书,创建两个引用指向它,用其中一个引用调用两次 buy(1),打印另一个引用看到的库存;
  4. 再创建一个内容完全相同的对象,用 == 与 equals 分别比较,记录结果。

验收方式:能解释第 3 步的结果为什么说明"两个引用是同一个对象"。

扩展任务:给 Book 加上"总销售额"统计——注意它是属于"这本书"还是"所有书"?想一想该用成员变量还是别的写法(后面的 static 会给出答案)。

11. 练习题 ​

基础题

  1. 判断:类是对象的一种。(对 / 错)
  2. 填空:new BankAccount("张三", 1000) 中,new 的作用是 ______。
  3. 简答:this 的含义是什么?为什么构造方法里需要它?

代码阅读题

java
public class Counter {
    private int count;
    public Counter(int initial) { this.count = initial; }
    public void increment() { count++; }
    public int getCount() { return count; }

    public static void main(String[] args) {
        Counter a = new Counter(0);
        Counter b = a;
        a.increment();
        b.increment();
        System.out.println(a.getCount());
    }
}
  1. 输出是多少?为什么?

编程题

  1. 写 Rectangle 类:字段 width、height;方法 area() 返回面积、perimeter() 返回周长、isSquare() 判断是否为正方形。构造方法里校验宽高必须为正数。

综合任务

  1. 用类与对象重写 《方法、参数与重载》 的练习:设计 ShoppingCart 类,支持 add(String name, long priceInCents, int count)、totalInCents()、itemCount()。要求:添加数量必须为正、总价不超过 long 范围,并在 main 里演示三次添加与一次失败添加。
分级提示

提示 1(方向):第 5 题的"正方形"判断是 width == height,注意字段类型选的什么。

提示 2(关键点):第 6 题需要存储多个商品,用两个并行数组会很别扭——这正是集合要解决的问题。先用数组硬写一遍,等学完 《集合》 再回来重构,你会更理解集合的价值。

提示 3(接近答案):第 6 题的 totalInCents() 每次遍历数组重新累加即可,先不要考虑缓存。

12. 参考答案 ​

基础题
  1. 错。对象是类创建的实例,反过来说才成立:对象属于某个类。
  2. 在堆上分配内存并调用构造方法创建对象,同时返回该对象的引用。
  3. this 指"当前对象"。构造方法中参数名与成员变量同名时,this.owner 明确指向成员变量,避免赋值变成"参数赋给参数"。
代码阅读题(第 4 题)

输出 2。

Counter b = a; 复制的是引用,a 和 b 指向同一个对象。a.increment() 和 b.increment() 操作的是同一个 count,所以结果是 2。

编程题(第 5 题)
java
package cn.kb.learn.oop;

public class Rectangle {
    private final int width;
    private final int height;

    public Rectangle(int width, int height) {
        if (width <= 0 || height <= 0) {
            throw new IllegalArgumentException("宽和高必须为正数");
        }
        this.width = width;
        this.height = height;
    }

    public int area() {
        return width * height;
    }

    public int perimeter() {
        return 2 * (width + height);
    }

    public boolean isSquare() {
        return width == height;
    }

    public static void main(String[] args) {
        Rectangle square = new Rectangle(4, 4);
        Rectangle rectangle = new Rectangle(3, 5);
        System.out.println(square.area() + " " + square.perimeter() + " " + square.isSquare());
        System.out.println(rectangle.area() + " " + rectangle.perimeter() + " " + rectangle.isSquare());
    }
}

预期输出:

text
16 16 true
15 16 false
综合任务(第 6 题)
java
package cn.kb.learn.oop;

public class ShoppingCart {
    private final String[] names = new String[10];
    private final long[] prices = new long[10];
    private final int[] counts = new int[10];
    private int size;

    public void add(String name, long priceInCents, int count) {
        if (count <= 0) {
            throw new IllegalArgumentException("数量必须为正");
        }
        if (size >= names.length) {
            throw new IllegalStateException("购物车已满");
        }
        names[size] = name;
        prices[size] = priceInCents;
        counts[size] = count;
        size++;
    }

    public long totalInCents() {
        long total = 0;
        for (int i = 0; i < size; i++) {
            total += prices[i] * counts[i];
        }
        return total;
    }

    public int itemCount() {
        int total = 0;
        for (int i = 0; i < size; i++) {
            total += counts[i];
        }
        return total;
    }

    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        cart.add("《Head First Java》", 9900, 1);
        cart.add("机械键盘", 39900, 1);
        cart.add("Type-C 线", 2900, 2);
        System.out.println("商品件数:" + cart.itemCount());
        System.out.println("总价(分):" + cart.totalInCents());

        try {
            cart.add("无线鼠标", 12900, 0);
        } catch (IllegalArgumentException exception) {
            System.out.println("预期内的失败:" + exception.getMessage());
        }
    }
}

为什么要先用数组硬写:你会立刻感受到"容量写死 10"、"三个并行数组要同步维护"的别扭。学完集合后,同样的需求可以用 List<CartItem> 表达,代码会短一半——那时你才算真正理解集合解决的问题。

13. 掌握标准 ​

  • [ ] 能区分类与对象,独立写出带构造方法的类;
  • [ ] 能说清 this 的作用;
  • [ ] 能用"栈里存引用、堆里存对象"解释引用赋值的影响;
  • [ ] 知道 == 比较的是地址,并知道内容比较该用什么。

14. 扩展资源 ​

15. 下一步学习 ​

《封装》 解决本节留下的问题:字段为什么必须是 private、如何设计对外暴露的方法。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引