Java实现的高效计数器

    在统计来自数据库或文本中某些内容的频率时,你可能经常会用到HashMap。本文对比了三种用HashMap实现的计数器。

1. 简单的计数器

    如果你使用这样一个计数器,你的代码可能如下:

String s = "one two three two three three";
String[] sArr = s.split(" ");

//naive approach
HashMap<String, Integer> counter = new HashMap<String, Integer>();

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。该方法看起来简单而直接,但它并不是最有效率的方法,它在如下方面欠考虑:

① 如果键(key)已经存在的话,containsKey()、get()就会方法被调用两次,这意味着要搜索map两次;

② 由于整数(Integer)是不可变的,每次循环都会创建一个新的整数对象保存新的计数值。

2. 改进的计数器

    自然而然的,我们希望用一个可变的整数值来避免创建过多的整数对象。因此,可以定义一个可变整数类,如下所示:

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<String, MutableInteger> newCounter = new HashMap<String, MutableInteger>();

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)存在时需要搜索两次map。

3. 高效的计数器

    HashMap.put(key, value)方法返回键(key)对应的值。这个方法很有用,我们可以直接使用旧值的引用来更新值,而不需要再多进行一次搜索。

HashMap<String, MutableInteger> efficientCounter = new HashMap<String, MutableInteger>();

for(String a : sArr) {
    MutableInteger initValue = new MutableInteger(1);
    MutableInteger oldValue = efficientCounter.put(a, initValue);

    if(oldValue != null) {
        initValue.set(oldValue.get() + 1);
    }
}

4. 性能差异

    可以使用下面的代码来测试上述三种方法在性能上的差异。性能测试循环次数为1百万次,实验结果如下所示:

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<String, Integer> counter = new HashMap<String, Integer>();
 
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<String, MutableInteger> newCounter = new HashMap<String, MutableInteger>();
 
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<String, MutableInteger> efficientCounter = new HashMap<String, MutableInteger>();
 
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);
    当你使用计数器时,你可能需要使用一个方法来根据值(value)对map进行排序,对此,你可以参照文章 《HashMap中常用的方法》

5. Keith的评论(如下所示)

    下面是我收到的最好的评论之一。

    添加下面一系列测试:

    1) 重构上述”改进的计数器“,用get()方法来替换containsKey()方法。通常,所需的元素都在HashMap中,因此可以将搜索次数从两次减少到一次。

    2) Michal提到了AtuomicInteger,下面也进行了相关的试验。

    3) 与单例的int数组相比,http://amzn.com/0748614079中提到这可能会使用更少的内存。

    我运行了测试程序3x次,争取每次对代码的改变都最小。需要注意的是,你可能无法做到在程序中做到上述改动,或者试验结果受影响较大,原因可能是垃圾回收期。

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<string, mutableinteger=""> efficientCounter2 = new HashMap<string, mutableinteger="">();
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(),使用AtomicInteger):

HashMap<string, atomicinteger=""> atomicCounter = new HashMap<string, atomicinteger="">();
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<string, int[]=""> intCounter = new HashMap<string, int[]="">();
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. 总结

    性能最高的是使用int数组的那个方法。

原文地址:Efficient Counter in Java

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值