首页 » 编写高质量代码:改善Java程序的151个建议 » 编写高质量代码:改善Java程序的151个建议全文在线阅读

《编写高质量代码:改善Java程序的151个建议》建议74:不推荐使用binarySearch对列表进行检索

关灯直达底部

对一个列表进行检索时,我们使用得最多的是indexOf方法,它简单、好用,而且也不会出错,虽然它只能检索到第一个符合条件的值,但是我们可以生成子列表后再检索,这样也就可以查找出所有符合条件的值了。

Collections工具类也提供一个检索方法:binarySearch,这个是干什么的?该方法也是对一个列表进行检索的,可查找出指定值的索引值,但是在使用这个方法时就有一些注意事项了,我们看如下代码:


public static void main(Stringargs){

List<String>cities=new ArrayList<String>();

cities.add(/"上海/");

cities.add(/"广州/");

cities.add(/"广州/");

cities.add(/"北京/");

cities.add(/"天津/");

//indexOf方法取得索引值

int index1=cities.indexOf(/"广州/");

//binarySearch查找到索引值

int index2=Collections.binarySearch(cities,/"广州/");

System.out.println(/"索引值(indexOf):/"+index1);

System.out.println(/"索引值(binarySearch):/"+index2);

}


先不考虑运行结果,直接看JDK上对binarySearch的描述:使用二分搜索法搜索指定列表,以获得指定对象。其实现的功能与indexOf是相同的,只是使用的是二分法搜索列表,所以估计两种方法返回结果是一样的,看结果:


索引值(indexOf):1

索引值(binarySearch):2


结果不一样,虽然说我们有两个“广州”这样的元素,但是返回的结果都应该是1才对呀,为何binarySearch返回的结果是2呢?问题就出在二分法搜索上,二分法搜索就是“折半折半再折半”的搜索方法,简单,而且效率高。看看JDK是如何实现的。


public static<T>int binarySearch(List<?extends Comparable<?super T>>list, T key){

if(list instanceof RandomAccess||list.size()<5000)

//随机存取列表或者元素数量少于5000的顺序存取列表

return Collections.indexedBinarySearch(list, key);

else

//元素数量大于5000的顺序存取列表

return Collections.iteratorBinarySearch(list, key);

}


ArrayList实现了RandomAccess接口,是一个顺序存取列表,使用了indexdBinarySearch方法,代码如下:


private static<T>int indexedBinarySearch(List<?extends Comparable<?super T>>

list, T key){

//默认上界

int low=0;

//默认下界

int high=list.size()-1;

while(low<=high){

//中间索引,无符号右移1位

int mid=(low+high)>>>1;

//中间值

Comparable<?super T>midVal=list.get(mid);

//比较中间值

int cmp=midVal.compareTo(key);

//重置上界和下界

if(cmp<0)

low=mid+1;

else if(cmp>0)

high=mid-1;

else

//找到元素

return mid;

}

//没有找到,返回负值

return-(low+1);

}


这也没啥说的,就是二分法搜索的Java版实现。注意看加粗字体部分,首先是获得中间索引值,我们的例子中也就是2,那索引值是2的元素值是多少呢?正好是“广州”,于是返回索引值2,正确,没问题。那我们再看看indexOf的实现,代码如下:


public int indexOf(Object o){

if(o==null){

//null元素查找

for(int i=0;i<size;i++)

if(elementData[i]==null)

return i;

}else{

//非null元素查找

for(int i=0;i<size;i++)

//两个元素是否相等,注意这里是equals方法

if(o.equals(elementData[i]))

return i;

}

//没找到,返回-1

return-1;

}


indexOf方法就是一个遍历,找到第一个元素值相等则返回,没什么玄机。回到我们的程序上来看,for循环的第二遍即是我们要查找的“广州”,于是就返回索引值1了,也正确,没有任何问题。

两者的算法都没有问题,难道是我们用错了?这还真是我们的错,因为二分法查询的一个首要前提是:数据集已经实现升序排列,否则二分法查找的值是不准确的。不排序怎么确定是在小区(比中间值小的区域)中查找还是在大区(比中间值大的区域)中查找呢?二分法查找必须要先排序,这是二分法查找的首要条件。

问题清楚了,藐视解决方法也很简单,使用Collections.sort排下序即可解决。但这样真的可以解决吗?想想看,元素数据是从Web或数据库中传递进来的,原本是一个有规则的业务数据,我们为了查找一个元素对它进行了排序,也就是改变了元素在列表中的位置,那谁来保证业务规则此时的正确性呢?所以说,binarySearch方法在此处受限了。当然,拷贝一个数组,然后再排序,再使用binarySeach查找指定值,也可以解决该问题。

使用binarySearch首先要考虑排序问题,这是我们编码人员经常容易忘记的,而且在测试期间还没发现问题(因为测试时“制造”的数据都很有规律),等到投入生产系统后才发现查找的数据不准确,又是一个大Bug产生了,从这点来看,indexOf要比binarySearch简单得多。

当然,使用binarySearch的二分法查找比indexOf的遍历算法性能上高很多,特别是在大数据集而且目标值又接近尾部时,binarySearch方法与indexOf相比,性能上会提升几十倍,因此在从性能的角度考虑时可以选择binarySearch。

注意 从性能方面考虑,binarySearch是最好的选择。