最近在看 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中添加了a和b,并在后續(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)方法,a和b的值當(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)誤解,因此還需要多多閱讀源碼呀!
比如不了解
StringBuffer和StringBuilder的話,很容易理所當(dāng)然的想既然String重寫了equals(Object obj),從而可以直接進(jìn)行值比較,就認(rèn)為StringBuffer和StringBuilder也應(yīng)當(dāng)如此,但是卻并非這樣。StringBuffer和StringBuilder都沒有重寫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 。本文鏈接:
- Github 博客:https://sooxin.github.io/posts/2017/9/14/lost-in-methods-of-ArrayList-in-java.html
- 簡(jiǎn)書:http://www.itdecent.cn/p/394d370cd6fe
轉(zhuǎn)載請(qǐng)保留本署名。