slug
type
status
category
date
summary
tags
password
icon

3.6.1

 

详解:

下面把这段程序按“从上到下 + 运行时会发生什么”的顺序,细致讲清楚。读完你会明白:为什么改了 a1 之后 p1 的地址也跟着变、toString() 是如何被自动调用的,以及如何用“防御性拷贝”消除这种连带效应(aliasing)。

代码逐段解读

1) Person 类定义与字段

  • Person 是公共类。
  • name 是私有实例变量,类型 String(不可变)。
  • addr 是私有实例变量,类型是自定义的 Address。注意它不是值,而是引用(reference)——指向堆上的一个 Address 对象。

2) Person 构造器:直接保存“引用”

  • initName 原样赋给 name
  • 关键点addr = initAddress; 只是把“引用”直接保存起来。以后任何对这个 Address 对象的修改(不管从 Person 外还是内)都会反映到 Person 里,因为两者指向同一块地址对象
  • 旁边的注释已经提醒:这并不总是最佳实践(会造成别名/共享可变对象的问题)。

3) Person.toString()

  • 返回一个多行字符串。
  • 拼接 addr 时,Java 会隐式调用 addr.toString(),因为是对象与字符串相加。

4) main:创建对象、打印、修改 city、再打印

  • a1:在堆上创建了一个 Address 对象(街道/城市/州/邮编如字面值)。
  • p1Person 保存了同一个 Address 引用。
  • System.out.println(p1);:会自动调用 p1.toString(),其中又会调用 addr.toString()
  • a1.setCity("changsha");直接修改了 a1 指向的那个 Address 对象的 city 字段
  • 再次 System.out.println(p1);:因为 p1.addra1 是同一个对象,city 已改成 "changsha",所以打印出的 Address 也变了。

5) Address 类:可变(有 setter)

  • 四个私有实例变量,都是 StringString 本身不可变,但 Address 包含可变的字段引用——通过 setter 可以把字段指向别的字符串)。
  • 简单构造,逐个赋值。
  • 自定义打印格式:第一行街道,第二行“城市, 州 邮编”。
  • 标准访问器,保持封装(外部不能直接访问私有字段,只能通过方法读)。
  • 提供 setCity,因此 Address可变类(mutable)。

运行过程的“内存/引用”图示(抽象表意)

  1. 执行 Address a1 = new Address(...);
      • 产生一个 Address 对象(假设地址为 @A),a1 引用 @A
  1. 执行 Person p1 = new Person("Skyler", a1);
      • p1.addr 也指向 @Aa1 同一个对象)。
  1. 执行 a1.setCity("changsha");
      • 修改的是 @A.city"changsha"
  1. 因为 p1.addr 仍然指向 @A,所以 p1 看到的地址自然也变了。

实际打印输出

第一次 System.out.println(p1);

p1.toString()"Name:Skyler\nAddress:" + addr.toString()

修改后 a1.setCity("changsha"); 再打印 p1

回答注释里的问题:“Did changing a1 change p1?”
是的。因为 p1 保存的是对同一个 Address 对象的引用,Address 又是可变的。

知识要点总结

  1. 引用赋值 ≠ 拷贝对象本身
    1. addr = initAddress; 只复制了“指针/引用”。两个变量指向同一个可变对象就会出现别名问题(aliasing)
  1. 可变对象 + 共享引用 = 外部修改会“渗透”
    1. AddresssetCity,所以任何持有该引用的代码都能改变其状态;变化对所有共享者可见。
  1. toString() 的隐式调用
    1. 字符串拼接里放对象,Java 会自动调用它的 toString()
  1. 封装(encapsulation)
    1. 字段 private、提供 getters/setters,是基本封装做法。但一旦暴露可变对象引用,就会破坏“控制修改”的边界。


 
 

3.6.2

详解:

 
这段代码是一个防御性拷贝(Defensive Copy)的经典示例,目的是演示对象之间的引用关系数据封装安全性。我们来逐行深入讲解。

🧩 一、整体结构概览

这段代码定义了两个类:
  • Person:表示一个人,包含 name(名字)和 addr(地址)。
  • Address:表示一个地址,包含街道、城市、省份和邮编。
Person 持有一个 Address 对象作为实例变量。

