成员变量、成员方法与对象引用 | JavaSE

成员变量、成员方法与对象引用

一、学习目标

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

  • 理解类成员(Member)的基本概念。
  • 能够区分成员变量与局部变量。
  • 能够解释成员变量为什么属于对象状态的一部分。
  • 能够定义并调用成员方法。
  • 能够理解对象调用成员方法时,方法处理的是对应对象的数据。
  • 能够使用 对象.成员变量对象.成员方法(...)
  • 能够解释对象变量保存的是引用值,而不是对象内容本身。
  • 能够分析两个引用变量指向同一个对象时的数据变化。
  • 能够理解引用变量默认或显式为 null 时的基本含义。
  • 能够理解对象成员变量创建后的默认值。
  • 能够绘制简单的“引用变量 → 对象”关系图。

二、核心知识

2.1 什么是类的成员

观察:

class Student {

    String name;
    double chinese;
    double math;

    void printTotalScore() {
        System.out.println(chinese + math);
    }
}

定义在类体中的:

name
chinese
math
printTotalScore()

都属于这个类的成员。

当前阶段主要学习两类:

成员变量
成员方法

2.2 什么是成员变量

例如:

class Student {

    String name;
    int age;
    double score;
}

这些定义在:

类中、方法外

的变量,属于成员变量。

它们用于描述对象具有的数据。

例如:

Student
├── name
├── age
└── score

因此也经常称为:

字段
Field

在后面的 Java 文档、反射和框架中:

Field

会是非常重要的专业术语。


2.3 当前阶段主要关注实例成员变量

以后学习 static 后,成员变量还会进一步区分:

实例变量
类变量 / 静态变量

但本章暂时只讨论最基础的:

每个对象自己拥有一份的数据。

例如:

class Student {
    String name;
    int age;
}

创建:

Student s1 = new Student();
Student s2 = new Student();

会形成:

对象 A
├── name
└── age


对象 B
├── name
└── age

两个对象分别拥有自己的状态。

static 的共享成员规则留到后续专章。


2.4 使用对象访问成员变量

格式:

对象变量.成员变量

例如:

Student s1 = new Student();

s1.name = "小雨";
s1.age = 21;

读取:

System.out.println(s1.name);
System.out.println(s1.age);

可以理解为:

先通过 s1 找到对象
↓
再访问这个对象中的 name / age

2.5 点运算符 . 的含义

例如:

s1.name

和:

s1.printInfo()

这里的:

.

可以理解为:

访问这个对象的某个成员。

因此:

对象.成员变量
对象.成员方法(...)

是面向对象代码中最常见的结构之一。


2.6 成员变量创建后有默认值

例如:

class Student {

    String name;
    int age;
    double score;
    boolean graduated;
}

然后:

Student student = new Student();

即使还没有手动赋值,对象中的这些字段也已经获得默认值。

常见情况:

| 成员变量类型 | 默认值 | | --------------------------- | ---------- | | byte / short / int / long | 0 | | float / double | 0.0 | | char | '\u0000' | | boolean | false | | 引用类型 | null |

例如:

System.out.println(student.name);
System.out.println(student.age);
System.out.println(student.score);
System.out.println(student.graduated);

对应的初始状态大致是:

null
0
0.0
false

2.7 成员变量和局部变量不是一回事

观察:

class Student {

    String name;

    void printScore() {
        double score = 100;
        System.out.println(score);
    }
}

这里:

String name;

是:

成员变量

而:

double score = 100;

定义在方法内部,是:

局部变量

2.8 成员变量与局部变量的基本区别

基础阶段可以先对比:

| 比较项 | 成员变量 | 局部变量 | | ------------ | -------------------- | ---------------------------------- | | 常见定义位置 | 类中、方法外 | 方法内部等局部区域 | | 主要作用 | 描述对象状态 | 服务于一次局部计算 | | 默认初始化 | 对象字段会获得默认值 | 普通局部变量不能依赖默认值直接读取 | | 生命周期 | 与对应对象状态有关 | 随局部执行区域存在 |

例如:

class Student {

    int age;

    void test() {
        int number;

        System.out.println(age);

        // System.out.println(number);
    }
}

age 创建对象后有:

0

而未明确赋值的局部变量:

number

不能直接读取。


2.9 成员方法

定义在类中的方法叫成员方法。

例如:

class Student {

    String name;
    double chinese;
    double math;

