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。
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;
}
}"Knight",health 输入 80。把 Object Bench 上的新对象命名为 avatar1。
avatar1
│
▼
┌─────────────────┐
│ Avatar object │
│ name = Knight │
│ health = 80 │
└─────────────────┘avatar1 是 reference variable。它保存的是指向这个 Avatar object 的 reference。1 Class 怎么会成为一种 Type?
先比较三种 variable declaration:
int level;
String username;
Avatar avatar;String
Java 已经提供的 class。
String 因此也是一种 reference type。
Avatar
我们自己写的 class。
写出 class Avatar 后,Avatar 也成为一种 reference type。
创建 GameProfile
新建 class GameProfile,复制下面的 scaffold。所有 TODO 都只是 comment,所以这份 scaffold 本身可以 Compile。
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。
u: "Alex"l: 5a: 使用 Object Bench 上已有的 avatar1把新对象命名为
profile1。
等价的 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 的一个 instance variable 保存了指向 Avatar object 的 reference。2 Two References, One Object
现在让 BlueJ 直接验证上面的 reference 图。
profile1 → printProfile()② 右键
avatar1 → setHealth(int) → 输入 20③ 再次调用
profile1.printProfile()
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这里被复制的 value 恰好是一个 object reference。
3 Object 作为 Method Argument
这一次我们创建一个不同的 Avatar,这样 parameter 的作用会更清楚。
"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。
public void healAvatar(Avatar target, int amount)
{
// TODO:使用 target 调用 heal method,并把 amount 作为 argument 传进去
}自己写完整的一行 Java,然后 Compile。
profile1 → healAvatar(Avatar target, int amount)target: avatar2amount: 30
等价的 Java method call:
profile1.healAvatar(avatar2, 30);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 结束后离开它的 scopetarget 修改 Mage,avatar2 之后看到的也是同一个已经被修改的 Mage object。4 Argument 和 Parameter:不是同一个 Variable
这一步不再创建新的 Robot,也不让 target 改指向。我们只把上一步真正发生的事情拆开看清楚。
profile1.healAvatar(avatar2, 30);Argument:avatar2
avatar2 是 method 外面已经存在的 variable。它指向 Mage。
Parameter:target
target 是 method 开始运行时创建的临时 variable。method 结束后,它离开 scope。
调用开始时发生什么?
Java 创建 parameter target,并用 avatar2 当前保存的 reference 来初始化它。为了理解,可以把这一瞬间想成:
Avatar target = avatar2;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 → 离开 scopetarget 存在的意义:它是 method 内部对“这一次传进来的 Avatar”使用的临时 variable。
因此同一个
healAvatar method 可以处理 avatar1、avatar2 或其他 Avatar,而不需要为每个 object 写一个新的 method。5 Method 返回一个 Object Reference
最后让 GameProfile 返回它保存的 Avatar reference。
把下面的 TODO comments 放到 GameProfile class 最后一个 } 之前。comments 不影响 Compile。
// TODO:写一个 public method
// method name:getAvatar
// parameter:没有
// return type:Avatar
// method body:return GameProfile 保存的 instance variable avatar根据四条 comment 自己写出整个 method,然后 Compile。
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?
newExample
Avatar a1 = new Avatar("Knight", 80);
Avatar a2 = a1;
Avatar a3 = a2;a1 ───┐
a2 ───┼────► one Avatar object
a3 ───┘Primitive vs Object Argument
Primitive
score = 80
│ copy
▼
x = 80parameter 得到 primitive value 的 copy。
Object Reference
avatar2 ───┐
├──► Mage
target ────┘parameter 得到 reference value 的 copy。
⚡ Quick Check:只画图,不改 Project
下面的题目用来练 reference tracing。直接在纸上或脑中画箭头即可,不需要把这些 method 加进当前 BlueJ project。
Challenge 1
Avatar a1 = new Avatar("Knight", 80);
Avatar a2 = a1;
a2.setHealth(10);a1.getHealth() 是多少?Challenge 2
public void changeHealth(Avatar target)
{
target.setHealth(5);
}Challenge 3
profile1.healAvatar(avatar2, 30);avatar2 和 parameter target 是同一个 variable 吗?它们是否指向同一个 object?Challenge 4
Avatar a2 = profile1.getAvatar();getAvatar() 只是 return avatar;,这一行创建了新的 Avatar object 吗?完成后核对
这里集中放完整代码和 Quick Check 答案。
Stage 1:GameProfile TODO 1–4
private Avatar avatar;avatar = a;System.out.println("Avatar: " + avatar.getName());System.out.println("Health: " + avatar.getHealth());Stage 3:healAvatar
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
public Avatar getAvatar()
{
return avatar;
}完整 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。