翻译人员: 铁锚

翻译时间: 2013年11月3日

原文链接: Efficient Counter in Java

我们经常使用 HashMap作为计数器(counter)来统计数据库或者文本中的某些东西.

本文将使用HashMap来实现计数器的3种不同方式进行对比。

1. 新手级计数器

如果使用这一类别的计数器,那么代码大致如下所示:

String source = "my name is name me and your name is her first her";

String[] words = source.split(" ");

// 新手级计数器

public static void testNaive(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

if(counter.containsKey(w)){

int oldValue = counter.get(w);

counter.put(w, oldValue+1);

} else {

counter.put(w, 1);

}

}

}

在每次循环中,判断是否包含了相应的key,如果包含,那么值在原来的基础上加1,如果没有,那就设置为1.

此种方式简单又直接,但并不是很有效率。效率不高的原因如下:

1.1 当一个key存在时,containsKey() 和 get() 分别调用了一次,这意味着对map进行了两次查找。

1.2 因为 Integer 是不可变的,每次循环在增加计数值的时候将会创建一个新的对象.

2. 入门级计数器

那么我们自然需要使用一个可变的整数来避免创建太多个Integer对象.可变整数类可以如下面所示来定义:

// 可变Integer

public static final class MutableInteger{

private int val;

public MutableInteger(int val){

this.val = val;

}

public int get(){

return this.val;

}

public void set(int val){

this.val = val;

}

// 为了方便打印

public String toString() {

return Integer.toString(val);

}

}

那么计数器可以用如下的方式来改进:

// 入门级计数器

public static void testBetter(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

if(counter.containsKey(w)){

MutableInteger oldValue = counter.get(w);

oldValue.set(oldValue.get()+1);// 因为是引用,所以减少了一次HashMap查找

} else {

counter.put(w, new MutableInteger(1));

}

}

}

因为不需要创建太多的Integer对象,看起来好了一些。然而,key存在的情况下,每次循环依然要进行两次查找.

3. 卓越级计数器

HashMap 的 put(key,value) 方法会返回key对应的当前value.了解这个特性,我们可以利用原有值来进行递增,并不需要多次的查找.

public static void testEfficient(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

MutableInteger initValue = new MutableInteger(1);

// 利用 HashMap 的put方法弹出旧值的特性

MutableInteger oldValue = counter.put(w, initValue);

if(oldValue != null){

initValue.set(oldValue.get() + 1);

}

}

}

4. 性能差异

为了测试这三种实现方式的性能,采用了下面的代码。先看看结果如何,性能测试分别执行了多次,对每一个数量级的测试,误差不算太大,所以取其中的一个结果排列如下:

10000000 次循环:

新手级计数器: 7726594902

入门级计数器: 6516014840

卓越级计数器: 5736574103

1000000 次循环:

新手级计数器: 777480106

入门级计数器: 642932000

卓越级计数器: 571867738

100000 次循环:

新手级计数器: 84323682

入门级计数器: 70176906

卓越级计数器: 61219664

10000 次循环:

新手级计数器: 13279550

入门级计数器: 7874100

卓越级计数器: 6460172

1000 次循环:

新手级计数器: 4542172

入门级计数器: 2933248

卓越级计数器: 992749

100 次循环:

新手级计数器: 3092325

入门级计数器: 1101695

卓越级计数器: 423942

10 次循环:

新手级计数器: 1993788

入门级计数器: 558150

卓越级计数器: 153156

1 次循环:

新手级计数器: 1625898

入门级计数器: 427494

卓越级计数器: 69473

从上面的输出可以看到,10000次的时候, 13:8:6 秒,相差很明显.特别是 新手级计数器和入门级计数器之间的比例,这说明创建对象是很耗资源的操作。

当然,次数更多的差距不明显的原因在于,触发了多次的GC垃圾回收,同时也证明了垃圾回收的代价确实很大。

完整的测试代码如下:

import java.util.HashMap;

