慎用Collection的add()接口

大三暑假实习遇到的的问题记录

一、问题描述

实习时写过下面一段代码:

Collection<ISynchro> allISynchros = synchroCache.values(); // synchroCache是HashMap
Collection<ISynchro> npcISynchros = synchroNpcCache.values(); // synchroNpcCache是HashMap
allISynchros.addAll(npcISynchros);

运行时抛出java.lang.UnsupportedOperationException异常。

二、原因分析

Collection接口的实现与继承关系如下图:

AbstractCollection是Java集合框架中Collection接口的直接实现类,Collection的子接口的实现类大都继承AbstractCollection,比如AbstractList和AbstractSet。

AbstractCollection实现了几个方法,也定义了几个抽象方法留给子类实现,因此是一个抽象类。两个抽象方法:

public abstract Iterator<E> iterator();
public abstract int size();

此外,注意add()和addAll()方法的实现:

/**
* {@inheritDoc}
*
* <p>This implementation always throws an
* <tt>UnsupportedOperationException</tt>.
*
* @throws UnsupportedOperationException {@inheritDoc}
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* @throws IllegalArgumentException {@inheritDoc}
* @throws IllegalStateException {@inheritDoc}
*/
public boolean add(E e) {
throw new UnsupportedOperationException();
}

/**
* {@inheritDoc}
*
* <p>This implementation iterates over the specified collection, and adds
* each object returned by the iterator to this collection, in turn.
*
* <p>Note that this implementation will throw an
* <tt>UnsupportedOperationException</tt> unless <tt>add</tt> is
* overridden (assuming the specified collection is non-empty).
*
* @throws UnsupportedOperationException {@inheritDoc}
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* @throws IllegalArgumentException {@inheritDoc}
* @throws IllegalStateException {@inheritDoc}
*
* @see #add(Object)
*/
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}

如果调用AbstractCollection的add()或addAll()方法,将抛出Unsupported OperationException异常。因此如果子类需要add的功能,需要重写add()方法。

文章开头的代码中synchroCache和synchroNpcCache均为HashMap,查看源代码,发现HashMap的values()方法实现如下:

/**
* Returns a {@link Collection} view of the values contained in this map.
* The collection is backed by the map, so changes to the map are
* reflected in the collection, and vice-versa. If the map is
* modified while an iteration over the collection is in progress
* (except through the iterator's own <tt>remove</tt> operation),
* the results of the iteration are undefined. The collection
* supports element removal, which removes the corresponding
* mapping from the map, via the <tt>Iterator.remove</tt>,
* <tt>Collection.remove</tt>, <tt>removeAll</tt>,
* <tt>retainAll</tt> and <tt>clear</tt> operations. It does not
* support the <tt>add</tt> or <tt>addAll</tt> operations.
*
* @return a view of the values contained in this map
*/
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}

final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<V> iterator() { return new ValueIterator(); }
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}

可以看出values()方法返回的vs属于Values内部类,该类继承自AbstractCollection,而且并未重写add()方法。因此如果对返回值调用addAll()或者add()方法,依然会抛出UnsupportedOperationException异常。因此今后在实际使用中要避免HashMap.values().add()&HashMap.values.addAll()。

三、解决方案

那么如果需要合并两个AbstractCollection该如何操作?

方案(1)将Collection转List,首先尝试强转的方法:

List<ISynchro> allISynchros = (List<ISynchro>)synchroCache.values();

强转报错java.lang.ClassCastException。因为Collection是List的父接口,而强制类型转换只能完成向上转型,即子类对象转为父类对象,反过来不行,因为子类有的方法父类可能没有。但将List向上转型为Collection是可行的。

后来看到ArrayList有个构造函数:

/**
* Constructs a list containing the elements of the specified
* collection, in the order they are returned by the collection's
* iterator.
*
* @param c the collection whose elements are to be placed into this list
* @throws NullPointerException if the specified collection is null
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}

可以接受一个Collection的参数,返回一个List,实现了将Collection转换为List,满足要求:

List<ISynchro> allISynchros = new ArrayList<ISynchro>(synchroCache.values());
List<ISynchro> npcISynchros = new ArrayList<ISynchro>(synchroNpcCache.values());

之后再调用ArrayList的addAll()方法即可。ArrayList的add()方法是重写的,实现了add功能,不会抛异常:

allISynchros.addAll(npcISynchros);

方案(2)使用CollectionUtils.union(collection1,collection2)方法:

Collection<ISynchro> allISynchros = synchroCache.values();
Collection<ISynchro> npcISynchros = synchroNpcCache.values();
CollectionsUtils.union(allISynchros, npcISynchros);
文章作者: Moon Lou
文章链接: https://loumoon.github.io/2018/09/18/慎用Collection的add()接口/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Moon's Blog