欢迎光临
我们一直在努力

Java 集合、泛型与 Android 容器选择

用户在搜索页输入关键字:Repository 返回一批 Article,页面要去重、按热度排序、交给 RecyclerView.Adapter<VH> 渲染;点击收藏后还要按 id 缓存,并把 UI 更新投递回主线程。此时问题不再是“记住每个集合的定义”,而是“哪一个契约能让数据在 Android 生命周期与线程边界中仍然正确”。
一句话结论:先按访问方式、身份语义、key 类型和线程边界选择集合,再用泛型把错误挡在编译期;只有测量证明需要时,才以 Android 专用容器换取内存或常数项收益。

版本边界 :本文以 Java 8 语言特性、Android Framework 概念和 AndroidX RecyclerView 的现代用法为背景;MessageQueue、SparseArray、LruCache 的公开职责相对稳定,内部实现会随 Android API 级别变化。ArrayMap 的位置和性能也会随 AndroidX 版本、设备与数据规模变化,以下均是概念性说明,不把内部细节当作稳定 API。

本文 补充而不重复 java_data_structures_android.md:前文已经讲过数组、链表、哈希表和树的基本原理;这里把它们提升为 Android API 设计、绑定、缓存和调度时可执行的选择规则。

先问四个问题,再选容器

一次选择只需先回答:是否按下标读、是否只关心成员存在、是否按 key 查值、是否真的需要先进先出或两端操作。复杂度是方向,不是脱离数据量和设备的裁决。

#mermaid-svg-ScmJVfaFkgdQqveB{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ScmJVfaFkgdQqveB .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ScmJVfaFkgdQqveB .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ScmJVfaFkgdQqveB .error-icon{fill:#552222;}#mermaid-svg-ScmJVfaFkgdQqveB .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ScmJVfaFkgdQqveB .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ScmJVfaFkgdQqveB .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ScmJVfaFkgdQqveB .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ScmJVfaFkgdQqveB .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ScmJVfaFkgdQqveB .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ScmJVfaFkgdQqveB .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ScmJVfaFkgdQqveB .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ScmJVfaFkgdQqveB .marker.cross{stroke:#333333;}#mermaid-svg-ScmJVfaFkgdQqveB svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ScmJVfaFkgdQqveB p{margin:0;}#mermaid-svg-ScmJVfaFkgdQqveB .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ScmJVfaFkgdQqveB .cluster-label text{fill:#333;}#mermaid-svg-ScmJVfaFkgdQqveB .cluster-label span{color:#333;}#mermaid-svg-ScmJVfaFkgdQqveB .cluster-label span p{background-color:transparent;}#mermaid-svg-ScmJVfaFkgdQqveB .label text,#mermaid-svg-ScmJVfaFkgdQqveB span{fill:#333;color:#333;}#mermaid-svg-ScmJVfaFkgdQqveB .node rect,#mermaid-svg-ScmJVfaFkgdQqveB .node circle,#mermaid-svg-ScmJVfaFkgdQqveB .node ellipse,#mermaid-svg-ScmJVfaFkgdQqveB .node polygon,#mermaid-svg-ScmJVfaFkgdQqveB .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ScmJVfaFkgdQqveB .rough-node .label text,#mermaid-svg-ScmJVfaFkgdQqveB .node .label text,#mermaid-svg-ScmJVfaFkgdQqveB .image-shape .label,#mermaid-svg-ScmJVfaFkgdQqveB .icon-shape .label{text-anchor:middle;}#mermaid-svg-ScmJVfaFkgdQqveB .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ScmJVfaFkgdQqveB .rough-node .label,#mermaid-svg-ScmJVfaFkgdQqveB .node .label,#mermaid-svg-ScmJVfaFkgdQqveB .image-shape .label,#mermaid-svg-ScmJVfaFkgdQqveB .icon-shape .label{text-align:center;}#mermaid-svg-ScmJVfaFkgdQqveB .node.clickable{cursor:pointer;}#mermaid-svg-ScmJVfaFkgdQqveB .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ScmJVfaFkgdQqveB .arrowheadPath{fill:#333333;}#mermaid-svg-ScmJVfaFkgdQqveB .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ScmJVfaFkgdQqveB .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ScmJVfaFkgdQqveB .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ScmJVfaFkgdQqveB .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ScmJVfaFkgdQqveB .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ScmJVfaFkgdQqveB .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ScmJVfaFkgdQqveB .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ScmJVfaFkgdQqveB .cluster text{fill:#333;}#mermaid-svg-ScmJVfaFkgdQqveB .cluster span{color:#333;}#mermaid-svg-ScmJVfaFkgdQqveB div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ScmJVfaFkgdQqveB .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ScmJVfaFkgdQqveB rect.text{fill:none;stroke-width:0;}#mermaid-svg-ScmJVfaFkgdQqveB .icon-shape,#mermaid-svg-ScmJVfaFkgdQqveB .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ScmJVfaFkgdQqveB .icon-shape p,#mermaid-svg-ScmJVfaFkgdQqveB .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ScmJVfaFkgdQqveB .icon-shape .label rect,#mermaid-svg-ScmJVfaFkgdQqveB .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ScmJVfaFkgdQqveB .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ScmJVfaFkgdQqveB .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ScmJVfaFkgdQqveB :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