public class TestCounter {

public static void main(String[] args) {

// 源字符串

String source = "my name is name me and your name is her first her";

// 计时,单位: 微秒

long startTime = 0;

long endTime = 0;

long duration = 0;

// 测试次数

int loop = 1 * 10000;

System.out.println(loop +" 次循环:");

startTime = System.nanoTime();

testNaive(source,loop);

endTime = System.nanoTime();

duration = endTime - startTime;

System.out.println("新手级计数器: " + duration);

//

startTime = System.nanoTime();

testBetter(source, loop);

endTime = System.nanoTime();

duration = endTime - startTime;

System.out.println("入门级计数器: " + duration);

//

startTime = System.nanoTime();

testEfficient(source, loop);

endTime = System.nanoTime();

duration = endTime - startTime;

System.out.println("卓越级计数器: " + duration);

}

// 新手级计数器

public static void testNaive(String source, int loop){

if(null == source){

return;

}

//

String[] words = source.split(" ");

for (int i = 0; i < loop; i++) {

testNaive(words);

}

}

public static void testNaive(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

if(counter.containsKey(w)){

int oldValue = counter.get(w);

counter.put(w, oldValue+1);

} else {

counter.put(w, 1);

}

}

}

// 可变Integer

public static final class MutableInteger{

private int val;

public MutableInteger(int val){

this.val = val;

}

public int get(){

return this.val;

}

public void set(int val){

this.val = val;

}

// 为了方便打印

public String toString() {

return Integer.toString(val);

}

}

// 入门级计数器

public static void testBetter(String source, int loop){

if(null == source){

return;

}

//

String[] words = source.split(" ");

for (int i = 0; i < loop; i++) {

testBetter(words);

}

}

public static void testBetter(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

if(counter.containsKey(w)){

MutableInteger oldValue = counter.get(w);

oldValue.set(oldValue.get()+1);// 因为是引用,所以减少了一次HashMap查找

} else {

counter.put(w, new MutableInteger(1));

}

}

}

// 卓越级计数器

public static void testEfficient(String source, int loop){

if(null == source){

return;

}

//

String[] words = source.split(" ");

for (int i = 0; i < loop; i++) {

testEfficient(words);

}

}

public static void testEfficient(String[] words){

HashMap counter = new HashMap();

for (String w : words) {

MutableInteger initValue = new MutableInteger(1);

// 利用 HashMap 的put方法弹出旧值的特性

MutableInteger oldValue = counter.put(w, initValue);

if(oldValue != null){

initValue.set(oldValue.get() + 1);

}

}

}

}

当你实用计数器的时候,很可能也需要根据值来进行排序的方法,请参考: the frequently used method of HashMap.

5. Keith网站评论列表

我觉得最好的评论如下:

添加了三个测试:

1) 重构了 “入门级计数器”,不使用containsKey,改为只使用get方法. 通常你需要的元素是存在于 HashMap 中的, 所以将 2 次查找精简为 1次.

2) 作者 michal 提到过的方式,使用 AtomicInteger来实现 .

3) 使用单个的int 数组来进行对比,可以使用更少的内存,参见 http://amzn.com/0748614079

我运行了测试程序3次,并挑选出最小的那个值(以减少干扰). 注意: 你不能在程序中让运行结果受到太多干扰,因为内存不足可能会受到gc垃圾回收器太多的影响.

新手级计数器: 201716122

入门级计数器: 112259166

卓越级计数器: 93066471

入门级计数器 (不使用 containsKey): 69578496

入门级计数器 (不使用 containsKey, with AtomicInteger): 94313287

入门级计数器 (不使用 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, 使用 AtomicInteger):

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. 结论

优胜者是使用int数组的方式.

参考文章

HashMap.put() HashMap.put()

相关阅读

1. ArrayList vs. LinkedList vs. Vector

Frequently Used Methods of Java HashMap

