Mr.Mou @ ShiShi AP Center

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

这是一份 BlueJ 动手实验。每一步只增加一个新概念: 写一点代码 → Compile → 在 Object Bench 观察 → 画 reference。

整节课只追踪两件事:
① 每次执行 new 时,创建了什么 object?
② 现在有哪些 reference 指向这些 object?

实验路线

点击卡片可以直接展开对应步骤。

0 BlueJ 准备:先建立 Avatar

新建一个 BlueJ Project,例如 Unit3_6_ObjectReferences,然后创建 class Avatar。

复制下面的完整代码并 Compile。

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;
    }
}
BLUEJ 操作 右键 Avatar class → 选择 constructor → name 输入 "Knight",health 输入 80。把 Object Bench 上的新对象命名为 avatar1。
avatar1
   │
   ▼
┌─────────────────┐
│ Avatar object   │
│ name = Knight   │
│ health = 80     │
└─────────────────┘
关键:avatar1 是 reference variable。它保存的是指向这个 Avatar object 的 reference。
1 Class 怎么会成为一种 Type?

先比较三种 variable declaration:

Compare the types
int level;
String username;
Avatar avatar;

String

Java 已经提供的 class。

String 因此也是一种 reference type。

Avatar

我们自己写的 class。

写出 class Avatar 后,Avatar 也成为一种 reference type。

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

创建 GameProfile

新建 class GameProfile,复制下面的 scaffold。所有 TODO 都只是 comment,所以这份 scaffold 本身可以 Compile。

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

    // TODO 1:在这里声明一个 Avatar 类型的 instance variable,变量名叫 avatar


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

        // TODO 2:把 parameter a 保存到 instance variable avatar

    }

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

        // TODO 3:输出 avatar 的 name,使用 avatar 的 getter


        // TODO 4:输出 avatar 的 health,使用 avatar 的 getter

    }
}

第一轮:完成 TODO 1 和 TODO 2,然后 Compile。

写代码前先说出来:
TODO 1 这一行的 type 为什么可以写成 Avatar?

Compile 成功后,再完成 TODO 3 和 TODO 4,然后再次 Compile。

BLUEJ 操作 右键 GameProfile class → constructor。
u: "Alex"
l: 5
a: 使用 Object Bench 上已有的 avatar1
把新对象命名为 profile1。

等价的 Java 写法:

Equivalent Java
GameProfile profile1 = new GameProfile("Alex", 5, avatar1);
图示说明后面的图会写 profile1.avatar 来表示 profile1 内部保存的 private field。在 BlueJ 实验中,请通过 profile1.printProfile() 来观察它。
avatar1 ─────────────┐
                     ▼
                ┌───────────────┐
                │ Avatar object │
                │ Knight        │
                │ HP = 80       │
                └───────────────┘
                     ▲
                     │
profile1.avatar ─────┘
GameProfile has an Avatar.
更准确地说:GameProfile 的一个 instance variable 保存了指向 Avatar object 的 reference。
2 Two References, One Object

现在让 BlueJ 直接验证上面的 reference 图。

BLUEJ 实验 ① 右键 profile1 → printProfile()
② 右键 avatar1 → setHealth(int) → 输入 20
③ 再次调用 profile1.printProfile()
先预测:第三步会显示 HP = 80 还是 HP = 20?用箭头图解释。

Constructor 调用时

constructor 的 parameter a 得到的是 avatar1 中 reference value 的 copy。

调用 constructor 的过程中:

avatar1 ──────────┐
a ────────────────┼──► Knight
profile1.avatar ──┘

Constructor 结束后

avatar1 ──────────┐
                  ├──► Knight
profile1.avatar ──┘

parameter a → constructor 结束后离开它的 scope
Java is pass by value.
这里被复制的 value 恰好是一个 object reference。
3 Object 作为 Method Argument

这一次我们创建一个不同的 Avatar,这样 parameter 的作用会更清楚。

BLUEJ 操作 再创建一个 Avatar:name = "Mage",health = 40。
Object Bench name:avatar2。
avatar1 ─────► Knight (HP 20)

avatar2 ─────► Mage   (HP 40)

profile1.avatar ─────► Knight

给 GameProfile 增加一个 method

把下面这段 method 放进 GameProfile class,位置在 class 最后一个 } 之前。TODO 仍然只是 comment,所以 method 在你动笔之前也能 Compile。

Add to GameProfile
public void healAvatar(Avatar target, int amount)
{
    // TODO:使用 target 调用 heal method,并把 amount 作为 argument 传进去

}

自己写完整的一行 Java,然后 Compile。

BLUEJ 实验 右键 profile1 → healAvatar(Avatar target, int amount)
target: avatar2
amount: 30

等价的 Java method call:

Equivalent Java
profile1.healAvatar(avatar2, 30);
先预测:Mage 的 health 会变成多少?Knight 会不会改变?

Method 运行过程中

profile1.avatar ─────► Knight

avatar2 ─────┐
             ├──────► Mage
target ──────┘

target 是 method 运行时创建的临时 parameter variable。调用时,Java 用 avatar2 当前保存的 reference 来初始化 target。因此它们一开始指向同一个 Mage object,但它们不是同一个 variable。

Method 结束后

profile1.avatar ─────► Knight

avatar2 ─────────────► Mage

target → method 结束后离开它的 scope
如果 method 通过 target 修改 Mage,avatar2 之后看到的也是同一个已经被修改的 Mage object。
4 Argument 和 Parameter:不是同一个 Variable

这一步不再创建新的 Robot,也不让 target 改指向。我们只把上一步真正发生的事情拆开看清楚。

Method call
profile1.healAvatar(avatar2, 30);

Argument:avatar2