业务数据

按位置读取

List 与 ArrayList

只判断是否存在

Set 与 HashSet

按 key 查值

key 是 int

SparseArray

Map 与 HashMap

两端进出

Deque 与 ArrayDeque

Queue 或领域模型

List、Set、Map:数据语义先于实现类

需要回答的问题首选接口与常见实现Android 中的判断
按 position 取第 N 项吗 List<T>,通常 ArrayList<T> RecyclerView 绑定按 position 读取;中间插删多时,先评估批处理和 Diff,而不是换成 LinkedList。
同一个 id 只能出现一次吗 Set<T>,通常 HashSet<T> 已收藏 id、已请求 id、已选中 id;元素身份必须稳定。
通过 key 找对象吗 Map<K, V>,通常 HashMap<K, V> 页面级索引、对象缓存、临时聚合;不要把遍历顺序当成 HashMap 契约。
需要顺序遍历吗 LinkedHashMap<K, V> 或排序后的 List<T> 展示顺序是 UI 语义时显式表达;缓存访问顺序可用 LruCache。

ArrayList 的按索引读取为 O(1),尾部追加均摊 O(1),中间移动是 O(n);HashSet 与 HashMap 的查找、插入通常为 O(1) 平均复杂度。这里的“通常”很重要:哈希质量、扩容、对象分配、GC 和数据量都可能主导一次真实页面刷新。

不要把 LinkedList 当作 Android 默认队列。它每个节点都有额外对象和引用,局部性也较差。进程内普通 FIFO 或栈需求优先用 ArrayDeque<T>;若任务涉及阻塞、延迟、线程池或主线程派发,应使用相应的并发组件、调度器或 Framework API,而不是拿一个 LinkedList 自己拼调度器。

Queue 与 Deque:顺序不是一回事

Queue<T> 表达“从一端进入、从另一端处理”;Deque<T> 额外表达“头尾都可操作”,也适合替代旧 Stack。Android 的 MessageQueue 则是线程 Looper 使用的框架队列:消息按计划执行时间组织,不能简单等同于无条件 FIFO 的 Queue,更不能由业务代码绕开 Handler 直接改它。

