Java 中的 ArrayList 踩坑记

最近在看 JDK8 中java.util.ArrayList的源码,发现其中一些方法的精妙,也启发了我写代码的一些方式。

除此以外,阅读中我注意到ArrayList里一些方法的内部实现,不加注意的话,在使用该方法过程中容易造成一些不必要的麻烦。

本文只提两个方法。

indexOf(Object o)

indexOf(Object o)方法用来返回某个元素在ArrayList实例中的索引,若这个元素不存在,则返回 -1 。需要注意的是这个方法内部比较非null元素时,使用的是equals(Object obj)方法。这本不是什么大问题,但是对有些重写了equals(Object obj)方法的类来说,就需要注意了。

举个例子,运行下面这串代码:

ArrayList<Integer> arr = new ArrayList<Integer>();
Integer a = new Integer(200);
Integer b = new Integer(200);
// 添加 a、b 到 arr 中
arr.add(a);
arr.add(b);
// 打印 arr
System.out.println("arr" + arr.toString());
// 移除 arr 中的元素 a
arr.remove(a);
// 打印移除 a 后的 arr
System.out.println("arr" + arr.toString());
// 打印 a 的索引
System.out.println("a 的索引:" + arr.indexOf(a));
// 打印 arr 中是否存在 a
System.out.println("a 是否存在:" + arr.contains(a));

输出结果如下:

arr[200, 200]
arr[200]
a 的索引:0
a 是否存在:true

结果变得奇怪了,我们虽然一开始在arr中添加了ab,并在后续操作中移除了a,但是查询a的索引和a的存在时,却出现了意想不到的结果。

其实看看indexOf(Object o)方法的源码就知道了,源码如下:

/**
 * Returns the index of the first occurrence of the specified element
 * in this list, or -1 if this list does not contain the element.
 * More formally, returns the lowest index <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
 * or -1 if there is no such index.
 */
public int indexOf(Object o) {
  if (o == null) {
    for (int i = 0; i < size; i++)
      if (elementData[i]==null)  return i;
  } else { // o 不为 null时
    for (int i = 0; i < size; i++)
      // 使用 equals() 方法判断元素是否相等
      if (o.equals(elementData[i]))  return i;
  }
  // 未查找到指定元素,则返回 -1
  return -1;
}

可以看到,在传入参数不为null的时候,使用了equals(Object obj)方法来判断参数是否与集合中的元素相等。我们知道在没有重写的情况下,equals(Object obj)方法内部其实就是使用==作比较,相当于比较两者的内存地址是否相等。而不巧的是Integer等一些特殊的类(如String),都有重写equals(Object obj)方法,因此变成了比较值,而非比较内存地址。这样就很清楚了,虽然用remove(a)删去了a,但是在indexOf(a)中,比较a与留在arr中的元素用的是equals(Object obj)方法,ab的值当然想等咯(都等于 200 ),于是就出现了上述的怪状。

至于contains(Object o)方法,其内部判断元素是否存在时,就是利用indexOf(Object o)方法查询某个元素的索引,若返回值为大于或等于零(即不为 -1 ),则表示该元素存在,返回true。所以也出现这样的情况。其实remove(Object o)方法也在其内部用了equals(Object obj)方法作比较,这里暂且不表。

如果不了解这种重写了equals(Object obj)方法的类和一些使用equals(Object obj)方法作比较的类,就会很容易出现误解,因此还需要多多阅读源码呀!

比如不了解StringBufferStringBuilder的话,很容易理所当然的想既然String重写了equals(Object obj),从而可以直接进行值比较,就认为StringBufferStringBuilder也应当如此,但是却并非这样。StringBufferStringBuilder都没有重写equals(Object obj)方法,因此调用方法还是进行的内存地址的比较(相当于使用==)。

remove(int index)

众所周知,remove(int index)用来删除集合中指定索引处的元素,似乎不会出现什么问题,那么先来看一个例子:

ArrayList<String> arr = new ArrayList<String>();
// 此处直接定义一个索引,通常应该由某个业务方法返回
Integer index = 0;
// 随意添加两个元素
arr.add("hello");
arr.add("world");
System.out.println("arr" + arr.toString());
// 删除 index 索引处的 "hello"
arr.remove(index);
// 再次打印 arr 集合
System.out.println("arr" + arr.toString());

输出结果如下:

arr[hello, world]
arr[hello, world]

相信有很多人已经看出原因所在了,如果没有看出请继续往下阅读。

这里我们先创建了一个集合对象arr,并分别放入两个字符串,接着我们想要删除索引index处的元素,即“hello”字符串,但是调用remove()后并未出现想要的结果,元素并未被删除。

那么肯定是哪里出了问题。我们看一下remove()方法的源码,可以发现其实有两个remove()方法。

// 第一个 remove() 方法
public E remove(int index) {
  rangeCheck(index);

  modCount++;
  E oldValue = elementData(index);

  int numMoved = size - index - 1;
  if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,numMoved);
        
  elementData[--size] = null; // clear to let GC do its work
  return oldValue;
}

// 第二个 remove 方法
public boolean remove(Object o) {
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        fastRemove(index);
        return true;
      }
  } else {
    for (int index = 0; index < size; index++)
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}

还出现了个remove(Object o)重载的方法,这个方法我们也不陌生。显然原来的代码中arr.remove(index)没有调用第一个remove(int index)。再仔细看看代码,index实际上是Integer类的对象,因此我们的代码最终调用的是第二个remove(Object o)。于是我们原意想删除索引 0 处的元素变成了删除元素中与index对应(equals)的元素。

其实这两个方法因传入的参数类型不同,乍看之下很容易区分。但是这里的坑在于有时使用者没有注意到自己的索引是一个包装类Integer对象,从而导致了想要调用的方法和实际调用方法不符。

后记:

仔细想一想,其实所谓的“坑”都是自己了解的过少导致的,如果不深入学习,迟早会遇到更多的坑,这些“后果”都是有“前因”的。阅读源码能学到很多东西,不只是上面提到的方法细节,还有一些高效率的方法以及代码风格,都值得去慢慢咀嚼。加油努力吧!

本文由 Sooxin 创建于 2017-09-14 。本文链接:

转载请保留本署名。