abstractlist是最省力的list实现起点,因其已实现除get(int)和size()外所有核心方法;必须重写这两个方法,否则运行时必崩,且需注意索引检查、类型安全与语义一致性。

为什么 AbstractList 是最省力的起点
直接继承 AbstractList 而不是从头写 List,是因为它已经帮你实现了除 get(int) 和 size() 以外所有核心方法——比如 add(E)、remove(int)、indexOf(Object) 都是基于这两个方法推导出来的。你只用提供数据怎么存、多大,其余逻辑它自动兜底。
常见错误现象:UnsupportedOperationException 突然抛出,往往是因为忘了重写 set(int, E) 却调用了 list.set(0, x) ——AbstractList 默认实现直接 throw 这个异常,不给你留情面。
- 必须重写
get(int)和size(),否则运行时必崩 - 如果集合只读,就重写
set(int, E)和add(int, E)并明确 throwUnsupportedOperationException - 如果支持修改,记得在
set()、add()、remove()后触发modCount++,否则迭代器会 fail-fast 报ConcurrentModificationException
如何让自定义 List 支持 foreach 和 Stream
Java 的 foreach 和 Stream.of() 依赖 Iterable 接口,而 AbstractList 已经实现了它,所以只要你继承了 AbstractList,就天然支持 for (E e : mylist) 和 mylist.stream()。
但要注意:默认的 iterator() 返回的是 AbstractList.Itr,它内部强依赖 get(int) 和 size(),所以如果你的 get() 实现有副作用(比如触发远程加载)、或 size() 不稳定(比如底层是动态过滤的视图),迭代行为就会不可靠。
立即学习“Java免费学习笔记(深入)”;
- 避免在
get(int)里做 IO 或复杂计算;如需懒加载,考虑用AbstractSequentialList+listIterator() - 若底层数据可能并发修改,别依赖默认迭代器;自己实现
iterator()并加锁或用Collections.synchronizedList()包一层 -
stream()返回的是Collection.stream()默认实现,它会调用spliterator(),而后者也基于get()/size()—— 所以性能瓶颈和迭代器一致
add() / remove() 的索引越界检查谁来负责
AbstractList 不做索引范围校验,它把 add(int, E) 和 remove(int) 的边界检查完全交给你。这是为了灵活性:有些集合(比如环形缓冲区)允许负索引或模运算定位。
典型翻车现场:用户传入 add(5, "x") 到一个 size=3 的列表,你的 add() 没检查 index > size,结果数组越界或逻辑错乱。
- 标准做法:在
add(int index, E element)开头加if (index size()) throw new IndexOutOfBoundsException(...) -
remove(int index)应该用if (index = size())—— 注意是>=,因为合法索引是[0, size-1] - 错误信息建议复用
Arrays.ArrayList的格式:"Index: " + index + ", Size: " + size(),保持诊断一致性
泛型擦除下如何避免 ClassCastException
泛型只在编译期存在,运行时 AbstractList 拿不到实际类型。如果你的集合底层用 Object[] 存储,又在 get(int) 里直接返回 (E) array[i],那当用户误存了非泛型类型(比如往 MyList<string></string> 里塞了 Integer),问题会延迟到取值时才暴露。
这不是 AbstractList 的锅,但容易被忽略:它不约束你用什么结构存数据,也就没法帮你做类型防护。
- 最稳妥是用
E[]数组(通过(E[]) new Object[capacity]创建),配合@SuppressWarnings("unchecked"),把风险控制在构造阶段 - 如果必须用
Object[],在add(E)里做Objects.requireNonNull(element),至少防止 null 导致后续 NPE 掩盖类型问题 - 别在
get()里二次强转;既然声明了E get(int),调用方信任你返回的就是E,转型应发生在存储侧
真正麻烦的从来不是写完 get() 和 size(),而是想清楚这个集合的语义边界:它是否允许 null?是否线程安全?indexOf() 是用 equals() 还是 ==?这些决定一旦定下来,所有基于 AbstractList 推导出的方法都会继承它——改起来比重写还疼。