#mermaid-svg-hmShXJ0zgQT9wVvX{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-hmShXJ0zgQT9wVvX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-hmShXJ0zgQT9wVvX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-hmShXJ0zgQT9wVvX .error-icon{fill:#552222;}#mermaid-svg-hmShXJ0zgQT9wVvX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-hmShXJ0zgQT9wVvX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-hmShXJ0zgQT9wVvX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-hmShXJ0zgQT9wVvX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-hmShXJ0zgQT9wVvX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-hmShXJ0zgQT9wVvX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-hmShXJ0zgQT9wVvX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-hmShXJ0zgQT9wVvX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-hmShXJ0zgQT9wVvX .marker.cross{stroke:#333333;}#mermaid-svg-hmShXJ0zgQT9wVvX svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-hmShXJ0zgQT9wVvX p{margin:0;}#mermaid-svg-hmShXJ0zgQT9wVvX .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-hmShXJ0zgQT9wVvX .cluster-label text{fill:#333;}#mermaid-svg-hmShXJ0zgQT9wVvX .cluster-label span{color:#333;}#mermaid-svg-hmShXJ0zgQT9wVvX .cluster-label span p{background-color:transparent;}#mermaid-svg-hmShXJ0zgQT9wVvX .label text,#mermaid-svg-hmShXJ0zgQT9wVvX span{fill:#333;color:#333;}#mermaid-svg-hmShXJ0zgQT9wVvX .node rect,#mermaid-svg-hmShXJ0zgQT9wVvX .node circle,#mermaid-svg-hmShXJ0zgQT9wVvX .node ellipse,#mermaid-svg-hmShXJ0zgQT9wVvX .node polygon,#mermaid-svg-hmShXJ0zgQT9wVvX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-hmShXJ0zgQT9wVvX .rough-node .label text,#mermaid-svg-hmShXJ0zgQT9wVvX .node .label text,#mermaid-svg-hmShXJ0zgQT9wVvX .image-shape .label,#mermaid-svg-hmShXJ0zgQT9wVvX .icon-shape .label{text-anchor:middle;}#mermaid-svg-hmShXJ0zgQT9wVvX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-hmShXJ0zgQT9wVvX .rough-node .label,#mermaid-svg-hmShXJ0zgQT9wVvX .node .label,#mermaid-svg-hmShXJ0zgQT9wVvX .image-shape .label,#mermaid-svg-hmShXJ0zgQT9wVvX .icon-shape .label{text-align:center;}#mermaid-svg-hmShXJ0zgQT9wVvX .node.clickable{cursor:pointer;}#mermaid-svg-hmShXJ0zgQT9wVvX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-hmShXJ0zgQT9wVvX .arrowheadPath{fill:#333333;}#mermaid-svg-hmShXJ0zgQT9wVvX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-hmShXJ0zgQT9wVvX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-hmShXJ0zgQT9wVvX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hmShXJ0zgQT9wVvX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-hmShXJ0zgQT9wVvX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hmShXJ0zgQT9wVvX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-hmShXJ0zgQT9wVvX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-hmShXJ0zgQT9wVvX .cluster text{fill:#333;}#mermaid-svg-hmShXJ0zgQT9wVvX .cluster span{color:#333;}#mermaid-svg-hmShXJ0zgQT9wVvX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-hmShXJ0zgQT9wVvX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-hmShXJ0zgQT9wVvX rect.text{fill:none;stroke-width:0;}#mermaid-svg-hmShXJ0zgQT9wVvX .icon-shape,#mermaid-svg-hmShXJ0zgQT9wVvX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-hmShXJ0zgQT9wVvX .icon-shape p,#mermaid-svg-hmShXJ0zgQT9wVvX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-hmShXJ0zgQT9wVvX .icon-shape .label rect,#mermaid-svg-hmShXJ0zgQT9wVvX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-hmShXJ0zgQT9wVvX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-hmShXJ0zgQT9wVvX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-hmShXJ0zgQT9wVvX :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

Handler 或 View

投递 Message

MessageQueue

按时间取下一条

Looper

目标回调

这张图回答“谁创建、谁持有、何时触发、结果交给谁”:调用方创建工作,目标线程的 Looper 持有并消费 MessageQueue,到期消息才会分发给目标回调。主线程上的耗时集合转换会让后续消息等待,因此大列表的 map、groupingBy、排序、去重和 Diff 输入准备应在后台完成;交给 UI 的是不可再被后台修改的快照,再在主线程提交渲染结果。