avatar2 是 method 外面已经存在的 variable。它指向 Mage。

Parameter:target

target 是 method 开始运行时创建的临时 variable。method 结束后,它离开 scope。

调用开始时发生什么?

Java 创建 parameter target,并用 avatar2 当前保存的 reference 来初始化它。为了理解,可以把这一瞬间想成:

Conceptual model
Avatar target = avatar2;
注意这不是你需要额外写进程序的一行代码,只是帮助理解 method call 时 parameter 怎样得到初始值。
avatar2 ─────┐
             ├────► Mage (HP 40)
target  ─────┘
最重要的一点:target 不是 avatar2 的另一个名字。它们是两个不同的 variables;只是 method 刚开始时,它们指向同一个 Mage object。

然后执行 target.heal(30)

target 顺着自己的 reference 找到 Mage,并修改这个 Mage object。因为 avatar2 也指向同一个 Mage,所以 method 结束后,从 avatar2 也能看到 HP 已经变成 70。

method 运行时:

avatar2 ─────┐
             ├────► Mage (HP 70)
target  ─────┘

method 结束后:

avatar2 ─────────► Mage (HP 70)
target → 离开 scope
target 存在的意义:
它是 method 内部对“这一次传进来的 Avatar”使用的临时 variable。
因此同一个 healAvatar method 可以处理 avatar1、avatar2 或其他 Avatar,而不需要为每个 object 写一个新的 method。
5 Method 返回一个 Object Reference

最后让 GameProfile 返回它保存的 Avatar reference。

把下面的 TODO comments 放到 GameProfile class 最后一个 } 之前。comments 不影响 Compile。

TODO for getAvatar
// TODO:写一个 public method
// method name:getAvatar
// parameter:没有
// return type:Avatar
// method body:return GameProfile 保存的 instance variable avatar

根据四条 comment 自己写出整个 method,然后 Compile。

BLUEJ 实验 右键 profile1 → 调用 getAvatar()。
BlueJ 显示返回的 object 后,使用 Get 把这个 returned object 放到 Object Bench,并命名为 avatar3。
先预测:avatar3 是一个新 Avatar object,还是另一个指向 Knight 的 reference?

调用 getAvatar() 本身没有执行新的 new Avatar(...)。

avatar1 ──────────┐
                  │
profile1.avatar ──┼────► Knight
                  │
avatar3 ──────────┘
最后验证 右键 avatar3 → setHealth(int) → 输入 1。
然后调用 profile1.printProfile()。
return avatar; 返回的是 reference value。Java 不会因为 return 一个 object reference 就自动复制整个 object。
✓ AP 题:怎么追踪 Reference?
① 标记每次执行的 new
② 画出每个 reference
③ method 结束时移除 local parameter

Example

Trace it
Avatar a1 = new Avatar("Knight", 80);
Avatar a2 = a1;
Avatar a3 = a2;
a1 ───┐
a2 ───┼────► one Avatar object
a3 ───┘
Reference 的数量和 object 的数量是两件不同的事。

Primitive vs Object Argument

Primitive

score = 80
   │ copy
   ▼
x = 80

parameter 得到 primitive value 的 copy。

Object Reference

avatar2 ───┐
           ├──► Mage
target ────┘

parameter 得到 reference value 的 copy。

两种情况都遵守同一个规则:Java is pass by value.
⚡ Quick Check:只画图,不改 Project

下面的题目用来练 reference tracing。直接在纸上或脑中画箭头即可,不需要把这些 method 加进当前 BlueJ project。

Challenge 1

Predict
Avatar a1 = new Avatar("Knight", 80);
Avatar a2 = a1;
a2.setHealth(10);
a1.getHealth() 是多少?

Challenge 2

Predict
public void changeHealth(Avatar target)
{
    target.setHealth(5);
}
如果把一个 health = 80 的 Avatar reference 传进去,method 结束后原来的 Avatar health 是多少?

Challenge 3

Predict
profile1.healAvatar(avatar2, 30);
method 运行时,avatar2 和 parameter target 是同一个 variable 吗?它们是否指向同一个 object?

Challenge 4

Predict
Avatar a2 = profile1.getAvatar();
如果 getAvatar() 只是 return avatar;,这一行创建了新的 Avatar object 吗?

完成后核对

这里集中放完整代码和 Quick Check 答案。

Stage 1:GameProfile TODO 1–4
TODO 1
private Avatar avatar;
TODO 2
avatar = a;
TODO 3
System.out.println("Avatar: " + avatar.getName());
TODO 4
System.out.println("Health: " + avatar.getHealth());
Stage 3:healAvatar
Answer
public void healAvatar(Avatar target, int amount)
{
    target.heal(amount);
}

Mage 从 HP 40 变成 HP 70;Knight 不受影响。

Stage 4:Argument vs Parameter

avatar2 是 argument;target 是 method 运行时创建的临时 parameter variable。它们不是同一个 variable。

avatar2 ─────┐
             ├────► Mage
target  ─────┘

method 刚开始时,它们指向同一个 Mage。target.heal(...) 修改的是这个共享的 Mage object;method 结束后 target 离开 scope,avatar2 仍然存在。

Stage 5:getAvatar
Answer
public Avatar getAvatar()
{
    return avatar;
}
完整 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.getName());
        System.out.println("Health: " + avatar.getHealth());
    }

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


    public Avatar getAvatar()
    {
        return avatar;
    }
}
Quick Check Answers
  • Challenge 1:10。a1 和 a2 指向同一个 object。
  • Challenge 2:5。method 修改了共享 object。
  • Challenge 3:不是同一个 variable;但 method 运行时,avatar2 和 target 指向同一个 Mage object。
  • Challenge 4:没有创建新的 Avatar object;得到的是另一个 reference。