抉择实施方案
从早些时候的那幅示意图可以看出,实际上只有三个荟萃组件:Map,List和Set。并且每个接口只有两种或三种实施方案。若需利用由一个特定的接口提供的成果,如何才气抉择到底采纳哪一种方案呢?
为领略这个问题,必需认识到每种差异的实施方案都有本身的特点、利益和缺点。好比在那张示意图中,可以看到Hashtable,Vector和Stack的“特点”是它们都属于“传统”类,所以不会滋扰原有的代码。但在另一方面,应只管制止为新的(Java 1.2)代码利用它们。
其他荟萃间的差别凡是都可归纳为它们详细是由什么“后推”的。换言之,取决于物理意义上用于实施方针接口的数据布局是什么。譬喻,ArrayList,LinkedList以及Vector(大抵等价于ArrayList)都实现了List接口,所以无论选用哪一个,我们的措施城市获得雷同的功效。然而,ArrayList(以及Vector)是由一个数组后推获得的;而LinkedList是按照通例的双重链接列表方法实现的,因为每个单独的工具都包括了数据以及指向列表内前后元素的句柄。正是由于这个原因,如果想在一个列表中部举办大量插入和删除操纵,那么LinkedList无疑是最得当的选择(LinkedList尚有一些特另外成果,成立于AbstractSequentialList中)。若非如此,就情愿选择ArrayList,它的速度大概要快一些。
作为另一个例子,Set既可作为一个ArraySet实现,亦可作为HashSet实现。ArraySet是由一个ArrayList后推获得的,设计成只支持少量元素,出格适合要求建设和删除大量Set工具的场所利用。然而,一旦需要在本身的Set中容纳大量元素,ArraySet的机能就会大打折扣。写一个需要Set的措施时,应默认选择HashSet。并且只有在某些非凡环境下(对机能的晋升有急切的需求),才应切换到ArraySet。
1. 抉择利用何种List
为体会各类List实施方案间的差别,最轻便的要领就是举办一次机能考试。下述代码的浸染是成立一个内部基本类,将其作为一个测试床利用。然后为每次考试都建设一个匿名内部类。每个这样的内部类都由一个test()要领挪用。操作这种要领,可以利便添加和删除测试项目。
//: ListPerformance.java
// Demonstrates performance differences in Lists
package c08.newcollections;
import java.util.*;
public class ListPerformance {
private static final int REPS = 100;
private abstract static class Tester {
String name;
int size; // Test quantity
Tester(String name, int size) {
this.name = name;
this.size = size;
}
abstract void test(List a);
}
private static Tester[] tests = {
new Tester("get", 300) {
void test(List a) {
for(int i = 0; i < REPS; i++) {
for(int j = 0; j < a.size(); j++)
a.get(j);
}
}
},
new Tester("iteration", 300) {
void test(List a) {
for(int i = 0; i < REPS; i++) {
Iterator it = a.iterator();
while(it.hasNext())
it.next();
}
}
},
new Tester("insert", 1000) {
void test(List a) {
int half = a.size()/2;
String s = "test";
ListIterator it = a.listIterator(half);
for(int i = 0; i < size * 10; i++)
it.add(s);
}
},
new Tester("remove", 5000) {
void test(List a) {
ListIterator it = a.listIterator(3);
while(it.hasNext()) {
it.next();
it.remove();
}
}
},
};
public static void test(List a) {
// A trick to print out the class name:
System.out.println("Testing " +
a.getClass().getName());
for(int i = 0; i < tests.length; i++) {
Collection1.fill(a, tests[i].size);
System.out.print(tests[i].name);
long t1 = System.currentTimeMillis();
tests[i].test(a);
long t2 = System.currentTimeMillis();
System.out.println(": " + (t2 - t1));
}
}
public static void main(String[] args) {
test(new ArrayList());
test(new LinkedList());
}
} ///:~
内部类Tester是一个抽象类,用于为特定的测试提供一个基本类。它包括了一个要在测试开始时打印的字串、一个用于计较测试次数或元素数量的size参数、用于初始化字段的一个构建器以及一个抽象要领test()。test()做的是最实际的测试事情。各类范例的测试都会合到一个处所:tests数组。我们用担任于Tester的差异匿名内部类来初始化该数组。为添加或删除一个测试项目,只需在数组里简朴地添加或移去一个内部类界说即可,其他所有事情都是自动举办的。
首先用元素填充通报给test()的List,然后对tests数组中的测试计时。由于测试用呆板的差异,功效虽然也会有所区别。这个措施的宗旨是展现出差异荟萃范例的相对机能较量。下面是某一次运行获得的功效:
范例 获取 重复 插入 删除
ArrayList 110 270 1920 4780
LinkedList 1870 7580 170 110
可以看出,在ArrayList中举办随时机见(即get())以及轮回重复是最划得来的;但对付LinkedList却是一个不小的开销。但另一方面,在列表中部举办插入和删除操纵对付LinkedList来说却比ArrayList划算得多。我们最好的做法也许是先选择一个ArrayList作为本身的默认起点。今后若发明由于大量的插入和删除造成了机能的低落,再思量换成LinkedList不迟。
2. 抉择利用何种Set
可在ArraySet以及HashSet间作出选择,详细取决于Set的巨细(假如需要从一个Set中得到一个顺序列表,请用TreeSet;注释⑧)。下面这个测试措施将有助于各人作出这方面的决议:
//: SetPerformance.java
package c08.newcollections;
import java.util.*;
public class SetPerformance {
private static final int REPS = 200;
private abstract static class Tester {
String name;
Tester(String name) { this.name = name; }
abstract void test(Set s, int size);
}
private static Tester[] tests = {
new Tester("add") {
void test(Set s, int size) {
for(int i = 0; i < REPS; i++) {
s.clear();
Collection1.fill(s, size);
}
}
},
new Tester("contains") {
void test(Set s, int size) {
for(int i = 0; i < REPS; i++)
for(int j = 0; j < size; j++)
s.contains(Integer.toString(j));
}
},
new Tester("iteration") {
void test(Set s, int size) {
for(int i = 0; i < REPS * 10; i++) {
Iterator it = s.iterator();
while(it.hasNext())
it.next();
}
}
},
};
public static void test(Set s, int size) {
// A trick to print out the class name:
System.out.println("Testing " +
s.getClass().getName() + " size " + size);
Collection1.fill(s, size);
for(int i = 0; i < tests.length; i++) {
System.out.print(tests[i].name);
long t1 = System.currentTimeMillis();
tests[i].test(s, size);
long t2 = System.currentTimeMillis();
System.out.println(": " +
((double)(t2 - t1)/(double)size));
}
}
public static void main(String[] args) {
// Small:
test(new TreeSet(), 10);
test(new HashSet(), 10);
// Medium:
test(new TreeSet(), 100);
test(new HashSet(), 100);
// Large:
test(new HashSet(), 1000);
test(new TreeSet(), 1000);
}
} ///:~
#p#分页标题#e#
⑧:TreeSet在本书写作时尚未成为一个正式的特性,但在这个例子中可以很轻松地为其添加一个测试。
最后对ArraySet的测试只有500个元素,而不是1000个,因为它太慢了。
范例 测试巨细 添加 包括 重复
|
Type |
Test size |
Add |
Contains |
Iteration |
|
10 |
22.0 |
11.0 |
16.0 |
|
|
TreeSet |
100 |
22.5 |
13.2 |
12.1 |
|
1000 |
31.1 |
18.7 |
11.8 |
|
|
10 |
5.0 |
6.0 |
27.0 |
|
|
HashSet |
100 |
6.6 |
6.6 |
10.9 |
|
1000 |
7.4 |
6.6 |
9.5 |
#p#分页标题#e#
举办add()以及contains()操纵时,HashSet显然要比ArraySet精彩得多,并且机能明明与元素的多寡干系不大。一般编写措施的时候,险些永远用不着利用ArraySet。
3. 抉择利用何种Map
选择差异的Map实施方案时,留意Map的巨细对付机能的影响是最大的,下面这个测试措施清楚地阐示了这一点:
//: MapPerformance.java
// Demonstrates performance differences in Maps
package c08.newcollections;
import java.util.*;
public class MapPerformance {
private static final int REPS = 200;
public static Map fill(Map m, int size) {
for(int i = 0; i < size; i++) {
String x = Integer.toString(i);
m.put(x, x);
}
return m;
}
private abstract static class Tester {
String name;
Tester(String name) { this.name = name; }
abstract void test(Map m, int size);
}
private static Tester[] tests = {
new Tester("put") {
void test(Map m, int size) {
for(int i = 0; i < REPS; i++) {
m.clear();
fill(m, size);
}
}
},
new Tester("get") {
void test(Map m, int size) {
for(int i = 0; i < REPS; i++)
for(int j = 0; j < size; j++)
m.get(Integer.toString(j));
}
},
new Tester("iteration") {
void test(Map m, int size) {
for(int i = 0; i < REPS * 10; i++) {
Iterator it = m.entries().iterator();
while(it.hasNext())
it.next();
}
}
},
};
public static void test(Map m, int size) {
// A trick to print out the class name:
System.out.println("Testing " +
m.getClass().getName() + " size " + size);
fill(m, size);
for(int i = 0; i < tests.length; i++) {
System.out.print(tests[i].name);
long t1 = System.currentTimeMillis();
tests[i].test(m, size);
long t2 = System.currentTimeMillis();
System.out.println(": " +
((double)(t2 - t1)/(double)size));
}
}
public static void main(String[] args) {
// Small:
test(new Hashtable(), 10);
test(new HashMap(), 10);
test(new TreeMap(), 10);
// Medium:
test(new Hashtable(), 100);
test(new HashMap(), 100);
test(new TreeMap(), 100);
// Large:
test(new HashMap(), 1000);
test(new Hashtable(), 1000);
test(new TreeMap(), 1000);
}
} ///:~
由于Map的巨细是最严重的问题,所以措施的计时测试按Map的巨细(或容量)来支解时间,以便获得令人信服的测试功效。下面列出一系列功效(在你的呆板上大概差异):
范例 测试巨细 置入 取出 重复
|
Type |
Test size |
Put |
Get |
Iteration |
|---|---|---|---|---|
|
|
10 |
11.0 |
5.0 |
44.0 |
|
Hashtable |
100 |
7.7 |
7.7 |
16.5 |
|
1000 |
8.0 |
8.0 |
14.4 |
|
|
|
10 |
16.0 |
11.0 |
22.0 |
|
TreeMap |
100 |
25.8 |
15.4 |
13.2 |
|
1000 |
33.8 |
20.9 |
13.6 |
|
|
|
10 |
11.0 |
6.0 |
33.0 |
|
HashMap |
100 |
8.2 |
7.7 |
13.7 |
|
1000 |
8.0 |
7.8 |
11.9 |
#p#分页标题#e#
纵然巨细为10,ArrayMap的机能也要比HashMap差——除重复轮回时以外。而在利用Map时,重复的浸染凡是并不重要(get()凡是是我们时间花得最多的处所)。TreeMap提供了精彩的put()以及重复时间,但get()的机能并不佳。可是,我们为什么仍然需要利用TreeMap呢?这样一来,我们可以不把它作为Map利用,而作为建设顺序列表的一种途径。树的本质在于它老是顺序分列的,不必出格举办排序(它的排序方法顿时就要讲到)。一旦填充了一个TreeMap,就可以挪用keySet()来得到键的一个Set“情形”。然后用toArray()发生包括了那些键的一个数组。随后,可用static要领Array.binarySearch()快速查找排好序的数组中的内容。虽然,也许只有在HashMap的行为不行接管的时候,才需要回收这种做法。因为HashMap的设计宗旨就是举办快速的检索操纵。最后,当我们利用Map时,首要的选择应该是HashMap。只有在少少数环境下才需要思量其他要领。
另外,在上面那张内外,有另一本机能问题没有反应出来。下述措施用于测试差异范例Map的建设速度:
//: MapCreation.java
// Demonstrates time differences in Map creation
package c08.newcollections;
import java.util.*;
public class MapCreation {
public static void main(String[] args) {
final long REPS = 100000;
long t1 = System.currentTimeMillis();
System.out.print("Hashtable");
for(long i = 0; i < REPS; i++)
new Hashtable();
long t2 = System.currentTimeMillis();
System.out.println(": " + (t2 - t1));
t1 = System.currentTimeMillis();
System.out.print("TreeMap");
for(long i = 0; i < REPS; i++)
new TreeMap();
t2 = System.currentTimeMillis();
System.out.println(": " + (t2 - t1));
t1 = System.currentTimeMillis();
System.out.print("HashMap");
for(long i = 0; i < REPS; i++)
new HashMap();
t2 = System.currentTimeMillis();
System.out.println(": " + (t2 - t1));
}
} ///:~
#p#分页标题#e#
在写这个措施期间,TreeMap的建设速度比其他两种范例明明快得多(但你应亲自实验一下,因为听说新版本大概会改进ArrayMap的机能)。思量到这方面的原因,同时由于前述TreeMap精彩的put()机能,所以假如需要建设大量Map,并且只有在今后才需要涉及大量检索操纵,那么最佳的计策就是:建设和填充TreeMap;今后检索量增大的时候,再将重要的TreeMap转换成HashMap——利用HashMap(Map)构建器。同样地,只有在事实证明晰实存在机能瓶颈后,才应体贴这些方面的问题——先用起来,再按照需要加速速度。