泛型:把 UI 数据边界写进类型

RecyclerView.Adapter<VH> 的绑定契约

RecyclerView.Adapter<VH> 的 VH 是 ViewHolder 类型参数。它让 onCreateViewHolder、onBindViewHolder 与 getItemViewType 围绕同一种 Holder 协作;不要把它退化为裸 RecyclerView.Adapter 再到处强转。数据列表也应通过 List<Article> 等具体类型表达,而不是 List<Object>。

#mermaid-svg-6wPo8MFyUNgYuzHN{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6wPo8MFyUNgYuzHN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6wPo8MFyUNgYuzHN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6wPo8MFyUNgYuzHN .error-icon{fill:#552222;}#mermaid-svg-6wPo8MFyUNgYuzHN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6wPo8MFyUNgYuzHN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6wPo8MFyUNgYuzHN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6wPo8MFyUNgYuzHN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6wPo8MFyUNgYuzHN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6wPo8MFyUNgYuzHN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6wPo8MFyUNgYuzHN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6wPo8MFyUNgYuzHN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6wPo8MFyUNgYuzHN .marker.cross{stroke:#333333;}#mermaid-svg-6wPo8MFyUNgYuzHN svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6wPo8MFyUNgYuzHN p{margin:0;}#mermaid-svg-6wPo8MFyUNgYuzHN .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-6wPo8MFyUNgYuzHN .cluster-label text{fill:#333;}#mermaid-svg-6wPo8MFyUNgYuzHN .cluster-label span{color:#333;}#mermaid-svg-6wPo8MFyUNgYuzHN .cluster-label span p{background-color:transparent;}#mermaid-svg-6wPo8MFyUNgYuzHN .label text,#mermaid-svg-6wPo8MFyUNgYuzHN span{fill:#333;color:#333;}#mermaid-svg-6wPo8MFyUNgYuzHN .node rect,#mermaid-svg-6wPo8MFyUNgYuzHN .node circle,#mermaid-svg-6wPo8MFyUNgYuzHN .node ellipse,#mermaid-svg-6wPo8MFyUNgYuzHN .node polygon,#mermaid-svg-6wPo8MFyUNgYuzHN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6wPo8MFyUNgYuzHN .rough-node .label text,#mermaid-svg-6wPo8MFyUNgYuzHN .node .label text,#mermaid-svg-6wPo8MFyUNgYuzHN .image-shape .label,#mermaid-svg-6wPo8MFyUNgYuzHN .icon-shape .label{text-anchor:middle;}#mermaid-svg-6wPo8MFyUNgYuzHN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6wPo8MFyUNgYuzHN .rough-node .label,#mermaid-svg-6wPo8MFyUNgYuzHN .node .label,#mermaid-svg-6wPo8MFyUNgYuzHN .image-shape .label,#mermaid-svg-6wPo8MFyUNgYuzHN .icon-shape .label{text-align:center;}#mermaid-svg-6wPo8MFyUNgYuzHN .node.clickable{cursor:pointer;}#mermaid-svg-6wPo8MFyUNgYuzHN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6wPo8MFyUNgYuzHN .arrowheadPath{fill:#333333;}#mermaid-svg-6wPo8MFyUNgYuzHN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6wPo8MFyUNgYuzHN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6wPo8MFyUNgYuzHN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6wPo8MFyUNgYuzHN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6wPo8MFyUNgYuzHN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6wPo8MFyUNgYuzHN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6wPo8MFyUNgYuzHN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6wPo8MFyUNgYuzHN .cluster text{fill:#333;}#mermaid-svg-6wPo8MFyUNgYuzHN .cluster span{color:#333;}#mermaid-svg-6wPo8MFyUNgYuzHN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6wPo8MFyUNgYuzHN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6wPo8MFyUNgYuzHN rect.text{fill:none;stroke-width:0;}#mermaid-svg-6wPo8MFyUNgYuzHN .icon-shape,#mermaid-svg-6wPo8MFyUNgYuzHN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6wPo8MFyUNgYuzHN .icon-shape p,#mermaid-svg-6wPo8MFyUNgYuzHN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6wPo8MFyUNgYuzHN .icon-shape .label rect,#mermaid-svg-6wPo8MFyUNgYuzHN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6wPo8MFyUNgYuzHN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6wPo8MFyUNgYuzHN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6wPo8MFyUNgYuzHN :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

