别忘了HashSet

由于长期间的思维惯性和对Java程序缺乏足够的性能重视,导致我在处理Java集合类时经常想当然的只使用List。

例如经常有这样的场景:给定一个集合M,要遍历其中的每一个元素是否在集合N中(需要自己创建)是否存在。那么可能就会机械性地写出以下代码:

// construct the collection N       
List<MyObject> list_N = new ArrayList<MyObject>();        
for (int i = 0; i < N; i++) {        
	// construct or get an Object[i]        
	list_N.add(MyObject_i);        
} 
// check the element in M is in collection N or not       
for (Object o : collection m) {        
	if (list_N.contains(o))        
		...        
	else        
	...   
}

乍一看也没太大问题,但是当N很大时,这样的写法就有性能上的隐患——因为List的contains()实现是遍历式的匹配,也就是说上述的代码的时间复杂度是O(M*N)。

而如果用HashSet来构造,在之后的contains()调用时就是哈希查找,只需要O(1)的复杂度,总的时间复杂度为O(M)。

下面用字符串做为测试对象(采用字符串默认的哈希函数)来看看这两者的实践耗时:

public static void main(String[] args) {       
	List<String> list = new ArrayList<String>();        
	Set<String> set = new HashSet<String>();        
	for (int i = 0; i < 10000; i++) {        
		String s = "t" + i;        
		list.add(s);        
		set.add(s);        
	}        
	long time1 = System.currentTimeMillis();        
	for (int i = 0; i < 100; i++) {        
		String s = "test" + i;        
		if (set.contains(s))        
			System.out.println(i);        
	}        
	long time2 = System.currentTimeMillis();        
	System.out.println("Spend " + (time2 - time1));        
	for (int i = 0; i < 100; i++) {        
		String s = "test" + i;        
		if (list.contains(s))        
			System.out.println(i);        
	}        
	long time3 = System.currentTimeMillis();        
	System.out.println("Spend " + (time3 - time2));        
}

在M为100,N为10000的测试场景中,这两者的时间差别在我机器上差了1~2个数量级。

所以,在这种集合数量很大的时候,应该率先想到hash。而在设计一些方法和接口的时候,多使用Collection,而不是直接设计成List,会给程度实现带来更大的自由度。