✳️ 二、Person 类分析

  • 这里定义了两个实例变量:
    • nameString类型,存储名字;
    • addrAddress 类型,代表一个地址对象。

(1) 构造方法(Constructor)

🚩 关键点:防御性拷贝(Defensive Copy)

  • 参数 initAddr 是一个对象引用。
  • 如果直接写 addr = initAddr;,那么 Person 持有的 addr 和外部传入的 initAddr同一个地址对象
    • 改变外部 initAddr 的内容,会影响 Person 内部的地址。
  • 为了避免这种“外部修改内部数据”的风险,我们创建了一个新的 Address 对象,并用 initAddr 的 getter 方法来复制它的字段。
  • 这样,Person 内部的地址就独立于外部变量。
防御性拷贝的目的是保护对象封装性(Encapsulation)

(2) setAddress 方法

这也是一个防御性拷贝方法。
  • 当我们想修改 Person 的地址时,传入的 Address 参数可能被其他地方共享。
  • 为了避免共享同一个对象(从而互相影响),这里重新创建一个新对象。
不是直接赋值 addr = otherAddr;,而是重新复制内容。

(3) toString 方法

  • 这会调用 Address 类中的 toString() 方法来打印完整地址。
  • 因为 addr 是对象,System.out.println(addr) 自动调用 addr.toString()

(4) main 方法:测试程序逻辑

第一阶段:

  • 创建 a1a2 两个地址。
  • a1 创建 p1。构造函数里进行了防御性拷贝。
  • 打印 p1,输出包括名字和地址。

第二阶段:修改 a1

  • a1 的城市改成 “changsha”。
  • 打印结果:
    • a1 的 city 改变。
    • p1 的 city 不会改变
🔍 原因:p1 持有的是 a1 的防御性拷贝(一个独立对象)。

第三阶段:修改 p1 的地址为 a2

  • 调用 setAddress,此时 p1 的地址变成了 a2拷贝
  • 打印:
    • a1 不受影响。
    • p1 的地址变成了 “Othertown”。

🏠 三、Address 类分析

每个字段都封装为 private,保证外部不能直接修改。

(1) 构造函数

用于初始化完整地址。

(2) toString 方法

用于打印美观的地址格式。

(3) Getter 与 Setter

  • Getter 用于访问私有属性;
  • Setter 用于修改属性;
  • 提供受控访问,符合封装原则。

🔍 四、程序输出分析(带注释)

运行结果大致如下:
解释:
  1. a1 → 不影响 p1 (因为防御性拷贝)。
  1. p1 → 不影响 a1 (同理)。

💡 五、核心概念总结

概念
含义
在代码中的体现
引用传递
对象变量存储的是引用(地址),而不是值
addr = initAddr; 会让两个对象共享同一个引用
防御性拷贝
为防止外部修改内部对象,创建新的副本
new Address(initAddr.getCity() …)
封装 (Encapsulation)
数据私有 + 通过getter/setter访问
所有 private 字段都有相应的 getter/setter
可变性 (Mutability)
Address 是可变类(有 set 方法)
所以才需要防御性拷贝
不变性 (Immutability)
如果 Address 不可变,就不需要防御性拷贝
(比如没有任何 setter)

📘 六、课堂扩展练习建议

  1. 试着去掉防御性拷贝(直接赋值 addr = initAddr;),重新运行看看结果。
  1. 在 Address 中添加 setStreet, setZipcode,再测试修改。
  1. 将 Address 改成完全不可变类(无 set 方法),看看是否还需要防御性拷贝。

 
下面我将为你画出这段程序运行时的内存引用关系图,展示对象之间的连接与变化过程。
我们会分为三个阶段看清楚**防御性拷贝(defensive copy)**如何保护数据。

🧠 背景知识:Java内存模型(简化版)

在Java中:
  • 局部变量(如a1, p1) 存在于「栈(Stack)」中;
  • 对象(new出来的东西) 存在于「堆(Heap)」中;
  • 栈变量存的是引用地址,指向堆里的对象。

🩵 阶段一:初始创建阶段

🔹 内存图 1(刚创建完 p1)

🟢 注意:
  • a1p1.addr两个不同的 Address 对象(@101 vs @102);
  • 这是防御性拷贝起作用的地方。

