用Java增加Map值的最有效方法


377

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

假设我正在使用地图(可能是HashMap)创建一个单词频率列表,其中每个键是一个带有要计算单词的String,值是一个Integer,每次找到该单词的标记时都会增加。

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

$map{$word}++;

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

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

当然,哪个依赖于较新的Java版本中的自动装箱功能。我想知道您是否可以建议一种更有效的递增此值的方法。避开Collections框架并改用其他东西,甚至有良好的性能原因吗?

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


我认为java.util.Hashtable将会是相同的。
jrudolph

2
当然可以,因为Hashtable实际上是Map。
whiskeysierra 2010年

Java 8:computeIfAbsent示例:stackoverflow.com/a/37439971/1216775
akhil_mittal

Answers:


366

一些测试结果

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

  • 我在问题中介绍的“ ContainsKey”方法
  • Aleksandar Dimitrov建议的“ TestForNull”方法
  • Hank Gay建议的“ AtomicLong”方法
  • jrudolph建议的“推动”方法
  • phax.myopenid.com建议的“ MutableInt”方法

方法

这是我做的...

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

结果

我将首先提供结果,并为感兴趣的人提供以下代码。

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

  • ContainsKey: 30.654秒(基准)
  • AtomicLong: 29.780秒(速度的1.03倍)
  • TestForNull: 28.804秒(速度的1.06倍)
  • 宝座 26.313秒(1.16倍的速度)
  • MutableInt: 25.747秒(1.19倍的速度)

结论

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

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

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

编码

这是每种方法的关键代码。

ContainsKey

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

MutableInt

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

3
伟大的工作,做得好。一个较小的注释-AtomicLong代码中的putIfAbsent()调用将实例化一个新的AtomicLong(0),即使地图中已经存在一个。如果将其调整为使用if(map.get(key)== null),则可能会改善这些测试结果。
利·考德威尔

2
最近,我使用类似于MutableInt的方法进行了相同的操作。我很高兴听到它是最佳解决方案(我只是假设它是未经任何测试的)。
Kip

很高兴得知您的速度比我快,Kip。;-)如果您发现该方法有任何缺点,请告诉我。
gregory

4
在Atomic Long的情况下,一步完成它不是更有效(因此,您只有1个昂贵的get操作,而不是2个)“” map.putIfAbsent(word,new AtomicLong(0))。incrementAndGet();
smartnut007

1
@gregory您是否考虑过Java 8 freq.compute(word, (key, count) -> count == null ? 1 : count + 1)?在内部,它执行的哈希查询要比少containsKey,因为存在lambda,因此将其与其他算法进行比较会很有趣。
TWiStErRob'8

255

现在,使用Java 8的方法更短Map::merge

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

它能做什么:

  • 如果不存在,则将1作为值
  • 否则将1加到与关联的值上

更多信息在这里


一直喜欢Java 8.这是原子的吗?还是应该用同步器包围它?
Tiina

4
这似乎对我不起作用,但对我来说 map.merge(key, 1, (a, b) -> a + b); 确实
有用

2
@Tiina Atomicity特性是特定于实现的,请参见。docs:“默认实现不保证此方法的同步性或原子性。任何提供原子性保证的实现都必须重写此方法并记录其并发属性。特别是,子接口ConcurrentMap的所有实现都必须记录该功能是否一次应用仅当该值不存在时才自动进行。”
jensgram '18

2
对于groovy来说,它不被接受Integer::sum为BiFunction,也不喜欢@russter回答它的编写方式。这对我Map.merge(key, 1, { a, b -> a + b})
有用

2
@russter,我知道您的评论是一年多以前的,但是您是否还记得为什么它不适合您?您收到编译错误还是该值未增加?
保罗

44

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

时间\空间结果:


2
谢谢,这真的很有帮助。将Guava的Multiset(例如HashMultiset)添加到基准中会很酷。
Cabad 2015年

34

Google Guava是您的朋友...

