在 Java 中递增 Map 值的最有效方法

2022-08-31 04:27:31

我希望这个问题对于这个论坛来说不是太基本,但我们会看到。我想知道如何重构一些代码以获得更好的性能,这些代码正在运行很多次。

假设我正在使用Map(可能是HashMap)创建一个词频列表,其中每个键都是一个字符串,其中包含正在计数的单词,该值是一个整数,每次找到单词的标记时都会递增。

在Perl中,递增这样的值将非常容易:

$map{$word}++;

但在Java中,它要复杂得多。以下是我目前的做法:

int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);

这当然依赖于较新Java版本中的自动装箱功能。我想知道您是否可以建议一种更有效的方法来递增这样的值。是否有良好的性能原因来避开集合框架而使用其他东西?

更新:我已经对几个答案进行了测试。见下文。


答案 1

现在,Java 8 使用 Map::merge 有一种更短的方法。

myMap.merge(key, 1, Integer::sum)

它的作用:

  • 如果不存在,则将 1 作为值
  • 否则,将 1 与链接到的值相加

更多信息请点击这里


答案 2

一些测试结果

这个问题我已经得到了很多很好的答案 - 谢谢大家 - 所以我决定运行一些测试并找出哪种方法实际上是最快的。我测试的五种方法是:

  • 我在问题中提出的“包含键”方法
  • 亚历山大·季米特洛夫提出的“TestForNull”方法
  • Hank Gay提出的“AtomicLong”方法
  • Jrudolph建议的“Trove”方法
  • phax.myopenid.com 建议的“MutableInt”方法

方法

这是我所做的...

  1. 创建了五个类,除了下面显示的差异之外,它们完全相同。每个类都必须执行我所介绍的场景的典型操作:打开一个 10MB 的文件并读取它,然后对文件中的所有单词标记执行频率计数。由于这平均只花了3秒,我让它执行频率计数(而不是I / O)10次。
  2. 对 10 次迭代的循环进行了计时,但没有对 I/O 操作进行计时,并记录了所花费的总时间(以时钟秒为单位),基本上使用 Java Cookbook 中的 Ian Darwin 方法
  3. 连续执行了所有五项测试,然后又进行了三次。
  4. 平均了每种方法的四个结果。

结果

我将首先向感兴趣的人介绍结果和下面的代码。

正如预期的那样,ContainsKey方法是最慢的,因此我将给出每个方法的速度与该方法的速度进行比较。

  • 包含密钥:30.654 秒(基线)
  • 原子龙:29.780秒(1.03倍)
  • 测试时间:28.804 秒(速度是原来的 1.06 倍)
  • 宝座:26.313秒(速度是1.16倍)
  • 可变Int:25.747 秒(速度是 1.19 倍)

结论

似乎只有 MutableInt 方法和 Trove 方法明显更快,因为只有它们的性能提升了 10% 以上。但是,如果线程是一个问题,AtomicLong可能比其他的更具吸引力(我不太确定)。我也用变量运行了TestForNull,但差异可以忽略不计。final

请注意,我没有分析不同方案中的内存使用情况。我很高兴听到任何对MutableInt和Trove方法如何影响内存使用有深刻见解的人。

就个人而言,我发现MutableInt方法最有吸引力,因为它不需要加载任何第三方类。因此,除非我发现它的问题,否则这就是我最有可能走的路。

代码

以下是每种方法的关键代码。

包含密钥

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
int count = freq.containsKey(word) ? freq.get(word) : 0;
freq.put(word, count + 1);

TestForNull

import java.util.HashMap;
import java.util.Map;
...
Map<String, Integer> freq = new HashMap<String, Integer>();
...
Integer count = freq.get(word);
if (count == null) {
    freq.put(word, 1);
}
else {
    freq.put(word, count + 1);
}

原子龙

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicLong;
...
final ConcurrentMap<String, AtomicLong> map = 
    new ConcurrentHashMap<String, AtomicLong>();
...
map.putIfAbsent(word, new AtomicLong(0));
map.get(word).incrementAndGet();

宝库

import gnu.trove.TObjectIntHashMap;
...
TObjectIntHashMap<String> freq = new TObjectIntHashMap<String>();
...
freq.adjustOrPutValue(word, 1, 1);

可变Int

import java.util.HashMap;
import java.util.Map;
...
class MutableInt {
  int value = 1; // note that we start at 1 since we're counting
  public void increment () { ++value;      }
  public int  get ()       { return value; }
}
...
Map<String, MutableInt> freq = new HashMap<String, MutableInt>();
...
MutableInt count = freq.get(word);
if (count == null) {
    freq.put(word, new MutableInt());
}
else {
    count.increment();
}

推荐