List Article

Adapter ArticleHolder

创建 ArticleHolder

绑定 Article

ArticleHolder

列表行视图


class ArticleAdapter(
private var items: List<Article>
) : RecyclerView.Adapter<ArticleHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleHolder =
ArticleHolder(ArticleRowBinding.inflate(LayoutInflater.from(parent.context), parent, false))

override fun onBindViewHolder(holder: ArticleHolder, position: Int) {
holder.bind(items[position])
}

override fun getItemCount(): Int = items.size
}

这段可改造示例刻意省略了 DiffUtil、点击回调、Payload、稳定 id 和生命周期收集;它只演示泛型如何把“文章数据只能交给文章 Holder”变成编译器可检查的事实。

下面补一个可直接放进 Android Java 源文件的有界缓存骨架。它保留 Kotlin 伪代码的教学作用,同时让 Java 读者看到 extends 上界如何参与真实 API 设计。

import android.util.LruCache;

interface CacheKey {
}

interface SizedValue {
int byteCount();
}

final class GenericLruCache<K extends CacheKey, V extends SizedValue>
extends LruCache<K, V> {
GenericLruCache(int maxBytes) {
super(maxBytes);
}

@Override
protected int sizeOf(K key, V value) {
return value.byteCount();
}
}

K extends CacheKey 限制 key 必须是缓存领域认可的 key 类型,V extends SizedValue 保证缓存始终能按 value 的字节数计量容量;因此 sizeOf 不需要强转或运行时类型判断。上界不会让 key 自动不可变:CacheKey 的实现仍应只用稳定字段实现 equals() 和 hashCode(),放入缓存后不得修改这些字段。

上界、下界与 PECS

  • fun <T : UiModel> renderAll(items: List<T>):T 有上界,调用者只能传入 UiModel 的子类型;适合需要读取共同属性的渲染逻辑。
  • List<out UiModel> 或 Java 的 List<? extends UiModel>:生产者只读出 UiModel,不能安全写入任意子类型。
  • MutableList<in Article> 或 Java 的 List<? super Article>:消费者可安全接收 Article,读出时只应当作 Any 或 Object。

口诀是 PECS:Producer Extends,Consumer Super。它不是为了炫技,而是避免把 Banner 写进“只应保存 Article”的列表。

Java 泛型通过 类型擦除 实现:运行时通常看不到 List<String> 与 List<Integer> 的元素实参。因此不能仅靠不同泛型实参重载方法,也不能指望反射天然恢复业务泛型。Android 的 Bundle、网络反序列化和跨进程数据边界需要明确的 Parcelable、序列化器或类型 token;不要把擦除后的集合当成自动安全的容器。

排序、相等性与 Diff:UI 正确性的三条线

Comparator<T> 负责排序规则,不等同于对象身份。列表显示“热度降序、发布时间降序”可以写成组合比较器;当比较结果为 0 时,TreeSet 或 TreeMap 会视其为同一排序 key,所以比较器必须与业务去重语义相容,或显式补上唯一 id。

val byRankThenTime = compareByDescending<Article> { it.rank }
.thenByDescending { it.publishedAt }
.thenBy { it.id }

equals() 与 hashCode() 是 HashMap、HashSet 的身份地基:相等对象必须给出相同 hash;不相等对象允许碰撞,但碰撞越多越慢。常见 Android 问题是把可变字段放进 key:

data class DraftKey(var accountId: String, var filter: String)

val cache = hashMapOf(DraftKey("u1", "all") to "result")
// 后续修改 filter 后,原条目仍在旧 hash 桶中,cache[key] 可能再也找不到它。