java计数器_Java高效计数器相关推荐

  1. java 网站计数器_Java页面计数器

    ---- 常常逛WWW的人,一定对许多起始页上的计数器感兴趣.每当你光临某个站点的起始页时,它的计数器就很亲切地告诉你,从某年某月某日开始,你是第几位光临的人.你可能也想在自己的HomePage内做计 ...

  2. 用java的io做一个代码计数器,如何制作Java页面计数器_java

    大庆采油六厂采油工艺研究所 王兵 王波 常常逛WWW的人,一定对许多起始页上的计数器感兴趣.每当你光临某个站点的起始页时,它的计数器就很亲切地告诉你,从某年某月某日开始,你是第几位光临的人.你可能也想 ...

  3. 用java的io做一个代码计数器_Java中的程序计数器是什么

    Java中的程序计数器是什么 程序计数器是当前线程正在执行的字节码的地址.程序计数器是线程隔离的,每一个线程在工作的时候都有一个独立的计数器. JAVA虚拟机管理的内存区域图 1.什么是程序计数器? ...

  4. java 并发计数器_Java 8 LongAdders:管理并发计数器的正确方法

    java 并发计数器 我只是买了新玩具,而Java 8有很多 . 这次我想谈谈我的最爱之一-并发加法器. 这是一组新的类,用于管理由多个线程编写和读取的计数器. 新的API有望显着提高性能,同时仍使事 ...

  5. java 计数器_java并发之计数器CountDownLatch原理

    java并发之计数器CountDownLatch原理 CountDownLatch简介 CountDownLatch顾名思义,count + down + latch = 计数 + 减 + 门闩(这么 ...

  6. java闭锁_Java闭锁_CountDownLatch

    Java闭锁_CountDownLatch 闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态.闭锁的作用相当于一扇门:在闭锁到达结束状态之前,这扇门一直是关闭的,并且没有任何线程能通过,当 ...

  7. java闭锁_Java并发工具类(闭锁CountDownLatch)

    闭锁是一种同步工具类,可以延迟线程的进度直到其到达终止状态. CountDownLatch是一种灵活的闭锁实现,它可以使一个或者多个线程等待一组事件的发生. 闭锁状态包含一个计数器,该计数器被初始化为 ...

  8. java丐帮_java多线程学习笔记(五)

    补充一个synchronized关键字的结论: 线程A先持有object对象的Lock锁,B线程可以以异步的方式调用object对象中的非synchronized类型的方法 A线程现持有object对 ...

  9. java丐帮_java多线程学习笔记(四)

    上一节讲到Synchronized关键字,synchronized上锁的区域:对象锁=方法锁/类锁 本节补充介绍一下synchronized锁重入: 关键字synchronized拥有锁重入的功能,也 ...

  10. 分别设计网页访问计数器,会话计数器,访问网站计数器。

    张继军 董卫 <java web 应用开发技术与实案列教程>课后习题之第三章第7题 网页访问计数器count1.jsp 会话计数器count2.jsp 访问网站访问计数器count3.js ...

最新文章

  1. python自动退出程序_python异常退出
  2. node学习笔记--模块加载
  3. bose耳机信号断续_挥汗如雨的夏季将至,5款运动型蓝牙耳机安利给你
  4. 超键、候选键、主键、外键、联合主键、复合主键
  5. 让一张图片随着手指的移动而移动
  6. Duilib教程-简单介绍
  7. ssl2331OJ1373-鱼塘钓鱼 之3【dp】
  8. jwt配置 restful_SpringBoot实现JWT保护前后端分离RESTful API
  9. vue变量传值_VUE 学习——父组件传值给子组件
  10. android能用svg格式,Android加载html中svg格式图片进行显示
  11. python3.0与2.x之间的区别
  12. 两个表点击分页的时候怎么判断点的是哪一个表_如何对指数估值,估值表怎么用?...
  13. python连接redis集群如何释放内存_python 连接redis集群
  14. python自动化控制_python用于自动化控制编程
  15. ashx中使用Session
  16. selenium弹窗无法定位_4.3 通过selenium 模拟浏览器抓取
  17. libxml2 知:介绍
  18. 网络操作系统之VyOS部署
  19. ESP32实现Wave(.wav)音频文件输出
  20. 高盛发布VR/AR研究报告

热门文章

  1. 给mBlock添加扩展模块
  2. iTunes降级操作
  3. 无线路由器和无线网卡的普及知识贴及选择(2019.05更新802.11AX网卡,3T3R wave2路由器推荐)
  4. FastDFS 原理 以及 简单使用
  5. 数据结构-带头双向循环链表
  6. NFC怎么复制房卡_为了省门禁卡的钱,买了NFC读卡器,到底值不值
  7. 信号与系统学习总结 第三章 傅里叶变换
  8. matlab删掉txt文件中的数据,matlab中读取txt数据文件(txt文本文档)
  9. 北京54坐标系转经纬度坐标系教程
  10. Vivado的下载和安装