☰
B03_数据类相等性与集合转换
2026/9/27 7:01:58 网站建设 项目流程

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:Boolean=false}funinspectEquality(){valfirst=ArticleValue(7,"旧标题")valsameContent=ArticleValue(7,"旧标题")valupdated=first.copy(title="新标题")first.expanded=truecheck(first==sameContent)check(first!==sameContent)check(first!=updated)check(first.id==updated.id)check(!first.copy().expanded)}

expanded 不在主构造函数中,因此没有进入自动生成的 equals 与 copy 参数。把临时 UI 属性放在类体里,可能让两个看起来显示不同的对象仍然相等。若再把这种对象交给按 equals 合并更新的状态容器,行为就更容易令人困惑。数据类生成规则

不应因此得出“所有字段都必须放进主构造”的机械结论。要先决定对象代表什么:若它代表完整界面状态,影响显示的事实应能进入明确的状态变更;若它代表业务实体,临时展开状态可能本来就不该混入实体。

copy 是浅拷贝,会共享什么

现在加入可变标签集合:

dataclassTaggedArticle(valid:Long,valtags:MutableList<String>)funinspectCopy(){valoriginal=TaggedArticle(1,mutableListOf("Android"))valcopied=original.copy()copied.tags.add("Kotlin")check(original.tags==listOf("Android","Kotlin"))check(original.tags===copied.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 更利于保留冲突证据,不能在排错前就把数据丢掉。

以上为课程自拟题。验收时用三组例子分别证明结构相等、引用不同和业务身份相同,并亲自完成浅拷贝实验。能解释数据如何变化,后续状态和缓存问题才有坚实基础。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询