# 类与对象：把数据和对数据的操作打包在一起

为什么需要类、字段与实例方法的关系、new 创建对象时到底发生了什么，以及引用复制带来的连锁结果

> 类与对象 · 构造器 · 引用与对象 · 约 6 分钟 · 10 月 17 日

## 本篇要点

1. 类把描述同一个东西的数据和操作这些数据的方法放进同一个花括号：字段装状态，方法定行为。
2. 类只是图纸，本身不产生数据；对象（实例）才是按图纸造出来的实物，每个对象各有自己的一份字段。
3. 实例方法不带 static，必须通过某个对象调用；点号前面那个对象，决定方法体里的字段指的是谁的那一份。
4. new 分四步：在堆上分配内存、把字段设成默认值、调用构造器、返回引用；等 new 这一行执行完，拿到的对象已经完整可用。
5. 变量里存的不是对象本身，而是指向堆上对象的引用；Account b = a 只复制引用，两个变量指向同一个对象。
6. 字段有默认值（int 为 0、boolean 为 false、引用为 null），局部变量没有，必须先赋值才能用。
7. Java 始终按值传递；当那个值是引用类型时，复制的是引用而不是对象，所以在方法里改对象字段，外面能看见。
8. 构造器名字与类名相同且没有返回类型；不写时编译器自动补一个无参构造器，字段保持默认值。

---

上一篇我们把“工序”打包成了方法，同一段逻辑换个参数就能反复用。但方法只打包了动作。动作要处理的数据呢？还是散落在 `main` 里的一个个变量。

假设要记录两个银行账户的余额：

```java
int balanceA = 100;
int balanceB = 250;
```

现在想给 A 存进 50 元。因为上一篇讲的按值传递，方法改不动外面的变量，只能靠返回值：

```java
public static int deposit(int balance, int amount) {
    return balance + amount;
}

balanceA = deposit(balanceA, 50);
```

这段能跑，但很别扭。“存钱”这件事没法表达成“某个账户存钱”，只能写成“把余额和金额算一算，结果再赋回某个变量”。而且账户一旦不止余额一个属性（还有个户名），变量就会成倍增加，编译器完全不知道 `balanceA` 和 `ownerA` 其实是同一个账户的两个属性——对编译器来说，它们只是两个碰巧一起出现的独立变量。

**类**（class）就是来解决这件事的：把描述同一个东西的数据，和操作这些数据的方法，写进同一个花括号里。

## 一个类长什么样

```java
public class Account {
    String owner;          // 字段：户名
    int balance;           // 字段：余额

    void deposit(int amount) {
        balance = balance + amount;
    }

    int getBalance() {
        return balance;
    }
}
```

拆开看：

- 最外层是类声明，类名首字母按惯例大写。之前所有代码都塞在 `main` 里，从现在起，一个程序通常由好几个类组成，每个类写一个文件，文件名和 `public` 的类名一致。
- 花括号里第一类东西是**字段**（field）。学过变量之后，它就是“声明在类里、而不是声明在方法里”的变量，用来装这个对象的状态。它还有个别名叫**实例变量**（instance variable），原因下面马上说。
- 第二类东西是方法。注意 `deposit` 里直接写了 `balance`，既没有把它当参数传进来，也没有 `static` 关键字。Oracle 把类体的三样内容说得很清楚：字段负责状态，方法负责行为，构造器负责初始化新对象 [1]、[4]。

上一篇说“`public static` 现在照抄就行，含义等学完类与对象再说”。现在可以说清楚了：`static` 表示这个方法属于类本身，不需要任何对象就能调用，`main` 就是这样——程序刚启动时，一个对象都还没造出来。不带 `static` 的方法叫**实例方法**（instance method），它必须通过某个具体的对象来调用。

## 类只是图纸，对象才是实物

上面那个类写完、编译好，内存里一个账户也没有。**类**是图纸：它规定“账户有户名和余额，并且能存钱”。**对象**（也叫**实例**，instance）才是按图纸造出来的实物，每个对象各有一份自己的 `owner`、自己的 `balance`。

造实物用 `new`：

```java
Account a = new Account();
a.owner = "张三";
a.deposit(100);

Account b = new Account();
b.owner = "李四";
b.deposit(50);
```

`a.deposit(100)` 里的点号读作“的”：让 `a` 这个对象执行它的 `deposit` 方法。这正好解释了 `deposit` 里为什么能直接写 `balance`——**点号前面那个对象，就是这次调用要操作的对象**，方法体里的 `balance` 指的就是它的余额字段。

同一段 `deposit` 代码，`a` 调用时改的是 `a` 的余额，`b` 调用时改的是 `b` 的余额。代码只有一份，字段每个对象一份，两边互不干扰 [4]。

## `new` 到底做了什么

`Account a = new Account();` 这一行包含两半：左边 `Account a` 声明了一个类型为 `Account` 的变量；右边 `new Account()` 负责造对象。`new` 执行时分四步 [2]、[6]：

