460

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

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

在 Perl 中,增加这样一个值非常容易:

$map{$word}++;

但是在Java中,它要复杂得多。这是我目前正在这样做的方式:

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

这当然依赖于较新 Java 版本中的自动装箱功能。我想知道您是否可以提出一种更有效的方法来增加这样的值。避免使用 Collections 框架并改用其他东西是否有很好的性能原因?

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

4

28 回答 28

415

部分测试结果

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

  • 我在问题中提出的“ContainsKey”方法
  • Aleksandar Dimitrov 建议的“TestForNull”方法
  • Hank Gay 提出的“AtomicLong”方法
  • jrudolph 建议的“Trove”方法
  • phax.myopenid.com 建议的“MutableInt”方法

方法

这就是我所做的......

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

结果

我将首先为感兴趣的人展示结果和下面的代码。

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

  • ContainsKey: 30.654 秒(基线)
  • AtomicLong: 29.780 秒(快 1.03 倍)
  • TestForNull: 28.804 秒(快 1.06 倍)
  • Trove: 26.313 秒(快 1.16 倍)
  • MutableInt: 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);

空测试

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

可变整数

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();
}
于 2008-09-20T11:59:00.267 回答
407

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

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

它能做什么:

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

更多信息在这里

于 2017-03-07T12:49:26.337 回答
48

2016年的一点研究:https ://github.com/leventov/java-word-count ,基准源码

每种方法的最佳结果(越小越好):

                 time, ms
kolobokeCompile  18.8
koloboke         19.8
trove            20.8
fastutil         22.7
mutableInt       24.3
atomicInteger    25.3
eclipse          26.9
hashMap          28.0
hppc             33.6
hppcRt           36.5

时间\空间结果:

于 2014-08-17T23:13:53.473 回答
40
Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0); // ensure count will be one of 0,1,2,3,...
map.put(key, count + 1);

这就是您使用简单代码增加值的方式。

益处:

  • 无需添加新类或使用可变 int 的另一个概念
  • 不依赖任何库
  • 易于理解到底发生了什么(没有太多抽象)

缺点:

  • 哈希映射将被搜索两次以获得 get() 和 put()。所以它不会是性能最高的代码。

理论上,一旦你调用 get(),你就已经知道 put() 应该放在哪里,所以你不必再次搜索。但是在哈希映射中搜索通常需要很短的时间,您可以忽略这个性能问题。

但是如果你对这个问题非常认真,你是一个完美主义者,另一种方法是使用合并方法,这(可能)比前面的代码片段更有效,因为你将(理论上)只搜索一次地图:(虽然这段代码乍一看并不明显,它简短而高效)

map.merge(key, 1, (a,b) -> a+b);

建议:在大多数情况下,您应该关心代码的可读性而不是很少的性能提升。如果您更容易理解第一个代码片段,请使用它。但是,如果您能够理解第二个罚款,那么您也可以去做!

于 2015-11-14T17:44:55.150 回答
38

作为我自己评论的后续行动:Trove 看起来像是要走的路。如果出于某种原因,您想坚持使用标准 JDK,ConcurrentMapAtomicLong可以使代码更好一点,尽管 YMMV

    final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.putIfAbsent("foo", new AtomicLong(0));
    map.get("foo").incrementAndGet();

1作为地图中的值保留为foo。实际上,增加对线程的友好性是这种方法所推荐的全部。

于 2008-09-17T09:40:23.190 回答
37

谷歌番石榴是你的朋友...

...至少在某些情况下。他们有这个不错的AtomicLongMap。特别好,因为您正在处理地图中的long as 值。

例如

AtomicLongMap<String> map = AtomicLongMap.create();
[...]
map.getAndIncrement(word);

也可以将超过 1 的值添加到:

map.getAndAdd(word, 112L); 
于 2012-09-04T15:08:39.930 回答
28

看看Google 收藏库中的这类东西总是一个好主意。在这种情况下,Multiset可以解决问题:

Multiset bag = Multisets.newHashMultiset();
String word = "foo";
bag.add(word);
bag.add(word);
System.out.println(bag.count(word)); // Prints 2

有类似 Map 的方法用于迭代键/条目等。内部实现当前使用 a HashMap<E, AtomicInteger>,因此您不会产生装箱成本。