💚 阶段二:修改 a1 的 city

🔹 内存图 2(修改 a1)

🧩 结果:
  • a1 改变;
  • p1 不受影响。
📄 打印结果:
✅ 说明:p1 内部地址独立,封装被保护。

💜 阶段三:改变 p1 的地址

其中:

🔹 内存图 3(修改 p1 地址后)

🟢 注意:
  • p1.setAddress(a2) 并不是让 p1.addr 指向 a2
  • 它创建了一个新的 Address 副本(@104);
  • 所以即使后来修改 a2p1 仍然不会受影响。

🔍 对比:如果没有防御性拷贝

假设构造函数写成:
那最初的情况变成:
当你执行:
此时 p1.addr.city 也会变成 "changsha"!
⚠️ 这就破坏了封装性。

🎯 总结表格

阶段
操作
对象变化
是否影响另一方
1
构造时使用防御性拷贝
生成独立副本
❌ 不影响
2
修改 a1 的 city
只改 a1
❌ 不影响 p1
3
修改 p1 的地址
只改 p1
❌ 不影响 a1
假如没有防御性拷贝
直接共享引用
改一个会影响另一个
⚠️ 会影响

 

 

3.6.3

详解:

下面把这段代码「逐行+运行过程」讲清楚,并顺带点出设计意图(防御性拷贝)、可访问性细节、以及可改进之处。

一、类结构与成员

Person

  • private String name;
  • private Address addr;
    • addr是一个**可变(mutable)**对象的引用。

构造器(防御性拷贝)

目的:避免“别处持有的 Address 引用”与 Person 内部 addr 共享同一对象而相互影响(别处改了地址,Person 的地址也跟着变)。
这种“被外部修改牵连”的风险就叫别名问题(aliasing),防御性拷贝是经典解法。

copyAddressFromPerson(Person otherPerson)

关键点
  • 同一个类内部,可以访问另一个同类对象private 字段(Java 规则)。因此 otherPerson.addr 是合法的。
  • 再次创建新对象并赋给 addr,从而让当前 Person 的地址成为“对方地址的拷贝”,不共享引用。

copyAddressFromAddress(Address otherAddr)

做法不同:这次不替换 addr 的对象实例,而是在原对象上逐字段 set
  • 结果上:this.addr 的数据会与 otherAddr 一致,但 this.addr 仍然是原来那个对象(对象身份 identity 不变)。
  • 好处:如果外部有人保留了对 this.addr 的引用(例如放到某集合里),它的身份不变。
  • 坏处:代码多、容易遗漏字段;如 addr 可能暂时处于“半更新状态”(异常中断时)。

toString()

  • 拼接时会自动调用 Address.toString()

Address

  • 4 个私有字段:street/city/state/zipcode,有全参构造器拷贝构造器get/set、以及
  • 因为有 setAddress可变类

二、main 的执行轨迹与打印结果

第一次打印(构造后)

  • p1.addra1 的拷贝,内容与 a1 一致。
  • 打印:

调用 p1.copyAddressFromPerson(p2);

  • p1.addr = new Address(p2.addr);
  • 此刻 p1.addr 变为p2.addr 的拷贝(是新对象,数据等于 p2 的地址)。

第二次打印

调用 p1.copyAddressFromAddress(a1);

  • 在当前 p1.addr 对象上逐字段 set,让它的内容改回与 a1 一致;
  • 注意:p1.addr 仍然是第二步中新建的那个对象(身份不变,内容更新)。

第三次打印


三、设计要点与对比

  1. 两种“拷贝地址”的方式
      • 替换对象copyAddressFromPerson
        • 简洁、安全,一步完成深拷贝(这里是浅/字段拷贝,但对 String 来说仍然安全,因为 String 不可变)。
        • 对象身份会改变。
      • 就地修改copyAddressFromAddress
        • 身份不变,适合外界也持有该对象引用的场景。
        • 容易遗漏字段,更新过程不具备原子性。
  1. 为什么防御性拷贝?
      • Address 可变;如果构造器直接 this.addr = initAddr;,那么外部对 initAddr 的修改会“泄漏”进 Person 内部,破坏封装。
  1. 同类访问 private 字段的规则
      • Person 类内部,可以访问任意 Person 实例private 字段(不是“同对象”,而是“同类”)。