    void printTotalScore() {
        System.out.println(chinese + math);
    }
}

其中:

printTotalScore()

是成员方法。

它描述:

Student 对象能够完成什么功能。


2.10 对象如何调用成员方法

格式:

对象变量.成员方法(...)

例如:

Student s1 = new Student();

s1.name = "小雨";
s1.chinese = 100;
s1.math = 90;

s1.printTotalScore();

可以理解:

s1
↓
找到 Student 对象
↓
让这个对象执行 printTotalScore()

2.11 方法为什么可以直接使用成员变量

例如:

class Student {

    String name;
    double chinese;
    double math;

    void printTotalScore() {
        System.out.println(
                name + ":" + (chinese + math)
        );
    }
}

方法中没有重新定义:

name
chinese
math

却能够直接使用这些成员变量。

原因可以先理解为:

当前成员方法是在某个 Student 对象上执行,它可以处理当前对象自己的成员数据。

例如:

s1.printTotalScore();

处理的是:

s1 所引用 Student 对象的数据

而:

s2.printTotalScore();

处理的是:

s2 所引用 Student 对象的数据

这里更底层涉及当前对象引用,也就是下一章会深入学习的:

this

本章先不展开。


2.12 同一个成员方法可以作用于不同对象

定义一次:

void printTotalScore() {
    System.out.println(
            name + ":" + (chinese + math)
    );
}

然后:

s1.printTotalScore();
s2.printTotalScore();

虽然执行的是同一个方法定义,但是处理的数据不同。

可以理解:

s1 调用
↓
处理对象 A 的数据


s2 调用
↓
处理对象 B 的数据

这就是:

类定义共同的行为,对象提供各自的状态。


2.13 对象引用到底是什么

观察:

Student s1 = new Student();

Java 中:

Student

是引用类型。

变量:

s1

保存的是:

一个引用值。

这个引用可以指向:

Student 对象

因此:

s1 ──→ Student 对象

2.14 为什么不推荐简单说“对象变量存地址”

源教学资料为了直观理解写道:

对象变量存储的是对象的地址。

作为早期内存示意图,这种说法容易理解。

但从 Java 语言层面,更准确的说法应该是:

引用类型变量保存引用值(reference value),这个引用可以指向对象。

我们没有必要假定 Java 语言向程序员暴露了某个可以直接参与普通整数计算的“真实物理内存地址”。

所以教程中统一使用:

对象引用
引用值

进行描述。


2.15 两个引用变量可以指向同一个对象

观察:

Student s1 = new Student();

s1.name = "小雨";

Student s2 = s1;

这里:

Student s2 = s1;

并没有:

new

所以没有创建新的 Student 对象。

只是把:

s1 当前保存的引用值

赋值给:

s2

于是:

s1 ──┐
     ├──→ Student 对象
s2 ──┘

2.16 一个引用修改对象,另一个引用也能看到

继续:

Student s1 = new Student();
s1.name = "小雨";

Student s2 = s1;

s2.name = "小星";

System.out.println(s1.name);

这里最终看到的是:

小星

原因不是:

修改 s2 导致自动同步 s1

而是:

s1s2 指向的是同一个对象。

真正被修改的是:

共同的 Student 对象

2.17 引用变量赋值与对象复制必须区分

例如:

Student s2 = s1;

做的是:

复制引用值

不是:

复制整个对象

所以:

对象数量:1
引用变量数量:2

这一点与上一章数组中的:

int[] b = a;

本质非常相似。

数组也是引用类型。


2.18 多次 new 才是多个对象

比较:

Student s1 = new Student();
Student s2 = s1;

这里只有:

1 次 new
→ 1 个 Student 对象

而:

Student s1 = new Student();
Student s2 = new Student();

有:

2 次 new
→ 2 个不同 Student 对象

这是非常重要的判断方法。


2.19 引用变量可以重新指向其他对象

例如:

Student s1 = new Student();
Student s2 = new Student();

s1 = s2;

最初:

s1 ──→ 对象 A
s2 ──→ 对象 B

执行:

s1 = s2;

以后:

s1 ──┐
     ├──→ 对象 B
s2 ──┘

s1 不再引用对象 A。

这里发生的是:

引用变量保存的引用值发生了变化。


2.20 什么是 null

引用变量还可以保存特殊值:

null

例如:

Student student = null;

表示:

student 当前没有引用任何 Student 对象。

可以画成:

student
   │
   └──→ null

