Mr.Mou @ ShiShi AP Center

AP CSA 3.6:用 BlueJ 看懂对象引用

3.6 最需要建立的是一个清楚的 reference mental model: object 在哪里?reference 指向哪里?

每次看代码,都问两个问题:
① 现在一共有几个 object? ② 哪些 reference 指向同一个 object?

先看整条路线

点击任意卡片,直接展开对应部分。

数 new
数 object
画 reference 箭头
1 Class 怎么会变成一种 Type?

先看三行代码:

Three instance variables
private int level;
private String username;
private Avatar avatar;

第三行中的 Avatar 出现在了 type 的位置。 原因是 Java 中:

定义一个 class,也就定义了一种新的 reference type。

当我们写:

Avatar.java
public class Avatar
{
    ...
}

从这一刻开始,Avatar 就可以像 String 一样出现在 variable declaration、parameter 和 return type 中。

代码 Type Variable 中保存什么
int level; primitive type 一个整数值
String username; class / reference type 一个 object reference
Avatar avatar; 我们定义的 class / reference type 一个 object reference
avatar
  │
  │ reference
  ▼
┌────────────────┐
│ Avatar object  │
└────────────────┘
检查理解: String 和 Avatar 有什么共同点?
★ BlueJ 准备:建立 Avatar.java

新建 BlueJ Project,例如:

Project name
Unit3_6_ObjectReferences

创建 class:Avatar,然后复制下面的完整代码。

Avatar.java
public class Avatar
{
    private String name;
    private int health;

    public Avatar(String n, int h)
    {
        name = n;
        health = h;
    }

    public String getName()
    {
        return name;
    }

    public int getHealth()
    {
        return health;
    }

    public void setHealth(int h)
    {
        health = h;
    }

    public void heal(int amount)
    {
        health += amount;
    }

    public void takeDamage(int amount)
    {
        health -= amount;
    }

    public String toString()
    {
        return name + " (HP: " + health + ")";
    }
}

Compile 后,在 Object Bench 创建:

Create an Avatar
new Avatar("Knight", 80)

object name 设为:

Object name
avatar1

右键 avatar1 → Inspect,把它想成:

avatar1
   │
   ▼
┌─────────────────┐
│ Avatar object   │
│ name = Knight   │
│ health = 80     │
└─────────────────┘
BlueJ Object Bench 帮助我们把 reference 可视化。它不是 Java memory 的真实图片, 但很适合用来追踪 object。
2 BlueJ Task 1:让 GameProfile 拥有一个 Avatar

创建第二个 class:GameProfile。 复制下面的 scaffold,再完成其中的空格。

GameProfile.java — scaffold
public class GameProfile
{
    private String username;
    private int level;

    // TODO 1
    // GameProfile has an Avatar.
    // 这里的 type 应该是什么?
    private ________ avatar;


    // TODO 2
    // constructor 需要接收一个 Avatar object 的 reference。
    public GameProfile(String u, int l, ________ a)
    {
        username = u;
        level = l;

        // 把 parameter 中的 reference 存进 instance variable。
        avatar = ________;
    }


    public void printProfile()
    {
        System.out.println("Username: " + username);
        System.out.println("Level: " + level);
        System.out.println("Avatar: " + avatar);
    }


    // TODO 3
    // 接收另一个 Avatar reference,然后让那个 Avatar 恢复 health。
    public void healAvatar(________ target, int amount)
    {
        ________.heal(amount);
    }


    // TODO 4
    // 返回这个 GameProfile 保存的 Avatar reference。
    public ________ getAvatar()
    {
        return ________;
    }
}

先完成 TODO 1 和 TODO 2,然后 Compile。

观察:
为什么 String 可以放在 type 的位置?
为什么 Avatar 也可以放在 type 的位置?

String

Java 已经提供的 class。

String username;

Avatar

我们自己写的 class。

Avatar avatar;

这时可以用一句很简单的话描述:

GameProfile has an Avatar.
这就是这里需要理解的 has-a relationship。
3 Experiment:Two References, One Object

Object Bench 上已经有:

Existing object
avatar1

