单例模式仅保证类的唯一实例,不保障其内部集合操作的安全性;常见错误包括将size误作最大索引、空集合未判空即访问、遍历时边界逻辑错乱,须在单例方法内主动校验索引并封装安全接口。

单例类本身不直接涉及集合边界问题,但当它内部管理一个集合(比如缓存列表、配置项数组、连接池等),而初学者在操作这个集合时忽视边界规范,事故就很容易发生——而且往往被误认为是“单例坏了”,其实根源在集合访问逻辑。
单例容器里的集合,不是“自动安全”的
很多初学者写单例时会这样设计:
- 用 private static List<String> cache = new ArrayList<>(); 存数据
- 提供 add(String item) 和 get(int index) 方法供外部调用
- 认为“单例保证了只有一个 cache 实例,所以操作肯定安全”
错在这里:单例只管“实例唯一”,不管“集合操作是否越界”。get(0) 没问题,get(cache.size()) 就直接抛 IndexOutOfBoundsException——哪怕调用一百次,每次都是同一个单例对象,错误也照常发生。
最常踩的三个边界坑
1. 把 size 当作最大索引用
写成 cache.get(cache.size()) 是典型错误。长度为 5 的列表,合法索引是 0~4,size() 返回 5,但索引 5 已越界。
2. 空集合未判空就取首尾
比如 cache.get(0) 或 cache.get(cache.size() - 1),一旦 cache 为空(size == 0),前者下标 0 越界,后者计算出 -1 再访问,同样崩溃。
3. 循环遍历时混用 for-i 和增强 for
在单例的集合上边遍历边 remove 元素,用普通 for 循环却没调整 i 的值,或用增强 for 删除导致 ConcurrentModificationException——这不是并发问题,是边界逻辑没对齐。
怎么写才真正靠谱
单例类暴露集合操作时,必须自带边界防护,不能把责任甩给调用方:
- 读取方法加校验:`get(int index)` 内部先 `if (index = cache.size()) throw new IllegalArgumentException(...)`
- 提供安全访问接口:如 `getFirst()` 返回 `cache.isEmpty() ? null : cache.get(0)`,`getLast()` 同理
- 避免暴露原始集合引用:不要写 `public List<String> getCache()`,改用 `public List<String> getCacheCopy()` 返回 `new ArrayList(cache)`,防外部误改
- 文档写清契约:比如注明 “`get(index)` 要求 0 ≤ index < size,越界抛 IllegalArgumentException”——这比让别人查源码更高效
单例的价值是控制对象生命周期,不是替你兜底集合逻辑。边界规范不是附加项,是集合操作的起点。

