...至少在某些情况下。他们有这个漂亮的AtomicLongMap。特别好,因为您正在处理地图中的长期价值。

例如

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

也可以将值加1以上:

map.getAndAdd(word, 112L); 

7
AtomicLongMap#getAndAdd采用原始类型long而不是包装类;这样做毫无意义new Long()。并且AtomicLongMap是参数化类型;您应该将其声明为AtomicLongMap<String>
Helder Pereira

32

@汉克·盖伊

作为对我自己的评论(而不是无用的评论)的跟进:Trove看起来很可行。如果出于某种原因,你想坚持使用标准的JDK,ConcurrentMapAtomicLong的可以使代码一个微小的一点更好,但情况因人而异。

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

1作为地图中的值保留foo。实际上,这种方法必须向人们推荐增加对线程的友好性。


9
putIfAbsent()返回该值。将返回的值存储在本地变量中并使用它来递增AndGet()而不是再次调用get可能是一个很大的改进。
smartnut007

如果指定的键尚未与Map中的值关联,则putIfAbsent可以返回null值,因此我将谨慎使用返回的值。docs.oracle.com/javase/8/docs/api/java/util/…–
bumbur

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

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


getOfDefault方法在Java 7中不可用。如何在Java 7中实现此目的?
坦维'16

1
然后,您可能不得不依靠其他答案。这仅适用于Java的8
off99555

1
+1为合并解决方案,这将是性能最高的功能,因为您只需要为哈希码计算支付1次时间(在您正使用它的Map正确支持该方法的情况下),而不是可能需要为此支付3次
Ferrybig '17

2
使用方法推断:map.merge(key,1,Integer :: sum)
earandap

25

查看Google收藏库中的此类内容始终是一个好主意。在这种情况下,Multiset可以解决问题:

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

有类似Map的方法可用于遍历键/条目等。目前,内部实现使用HashMap<E, AtomicInteger>,因此您不会产生装箱费用。


上面的回答需要反映tovares的反应。因为它发布的API已经改变(3年前:))
史蒂夫

count()多重集上的方法是否在O(1)或O(n)时间(最坏情况)下运行?关于这一点,文档尚不清楚。
亚当·帕金

我对这种事情的算法是:if(hasApacheLib(thing))return apacheLib; 否则,如果(hasOnGuava(thing))返回番石榴。通常我不走这两个步骤。:)
digao_mb 2015年

22

您应该意识到以下事实:您最初的尝试

int count = map.containsKey(word)吗?map.get(word):0;

包含两个可能在地图上昂贵的操作,分别是containsKeyget。前者执行的操作可能与后者非常相似,因此您要完成两次相同的工作!

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

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

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

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

另外请注意,这是非常重要的,那HashMap小号可以包含nulls定义。因此,并非每个返回的用户null都说“没有这样的元素”。在这方面,containsKey表现不同get在实际告诉你是否有这样的元素。有关详细信息,请参考API。

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

要完成答案(由于编辑功能,我首先忘记输入答案了!),本机执行此操作的最佳方法是将其get放入final变量中,然后使用进行检查nullput返回1。该变量应该是final因为它仍然是不可变的。编译器可能不需要此提示,但是用这种方式更清晰。

最终的HashMap地图= generateRandomHashMap();
最终对象键= fetchSomeKey();
最终整数i = map.get(key);
如果(i!= null){
    map.put(i + 1);
}其他{
    // 做一点事
}

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


为了避免Groovy中的初始未映射/空值的问题,我最终这样做:counts.put(key,(counts.get(key)?:0)+ 1)// ++的版本过于复杂
Joe Atzberger

2
或者,最简单的方法是:counts = [:]。withDefault {0} // ++离开
Joe Atzberger 2013年

18

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

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(即使使用Integer.valueOf)相比,开销也不应该太多。


5
您不想在第一次将MutableInt放置在地图上时将其关闭吗?
汤姆·霍顿