2.21 null 与“空对象”不是一回事

不要理解成:

null
=
有一个 Student 对象,只是里面什么都没有

不是。

Student student = null;

表示:

当前没有 Student 对象可以通过这个引用访问。


2.22 对 null 访问实例成员

例如:

Student student = null;

System.out.println(student.name);

或者:

student.printInfo();

运行时会出现:

NullPointerException

因为:

student

没有指向一个可以访问成员的对象。

所以以后看到:

NullPointerException

首先就应该思考:

我正在通过哪个引用访问对象?这个引用是否为 null?


2.23 对象成员变量也是可以引用其他对象的

例如:

class Student {
    String name;
    Book book;
}

这里:

book

本身也是一个引用类型成员变量。

创建:

Student student = new Student();

此时:

student.book

默认是:

null

以后:

student.book = new Book();

就可以形成:

student
   ↓
Student 对象
   │
   └── book ──→ Book 对象

这就是大型对象模型的基本组成方式:

对象之间通过引用建立关系。


三、使用方法

3.1 成员变量与成员方法

public class Student {

    String name;
    double chinese;
    double math;

    public void printTotalScore() {

        double total =
                chinese + math;

        System.out.println(
                name + "的总成绩:"
                        + total
        );
    }

    public void printAverageScore() {

        double average =
                (chinese + math) / 2;

        System.out.println(
                name + "的平均成绩:"
                        + average
        );
    }
}

这里:

成员变量:
name
chinese
math

成员方法:
printTotalScore()
printAverageScore()

局部变量:
total
average

3.2 创建和使用对象

public class StudentTest {

    public static void main(String[] args) {

        Student s1 =
                new Student();

        s1.name = "小雨";
        s1.chinese = 100;
        s1.math = 98;

        s1.printTotalScore();
        s1.printAverageScore();
    }
}

3.3 两个对象分别调用同一方法

Student s1 = new Student();

s1.name = "小雨";
s1.chinese = 100;
s1.math = 98;


Student s2 = new Student();

s2.name = "小星";
s2.chinese = 80;
s2.math = 90;


s1.printTotalScore();
s2.printTotalScore();

同一个:

printTotalScore()

分别处理两个对象自己的数据。


3.4 验证成员变量默认值

class User {

    String username;
    int age;
    double balance;
    boolean enabled;
}

测试:

public class UserTest {

    public static void main(String[] args) {

        User user = new User();

        System.out.println(user.username);
        System.out.println(user.age);
        System.out.println(user.balance);
        System.out.println(user.enabled);
    }
}

当前重点是观察:

对象字段创建时具有默认值。


3.5 验证引用共享

public class ReferenceDemo {

    public static void main(String[] args) {

        Student s1 =
                new Student();

        s1.name = "小雨";

        Student s2 = s1;

        s2.name = "小星";

        System.out.println(s1.name);
        System.out.println(s2.name);
    }
}

画图:

s1 ──┐
     ├──→ Student
s2 ──┘
        name = "小星"

3.6 验证两个独立对象

Student s1 = new Student();
Student s2 = new Student();

s1.name = "小雨";
s2.name = "小星";

System.out.println(s1.name);
System.out.println(s2.name);

画图:

s1 ──→ Student A
       name = 小雨


s2 ──→ Student B
       name = 小星

3.7 对象数组

之前我们使用:

int[] numbers;
String[] names;

现在还可以:

Student[] students =
        new Student[3];

注意:

Student[]

数组中保存的元素是:

Student 引用。

刚创建时:

students[0]
students[1]
students[2]

默认都是:

null

必须进一步:

students[0] = new Student();

才能让:

students[0]

引用一个真正的 Student 对象。


3.8 对象数组的基本模型

例如:

Student[] students =
        new Student[2];

students[0] = new Student();
students[1] = new Student();

students[0].name = "小雨";
students[1].name = "小星";

结构:

students
   ↓
┌────────┬────────┐
│ ref    │ ref    │
└──┬─────┴──┬─────┘
   │        │
   ↓        ↓

Student A  Student B
name 小雨   name 小星

这正是:

数组
+
引用
+
对象

的组合。


四、原理与进阶

4.1 对象字段在实例创建时获得独立存储

每创建一个新的类实例,对象就具有对应实例字段。

例如:

Student s1 = new Student();
Student s2 = new Student();

如果 Student 有:

String name;
int age;

那么可以抽象为:

对象 A
├── name
└── age

对象 B
├── name
└── age

这就是为什么实例状态互相独立。


4.2 默认值发生在对象字段初始化阶段

例如:

Student s1 = new Student();

对象中的:

String → null
int → 0
double → 0.0
boolean → false

并不是:

JVM 随机留下了一块旧内存。

而是 Java 语言定义的默认初始化语义。


4.3 引用值不是对象本身

Java 的类型系统中:

基本类型变量
→ 保存基本类型值

引用类型变量
→ 保存引用值

例如:

int age = 20;

age 保存:

20

而:

Student s1 = new Student();

s1 保存:

对 Student 对象的引用

这两类变量的行为差异,将直接影响:

赋值
参数传递
对象共享
null
数组
集合
多态

4.4 引用赋值为什么会产生“联动”

例如:

Student a = new Student();

Student b = a;

之后:

b.age = 100;

通过:

a.age

也能看到 100

不是:

a.age 自动从 b.age 同步

而是:

a 和 b
↓
引用同一个对象

对象只有一个。


4.5 Java 不保证程序员能直接看到“真实对象地址”

有些教学示意图会写:

0x12345678

表示对象地址。

这种图可以帮助理解:

不同引用是否指向同一对象

但不要误以为普通 Java 程序可以像 C 指针那样:

直接读取地址
地址加减
通过裸地址访问内存

Java 语言暴露的是:

reference

而不是供普通程序任意操作的裸内存指针。


4.6 对象引用与垃圾回收的初步联系

例如:

Student s1 =
        new Student();

s1 =
        new Student();

第一次创建的 Student 对象如果:

已经没有任何可达引用能够访问

那么它将来可能成为垃圾回收器处理的对象。

这里只建立认识:

对象
≠
永远随着创建它的变量一起存在

真正 GC、堆、可达性分析属于 JVM 专门知识,不在本章展开。


五、实践应用

真实项目中的对象通常不是孤立存在。

例如订单系统:

User
 ↓
Order
 ↓
OrderItem
 ↓
Product

博客:

Article
 ├── Author
 ├── Tag[]
 └── Category

学校:

Student
 ├── ClassRoom
 └── Course[]

背后的基础机制仍然是:

对象
+
成员变量
+
成员方法
+
对象引用

因此本章实际上是在建立:

整个 Java 对象图(Object Graph)的最小基础。


六、常见问题

6.1 成员变量一定需要自己初始化吗?

对象创建时,字段会得到 Java 定义的默认值。

但是业务上是否应该依赖:

0
null
false

作为有效状态,需要根据具体设计判断。


6.2 局部变量也有这些默认值吗?

不能这样理解。

普通局部变量必须在满足明确赋值规则以后才能读取。

例如:

int number;

// System.out.println(number);

不能直接读取。


6.3 成员方法为什么不用传 name 就能使用 name?

因为:

name

是对象的成员变量。

成员方法在某个对象上执行时,可以访问当前对象的实例成员。

更加完整的 this 解释放到下一章。


6.4 Student s2 = s1; 会创建新对象吗?

不会。

因为没有:

new Student()

它只是复制引用值。


6.5 Student s1 = new Student(); Student s2 = new Student(); 呢?

这里执行了两次:

new Student()

所以是两个不同对象。


6.6 null 是不是一个空 Student 对象?

不是。

null 表示:

当前引用没有指向对象。


6.7 为什么会出现 NullPointerException?

例如:

Student student = null;

student.printInfo();

程序想通过:

student

访问实例成员,但这个引用并没有指向任何 Student 对象。


6.8 对象变量里保存的是内存地址吗?

入门阶段可以用“地址”图辅助理解。

但准确 Java 术语应该说:

保存引用值。

不要把 Java 引用直接等同于可供程序员随意操作的裸内存地址。


6.9 s1 == s2 比较的是什么?

当两边都是引用类型变量时:

s1 == s2

关注的是:

两个引用是否指向同一个对象。

例如:

Student s1 = new Student();
Student s2 = s1;

则两者引用同一对象。

对象“业务内容是否相等”是另一个问题,未来学习 equals() 时再系统处理。


6.10 一个对象能不能引用另一个对象?

可以,而且非常常见。

例如:

class Order {
    User user;
    Product product;
}

对象关系正是大型 Java 系统建模的重要组成部分。


七、练习与验收