于 2008-09-17T16:58:25.180 回答
23

您应该意识到您最初的尝试

int count = map.containsKey(word) ?地图.get(字):0;

在地图上包含两个可能代价高昂的操作,即containsKeyget。前者执行的操作可能与后者非常相似,因此您要做两次相同的工作!

如果您查看 Map 的 API,get通常会null在地图不包含请求的元素时返回操作。

请注意,这将使解决方案像

map.put(key, map.get(key) + 1);

危险,因为它可能会产生NullPointerExceptions。你应该先检查一下null

另请注意,这非常重要,HashMaps可以根据定义包含nulls。因此,并非每个返回的人null都说“没有这样的元素”。在这方面,containsKey行为get实际告诉你是否存在这样的元素不同。有关详细信息,请参阅 API。

但是,对于您的情况,您可能不想区分存储的null和“noSuchElement”。如果您不想允许nulls,您可能更喜欢Hashtable. 使用其他答案中已经提出的包装库可能是手动处理的更好解决方案,具体取决于应用程序的复杂性。

要完成答案(我一开始忘了把它放进去,多亏了编辑功能!),最好的原生方法是get进入一个final变量,检查nullput1. 该变量应该是final因为它无论如何都是不可变的。编译器可能不需要这个提示,但这样会更清楚。

final HashMap map = generateRandomHashMap();
最终对象键 = fetchSomeKey();
最终整数 i = map.get(key);
如果(我!= null){
    map.put(i + 1);
} 别的 {
    // 做点什么
}

如果你不想依赖自动装箱,你应该说类似的话map.put(new Integer(1 + i.getValue()));

于 2008-09-17T10:20:32.283 回答
20

另一种方法是创建一个可变整数:

