Java不借助API实现ArrayList不可变(含类内不可变)的面试解决方案
嘿,这个问题问到点子上了——刚好是面试里很考验基础的理论场景,我来一步步给你捋清楚~
你要的是不依赖Collections.unmodifiableList()这类API,让ArrayList在「类内部、派生类、外部代码」三个层面都完全不可变,还要搞懂和hashCode()的关联——咱们逐个击破。
private final只是基础操作,但它只能保证列表的引用不被替换,里面的元素还是能通过add()/remove()修改。要实现真正的不可变,得从「封装+限制修改入口」入手:
1. 彻底封装底层ArrayList,杜绝任何修改可能
首先,把底层的ArrayList用private final修饰,但关键是类内部也绝对不调用它的任何修改方法,只在初始化时一次性填充所有元素:
public final class ImmutableMyList<T> { // 底层容器,完全封装,类内部也只做只读操作 private final ArrayList<T> innerList; // 构造方法一次性传入所有元素,完成初始化 public ImmutableMyList(Collection<? extends T> elements) { // 复制传入的集合,防止外部修改原集合影响内部 this.innerList = new ArrayList<>(elements); } // 只暴露只读方法 public T get(int index) { return innerList.get(index); } public int size() { return innerList.size(); } // 迭代器也要做只读限制,禁止remove操作 public Iterator<T> iterator() { return new Iterator<T>() { private final Iterator<T> innerIter = innerList.iterator(); @Override public boolean hasNext() { return innerIter.hasNext(); } @Override public T next() { return innerIter.next(); } @Override public void remove() { throw new UnsupportedOperationException("这是不可变列表,禁止修改"); } }; } }
这个类里,底层ArrayList完全被“锁死”:外部拿不到它的引用,类内部也只调用get()/size()这类只读方法,根本碰不到修改接口。
2. 为什么和hashCode()有关?
面试里提到这个点,是因为不可变对象的核心契约之一是hashCode固定不变——这样它才能安全地作为HashMap、HashSet的键。
如果你的“不可变”列表实际上还能被修改,那底层ArrayList的hashCode会跟着元素变化,导致用它当键时,HashMap出现找不到值的异常。所以在我们的自定义类里,必须重写hashCode()和equals(),并且因为底层列表不会被修改,hashCode的值永远固定:
@Override public int hashCode() { // 因为innerList不会被修改,所以hashCode永远稳定 return innerList.hashCode(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ImmutableMyList<?> that = (ImmutableMyList<?>) o; return innerList.equals(that.innerList); }
反过来,如果允许底层列表被修改,hashCode就会变化,直接违背了不可变对象的设计初衷——这就是两者的关联点。
3. 确保派生类也无法突破限制
如果你的类允许被继承,一定要给类加上final修饰,防止派生类重写方法来偷偷修改底层列表:
// 加final,禁止继承,彻底封死派生类的修改可能 public final class ImmutableMyList<T> { // ... 其他代码不变 }
如果不能用final,那就要把所有方法都声明为final,不让派生类修改方法逻辑。
很多人以为private final就够了,但类内部还是能写innerList.add(item)——所以核心是类内部要自觉遵守只读约定:
- 只在构造方法里初始化底层列表,之后再也不调用任何修改方法;
- 绝对不要在类内部的其他方法里操作
innerList的修改接口(比如add()/clear()); - 初始化时一定要复制传入的集合,别直接引用外部的集合(避免外部修改影响内部)。
内容的提问来源于stack exchange,提问作者Dharini

