Java 中的 ArrayList 踩坑記

最近在看 JDK8 中java.util.ArrayList的源碼,發(fā)現(xiàn)其中一些方法的精妙,也啟發(fā)了我寫代碼的一些方式。

除此以外,閱讀中我注意到ArrayList里一些方法的內(nèi)部實(shí)現(xiàn),不加注意的話,在使用該方法過程中容易造成一些不必要的麻煩。

本文只提兩個(gè)方法。

indexOf(Object o)

indexOf(Object o)方法用來返回某個(gè)元素在ArrayList實(shí)例中的索引,若這個(gè)元素不存在,則返回 -1 。需要注意的是這個(gè)方法內(nèi)部比較非null元素時(shí),使用的是equals(Object obj)方法。這本不是什么大問題,但是對(duì)有些重寫了equals(Object obj)方法的類來說,就需要注意了。

舉個(gè)例子,運(yùn)行下面這串代碼:

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));

輸出結(jié)果如下:

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

結(jié)果變得奇怪了,我們雖然一開始在arr中添加了ab,并在后續(xù)操作中移除了a,但是查詢a的索引和a的存在時(shí),卻出現(xiàn)了意想不到的結(jié)果。

其實(shí)看看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 ? get(i)==null : 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時(shí)
    for (int i = 0; i < size; i++)
      // 使用 equals() 方法判斷元素是否相等
      if (o.equals(elementData[i]))  return i;
  }
  // 未查找到指定元素,則返回 -1
  return -1;
}

可以看到,在傳入?yún)?shù)不為null的時(shí)候,使用了equals(Object obj)方法來判斷參數(shù)是否與集合中的元素相等。我們知道在沒有重寫的情況下,equals(Object obj)方法內(nèi)部其實(shí)就是使用==作比較,相當(dāng)于比較兩者的內(nèi)存地址是否相等。而不巧的是Integer等一些特殊的類(如String),都有重寫equals(Object obj)方法,因此變成了比較值,而非比較內(nèi)存地址。這樣就很清楚了,雖然用remove(a)刪去了a,但是在indexOf(a)中,比較a與留在arr中的元素用的是equals(Object obj)方法,ab的值當(dāng)然想等咯(都等于 200 ),于是就出現(xiàn)了上述的怪狀。

至于contains(Object o)方法,其內(nèi)部判斷元素是否存在時(shí),就是利用indexOf(Object o)方法查詢某個(gè)元素的索引,若返回值為大于或等于零(即不為 -1 ),則表示該元素存在,返回true。所以也出現(xiàn)這樣的情況。其實(shí)remove(Object o)方法也在其內(nèi)部用了equals(Object obj)方法作比較,這里暫且不表。

如果不了解這種重寫了equals(Object obj)方法的類和一些使用equals(Object obj)方法作比較的類,就會(huì)很容易出現(xiàn)誤解,因此還需要多多閱讀源碼呀!

比如不了解StringBufferStringBuilder的話,很容易理所當(dāng)然的想既然String重寫了equals(Object obj),從而可以直接進(jìn)行值比較,就認(rèn)為StringBufferStringBuilder也應(yīng)當(dāng)如此,但是卻并非這樣。StringBufferStringBuilder都沒有重寫equals(Object obj)方法,因此調(diào)用方法還是進(jìn)行的內(nèi)存地址的比較(相當(dāng)于使用==)。

remove(int index)

眾所周知,remove(int index)用來刪除集合中指定索引處的元素,似乎不會(huì)出現(xiàn)什么問題,那么先來看一個(gè)例子:

ArrayList<String> arr = new ArrayList<String>();
// 此處直接定義一個(gè)索引,通常應(yīng)該由某個(gè)業(yè)務(wù)方法返回
Integer index = 0;
// 隨意添加兩個(gè)元素
arr.add("hello");
arr.add("world");
System.out.println("arr" + arr.toString());
// 刪除 index 索引處的 "hello"
arr.remove(index);
// 再次打印 arr 集合
System.out.println("arr" + arr.toString());

輸出結(jié)果如下:

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

相信有很多人已經(jīng)看出原因所在了,如果沒有看出請(qǐng)繼續(xù)往下閱讀。

這里我們先創(chuàng)建了一個(gè)集合對(duì)象arr,并分別放入兩個(gè)字符串,接著我們想要?jiǎng)h除索引index處的元素,即“hello”字符串,但是調(diào)用remove()后并未出現(xiàn)想要的結(jié)果,元素并未被刪除。

那么肯定是哪里出了問題。我們看一下remove()方法的源碼,可以發(fā)現(xiàn)其實(shí)有兩個(gè)remove()方法。

// 第一個(gè) 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;
}

// 第二個(gè) 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;
}

還出現(xiàn)了個(gè)remove(Object o)重載的方法,這個(gè)方法我們也不陌生。顯然原來的代碼中arr.remove(index)沒有調(diào)用第一個(gè)remove(int index)。再仔細(xì)看看代碼,index實(shí)際上是Integer類的對(duì)象,因此我們的代碼最終調(diào)用的是第二個(gè)remove(Object o)。于是我們?cè)庀雱h除索引 0 處的元素變成了刪除元素中與index對(duì)應(yīng)(equals)的元素。

其實(shí)這兩個(gè)方法因傳入的參數(shù)類型不同,乍看之下很容易區(qū)分。但是這里的坑在于有時(shí)使用者沒有注意到自己的索引是一個(gè)包裝類Integer對(duì)象,從而導(dǎo)致了想要調(diào)用的方法和實(shí)際調(diào)用方法不符。

后記:

仔細(xì)想一想,其實(shí)所謂的“坑”都是自己了解的過少導(dǎo)致的,如果不深入學(xué)習(xí),遲早會(huì)遇到更多的坑,這些“后果”都是有“前因”的。閱讀源碼能學(xué)到很多東西,不只是上面提到的方法細(xì)節(jié),還有一些高效率的方法以及代碼風(fēng)格,都值得去慢慢咀嚼。加油努力吧!

本文由 Sooxin 創(chuàng)建于 2017-09-14 。本文鏈接:

轉(zhuǎn)載請(qǐng)保留本署名。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • java筆記第一天 == 和 equals ==比較的比較的是兩個(gè)變量的值是否相等,對(duì)于引用型變量表示的是兩個(gè)變量...
    jmychou閱讀 1,644評(píng)論 0 3
  • Collection ├List │├LinkedList │├ArrayList │└Vector │└Stac...
    AndyZX閱讀 959評(píng)論 0 1
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,637評(píng)論 18 399
  • 一、基本數(shù)據(jù)類型 注釋 單行注釋:// 區(qū)域注釋:/* */ 文檔注釋:/** */ 數(shù)值 對(duì)于byte類型而言...
    龍貓小爺閱讀 4,441評(píng)論 0 16
  • 2016/01/29 『白月光』 陽光透過了紗窗 溫暖的蕩起了波漾 想要呼吸新鮮de胃道 卻被玻璃生生止住了腳步。...
    蒙奇奇_阿蒙蠻好的閱讀 267評(píng)論 0 1

友情鏈接更多精彩內(nèi)容