Simple Java—Collections(一)Java高效计数器

Translate from Efficient Counter in Java

Java中的高效计数器

你可能经常需要统计一段文本或数据库中某些东西(例如单词)的出现频率。在Java中,使用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);
    }
}

每次循环,检查map中是否已经有该字符,如果有则value+1;如果没有,则放入map,把value置为1;
这种方法很简单,但是效率不够,体现在以下两点:

  • 当key值已经存在时,调用了两次方法:containsKey()、get(),这意味着要在map中检索两次
  • 由于Integer是不可变的,每个循环增加key的value时会创建一个新的对象

2. 优化计数器

为了避免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);
    }
}

通过这个可变的Integer对象,改进后的计数器代码如下:

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));
    }
}

优化了两个地方:

  1. 每次value+1时不需要重新创建Integer对象了
  2. 改变value值不需要重新put()进去

但是,这个方法仍然进行了两次检索

3. 高效计数器

由于 HashMap.put(key, value)方法会返回这个key值原有的value,我们可以利用这一点避免两次检索
(译者注:但是每次循环又增加了一个对象创建的开销)

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. 三种方法的效率表现

为了体现三种方法的效率,通过代码循环了一百万次进行测试,结果如下:

Naive Approach : 222796000
Better Approach: 117283000
Efficient Approach: 96374000

结果可以近似表达为:223:117: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);

5. Keith网站上的更好方法

增加了三个测试:

  1. 重构优良计数器,用get方法取代了containKey,这样HashMap里面的两次检索减成了一个
  2. 取消自定义可变类型的Integer类,用Java中的AtomicInteger
  3. 根据How to Do Text Analysis with Java 一书,value改为用单个int数组存储,可使用更少内存。

我运行这个改进测试案例三次,取结果中最小的值来避免其他程序的影响。注意,你不能让你的程序受到太多影响干扰。例如内存不足会让垃圾回收机制器进行回收。

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]++;
        }
    }
}

6. 结论

efficient-counter.png

从图可以看出,最终胜利者是最后一个使用int[]数组的方法

参考:
1.Most efficient way to increment a Map value in Java.
2.[HashMap.put()](http://docs.oracle.com/javase/6/docs/api/java/util/HashMap.html#put(K, V))

7. 译者补充

这里进一步对高效计数器和改进版的高效计数器进行一下对比
测试字符串"one two three two three three",都采用int[]数组存储

  1. 原本高效计数器:去除了两次检索,但每次循环都声明了一个新对象
for (String string : sArray) {
    int[] n=new int[]{1};
    int[] num = map.put(string,n);
    if (num != null)
        n[0]=++num[0];
}
  1. 网站改进版:需要一次检索,但没有声明多余的对象
for (String  key: sArray) {
    int[] n=map.get(key);
    if(n==null)
        map.put(key, new int[]{1});
    else
        n[0]++;
}

三次运行得出最终耗时结果

48396425
26244898

在这个例子我们似乎可以看出,依旧是改进版的计数器更高效。
但如果把字符串延长10倍,再次运行

21581658187
22019343923

此时两种方法时间花费特别接近,如果把字符串继续延长,改进版的效率则不一定比原来的高。

所以这里得出我的结论:
没有最高效的计数器,只有最合适的计数器。在字符串比较小的情况下,本文末尾结论的计数器最高效。但字符串增加, HashMap检索代价增加的情况下,则第一种高效计数器更好。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,580评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,560评论 18 399
  • java笔记第一天 == 和 equals ==比较的比较的是两个变量的值是否相等,对于引用型变量表示的是两个变量...
    jmychou阅读 1,483评论 0 3
  • 背景 一年多以前我在知乎上答了有关LeetCode的问题, 分享了一些自己做题目的经验。 张土汪:刷leetcod...
    土汪阅读 12,719评论 0 33
  • 这两天一直在读《一生的计划》这本书,作者是格莱恩.布兰德。这本小小的书始终放在我随身携带的包包里,最近这段时...
    禾苗青青阅读 642评论 2 1