四、核心总结

  • 代码的主线是演示防御性拷贝可变对象的封装保护
  • copyAddressFromPerson新建对象替换,数据来自对方的地址(通过拷贝构造器)。
  • copyAddressFromAddress原对象就地更新,逐字段 set
  • main 的三次打印依次是:p1 初始地址(来自 a1)→ p2 的地址 → 又改回 a1 的地址;但第二、三步里 addr 对象的“身份”是否改变是不同的:第二步改变了(新对象),第三步没改变(同对象被修改)。
 
 

 

3.6.4

详解:

非常好,这是一段非常典型的 Java 面向对象编程示例,用于讲解“防御性拷贝 (defensive copy)”与“对象可变性 (mutability)”的概念。下面我们逐行、分层地进行详细讲解与运行逻辑分析

🧩 第一部分:类结构概览

这段代码定义了两个类:
  1. Person:表示一个人,包含名字 (name) 和一个地址 (addr)。
  1. Address:表示一个地址对象,包含街道、城市、省份和邮政编码。
关系:
👉 每个 Person 对象 有一个 Address 对象(“has-a” 关系,组合关系)。

🧱 第二部分:Person 类详解

  • name:字符串类型,存储人名。
  • addr:类型为 Address 的实例变量。
    • ➜ 每个 Person 拥有一个地址对象。

🏗️ 构造方法(constructor)

✅ 解读:

  • 当创建一个 Person 对象时,传入两个参数:名字和地址。
  • 但是——重点在于第二行注释 “Defensive copy”
    • 这行代码不是直接做 addr = initAddr;
      而是调用 Address 的构造函数 新建了一个独立的地址对象
      并用 initAddr 的 getter 方法来复制数据。

🚨 为什么要这样做?

👉 因为如果直接写:
Person 的地址变量 addr 和外部传入的 Address 参数 initAddr 会指向同一个对象。
修改外部的 Address 对象时,会直接影响到 Person 内部的地址 —— 这是引用共享 (aliasing) 的问题。
使用防御性拷贝(Defensive Copy)可以避免这种副作用。

📬 Getter 方法

  • 返回地址对象。
  • ⚠️ 这里并没有做防御性拷贝(也就是没有返回一个新建的 Address)。
  • 因此:getAddress() 返回的对象依然是 Person 内部地址的引用
如果外部拿到这个引用并修改它,会直接影响 Person 的内部状态

🖨️ toString() 方法

  • 返回一个格式化字符串,用于打印 Person 对象。
  • 注意:addr 是一个 Address 对象,
    • 所以在字符串拼接时,会自动调用它的 toString() 方法。