完成并 Compile GameProfile 后,创建:

Create a GameProfile
new GameProfile("Alex", 5, avatar1)

object name 设为:

Object name
profile1

第三个 argument 是 avatar1。这一步没有执行新的 new Avatar(...)。

avatar1 ─────────────┐
                     │
                     ▼
                ┌───────────────┐
                │ Avatar object │
                │ Knight        │
                │ HP = 80       │
                └───────────────┘
                     ▲
                     │
profile1.avatar ─────┘
两个 reference,一个 Avatar object。

现在让 BlueJ 证明这件事

  1. 右键 profile1 → 调用 printProfile()
  2. 右键 avatar1 → 调用 setHealth(20)
  3. 再次调用 profile1.printProfile()
先预测: profile1 看到的 Avatar health 是 80 还是 20? 画一张 reference 图说明理由。

Constructor 里发生了什么?

创建 profile1 时:

Constructor call
new GameProfile("Alex", 5, avatar1)

parameter a 得到的是 avatar1 中 reference value 的 copy:

avatar1 ─────┐
             ▼
          Avatar
             ▲
a ───────────┘

当 constructor 再执行:

Store the reference
avatar = a;

就形成:

avatar1 ──────────┐
a ────────────────┼──► same Avatar object
profile1.avatar ──┘
Java is pass by value.
当 argument 是 object reference 时,被复制的 value 就是那个 reference。
4 BlueJ Task 2:Object 也可以作为 Method Argument

现在完成 scaffold 中的 TODO 3:

TODO 3
public void healAvatar(________ target, int amount)
{
    ________.heal(amount);
}

Compile 后,在 BlueJ 中调用:

Method call
profile1.healAvatar(avatar1, 30)
先预测: 调用后,avatar1 的 health 会不会改变?

method 运行时,target 是一个新的 parameter variable。 它得到的是 reference value 的 copy:

avatar1 ─────┐
             ▼
          Avatar
             ▲
target ──────┘
关键问题: target.heal(amount) 改变的是 parameter variable 本身, 还是它所指向的 Avatar object?

实验完成后,再调用:

Check the result
profile1.printProfile()
5 Experiment:修改 Object vs 修改 Reference

在 GameProfile 中加入下面这个 method,并补全 type:

tryToReplace scaffold
public void tryToReplace(________ target)
{
    target = new Avatar("Robot", 100);
}

Compile 后调用:

Method call
profile1.tryToReplace(avatar1)
先预测: 调用结束后,avatar1 会指向 Knight 还是 Robot?

method 开始时:

avatar1 ─────┐
             ▼
          Knight
             ▲
target ──────┘

执行:

Reassign the parameter
target = new Avatar("Robot", 100);

完成这张图:

avatar1 ─────────► __________

target  ─────────► __________

然后在 BlueJ 中依次:

  1. 调用 tryToReplace
  2. Inspect avatar1
  3. 调用 profile1.printProfile()

把两种操作并排看

操作 A

Modify object
target.heal(30);

想一想:谁发生了变化?

操作 B

Reassign reference
target = new Avatar("Robot", 100);

想一想:谁发生了变化?

完成下面两句:

target.heal(30) → 修改 ____________________
target = new Avatar(...) → 修改 ____________________
6 BlueJ Task 3:Method 返回一个 Object

现在完成 scaffold 中的 TODO 4:

TODO 4
public ________ getAvatar()
{
    return ________;
}
如果 method 返回的是一个 Avatar object 的 reference, return type 应该是什么?

Compile 后:

  1. 右键 profile1
  2. 调用 getAvatar()
  3. 让 BlueJ 显示返回的 object
  4. 如果 BlueJ 提供 Get,把返回的 object 放到 Object Bench
  5. 给它一个名字,例如 avatar2

现在 Object Bench 上可能有:

avatar1
profile1
avatar2
先判断: 现在一共有几个 Avatar object?
用 new Avatar(...) 实际执行了几次来判断。

如果 avatar2 和 profile1.avatar 指向同一个 object:

Test the returned reference
avatar2.setHealth(1)

然后再次调用:

Check
profile1.printProfile()