class MutableInt {
  int value = 0;
  public void inc () { ++value; }
  public int get () { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt> ();
MutableInt value = map.get (key);
if (value == null) {
  value = new MutableInt ();
  map.put (key, value);
} else {
  value.inc ();
}

当然,这意味着创建一个额外的对象,但与创建一个整数(即使使用 Integer.valueOf)相比的开销不应该那么多。

于 2008-09-17T09:47:03.020 回答
12

您可以在Java 8提供的接口中使用computeIfAbsent方法。Map

final Map<String,AtomicLong> map = new ConcurrentHashMap<>();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("B", k->new AtomicLong(0)).incrementAndGet();
map.computeIfAbsent("A", k->new AtomicLong(0)).incrementAndGet(); //[A=2, B=1]

该方法computeIfAbsent检查指定的键是否已与值关联?如果没有关联值,则它尝试使用给定的映射函数计算其值。在任何情况下,它都会返回与指定键关联的当前(现有或计算的)值,如果计算的值为 null,则返回 null。

附带说明一下,如果您遇到多个线程更新一个公共总和的情况,您可以查看LongAdder类。在高争用情况下,该类的预期吞吐量明显高于AtomicLong,但代价是更高的空间消耗。

于 2016-05-25T14:21:13.393 回答
9

很简单,使用内置函数Map.java如下

map.put(key, map.getOrDefault(key, 0) + 1);
于 2019-03-25T15:33:42.373 回答
8

内存轮换在这里可能是一个问题,因为大于或等于 128 的 int 的每个装箱都会导致对象分配(请参阅 Integer.valueOf(int))。尽管垃圾收集器非常有效地处理短期对象,但性能会受到一定程度的影响。

如果您知道增量的数量将大大超过键的数量(在这种情况下为=words),请考虑使用 int 持有者。Phax 已经为此提供了代码。又是这样,有两个变化(持有者类设为静态,初始值设置为 1):

static class MutableInt {
  int value = 1;
  void inc() { ++value; }
  int get() { return value; }
}
...
Map<String,MutableInt> map = new HashMap<String,MutableInt>();
MutableInt value = map.get(key);
if (value == null) {
  value = new MutableInt();
  map.put(key, value);
} else {
  value.inc();
}

如果您需要极高的性能,请寻找直接针对原始值类型量身定制的 Map 实现。jrudolph 提到了GNU Trove

顺便说一下,这个主题的一个很好的搜索词是“直方图”。

于 2008-09-17T16:25:48.067 回答
5

与其调用 containsKey(),不如调用 map.get 并检查返回值是否为空,这样更快。

    Integer count = map.get(word);
    if(count == null){
        count = 0;
    }
    map.put(word, count + 1);
于 2008-09-17T10:14:32.360 回答
4

我建议使用 Java 8 Map::compute()。它也考虑密钥不存在的情况。

Map.compute(num, (k, v) -> (v == null) ? 1 : v + 1);
于 2019-09-08T01:34:18.977 回答
3

有几种方法:

  1. 使用类似于 Google Collections 中包含的集合的 Bag 算法。

  2. 创建可以在 Map 中使用的可变容器:


    class My{
        String word;
        int count;
    }

并使用 put("word", new My("Word") ); 然后您可以检查它是否存在并在添加时增加。

避免使用列表滚动您自己的解决方案,因为如果您进行内循环搜索和排序,您的性能会很糟糕。第一个 HashMap 解决方案实际上非常快,但在 Google Collections 中找到的类似解决方案可能更好。

使用 Google Collections 计算单词,看起来像这样:



    HashMultiset s = new HashMultiset();
    s.add("word");
    s.add("word");
    System.out.println(""+s.count("word") );


使用 HashMultiset 非常优雅,因为袋算法正是您在计算单词时所需要的。

于 2008-09-17T09:19:50.447 回答
3

你确定这是一个瓶颈吗?你做过性能分析吗?

尝试使用 NetBeans 分析器(它是免费的,内置于​​ NB 6.1)来查看热点。

最后,JVM 升级(比如从 1.5->1.6)通常是一种廉价的性能提升器。即使是内部版本号的升级也可以提供良好的性能提升。如果您在 Windows 上运行并且这是一个服务器类应用程序,请在命令行上使用 -server 来使用 Server Hotspot JVM。在 Linux 和 Solaris 机器上,这是自动检测的。

于 2008-09-17T12:12:33.870 回答
3

Google Collections HashMultiset :
- 使用起来非常优雅
- 但会消耗 CPU 和内存

最好的方法是:(Entry<K,V> getOrPut(K); 优雅且低成本)

这样的方法将只计算一次哈希和索引,然后我们可以对条目做我们想做的事情(替换或更新值)。

更优雅:
- 采取HashSet<Entry>
- 扩展它,以便get(K)在需要时放置一个新条目
- 条目可以是您自己的对象。
-->(new MyHashSet()).get(k).increment();

于 2010-11-26T09:20:32.870 回答
3

MutableInt 方法的一种变体可能更快,如果有点破解的话,是使用单元素 int 数组:

Map<String,int[]> map = new HashMap<String,int[]>();
...
int[] value = map.get(key);
if (value == null) 
  map.put(key, new int[]{1} );
else
  ++value[0];

如果您可以使用此变体重新运行性能测试,那将会很有趣。这可能是最快的。


编辑:上述模式对我来说效果很好,但最终我改用 Trove 的集合来减少我正在创建的一些非常大的地图中的内存大小——而且它也更快。

一个非常好的特性是TObjectIntHashMap该类有一个adjustOrPutValue调用,取决于该键是否已经存在一个值,将放置一个初始值或增加现有值。这非常适合递增:

TObjectIntHashMap<String> map = new TObjectIntHashMap<String>();
...
map.adjustOrPutValue(key, 1, 1);
于 2012-07-02T03:29:28.813 回答
2

“put”需要“get”(确保没有重复键)。
所以直接做一个“put”,
如果有之前的值,再做一个加法:

Map map = new HashMap ();

MutableInt newValue = new MutableInt (1); // default = inc
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.add(oldValue); // old + inc
}

如果计数从 0 开始,则加 1:(或任何其他值...)

Map map = new HashMap ();

MutableInt newValue = new MutableInt (0); // default
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.setValue(oldValue + 1); // old + inc
}

注意:此代码不是线程安全的。使用它来构建然后使用地图,而不是同时更新它。

优化:在一个循环中,保持旧值成为下一个循环的新值。

Map map = new HashMap ();
final int defaut = 0;
final int inc = 1;

