你也许经常需要一个计数器来了解数据库或文本文件中一些事务出现的频率(例如单词)。通过在Java中计数器可以通过HashMap可以轻松实现计数器。本文比较了实现不同的计数器方法。
更新: 查看Java8 计数器,写一个计数器现在只是简单的2行代码。
1.基本计数器
朴素计数器可以如下实现:
String s = "one two three two three three";
String[] sArr = s.split(" ");
//naive approach
HashMap counter = new HashMap();
for (String a : sArr) {
if (counter.containsKey(a)) {
int oldValue = counter.get(a);
counter.put(a, oldValue + 1);
} else {
counter.put(a, 1);
}
}
在每个循环中,你检查key是否存在。如果是,则将旧值增加1,否则将其设置为1,这种方法很简单直接,但是它不是最有效的方法,由于以下方法,该方法被认为是低效的。
containsKey(),get()在一个key存在时候,被调用了两次,这意味了搜索了Map两次。
既然Integer是不可变的,每次循环将创建一个新的值来替代增加的旧值。
2.更好的计数器
自然地,我们希望有一个可变的Integer来避免穿件很多Integer对象。一个可变的Integer 类如下定义:
class MutableInteger {
private int val;
public MutableInteger(int val) {
this.val = val;
}
public int get() {
return val;
}
public void set(int val) {
this.val = val;
}
//used to print value convinently
public String toString(){
return Integer.toString(val);
}
}
计数器代码改变如下:
HashMap newCounter = new HashMap();
for (String a : sArr) {
if (newCounter.containsKey(a)) {
MutableInteger oldValue = newCounter.get(a);
oldValue.set(oldValue.get() + 1);
} else {
newCounter.put(a, new MutableInteger(1));
}
}
这是更好,因为不需要创建许多Integer对象。但是存在key键,仍然搜索两次。
3. 更高效计数器
HashMap.put(key, value) 方法返回键的当前值。这是有用的,因为我们可以使用旧值的引用来更新值,而不用再搜索一次。
HashMap efficientCounter = new HashMap();
for (String a : sArr) {
MutableInteger initValue = new MutableInteger(1);
MutableInteger oldValue = efficientCounter.put(a, initValue);
if(oldValue != null){
initValue.set(oldValue.get() + 1);
}
}
4.性能表现差异
为测试三种不同方法的性能,使用下面的代码,性能测试100万次,原始结果如下:
Naive Approach : 222796000
Better Approach: 117283000
Efficient Approach: 96374000
差别是显著的,223 vs 117 vs 96 基础计数器和高效计数器相差巨大,说明创建对象是昂贵的。
String s = "one two three two three three";
String[] sArr = s.split(" ");
long startTime = 0;
long endTime = 0;
long duration = 0;
// naive approach
startTime = System.nanoTime();
HashMap counter = new HashMap();
for (int i = 0; i < 1000000; i++)
for (String a : sArr) {
if (counter.containsKey(a)) {
int oldValue = counter.get(a);
counter.put(a, oldValue + 1);
} else {
counter.put(a, 1);
}
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("Naive Approach : " + duration);
// better approach
startTime = System.nanoTime();
HashMap newCounter = new HashMap();
for (int i = 0; i < 1000000; i++)
for (String a : sArr) {
if (newCounter.containsKey(a)) {
MutableInteger oldValue = newCounter.get(a);
oldValue.set(oldValue.get() + 1);
} else {
newCounter.put(a, new MutableInteger(1));
}
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("Better Approach: " + duration);
// efficient approach
startTime = System.nanoTime();
HashMap efficientCounter = new HashMap();
for (int i = 0; i < 1000000; i++)
for (String a : sArr) {
MutableInteger initValue = new MutableInteger(1);
MutableInteger oldValue = efficientCounter.put(a, initValue);
if (oldValue != null) {
initValue.set(oldValue.get() + 1);
}
}
endTime = System.nanoTime();
duration = endTime - startTime;
System.out.println("Efficient Approach: " + duration);
在你进行计数的时候,可能需要Map的按值排序,可以看下Map按照值排序.
5.来自Keith解决方案
添加了几个测试:
1) 重构“更好方法”只是调用get而不是containsKey,通常你想要的元素在HashMap,只需要搜索一次。
2)添加一个AtomicInteger来进行测试,
3)与单独的int数组相比,根据http://amzn.com/0748614079使用较少的内存
我运行测试程序三次,并且采取最小的值,以消除与其他程序的方差。注意,你不能在程序中这样做,这样结果可能受GC影响而不同。
Naive: 201716122
Better Approach: 112259166
Efficient Approach: 93066471
Better Approach (without containsKey): 69578496
Better Approach (without containsKey, with AtomicInteger): 94313287
Better Approach (without containsKey, with int[]): 65877234
更好的办法(没有containsKey):
HashMap efficientCounter2 = new HashMap();
for (int i = 0; i < NUM_ITERATIONS; i++) {
for (String a : sArr) {
MutableInteger value = efficientCounter2.get(a);
if (value != null) {
value.set(value.get() + 1);
} else {
efficientCounter2.put(a, new MutableInteger(1));
}
}
}
更好的办法(没有containsKey,用AutomicInteger):
HashMap atomicCounter = new HashMap();
for (int i = 0; i < NUM_ITERATIONS; i++) {
for (String a : sArr) {
AtomicInteger value = atomicCounter.get(a);
if (value != null) {
value.incrementAndGet();
} else {
atomicCounter.put(a, new AtomicInteger(1));
}
}
}
更好的方法(没用containsKey ,用int[]):
HashMap intCounter = new HashMap();
for (int i = 0; i < NUM_ITERATIONS; i++) {
for (String a : sArr) {
int[] valueWrapper = intCounter.get(a);
if (valueWrapper == null) {
intCounter.put(a, new int[] { 1 });
} else {
valueWrapper[0]++;
}
}
}
Guava的MultiSet可能更快。
6 .结论
计数器效率比较