缓存 key、选择集合元素与 DiffUtil 的身份字段应是不可变的。更糟的是错误实现 equals 或只改了 equals 没改 hashCode:集合可能出现“看起来相同却 contains 为 false”、去重失败或缓存泄漏。data class 对值对象通常合适;实体对象则应明确是否按稳定 id、业务字段或引用身份比较,不能让 IDE 生成结果替你做领域决策。

DiffUtil.ItemCallback<T> 把两个问题拆开:areItemsTheSame 判断是否是同一个业务实体,areContentsTheSame 判断该实体展示内容是否变化。它不应该直接依赖会被异步线程继续修改的可变列表。

object ArticleDiff : DiffUtil.ItemCallback<Article>() {
override fun areItemsTheSame(oldItem: Article, newItem: Article) =
oldItem.id == newItem.id

override fun areContentsTheSame(oldItem: Article, newItem: Article) =
oldItem == newItem
}

这里的前提是 Article.equals 覆盖了 UI 真正展示的内容。若封面加载状态、收藏状态或局部进度不在 equals 中,应使用显式字段比较或 Payload;否则会出现数据变了但行未刷新的错觉。

int key、对象 key 与 Android 容器

SparseArray 不是 HashMap<Integer, V> 的万能替代

SparseArray<E> 专为 int 到对象的映射设计,避开 Integer 装箱,使用有序 key 与数组存储。它通常更节省内存,遍历时可按 index 访问 key/value;查找大致是 O(log n),在中间插入或删除可能移动数组,约为 O(n)。对于资源 id、View id、较小到中等规模的 int-key 表,这是很有价值的移动端取舍。

但它并不普遍更快或更好:HashMap 平均查找常为 O(1),对象 key 自然适配;大量随机更新、极大规模或特定 ART/JIT 设备上,真实结果需要 profiler 和基准测试。不要因为“Android 提供了 SparseArray”就替换所有 Map。

ArrayMap<K, V> 也是数组式 Map,支持对象 key;它可减少小 Map 的对象开销,但查找与插入的常数、增长和版本实现会影响结果。这里仅将它视作“内存敏感、小映射的候选项”:请根据所用 AndroidX / Framework 版本查官方文档与源码,再针对目标设备测量,不把它当成跨版本的绝对性能结论。

练习:HashMap 还是 SparseArray