观察结果,再画出三个 reference 的箭头。

✓ AP 题怎么追踪 Reference?

代码变长以后,继续使用同一套方法。

第一步:数 new

Example
Avatar a1 = new Avatar("Knight", 80);
Avatar a2 = a1;
Avatar a3 = a2;

这里真正执行的 new Avatar(...) 只有一次。

第二步:数 object

1 个 Avatar object

第三步:画 reference

a1 ───┐
a2 ───┼──► Avatar object
a3 ───┘
Reference 的数量 ≠ Object 的数量。

Primitive 和 Object 放在一起比较

Primitive argument

score = 80
   │ copy
   ▼
x = 80

复制的是 primitive value。

Object argument

avatar1 ───┐
           ▼
        Avatar
           ▲
target ────┘

复制的是 reference value。

Java 的规则始终一致:pass by value。
区别只是被复制的 value 是 primitive value,还是 reference value。
⚡ BlueJ Challenge:先画,再运行

Challenge 1

Challenge 1
Avatar a1 = new Avatar("Knight", 80);
Avatar a2 = a1;

a2.setHealth(10);
a1.getHealth() 是多少?

Challenge 2

Method
public void change(Avatar target)
{
    target.setHealth(5);
}
Run
Avatar a1 = new Avatar("Knight", 80);
profile1.change(a1);
a1.getHealth() 是多少?

Challenge 3

Method
public void change(Avatar target)
{
    target = new Avatar("Robot", 100);
}
Run
Avatar a1 = new Avatar("Knight", 80);
profile1.change(a1);
a1 现在指向 Knight 还是 Robot?

Challenge 4

Return a reference
Avatar a2 = profile1.getAvatar();
a2 和 profile1.avatar 是两个不同的 Avatar object, 还是两个 reference 指向同一个 Avatar object?

最后的 Mental Model

avatar1 ────────────┐
                    │
profile1.avatar ────┼──► Avatar object
                    │
avatar2 ────────────┘

上图中有 3 个 reference,但只有 1 个 Avatar object。

遇到 reference 题:

数 new
数 object
画 reference 箭头

完成后核对

GameProfile.java 完整版本
GameProfile.java
public class GameProfile
{
    private String username;
    private int level;
    private Avatar avatar;

    public GameProfile(String u, int l, Avatar a)
    {
        username = u;
        level = l;
        avatar = a;
    }

    public void printProfile()
    {
        System.out.println("Username: " + username);
        System.out.println("Level: " + level);
        System.out.println("Avatar: " + avatar);
    }

    public void healAvatar(Avatar target, int amount)
    {
        target.heal(amount);
    }

    public Avatar getAvatar()
    {
        return avatar;
    }
}
Experiment:Two References, One Object

avatar1.setHealth(20) 后, profile1.printProfile() 看到的 health 也是 20。

avatar1 ─────────────┐
                     ▼
                  Avatar
                     ▲
profile1.avatar ─────┘
TODO 3:healAvatar
Answer
public void healAvatar(Avatar target, int amount)
{
    target.heal(amount);
}

target 和 caller 中的 reference 指向同一个 Avatar object, 所以 target.heal(...) 修改的是共享 object。

tryToReplace
Answer
public void tryToReplace(Avatar target)
{
    target = new Avatar("Robot", 100);
}

执行重新赋值后:

avatar1 ─────────► Knight

target  ─────────► Robot

target 改变了自己保存的 reference。 caller 中的 avatar1 仍然指向原来的 Knight object。

修改 Object vs 修改 Reference
target.heal(30)
→ 修改 target 指向的 object

target = new Avatar(...)
→ 修改 target 自己保存的 reference
TODO 4:getAvatar
Answer
public Avatar getAvatar()
{
    return avatar;
}

return avatar; 返回的是 reference value。 它不会自动创建一个新的 Avatar object。

profile1.avatar ───┐
                   ▼
                Avatar
                   ▲
avatar2 ───────────┘
Challenge Answers

Challenge 1: 10

Challenge 2: 5

Challenge 3: a1 仍然指向 Knight

Challenge 4: 两个 reference 指向同一个 Avatar object