1. 在**堆**（heap）上划出一块内存，大小刚好放得下这个对象的所有字段。
2. 把这块内存里每个字段设成该类型的**默认值**：`int` 是 0，`double` 是 0.0，`boolean` 是 false，引用类型是 `null`。这是字段和局部变量的一个重要区别——方法里的局部变量不赋初值就不能用，字段不写也有默认值 [5]。
3. 调用**构造器**（constructor），把字段设成你想要的初始值。
4. 返回一个**引用**（reference），相当于这块内存的门牌号。

也就是说，`a` 这个变量里装的不是一个完整的账户，而是一个门牌号（引用）。这个区分马上能解释一件反直觉的事：

```java
Account a = new Account();
a.deposit(100);

Account b = a;          // 复制的只是引用
b.deposit(50);

System.out.println(a.getBalance());   // 打印 150
```

`Account b = a;` 并没有造出第二个账户，只是把门牌号抄了一份。`a` 和 `b` 指向堆上的同一个对象，通过谁改都是同一个 `balance`，所以结果是 150 而不是 100。

这也能和上一篇的按值传递接上：Java 永远按值传递，只不过当那个值是引用类型时，复制的那份值就是引用，而不是对象本身。所以把对象传进方法、在方法里改它的字段，外面看得见；而传一个 `int` 进去改它，外面看不见——这是同一套规则在两个地方的结果。

## 让对象一出生就带好数据：构造器

`new Account()` 里那对空括号，其实是在调用一个不带参数的构造器。你没写，编译器会替你补一个。构造器的语法特点很好认：**名字必须和类名一样，而且没有返回类型**（这点和方法不同）[3]。

如果想让账户一造出来就带着户名和余额，就自己写一个：

```java
public class Account {
    String owner;
    int balance;

    public Account(String who, int start) {
        owner = who;
        balance = start;
    }

    void deposit(int amount) {
        balance = balance + amount;
    }
    // getBalance 等同前
}
```

```java
Account a = new Account("张三", 100);
```

这一行的执行顺序是：先划内存、把字段清成默认值，再跑这个构造器把 `owner`、`balance` 设好，最后才把引用交回给 `a` [2]。等 `new` 那一行执行完，`a` 指向的对象已经是完整可用的，不可能拿到一个“余额还没设好”的半成品。

（上面参数特意取名 `who`、`start`：如果参数名和字段重名，方法体里的名字会优先指参数，处理办法留到下一篇。）

## 回过头看

现在三个概念可以串起来了：**类**是图纸，**对象**是照图纸造出来的实物，**`new`** 是“造一个”这个动作——划分内存、设默认值、跑构造器、交回引用。字段和实例方法都挂在对象上：字段属于每个对象自己，实例方法只有一份代码，但调用时靠点号前面那个对象决定“给谁干活”。

之前那种“余额和方法分开、靠参数和返回值来回倒腾”的写法，到这里就换成了一句 `a.deposit(50)`。数据和操作它的方法终于待在了一起。

## 术语表

- 类（class）：把描述同一个东西的数据和操作这些数据的方法写在同一个花括号里的图纸，用来规定这类东西有什么、能做什么。
- 对象（实例，instance）：用 new 按类这张图纸造出来的实物，每个对象都各有自己的一份字段。
- 字段（field，也叫实例变量）：声明在类里而不是方法里的变量，用来装对象的状态。
- 实例方法（instance method）：不带 static、必须通过某个对象（点号前面那个）来调用的方法，方法体里可以直接写字段名。
- 堆（heap）：new 为对象分配内存的地方，对象就放在这里。
- 引用（reference）：变量里存的门牌号，指向堆上的某个对象；复制引用不会复制对象。
- 构造器（constructor）：名字与类名相同、没有返回类型的一段代码，new 时在字段设好默认值之后运行，用来把对象初始化成可直接使用的状态。

## 来源

1. [Oracle Java Tutorials: Declaring Classes — 类体里放字段、方法与构造器](https://docs.oracle.com/javase/tutorial/java/javaOO/classdecl.html)
2. [Oracle Java Tutorials: Creating Objects — new 分配内存、调用构造器并返回引用](https://docs.oracle.com/javase/tutorial/java/javaOO/objectcreation.html)
3. [Oracle Java Tutorials: Providing Constructors for Your Classes — 构造器命名与默认构造器](https://docs.oracle.com/javase/tutorial/java/javaOO/constructors.html)
4. [Oracle Java Tutorials: Summary of Creating and Using Classes and Objects — 字段存状态、方法实现行为，实例各有一份实例变量](https://docs.oracle.com/javase/tutorial/java/javaOO/summaryclasses.html)
5. [Artima: Object Initialization in Java — 字段默认值表，局部变量无默认值](https://www.artima.com/articles/object-initialization-in-java)
6. [JVM 规范：new 指令 — 从堆上分配内存并把实例变量初始化为默认值](https://docs.oracle.com/javase/specs/jvms/se6/html/Instructions2.doc10.html)

---

原文：https://pangzhengboyin.com/articles/java-classes-objects-new-operator-1a9012f2

> **庞征博引** · 想学的，慢慢都会
>
> 庞征博引是把想学的东西写成连载的 AI 学习工具。说出想学什么，它会先了解你的基础，再把主题写成一篇篇 5–10 分钟能读完的文章；边读边问，接下来学什么跟着你走。这篇就是这样写出来的。
>
> 开始你自己的连载 → https://pangzhengboyin.com
