发现实现Collection借口中addAll()方法存在一个微妙的陷阱

在实现 Collection 接口的 addAll() 方法时,如果集合自身作为参数传入,可能会导致无限循环。本文剖析了这一问题,并对比 JDK 中 ArrayList 的实现方式,揭示了 System.arraycopy() 方法如何避免该陷阱。

发现实现Collection借口中addAll()方法存在一个微妙的陷阱

今天我在自己构建JDK中的ArrayList时发现了一个有趣的陷阱。现在把它陈述出来:
对于 boolean Collection<E>.addAll(Collection<? extends E> c)方法,源码里面
有这样的说明:
<------------------------------------------------------------------->
   /**
         * Adds all of the elements in the specified collection to this set if
         * they're not already present  (optional operation) If the specified
         * collection is also a set, the <tt>addAll</tt> operation effectively
         * modifies this set so that its value is the <i>union</i> of the two
         * sets.   The behavior of this operation is unspecified if the specified
     * collection is modified while the operation is in progress.
     *
     *........
     *........
<------------------------------------------------------------------->

对于第一句话有个补充说明(optional operation:非限制的操作),这就意味着有这
样一种情况可能发生:
       假如有一个类引用了Collection借口(就拿JDK中的ArrayList类来说吧),并没
有限制不能向里面添加已经存在了的元素。即:
以下操作合法:
         Collection c = new ArrayList();
         c.add("a");
         c.add("b");
         c.addAll(c);
以上的代码是合法的。而且结果是有两个a和两个b。

今天我自己在编写一个MyArrayList类时也引用Collection等借口。有意思的是在实
现addAll()方法时遇到了问题。
这个问题原来是个很微妙的陷阱。现在我把它列出来。

我自己写实现的boolean addAll(Collection<? extends E> c)方法如下,是有个
bug的。后来才发现(太隐秘了,开始还没发现)。

<------------------------------------------------------------------->
public class MyArrayList extends AbstractList<E>
               implements List<E>, RandomAccess, Cloneable, java.io.Serializable
               {
              
                      private int size;
                      //......
                      
                        public boolean addAll(Collection<? extends E> c) {
                               Object[] a = c. toArray();           //把c转化为数组

                     ensureCapacity( size + c.size() );              //分配新的空间
                     
                     for(int i =0 ; i < elementData.length ; i++){              //逐个添加
                              add( a[ i ] );
                     }
                    
             
                      //......
               
               }
<------------------------------------------------------------------->

这断代码看起来没有问题,运行起来好像也没有什么问题,就连有重复数据的两个
数组也可以相连。
但是这断代码有个严重的错误:如下:
              Collection c = new MyArrayList();
              c.add("a");
              c.add("b");
              c.addAll(c);
这个时候就出错了,而且很严重,死循环了,不出来了。我想了很久才会过意来,
原来Collection源代码中那句注解的意义就在这里了。“ The behavior of this op-
eration is unspecified if the specified
collection is modified while the operation is in progress.”
         之所以会死是因为被添加的那个c被改变了,c在不断的往自己里面添加自己,
         当然也就无止境了。这个操作的行为在运行时的确是不确定的。
         但是,我拿JDK的ArrayList也做同样的操作,通过了,而且结果也正是我想要
         的两个a两个b。为什么它就可以实现呢?
         它是怎么实现的呢?后来我看了ArrayList的源代码,原来关键在于它用到了一
         个System.arraycopy()方法。
         ArrayList的源代码这样写道:
<-------------------------------------------------------------------> 
         public boolean addAll(Collection<? extends E> c) {
       Object[] a = c.toArray();
               int numNew = a.length;
       ensureCapacity(size + numNew);   // Increments modCount
               System.arraycopy(a, 0, elementData, size, numNew);                   //可见此处是关键
               size += numNew;
       return numNew != 0;
       }
<------------------------------------------------------------------->

于是我查了一下System.arraycopy()方法的代码。
里面有这样一条comment。
<------------------------------------------------------------------->
         * If the <code>src</code> and <code>dest</code> arguments refer to the
         * same array object, then the copying is performed as if the
         * components at positions <code>srcPos</code> through
         * <code>srcPos+length-1</code> were first copied to a temporary
         * array with <code>length</code> components and then the contents of
         * the temporary array were copied into positions
         * <code>destPos</code> through <code>destPos+length-1</code> of the
         * destination array.
<------------------------------------------------------------------->

通过这断解释可以看到System.arraycopy()的思想,其实也是可以想到的,就是把
“自己加自己”也变成“自己加别人”,最简单的方法就是找个临时变量。就是如果加
如的是自己,则先把自己Copy到一个临时的数组里面,然后再对这个临时文件操作即可。

今天又发现并解决了这个问题,算是有点收获,并且JDK中的ArrayList自己也模拟的
差不多了。
Java 中的 `Map` 接口并没有直接提供 `addAll` 方法。与 `Collection` 接口中的 `addAll(Collection<? extends E> c)` 方法不同,`Map` 接口通过 `putAll(Map<? extends K,? extends V> m)` 方法实现类似的功能[^3]。 ### `putAll` 方法的使用方式 该方法用于将指定 `Map` 中的所有键值对复制到当前 `Map` 实例中。如果当前 `Map` 中已经包含某些键,则这些键对应的值会被新提供的 `Map` 中的值覆盖。 #### 方法定义 ```java void putAll(Map<? extends K, ? extends V> m); ``` #### 示例代码 ```java import java.util.HashMap; import java.util.Map; public class MapPutAllExample { public static void main(String[] args) { // 创建第一个Map Map<String, Integer> map1 = new HashMap<>(); map1.put("one", 1); map1.put("two", 2); // 创建第二个Map Map<String, Integer> map2 = new HashMap<>(); map2.put("three", 3); map2.put("four", 4); // 将map2中的所有键值对添加到map1中 map1.putAll(map2); // 输出合并后的map1 System.out.println(map1); // 输出 {one=1, two=2, three=3, four=4} } } ``` ### 注意事项 - 如果两个 `Map` 中存在相同的键,`putAll` 方法会用传入的 `Map` 中的值覆盖目标 `Map` 中的原有值。 - `putAll` 方法的时间复杂度取决于具体的 `Map` 实现。例如,对于 `HashMap`,其时间复杂度通常为 O(n),其中 n 是传入的 `Map` 的大小[^3]。 ### 使用场景 `putAll` 方法适用于需要将多个 `Map` 合并为一个 `Map` 的情况。例如,在配置管理中,可以使用 `putAll` 方法将默认配置与用户自定义配置合并,确保最终的配置集合包含所有必要的键值对。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值