Companion to the AP-Style MC Practice SetAP 风格选择题练习的解析配套
Unit 3: Class Creation第 3 单元:类的创建CSA
Multiple Choice)—— 详细解析Each item restates the prompt and choices, marks the correct letter, and gives a brief justification. Trap distractors are called out where useful.每道题重述题干与选项、标出正确答案字母,并给出简明解析;对容易混淆的干扰项(trap distractor)单独提示。
What does this.size = size; do in the constructor?构造方法中的 this.size = size; 做什么?
size to the instance variable size.把参数 size 赋给同名的实例变量 size。size refers to the parameter. The prefix this. reaches past the parameter and refers explicitly to the instance variable of the current object.
=: this.size → instance variable.=: bare size → parameter.size = size; (no this.) self-assigns the parameter and leaves the field at its default value.
shadow)实例变量——不加前缀的 size 指的是参数。前缀 this. 跨过参数,明确指向当前对象的实例变量。
= 左侧:this.size → 实例变量。= 右侧:裸的 size → 参数。size = size;(没有 this.),就是把参数自己赋给自己,实例变量保持默认值不变。
Which statement about a static variable is true?下列关于 static 变量的描述,哪一项是正确的?
null regardless of type.无论类型为何都被自动初始化为 null。static binds the variable to the class, not to any individual instance. All instances share the same single storage location.
ClassName.varName.0, boolean gets false, references get null) — not "null regardless of type."final.static 把变量绑定到类,而不是任何具体实例。所有实例共享同一个存储位置(类级别变量 class-level variable)。
ClassName.varName。0,boolean 为 false,引用类型为 null)—— 不是"无论什么类型都为 null"。final,否则可以重新赋值。A class with no declared constructors:没有声明任何构造方法的类:
public Foo() {} — so new Foo() works and instance variables fall back to their type defaults.
Important caveat: as soon as you declare any explicit constructor (e.g. public Foo(int x)), the compiler will no longer supply the no-arg default. new Foo() then becomes a compile-time error unless you write the no-arg form yourself. This is a common AP trap.
public Foo() {}——所以 new Foo() 可用,实例变量取各自类型的默认值。
重要注意:只要你显式声明了任何构造方法(例如 public Foo(int x)),编译器就不再自动提供无参版本。这时 new Foo() 会编译错误,除非你自己再写一个无参构造。这是 AP 常见陷阱。
Box a = new Box(5); Box b = a; b.n = 10; a.n += b.n; println(a.n + " " + b.n);Box a = new Box(5); Box b = a; b.n = 10; a.n += b.n; println(a.n + " " + b.n);
5 1015 1020 1020 20Box b = a, both a and b reference the same heap object. Any mutation through one is visible through the other.
n = 5; both a and b point to it.b.n = 10 → the shared object's n is now 10. Reading a.n here would also give 10.a.n += b.n reads a.n (= 10), reads b.n (also 10 — same object!), writes a.n = 20.a.n and b.n read the shared n = 20."20 20".
Trap (C) "20 10" is the classic mistake — assumes b is an independent copy of a. Java has no implicit cloning; assignment of references is shallow. (B) "15 10" reads a.n as if it were still 5 when the += evaluates.
Box b = a 之后,a 与 b 引用同一个堆上的对象(别名 aliasing)。通过任一个的修改对另一个都可见。
n = 5;a 和 b 都指向它。b.n = 10 → 共享对象的 n 现在是 10。此时读 a.n 也得到 10。a.n += b.n 读 a.n(= 10),读 b.n(也是 10 —— 同一个对象!),写 a.n = 20。a.n 与 b.n 都读共享的 n = 20。"20 20"。
陷阱 (C) "20 10" 是最常见的错误——以为 b 是 a 的独立副本。Java 没有隐式克隆;引用赋值是浅层(shallow)的。(B) "15 10" 在 += 求值时把 a.n 当成仍然是 5。
Sensor with static int total; update(r) does total -= reading; reading = r; total += r;.
s1 = new Sensor(10); s2 = new Sensor(20); s1.update(50); s2.update(0); println(Sensor.total());Sensor 类含有 static int total;update(r) 做 total -= reading; reading = r; total += r;。
s1 = new Sensor(10); s2 = new Sensor(20); s1.update(50); s2.update(0); println(Sensor.total());
30507080total is class-wide; each Sensor contributes only its current reading. The mutator carefully removes the old contribution before adding the new one.
step | reading(s1) reading(s2) total
new Sensor(10) | 10 — 10
new Sensor(20) | 10 20 30
s1.update(50): |
total -= reading(=10) | 10 20 20
reading = 50 | 50 20 20
total += 50 | 50 20 70
s2.update(0): |
total -= reading(=20) | 50 20 50
reading = 0 | 50 0 50
total += 0 | 50 0 50
Final total = 50.
Trap (C) 70 skips the second update entirely. (D) 80 forgets to subtract the old reading before adding the new one (treats every update as a fresh add). (A) 30 ignores all updates.
total 是类级别变量;每个 Sensor 只贡献它当前的 reading。修改器(mutator)在加入新值前会先减去旧值,维持总和正确。
step | reading(s1) reading(s2) total
new Sensor(10) | 10 — 10
new Sensor(20) | 10 20 30
s1.update(50): |
total -= reading(=10) | 10 20 20
reading = 50 | 50 20 20
total += 50 | 50 20 70
s2.update(0): |
total -= reading(=20) | 50 20 50
reading = 0 | 50 0 50
total += 0 | 50 0 50
最终 total = 50。
陷阱 (C) 70 直接漏掉了第二次 update。(D) 80 忘了在加入新值前先减去旧值(把每次 update 都当成新加项)。(A) 30 完全无视所有 update。
Constructor body is x = x; (no this.); new Point(7).getX()?构造方法体是 x = x;(没有 this.);new Point(7).getX() 是?
07-1x shadows the instance variable x. Without the this. qualifier, both sides of x = x; refer to the parameter — it's a self-assignment of the parameter to itself.
x is never touched, so it keeps its default value 0.getX() returns the instance variable → 0.this.x = x; as in Q1. This bug is silent and embarrassingly common in beginner Java.
x 遮蔽了同名的实例变量 x。没有 this. 前缀时,x = x; 的两侧都指向参数——这就成了"参数自我赋值"。
x 完全没被赋值,保留默认值 0。getX() 返回实例变量 → 0。this.x = x;。这种 bug 静悄悄发生,在初学者代码里非常常见。
replace(b) { b = new Box(99); } vs mutate(b) { b.n = 99; }. After replace(one); mutate(two); — final one.n and two.n?replace(b) { b = new Box(99); } 与 mutate(b) { b.n = 99; } 对比。执行 replace(one); mutate(two); 之后,one.n 与 two.n 的最终值?
1 21 9999 9999 2replace(one): the parameter b initially aliases the same object as one. Then b = new Box(99) makes b point to a brand-new heap object — one in the caller is unaffected, so one.n is still 1.mutate(two): the parameter b aliases two's object. b.n = 99 mutates that shared object — two.n is now 99."1 99".
Trap (C) "99 99" is the most common misconception — students think a method can reassign references in the caller. It cannot; that would require return values or a wrapping object. (A) "1 2" assumes nothing happens to either. (D) "99 2" swaps which method does which.
pass by value):对象参数传的是引用的本地副本。重新赋值参数只改变这个本地副本;通过参数修改对象本身才会影响共享对象。
replace(one):参数 b 起初与 one 别名同一对象。然后 b = new Box(99) 让 b 指向一个全新的堆对象——调用方的 one 不受影响,one.n 仍为 1。mutate(two):参数 b 与 two 引用同一对象。b.n = 99 修改这个共享对象——two.n 变为 99。"1 99"。
陷阱 (C) "99 99" 是最常见的错觉——学生以为方法能在调用方那边重新绑定引用。它做不到;要做到需要返回值或者用一个包装对象。(A) "1 2" 以为两者都没变化。(D) "99 2" 把两个方法的角色颠倒了。
Score s = new Score(10); s.add(20); s.cap(15); s.add(5); s.cap(50); println(s.get()); — cap(max) sets v = max only when v > max.Score s = new Score(10); s.add(20); s.cap(15); s.add(5); s.cap(50); println(s.get()); —— cap(max) 仅在 v > max 时把 v 设为 max。
15203035step | v before cap fires? v after
init | — — 10
add(20) | 10 — 30
cap(15) | 30 30 > 15 ✓ 15
add(5) | 15 — 20
cap(50) | 20 20 > 50 ✗ 20
Final v = 20.
Trap (A) 15 assumes the last cap still constrains the value (it doesn't — 20 <= 50 so the cap is a no-op). (C) 30 ignores the first cap, picturing it as "set v to max" rather than "constrain v if it exceeds max." (D) 35 ignores both caps.
cap 的判定条件。
step | v 调用前 cap 触发? v 调用后
init | — — 10
add(20) | 10 — 30
cap(15) | 30 30 > 15 ✓ 15
add(5) | 15 — 20
cap(50) | 20 20 > 50 ✗ 20
最终 v = 20。
陷阱 (A) 15 以为最后那个 cap 仍然在限制值(不是——20 <= 50,所以 cap 是空操作)。(C) 30 漏掉第一个 cap,把它误解为"把 v 设为 max"而不是"当 v 超过 max 时才限制"。(D) 35 完全忽略两个 cap。
inc() and dec() each return this after mutating n. int x = c.inc().inc().inc().dec().peek(); println(x + " " + c.peek());inc() 与 dec() 都在修改 n 后 return this。int x = c.inc().inc().inc().dec().peek(); println(x + " " + c.peek());
0 22 02 24 4inc() / dec() returns this — i.e., the same Counter object on which it was called. So every link in the chain operates on the same instance c.
step | what runs n after
c.inc() | n++; return c 1
.inc() | n++; return c 2
.inc() | n++; return c 3
.dec() | n--; return c 2
.peek() | return n 2 → assigned to x
Since the chain never made a copy of c, c.peek() after the chain returns the same value: 2. Output: "2 2".
Trap (A) "0 2" assumes x captures c's state at the start of the chain (it doesn't — it captures the value of .peek() at the end). (B) "2 0" reverses that misconception. (D) "4 4" assumes n keeps incrementing for each link without recognizing the dec().
inc() / dec() 都 return this——即在它身上被调用的同一个 Counter 对象。所以链式(fluent interface)中的每一环都作用在同一个实例 c 上。
step | 运行内容 n 之后
c.inc() | n++; return c 1
.inc() | n++; return c 2
.inc() | n++; return c 3
.dec() | n--; return c 2
.peek() | return n 2 → 赋给 x
链式调用从未给 c 做副本,所以链式结束后再调 c.peek() 仍然得到 2。输出:"2 2"。
陷阱 (A) "0 2" 以为 x 抓取的是链式开始时 c 的状态(其实抓的是结尾 .peek() 的返回值)。(B) "2 0" 把上面那个误解颠倒。(D) "4 4" 以为 n 每一环都递增,忽略了 dec()。
p == q vs p == r where r = p and q = new Pt(1,2).p == q 与 p == r,其中 r = p,q = new Pt(1,2)。
true truefalse truetrue falsefalse false== operator on objects compares references, not contents.
p and q are made by separate new Pt(...) calls — different objects in memory, regardless of identical contents. p == q → false.r = p copies the reference, so r points to the same object. p == r → true..equals(...); == is "do they refer to literally the same instance?"
== 比较的是引用地址,不是内容。
p 与 q 分别由两次 new Pt(...) 产生——尽管字段值相同,它们在内存里是不同的对象。p == q → false。r = p 复制引用,所以 r 指向同一个对象。p == r → true。.equals(...);== 是"是否指向同一个实例"。
drain() redeclares int level = 0; locally — final getLevel()?drain() 内部重新声明了局部 int level = 0; —— 最终 getLevel() 返回?
90100-100int level = 0; declares a new local variable named level inside drain(). From that point in the method, any unqualified reference to level refers to the local, not the instance variable.
level: 0 → -10. (Then disappears when drain returns.)level: untouched, still 100.getLevel() reads the instance field → 100. Trap (A) assumes the mutation reaches the instance variable. To actually mutate the instance, drop the int declaration: level -= 10; alone would resolve to the instance variable (with no shadowing local).
int level = 0; 这一行在 drain() 内部声明了一个新的局部变量,名字也叫 level。从这行开始到方法结束,所有不加前缀的 level 都指向局部变量,而不是实例变量。
level:0 → -10(drain 返回后即消失)。level:未被触及,仍为 100。getLevel() 读实例字段 → 100。陷阱 (A) 以为修改作用到了实例变量。要真正修改实例变量,应去掉 int 声明:只写 level -= 10;(没有同名局部遮蔽时)就会指向实例变量。
public static int getCount() { return count; } where count is a non-static field.public static int getCount() { return count; },其中 count 是非静态字段。
0.能编译;返回 0。NullPointerException.运行时抛出 NullPointerException。static method is associated with the class, not an instance — so there is no implicit this. Inside it, an unqualified reference to a non-static field is ambiguous: "which instance's count?" The compiler refuses.
count static (one shared counter for the class — see Q5).getCount() non-static, so it has an implicit this to read from.0), but the compiler won't even let the code reach run time.
static 方法绑定到类,不绑定任何实例——所以没有隐式的 this。在它内部,对非静态字段的无前缀引用是歧义的:"究竟是哪个实例的 count?" 编译器拒绝。
count 改为 static(类共享同一个计数器——见 Q5)。getCount() 改为非静态,这样它就有隐式 this 可以读取。0),但编译器根本不让代码跑到运行时。
p.name = "Bob"; from outside Person, where name is private.在 Person 外部写 p.name = "Bob";,其中 name 是 private。
name.能编译;name 被设值。IllegalAccessException.运行时抛出 IllegalAccessException。private restricts access to within the same class declaration only — not even other classes in the same package can read or write it directly. Attempting p.name = "Bob"; from outside Person is rejected at compile time.
Person can read/write name freely.setName(String), ideally with validation.private at compile time, so the run-time exception class isn't involved.
private 把访问范围限制在同一个类声明内部——即使是同一包中的其他类也不能直接读写。在 Person 外部写 p.name = "Bob"; 会在编译期被拒绝。
Person 内部的代码可以自由读写 name。setName(String),最好带校验。private,根本轮不到运行时异常类登场。
After modify(x, w) assigns x = 999 and w.n = 999: println(x + " " + w.n)?modify(x, w) 里执行 x = 999 与 w.n = 999 之后,println(x + " " + w.n) 输出什么?
5 5999 9995 999999 5x: the parameter inside modify is a copy of the value 5. Reassigning the local copy to 999 has zero effect on the caller's x. Caller's x stays 5.w: the parameter inside modify is a copy of the reference — it points to the same Wrap object. The dot access w.n = 999 reaches across the reference and mutates the shared object. Caller sees w.n = 999.pass by value)——但对于对象,传过去的值是引用的一个副本。
x:modify 内的参数是值 5 的副本。把这个本地副本重新赋为 999 对调用方的 x 没有任何影响。调用方的 x 仍为 5。w:modify 内的参数是引用的副本——它指向同一个 Wrap 对象。w.n = 999 透过这个引用修改共享对象。调用方看到 w.n = 999。Tag uses id = ++seq; per instance. a = new Tag("alpha"); b = new Tag("beta"); c = a; a = new Tag("alpha"); println(a + " " + b + " " + c);Tag 每次构造时 id = ++seq;。a = new Tag("alpha"); b = new Tag("beta"); c = a; a = new Tag("alpha"); println(a + " " + b + " " + c);
#1:alpha #2:beta #1:alpha#3:alpha #2:beta #3:alpha#3:alpha #2:beta #1:alpha#1:alpha #2:beta #3:alphaseq increments globally, each new object gets a snapshot of seq as its id, and references can be reassigned to point to different objects on the heap. The objects themselves stay put.
step | heap a points to b points to c points to seq
new Tag("alpha") | O1 {id:1, label:"alpha"} O1 — — 1
new Tag("beta") | O1, O2 {id:2, label:"beta"} O1 O2 — 2
c = a | (no new object — just aliasing) O1 O2 O1 2
a = new Tag("alpha")| O1, O2, O3 {id:3, label:"alpha"} O3 O2 O1 3
At println time, the implicit toString() calls give:
a → O3 → "#3:alpha"b → O2 → "#2:beta"c → O1 → "#1:alpha" (still pointing to the original)"#3:alpha #2:beta #1:alpha".
Trap (B) "#3:alpha … #3:alpha" is the classic mistake: assumes c "follows" a after reassignment. It doesn't — c captured the reference at the moment c = a ran. Reassigning a later doesn't reach back through c. (D) reverses the trap (assumes a follows the original instead).
seq 在全局递增;每个新对象把当时的 seq 抓取为自己的 id;引用可以被重新绑定到堆上的不同对象。对象本身不会被"挪动"。
step | 堆 a 指向 b 指向 c 指向 seq
new Tag("alpha") | O1 {id:1, label:"alpha"} O1 — — 1
new Tag("beta") | O1, O2 {id:2, label:"beta"} O1 O2 — 2
c = a | (没有新对象——只是别名) O1 O2 O1 2
a = new Tag("alpha")| O1, O2, O3 {id:3, label:"alpha"} O3 O2 O1 3
println 时隐式调用 toString():
a → O3 → "#3:alpha"b → O2 → "#2:beta"c → O1 → "#1:alpha"(仍指向最初那个)"#3:alpha #2:beta #1:alpha"。
陷阱 (B) "#3:alpha … #3:alpha" 是典型错误:以为 c 在 a 被重新赋值后"跟着 a 一起变"。不会——c 在 c = a 执行那一刻就把引用抓死了。之后再改 a 不会反过来影响 c。(D) 把陷阱反过来(误以为 a 还跟着原值走)。