5
Apache的commons-lang已经为您编写了MutableInt。
SingleShot

11

您可以在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占用更多空间。


为什么并发Hashmap和AtomicLong?
ealeon '19

7

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

如果您知道增加的次数将大大超过键的数目(在这种情况下,=字),请考虑使用int持有人。Phax已经为此提供了代码。再次出现以下两个更改(将holder类设为静态,并将初始值设置为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

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


5

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

    Integer count = map.get(word);
    if(count == null){
        count = 0;
    }
    map.put(word, count + 1);

3

您确定这是瓶颈吗?您是否进行过任何性能分析?

尝试使用NetBeans探查器(它是免费的且内置于NB 6.1中)来查看热点。

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


3

有两种方法:

  1. 像Google Collections中包含的集合那样使用Bag的算法。

  2. 创建可在地图中使用的可变容器:


    class My{
        String word;
        int count;
    }

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

避免使用列表来滚动自己的解决方案,因为如果您进行innerloop搜索和排序,您的性能将会发臭。第一个HashMap解决方案实际上是非常快的,但是像Google收藏夹中找到的那样可能更好。

使用Google收藏夹计数单词的过程看起来像这样:



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


使用HashMultiset非常精巧,因为在计算单词时便需要袋算法。


3

我认为您的解决方案将是标准方法,但是-正如您自己指出的那样-这可能不是最快的方法。

您可以看一下GNU Trove。那是一个包含各种快速原始集合的库。你的榜样将使用TObjectIntHashMap其中有一个方法adjustOrPutValue这不正是你想要的东西。


到TObjectIntHashMap的链接已断开。这是正确的链接:trove4j.sourceforge.net/javadocs/gnu/trove/map/…– Erel
Segal-Halevi

谢谢,埃雷尔,我修复了链接。
jrudolph 2011年

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

3

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

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

这种方法只计算一次哈希和索引,然后我们可以对条目进行所需的操作(替换或更新值)。

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


3

很简单,只需使用内置的功能Map.java为跟随

map.put(key, map.getOrDefault(key, 0) + 1);

这不会增加值,它只是设置当前值;如果没有为键分配任何值,则设置为0。
siegi,

您可以将值增加++... OMG,它是如此简单。@siegi
sudoz

记录:++在该表达式中的任何地方都不起作用,因为需要一个变量作为其操作数,但只有值。您添加的+ 1作品虽然。现在您的解决方案与off99555s answer中的相同。
siegi '19

2

“放置”需要“获取”(以确保没有重复的密钥)。
因此,直接进行“放置”,
如果有先前的值,则进行加法:

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
}

如果count从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
  }
}


1

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

最大的成本是必须使用方法两次搜索地图。在我这里,您只需要执行一次。只需获取值(如果不存在,它将被初始化)并将其递增。


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”。


1

@Vilmantas Baranauskas:关于这个答案,如果我有代表点,我会发表评论,但我没有。我想指出,那里定义的Counter类不是线程安全的,因为仅同步inc()而不同步value()是不够的。除非已通过更新建立事前发生的关系,否则其他调用value()的线程不能保证看到该值。


如果要引用某人的答案,请在顶部使用@ [用户名],例如@Vilmantas Baranauskas <内容在此处>
Hank Gay

我进行了修改以清理它。
Alex Miller

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

1

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

HashBag由a支持,该a MutableObjectIntMap存储原始int而不是Counter对象。这样可以减少内存开销并提高执行速度。

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的提交者。


1

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

Map.compute(num, (k, v) -> (v == null) ? 1 : v + 1);

mymap.merge(key, 1, Integer::sum)
挪威

-2

由于许多人都在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}

-2

Java 8中的简单方法如下:

final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
    map.computeIfAbsent("foo", key -> new AtomicLong(0)).incrementAndGet();

-3

希望我能正确理解您的问题,我是从Python进入Java的,以便您能为您提供帮助。

如果你有

map.put(key, 1)

你会做

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

希望这可以帮助!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.