MutableInt oldValue = new MutableInt (default);
while(true) {
  MutableInt newValue = oldValue;

  oldValue = map.put (key, newValue); // insert or...
  if (oldValue != null) {
    newValue.setValue(oldValue + inc); // ...update

    oldValue.setValue(default); // reuse
  } else
    oldValue = new MutableInt (default); // renew
  }
}
于 2010-11-23T15:57:46.170 回答
1

例如,各种原始包装器Integer是不可变的,因此实际上没有更简洁的方法来完成您所要求的操作,除非您可以使用AtomicLong之类的东西来完成。我可以在一分钟内试一试并更新。顺便说一句,Hashtable Collections Framework的一部分。

于 2008-09-17T09:17:37.363 回答
1

我将使用 Apache Collections Lazy Map(将值初始化为 0)并使用来自 Apache Lang 的 MutableIntegers 作为该映射中的值。

最大的成本是必须在您的方法中搜索两次地图。在我的情况下,您只需执行一次。只需获取值(如果不存在,它将被初始化)并增加它。

于 2008-09-17T10:21:19.690 回答
1

函数式 Java库的数据结构在最新的主干头中TreeMap有一个方法:update

public TreeMap<K, V> update(final K k, final F<V, V> f)

示例用法:

import static fj.data.TreeMap.empty;
import static fj.function.Integers.add;
import static fj.pre.Ord.stringOrd;
import fj.data.TreeMap;

public class TreeMap_Update
  {public static void main(String[] a)
    {TreeMap<String, Integer> map = empty(stringOrd);
     map = map.set("foo", 1);
     map = map.update("foo", add.f(1));
     System.out.println(map.get("foo").some());}}

该程序打印“2”。

于 2009-05-12T22:18:35.850 回答
1

如果您使用的是Eclipse Collections,您可以使用HashBag. 就内存使用而言,这将是最有效的方法,并且在执行速度方面也将表现良好。

HashBagMutableObjectIntMap存储原始整数而不是Counter对象的 a 支持。这减少了内存开销并提高了执行速度。

HashBag提供您需要的 API,因为它还Collection允许您查询项目的出现次数。

这是Eclipse Collections Kata中的一个示例。

MutableBag<String> bag =
  HashBag.newBagWith("one", "two", "two", "three", "three", "three");

Assert.assertEquals(3, bag.occurrencesOf("three"));

bag.add("one");
Assert.assertEquals(2, bag.occurrencesOf("one"));

bag.addOccurrences("one", 4);
Assert.assertEquals(6, bag.occurrencesOf("one"));

注意:我是 Eclipse Collections 的提交者。

于 2013-09-13T18:03:56.763 回答
1

我不知道它的效率如何,但下面的代码也可以。您需要BiFunction在开始时定义 a 。另外,您可以使用此方法进行更多操作。

public static Map<String, Integer> strInt = new HashMap<String, Integer>();

public static void main(String[] args) {
    BiFunction<Integer, Integer, Integer> bi = (x,y) -> {
        if(x == null)
            return y;
        return x+y;
    };
    strInt.put("abc", 0);


    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abc", 1, bi);
    strInt.merge("abcd", 1, bi);

    System.out.println(strInt.get("abc"));
    System.out.println(strInt.get("abcd"));
}

输出是

3
1
于 2016-05-18T10:00:23.000 回答
1

使用流和 计数getOrDefault

String s = "abcdeff";
s.chars().mapToObj(c -> (char) c)
 .forEach(c -> {
     int count = countMap.getOrDefault(c, 0) + 1;
     countMap.put(c, count);
  });
于 2021-06-28T07:48:45.517 回答
-3

由于很多人在 Java 主题中搜索 Groovy 的答案,因此您可以在 Groovy 中执行以下操作:

dev map = new HashMap<String, Integer>()
map.put("key1", 3)

map.merge("key1", 1) {a, b -> a + b}
map.merge("key2", 1) {a, b -> a + b}
于 2018-02-10T00:16:27.983 回答
-3

希望我能正确理解你的问题,我是从 Python 来到 Java 的,所以我可以理解你的挣扎。

如果你有

map.put(key, 1)

你会做的

map.put(key, map.get(key) + 1)

希望这可以帮助!

于 2019-02-03T20:25:41.017 回答
-3

java 8中简单易行的方法如下:

final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.computeIfAbsent("foo", key -> new AtomicLong(0)).incrementAndGet();
于 2019-06-22T04:36:14.473 回答