页面需要缓存 300 个 View,key 是稳定的 @IdRes int;写一个小基准分别填充、随机读取、删除 10% 条目,并用 Android Studio Memory Profiler 观察分配。预期练习答案不是固定容器:

  • 若只接受 int key 且内存分配可见,先试 SparseArray<View>。
  • 若 key 变为服务端 String,或逻辑要求对象 key,选 HashMap<String, View>。
  • 若 profile 显示容器不在热点,不为这 300 项提前复杂化;保留清晰的 Map 接口即可。
  • 缓存:用泛型和容量边界保护主线程

    LruCache<K, V> 是 Framework 提供的内存缓存边界:它以最近使用情况决定淘汰,不负责网络、磁盘、失效策略、线程安全组合操作或生命周期自动清理。key 仍要不可变,value 的大小计量也应贴近内存成本。

    #mermaid-svg-q8Te6Q6VoDVmaEMW{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q8Te6Q6VoDVmaEMW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q8Te6Q6VoDVmaEMW .error-icon{fill:#552222;}#mermaid-svg-q8Te6Q6VoDVmaEMW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q8Te6Q6VoDVmaEMW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q8Te6Q6VoDVmaEMW .marker.cross{stroke:#333333;}#mermaid-svg-q8Te6Q6VoDVmaEMW svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q8Te6Q6VoDVmaEMW p{margin:0;}#mermaid-svg-q8Te6Q6VoDVmaEMW .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-q8Te6Q6VoDVmaEMW .cluster-label text{fill:#333;}#mermaid-svg-q8Te6Q6VoDVmaEMW .cluster-label span{color:#333;}#mermaid-svg-q8Te6Q6VoDVmaEMW .cluster-label span p{background-color:transparent;}#mermaid-svg-q8Te6Q6VoDVmaEMW .label text,#mermaid-svg-q8Te6Q6VoDVmaEMW span{fill:#333;color:#333;}#mermaid-svg-q8Te6Q6VoDVmaEMW .node rect,#mermaid-svg-q8Te6Q6VoDVmaEMW .node circle,#mermaid-svg-q8Te6Q6VoDVmaEMW .node ellipse,#mermaid-svg-q8Te6Q6VoDVmaEMW .node polygon,#mermaid-svg-q8Te6Q6VoDVmaEMW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q8Te6Q6VoDVmaEMW .rough-node .label text,#mermaid-svg-q8Te6Q6VoDVmaEMW .node .label text,#mermaid-svg-q8Te6Q6VoDVmaEMW .image-shape .label,#mermaid-svg-q8Te6Q6VoDVmaEMW .icon-shape .label{text-anchor:middle;}#mermaid-svg-q8Te6Q6VoDVmaEMW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q8Te6Q6VoDVmaEMW .rough-node .label,#mermaid-svg-q8Te6Q6VoDVmaEMW .node .label,#mermaid-svg-q8Te6Q6VoDVmaEMW .image-shape .label,#mermaid-svg-q8Te6Q6VoDVmaEMW .icon-shape .label{text-align:center;}#mermaid-svg-q8Te6Q6VoDVmaEMW .node.clickable{cursor:pointer;}#mermaid-svg-q8Te6Q6VoDVmaEMW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q8Te6Q6VoDVmaEMW .arrowheadPath{fill:#333333;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q8Te6Q6VoDVmaEMW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q8Te6Q6VoDVmaEMW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q8Te6Q6VoDVmaEMW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q8Te6Q6VoDVmaEMW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q8Te6Q6VoDVmaEMW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q8Te6Q6VoDVmaEMW .cluster text{fill:#333;}#mermaid-svg-q8Te6Q6VoDVmaEMW .cluster span{color:#333;}#mermaid-svg-q8Te6Q6VoDVmaEMW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q8Te6Q6VoDVmaEMW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q8Te6Q6VoDVmaEMW rect.text{fill:none;stroke-width:0;}#mermaid-svg-q8Te6Q6VoDVmaEMW .icon-shape,#mermaid-svg-q8Te6Q6VoDVmaEMW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q8Te6Q6VoDVmaEMW .icon-shape p,#mermaid-svg-q8Te6Q6VoDVmaEMW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q8Te6Q6VoDVmaEMW .icon-shape .label rect,#mermaid-svg-q8Te6Q6VoDVmaEMW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q8Te6Q6VoDVmaEMW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q8Te6Q6VoDVmaEMW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q8Te6Q6VoDVmaEMW :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    读取请求

    LruCache K V

    缓存命中

    返回缓存值

    加载或计算

    写入缓存

    下面是 简化伪代码 ,只描述类型和淘汰边界;生产代码还需要线程策略、异常记录、取消、位图大小计算和测试。

    class MemoryCache<K : Any, V : Any>(maxEntries: Int) {
    private val cache = object : LruCache<K, V>(maxEntries) {}

    fun getOrLoad(key: K, loader: () -> V): V {
    cache.get(key)?.let { return it }
    return loader().also { cache.put(key, it) }
    }
    }

    注意这个 getOrLoad 不是并发去重实现:两个线程可能同时 miss 并加载同一 key。若 loader 耗时或触及网络,放在后台协调;不要在主线程执行,也不要仅因 LruCache 存在就忽略取消和失败重试。

    源码锚点:责任而非背诵内部字段

    源码锚点主要责任谁持有或调用选择时回答的问题
    RecyclerView.Adapter<VH> 创建并绑定指定类型的 Holder RecyclerView 驱动回调,业务提供数据 数据类型能否贯穿创建与绑定而不强转?
    DiffUtil.ItemCallback<T> 分离实体身份与内容变化 ListAdapter 或差分提交方 哪些字段定义同一项,哪些字段影响视觉结果?
    SparseArray<E> int key 到对象的紧凑映射 页面、View 层或 Framework 局部表 key 是否真是 int,内存收益是否值得对数查找与数组移动?
    LruCache<K, V> 有容量上限的内存缓存 Repository 或缓存层 命中、miss、淘汰和 value 大小由谁定义?
    MessageQueue 保存目标线程待分发的消息 对应线程的 Looper 哪些工作必须回到目标线程,何时才可执行?
    ArrayMap<K, V> 数组式对象 key 映射 Framework 或 AndroidX 局部代码 当前版本与真实 profile 是否支持这项内存取舍?

    这些锚点的内部字段、锁策略和优化路径不是稳定承诺。阅读源码时沿着“入口 API -> 持有者 -> 调度或差分 -> 缓存或执行边界”追踪,比记住一个私有字段名更能迁移到新的 AndroidX 版本。

    复杂度是决策工具,不是面试口令

    操作常见选择典型复杂度Android 决策提醒
    列表按 position 读取 ArrayList O(1) 适合 Adapter;批量更新优先配合 Diff。
    成员去重 HashSet 平均 O(1) 依赖不可变、正确的相等性。
    对象 key 查询 HashMap 平均 O(1) 不承诺顺序;留意 hash 和 GC。
    int key 查询 SparseArray 约 O(log n) 以内存和 key 类型换取取舍,不是默认优化。
    头尾推入弹出 ArrayDeque O(1) 适合进程内轻量队列或栈。
    最近使用淘汰 LruCache 接近 O(1) 容量、value 大小和线程模型更关键。

    主线程上一次 O(n log n) 排序未必一定造成卡顿,小集合也可能完全无感;反过来,一次巨大 O(n) 映射也可能超过一帧预算。用 Trace、Profiler 和代表性数据测量,再决定是否改变集合或把变换迁到后台。

    本文刻意省略的内容

    • 不展开 JDK 哈希树化阈值、SparseArray 删除标记和各 Android 版本的私有实现。
    • 不把 ConcurrentHashMap、协程 Flow 背压、Room 查询和线程池队列当作普通集合的替代教程。
    • 不提供通用图片缓存方案;真实图片加载需要磁盘层、请求取消、生命周期和内存压力响应。
    • 不讨论 Compose 的 SnapshotStateList 细节;它有自己的状态观察语义,不能只按 ArrayList 理解。

    上线前检查清单

    • 对外暴露的是 List、Set、Map 等接口,而非不必要的具体实现。
    • RecyclerView.Adapter<VH>、列表模型和 DiffUtil.ItemCallback<T> 的类型参数一致。
    • areItemsTheSame 用稳定身份,areContentsTheSame 覆盖实际 UI 内容。
    • 用于 Map、Set、缓存的 key 不会在放入后修改,且 equals / hashCode 成对正确实现。
    • int key 且内存敏感时评估过 SparseArray;对象 key 时没有勉强替换 HashMap。
    • 大集合转换、排序与加载不阻塞主线程;提交给 UI 的列表是稳定快照。
    • 缓存的容量、大小度量、miss 加载线程和失败策略都有明确归属。

    面试表达

    我先从访问模式和正确性选集合:Adapter 的展示序列用 List,去重 id 用 Set,按对象或字符串查找用 HashMap;只有 int key 且内存敏感时才评估 SparseArray。泛型把数据和 Holder 的边界放到编译期,equals、hashCode 与 DiffUtil 决定身份是否一致;最后用复杂度配合主线程预算和 profile 验证,而不靠“某容器一定更快”的结论。

    下一步可以把这些容器边界接到 异常与日志 :为 cache miss、类型转换失败和主线程慢变换定义可观测日志,同时避免把业务对象或敏感 key 直接写入日志。

    赞(0)
    未经允许不得转载:171主机测评 » Java 集合、泛型与 Android 容器选择
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址