7.1 知识问答

  1. 什么是成员变量?
  2. 什么是成员方法?
  3. 成员变量主要描述对象的什么?
  4. 成员方法主要描述对象的什么?
  5. 成员变量和局部变量主要有哪些区别?
  6. 为什么对象字段创建后具有默认值?
  7. 成员方法为什么可以访问对象成员变量?
  8. 对象.成员. 应该如何理解?
  9. 引用变量保存的是什么?
  10. 为什么“对象变量保存地址”只能作为入门类比?
  11. Student b = a; 是否复制对象?
  12. 两个引用变量指向同一对象时,一个引用修改对象后为什么另一个能够看到?
  13. 什么是 null
  14. null 与“空对象”有什么区别?
  15. 为什么访问 null 引用可能产生 NullPointerException
  16. 对象数组中保存的是 Student 对象本身还是 Student 引用?

7.2 代码阅读

禁止运行:

class Student {
    String name;
    int age;
}

public class ReferenceRead01 {

    public static void main(String[] args) {

        Student s1 =
                new Student();

        s1.name = "小雨";
        s1.age = 21;

        Student s2 = s1;

        s2.name = "小星";

        System.out.println(s1.name);
        System.out.println(s2.name);
    }
}

要求:

  1. 一共创建几个 Student 对象?
  2. 一共有几个 Student 引用变量?
  3. s1s2 是否指向同一个对象?
  4. 预测两行输出。
  5. 画引用关系图。

继续:

class User {
    String name;
    int age;
    double balance;
    boolean enabled;
}

public class FieldRead01 {

    public static void main(String[] args) {

        User user =
                new User();

        System.out.println(user.name);
        System.out.println(user.age);
        System.out.println(user.balance);
        System.out.println(user.enabled);
    }
}

要求:

  1. 不运行预测结果。
  2. 解释每个成员变量为什么已经有初始状态。

7.3 手写代码

  1. 创建 Student 类,包含 3 个成员变量、2 个成员方法。
  2. 创建两个 Student 对象,分别保存不同数据。
  3. 设计 Product 类,让成员方法能够直接使用商品自己的成员变量。
  4. 写代码证明两个不同 new Student() 的数据互相独立。
  5. 写代码证明两个引用变量可以指向同一个 Student 对象。
  6. 创建长度为 3 的 Student[],并为三个位置分别创建 Student 对象。
  7. 画出对象数组对应的引用图。

7.4 Debug

分析:

Student[] students =
        new Student[3];

students[0].name = "小雨";

要求:

  1. 为什么 new Student[3] 不等于创建了 3 个 Student 对象?
  2. students[0] 当前是什么?
  3. 程序可能出现什么问题?
  4. 修复代码并画出引用结构。

继续:

Student s1 = new Student();
Student s2 = s1;

s2.age = 100;

某同学认为:

Java 自动把 s2 的 age 同步到了 s1。

要求:

  1. 判断这句话是否准确。
  2. 用对象数量和引用数量解释真实原因。

7.5 综合训练

将原来的平行数组学生系统:

String[] names = ...;
double[] chinese = ...;
double[] math = ...;

重构为:

Student[]

要求:

  1. Student 至少包含姓名、语文成绩、数学成绩。
  2. Student 至少提供总分与平均分行为。
  3. 创建至少 3 个 Student 对象。
  4. 将对象放入 Student 数组。
  5. 遍历数组并让每个对象输出自己的总分和平均分。
  6. 画出:
    • Student 类
    • Student[] 数组对象
    • 三个 Student 对象
    • 对应引用关系

7.6 本章验收

关闭资料后确认:

  • [ ] 能区分成员变量和局部变量。
  • [ ] 能说出常见实例成员变量默认值。
  • [ ] 能从零定义成员方法。
  • [ ] 能使用对象访问成员变量和调用成员方法。
  • [ ] 能解释同一个方法为什么可以处理不同对象的数据。
  • [ ] 能准确说明引用变量保存的是引用值。
  • [ ] 不再机械地把 Java 引用说成“可直接操作的物理地址”。
  • [ ] 能解释两个引用变量指向同一对象的行为。
  • [ ] 能解释 null
  • [ ] 能定位基础 NullPointerException。
  • [ ] 能画出对象数组和对象之间的引用关系。

如果看到:

Student s2 = s1;

仍然认为:

“Java 又复制了一份完整 Student 对象。”

那么本章的对象引用模型还没有真正建立。