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<String, Integer> counter = new HashMap<String, Integer>();
- 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<String, MutableInteger> counter = new HashMap<String, MutableInteger>();
- 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<String, MutableInteger> counter = new HashMap<String, MutableInteger>();
- 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<String, Integer> counter = new HashMap<String, Integer>();
- 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<String, MutableInteger> counter = new HashMap<String, MutableInteger>();
- 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<String, MutableInteger> counter = new HashMap<String, MutableInteger>();
- 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<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数组的方式.
Java高效计数器的更多相关文章
- Java高效读取大文件
1.概述 本教程将演示如何用Java高效地读取大文件.这篇文章是Baeldung (http://www.baeldung.com/) 上“Java——回归基础”系列教程的一部分. 2.在内存中读取 ...
- Java高效读取大文件(转)
1.概述 本教程将演示如何用Java高效地读取大文件.这篇文章是Baeldung(http://www.baeldung.com/) 上“Java——回归基础”系列教程的一部分. 2.在内存中读取 读 ...
- java高效判断素数
java高效判断素数 package solution; public class Prime { // 偶数可以由有两个素数相加得到, 一个偶数可能有多个这样的两个素数, 请寻找到 这样两个素数,让 ...
- Java高效编程:总结分享
参考资料:慕课网:Java高效编程收费实战课程.博客园.CSDN.菜鸟教程以及其他文档. 篇幅受限,不太想针对每个点都写篇博客,有的地方可能写的不是很详细,一笔带过了.如果你觉得那个点在项目中用得上可 ...
- 剑指Java高效编程教程
教程介绍 所谓"武以快为尊,天下武功唯快不破".本课程剑指Java高效编程,致力于从"技术"和"工具"两大 维度提高编程效率,帮助广大程序员 ...
- Java实现的高效计数器
本文转载地址: http://blog.csdn.net/snarlfuture/article/details/17049731 在统计来自数据库或文本中某些内容的频率时,你可 ...
- Java并发计数器探秘
前言 一提到线程安全的并发计数器,AtomicLong 必然是第一个被联想到的工具.Atomic* 一系列的原子类以及它们背后的 CAS 无锁算法,常常是高性能,高并发的代名词.本文将会阐释,在并发场 ...
- Java高效编程之三【类和接口】
本部分包含的一些指导原则,可以帮助哦我们更好滴利用这些语言元素,以便让设计出来的类更加有用.健壮和灵活. 十二.使类和成员的访问能力最小化 三个关键词访问修饰符:private(私有的=类级别的).未 ...
- Java 高效检查一个数组中是否包含某个值
如何检查一个数组(未排序)中是否包含某个特定的值?在Java中,这是一个非常有用并又很常用的操作.同时,在StackOverflow中,有时一个得票非常高的问题.在得票比较高的几个回答中,时间复杂度差 ...
随机推荐
- HBase_在Linux上安装以及运用
1.上传解压文件 文件:hbase-1.0.1.1-bin.tar 2.更改配置文件 在hbase-env.sh中, export JAVA_HOME=/home/lang/software/jdk1 ...
- Activity的Task详解
1.Task Task是一个具有栈结构(后进先出)的容器,可以放置多个Activity实例.启动一个应用,系统就会为之创建一个Task,来放置根Activity.默认情况下,一个Activity启动另 ...
- iOS各框架功能简述以及系统层次结构简单分析
iOS各个框架所对应的功能简单介绍 iOS系统结构层次:
- preg_*匹配的字符串长度限制问题以及nginx,php上传文件过大问题
问题背景 使用插件上传高清图片,用的插件base64转码的,上传失败,接口提示:413 (Request Entity Too Large) 问题分析与解决 首先想到的是nginx和php的服务器配 ...
- (一)一起学 Java Collections Framework 源码之 概述
. . . . . 目录 (一)一起学 Java Collections Framework 源码之 概述 JDK 中很多类 LZ 已经使用了无数次,但认认真真从源码级研究过其原理的还只占少数,虽然从 ...
- 光场相机重聚焦之三——Matlab光场工具包使用、重聚焦及多视角效果展示
这一小节说一下Matlab光场工具包的使用,展示重聚焦和多视角的效果. 从Lytro illum中导出的raw数据为.lfp格式的光场图像文件(约52M大小),该文件包含以下几部分:光场图像数据raw ...
- android参数传递的几种方法
Intent Intent i=new Intent(当前Activity.this,目标Activity.class); 1.传单值 传入: i. i.putExtra("名称" ...
- CF Manthan, Codefest 16 G. Yash And Trees 线段树+bitset
题目链接:http://codeforces.com/problemset/problem/633/G 大意是一棵树两种操作,第一种是某一节点子树所有值+v,第二种问子树中节点模m出现了多少种m以内的 ...
- Python数据处理进阶——pandas
对于python进行数据处理来说,pandas式一个不得不用的包,它比numpy很为强大.通过对<利用python进行数据分析>这本书中介绍pandas包的学习,再加以自己的理解,写下这篇 ...
- [内存管理]linux内存管理 之 内存节点和内存分区
Linux支持多种硬件体系结构,因此Linux必须采用通用的方法来描述内存,以方便对内存进行管理.为此,Linux有了内存节点.内存区.页框的概念,这些概念也是一目了然的. 内存节点:主要依据CPU访 ...