🧪 主方法 main()

  1. 创建一个新的 Address 对象:
    1. 把它传入 Person 构造函数。
        • 在构造函数里,Person 创建了防御性拷贝。
        • 所以:Person 内部地址与传入地址是两个不同的对象

    🖨️ 打印初始状态

    输出类似:

    🧩 获取地址并修改

    • 通过 getAddress() 拿到 Person 的地址引用。
    • 修改 a 的邮编为 "11111"
    💡 由于 getAddress() 返回的引用直接指向 p1.addr
    修改 a 其实就是修改了 p1 内部的地址对象。

    🔁 再次打印

    输出变为:

    🧠 小结:关于防御性拷贝的局限性

    • 构造函数里:✅ 做了防御性拷贝(安全)
    • getAddress():❌ 没有防御性拷贝(不安全)
    因此:
    • 改变外部原始的 Address 对象,不会影响 Person
    • 但通过 p1.getAddress() 拿到引用并修改,会影响 Person 的内部数据

    🧱 第三部分:Address 类详解

    这四个属性构成一个可变的地址对象。

    🏗️ 构造方法

    简单地初始化所有字段。

    📬 Getter / Setter 方法

    • Getter 用于读取字段。
    • Setter 用于修改字段。
    • 有了 Setter,就说明这个类是可变的 (mutable)
    这使得外部代码可以轻易改变对象内部状态。

    🖨️ toString()

    用于打印美观格式的地址。

    🧾 程序整体运行结果

    完整输出为:

    🧭 总结表

    概念
    示例
    结果
    构造函数防御性拷贝
    addr = new Address(...)
    ✅ 安全
    Getter 返回原始引用
    return addr;
    ⚠️ 不安全
    修改原始参数 Address
    不影响 Person
    修改 getAddress() 返回的对象
    会影响 Person
    Address 类可变性
    有 setter
    ⚠️ 可被修改

     

     

    3.6.8

    详解:

    下面把这段代码从“类设计—构造—方法—main 调用—输出—隐患与改进”逐层拆给你。

    1) 两个类与它们的职责

    Address(地址)
    • 4 个私有字段:street/city/state/zipcode
    • 构造器把 4 个入参逐一赋值给字段。
    • toString() 以两行的格式输出:
      • Getter 提供只读访问。
      • 两个 Setter(setCity/setZipcode)让对象可变(mutable)。
      Person(人)
      • 2 个私有字段:nameaddr(类型就是上面的 Address)。
      • 构造器做了防御性拷贝(defensive copy):不是直接存参 initAddr 的引用,而是用它的 getters 取出各字段,再 new Address(...) 复制一份保存到 addr。这样外部以后就算修改传入的 Address 对象,也不会影响Person 内部保存的地址。
      • getAddress() 直接返回内部 addr引用(这一点后面会讨论利弊)。
      • toString()nameaddr 拼起来打印;注意 addr 会调用它自己的 toString(),因此是多态/委托式输出。

      2) main 的执行过程(逐行)

      • 右边先创建一个临时的 Address 对象(称作 A0)。
      • 调用 Person 构造器:
        • name = "Kay"
        • 关键:addr = new Address(initAddr.getStreet(), ...) 再创建一个新的地址对象(称作 A1)。
        • 结果:kay.addr 指向 A1,而传入的临时 A0 与 A1 内容相同但不是同一个对象
      • 调用链从左到右:
          1. kay.getAddress() —— 返回 kay 内部保存的 A1 的引用
          1. 取该地址的 state —— 字符串 "Massachusetts"
          1. 对字符串做 substring(0, 2) —— 取前两个字符 "Ma"
          1. println 输出。
      程序的实际输出
      注意:这里没有调用 System.out.println(kay);,所以不会打印 Person 或 Address 的多行字符串,只会输出 Ma 和一个换行。

      3) 为何在构造器里要“防御性拷贝”?

      • 如果写成 addr = initAddr;(直接引用赋值),那么外部代码后续对 initAddr 的任何修改都会反映到 Person 内部,这叫别名问题(aliasing),破坏封装。
      • 现在的做法是深拷贝一层:新建一个 Address,把字段值拷进来,从而与外部的 Address 脱钩。
      但也要看到一个不对称
      • 构造时做了拷贝(安全 ✅);
      • getAddress() 把内部引用直接暴露出去了(不安全 ❌)。调用者拿到引用后仍可 setCity / setZipcode 改动从而间接修改 Person 的内部状态

      4) 可能的风险点与边界情况

      • substring(0, 2) 要求 state.length() >= 2。本例 "Massachusetts" 足够长;若 state 只有 1 个字符,运行时会抛 StringIndexOutOfBoundsException
      • getAddress() 返回可变对象的活引用(live reference),任何外部 kay.getAddress().setCity("...") 都会改变 kay 的内部地址。
      • 构造器里没做 null 检查:若 initAddr == null 会 NPE;若 initName == null 也可能导致后续字符串拼接出现 "Name:null" 的语义问题。

      5) 复杂度 & 调用链说明

      • getAddress()getState() 都是 O(1)。
      • substring(0, 2) 对于 Java 8+ 的实现,会创建一个新字符串对象,长度为 2,时间/空间近似 O(1)。
      • 整个打印语句是一次方法链式调用(method chaining),读作:“从 kay 取地址 → 取州名 → 取前两个字符 → 打印”。

      小结

      • 这段代码示范了封装组合Person 包含 Address)、以及防御性拷贝
      • 当前实现“构造器安全 / getter 暴露”的不一致会留下可变别名风险。若作作业/面试题,常见加分点就是:Address 不可变getAddress() 返回拷贝,并补上 null 校验与必要的更新方法。
      • 本程序的控制台输出仅为:Ma

       

      3.6.9

      详解:

       
      下面把这段代码从结构、每行在做什么、运行时会发生什么,以及改进点都讲清楚。代码分成两个类:Date(自定义的日期类)和 Friend(朋友,带生日)。

      代码逐段解读

      Date

      作用:存储一个日期(以月/日/年的整型数字),并提供读写与打印方法。
      • 实例变量
        • 三个私有字段,封装(encapsulation)做得对:外部不能直接改,必须通过方法。
      • 构造器
        • 用传入的 3 个参数初始化对象。这里没有做有效性校验(比如 1≤month≤12),这既简单也意味着可能存入非法日期(改进点见下文)。
      • getters
        • 典型取值方法,配合私有字段实现只读访问。
      • setter(示例)
        • 只给了 year 的 setter,说明这个类是可变的(mutable)。如果提供更多 setter,外部就能随时改日期的各个部分。
      • toString
        • 打印形如 11/6/2007。注意:没有补零(如 03/09/2025),只是简单的数字拼接。

      Friend

      作用:保存朋友的名字与生日,并能判断“今天是不是他生日”。
      • 实例变量
        • 生日使用自定义的 Date 类,而不是 Java 内建的日期类(比如 LocalDate)。
      • 构造器(防御性拷贝)
        • 关键点:防御性拷贝(defensive copy)。因为 Date可变对象,如果直接保存传进来的引用,那么外部改了原来的 DateFriend 里的生日也会被“牵连”。这里用 getXxx() 重新 new 了一个 Date,确保 Friend 内部保存的是自己的副本,不受外部影响。
      • toString
        • 这里拼接时会隐式调用 birthdate.toString(),所以会显示为 Name: Alex, Birthday: 11/6/2007
      • isBirthday
        • 只比较,忽略(现实中也确实是这样判断生日)。如果需要“今天是否是出生那一年的那一天”就不对了,但作为生日判定是合理的。
      • main 测试
          1. 新建一个生日为 2007/11/06 的 Date
          1. 用它创建 Friend("Alex", …)。构造器会拷贝这个日期进去。
          1. 新建一个“今天”的日期 2025/11/06。
          1. 打印朋友信息(调用 toString)。
          1. 调用 isBirthday(today):由于月=11、日=6 相同,返回 true

      运行输出(按当前代码)

      关键知识点与设计意图

      1. 封装(Encapsulation)
        1. 私有字段 + 公有 getter/setter 是最基础的封装方式,控制数据读写。
      1. 可变对象的风险
        1. Date 提供了 setYear,说明它是 mutable。一旦对象可变,共享引用就可能带来“外部修改影响内部”的问题。
      1. 防御性拷贝(Defensive Copy)
        1. Friend 构造器里对传入的 Date 做了拷贝,避免上面的问题。
          • 如果将来添加 getBirthdate(),也应该返回一个副本,而不是内部引用:
            • 否则调用者拿到内部引用后,就可以直接 setYear 改掉朋友的生日。
      1. isBirthday 的判定逻辑
        1. 现实里过生日比的是“月日”,不比“年”,所以忽略年份是合理选择。

      一步步追踪一次“可变性”的对比实验

      看清防御性拷贝的价值,可做这个小实验(想象运行):
      结论Friend 内部生日不会变,因为 Friend 保存的是自己的副本。打印仍是 11/6/2007
      总结
      这段代码展示了两个核心 OOP 概念:封装防御性拷贝。因为 Date 是可变对象,Friend 在接收它时做了拷贝,避免了共享可变状态带来的“外部改、内部跟着变”的隐患。isBirthday 正确地只比较月日。想把代码进一步工程化,建议引入输入校验、不可变对象(或直接使用 LocalDate)、以及更完善的 toString/equals/hashCode

       
      ilcoxon Signed-Rank Test in Details Taylor Series
      Loading...
      目录
      0%
      现代数学启蒙
      现代数学启蒙
      推广现代数学🍚
      公告
      🎉现代数学启蒙(MME:Modern Mathematics Enlightenment)欢迎您🎉
      -- 感谢您的支持 ---
       
      目录
      0%