
Android 基础补强 B03同一篇文章不等于同一个对象数据类与集合的边界摘要文章更新、列表去重和状态刷新都依赖“相等”的含义。本篇从数据类生成规则出发区分业务身份、结构相等和引用相同并用浅拷贝与哈希集合实验说明隐藏的可变性风险。标签Kotlin、data class、集合、equals、不可变状态先回答“相同”到底指什么一篇文章改了标题还是不是原来那篇文章业务上通常是因为 ID 未变内容上已经不同内存里也可能是新创建的对象。三个答案可以同时成立。如果代码把它们混成一个概念列表更新、去重和缓存就容易出现难以解释的结果。本篇对应《第一行代码》2.5.4 数据类与单例类、2.6 集合与 Lambda。D02 已经使用 map 和 copy 清洗数据这里继续深入它们依赖的相等性规则重点不是再写一遍筛选函数。Kotlin 的 表达结构相等最终由适用的 equals 语义决定 判断引用是否相同。对数据类自动生成的相等性通常基于主构造函数中的属性。业务 ID 相同则是我们主动选择的一条领域规则不会自动取代全部属性比较。Kotlin 相等性主构造属性与类体属性的区别下列示例仅使用 Kotlin 标准库可以放入独立练习文件未在本次写稿中编译。dataclassArticleValue(valid:Long,valtitle:String){varexpanded:Booleanfalse}funinspectEquality(){valfirstArticleValue(7,旧标题)valsameContentArticleValue(7,旧标题)valupdatedfirst.copy(title新标题)first.expandedtruecheck(firstsameContent)check(first!sameContent)check(first!updated)check(first.idupdated.id)check(!first.copy().expanded)}expanded 不在主构造函数中因此没有进入自动生成的 equals 与 copy 参数。把临时 UI 属性放在类体里可能让两个看起来显示不同的对象仍然相等。若再把这种对象交给按 equals 合并更新的状态容器行为就更容易令人困惑。数据类生成规则不应因此得出“所有字段都必须放进主构造”的机械结论。要先决定对象代表什么若它代表完整界面状态影响显示的事实应能进入明确的状态变更若它代表业务实体临时展开状态可能本来就不该混入实体。copy 是浅拷贝会共享什么现在加入可变标签集合dataclassTaggedArticle(valid:Long,valtags:MutableListString)funinspectCopy(){valoriginalTaggedArticle(1,mutableListOf(Android))valcopiedoriginal.copy()copied.tags.add(Kotlin)check(original.tagslistOf(Android,Kotlin))check(original.tagscopied.tags)}新 Article 对象并不意味着嵌套对象也被复制。这里两份对象共享同一标签列表修改一边会影响另一边。若需要独立标签集合应在边界显式复制例如把可变输入转成不会再通过别名修改的只读快照并继续检查元素本身是否可变。List 接口提供只读访问不等于深度不可变。toList 也不是递归深拷贝。标签元素若是字符串本身不可变问题较简单元素若又包含可变集合就要继续往下分析对象图。实际工程最好减少这种多层可变共享而不是到处补深拷贝工具。去重需要选择正确的相等标准distinct 使用元素相等性distinctBy 允许指定键。两条文章 ID 相同、标题不同可能不会被 distinct 合并但会被 distinctBy { it.id } 视为同一个键。选择哪一种取决于需求是“去掉完全相同的快照”还是“每个业务身份只保留一条”。associateBy 可以建立 ID 索引但遇到重复键会有覆盖行为groupBy 则保留同组多个元素。它们都可以把列表变成查找结构却表达不同信息。需要分析冲突的排错工具适合 groupBy需要单一索引的仓库则必须先定义重复键策略。集合转换文档对文章列表来说还应考虑顺序。先过滤再去重与先去重再过滤可能留下不同数据把集合转为 map 再还原也不应被当作万能清洗方法。用受控输入证明自己的规则比展示一条很长的函数链更有说服力。哈希集合为什么怕可变键如果某对象参与 equals 和 hashCode 的字段被修改它放入 HashSet 或作为 HashMap 的键后查找可能失败。集合按加入时的哈希关系组织对象字段变化后使用的新哈希值不一定还能定位原位置。文章业务索引通常以稳定 Long ID 为键比把整个可变文章对象当键更容易维护。若把 data class 的主构造属性写成 var也不意味着“数据类自动帮我保持哈希集合正确”。语言生成方法没有承担集合中的身份迁移。实验可以定义一个只有 var id 的数据类加入 HashSet 后修改 id检查 contains 的行为具体结果取决于哈希分布但这种用法本身已破坏可稳定查找的前提。更可靠的验收是避免修改集合键或在明确移除后重新加入并解释为什么重新插入有必要。把规则带回界面更新列表判断同一条目可以使用 ID判断内容是否变化则看显示所需字段Compose key、RecyclerView 的条目比较与业务去重虽然都提到“身份”但发生在不同环节。不能因为给列表设置了 ID就认为 StateFlow 一定会发出任意内部属性变更。合理的更新路径是根据 ID 找到目标以新值表达变化再让状态容器观察这次明确更新。不要先在旧对象内部偷偷修改再把同一个对象重新交回去最后猜测框架为何没刷新。三道原创面试问答1. 同 ID 的两个数据类对象一定 吗不一定主构造中的其他属性不同就可能不相等。追问列表如何判断业务同一项明确按 ID 比较而不是假设 equals 只比较 ID。2. copy 后一定可以独立修改吗不一定它是浅拷贝嵌套引用可能共享。追问把 MutableList 改成 List 是否足够只读接口不保证底层没有其他可变别名还要检查创建和传递方式。3. distinctBy 与 groupBy 应怎样选择前者表达按键挑选代表元素后者保留组内全部元素。追问要调查重复文章来源用哪个更合适groupBy 更利于保留冲突证据不能在排错前就把数据丢掉。以上为课程自拟题。验收时用三组例子分别证明结构相等、引用不同和业务身份相同并亲自完成浅拷贝实验。能解释数据如何变化后续状态和缓存问题才有坚实基础。