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

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

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

在Perl中,增加这样一个值非常简单:

$map{$word}++;

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

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

这当然依赖于新Java版本中的自动装箱特性。我想知道您是否可以建议一种更有效的方法来增加这个值。是否有更好的性能理由避开Collections框架而使用其他框架呢?

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

375450 次浏览

各种基元包装器,例如Integer是不可变的,因此实际上没有更简洁的方法来完成你请求除非的事情,你可以用类似AtomicLong的东西来完成。我可以在一分钟内进行更新。顺便说一句,哈希表 集合框架的一部分。

有几种方法:

  1. 使用袋子算法,比如谷歌集合中包含的集合。

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


class My{
String word;
int count;
}

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

避免使用列表来滚动您自己的解决方案,因为如果您使用内循环搜索和排序,您的性能将会非常糟糕。第一个HashMap解决方案实际上相当快,但在谷歌Collections中找到的合适的解决方案可能更好。

使用谷歌集合计数单词,看起来像这样:




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



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

作为我自己评论的后续: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的映射中的值。实际上,增加线程的友好性是这种方法所推荐的。

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

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)相比,开销不应该那么多。

与其调用containsKey(),不如直接调用map更快。获取并检查返回值是否为空。

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

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

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

contains two potentially expensive operations on a map, namely containsKey and get. The former performs an operation potentially pretty similar to the latter, so you're doing the same work twice!

If you look at the API for Map, get operations usually return null when the map does not contain the requested element.

Note that this will make a solution like

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

危险,因为它可能产生__abc0。你应该先检查null。 < p > < p > 还要注意,这是非常重要的,HashMaps 可以根据定义包含nulls。所以并不是每个返回的null都表示“没有这样的元素”。在这方面,containsKey表现为不同的get实际上告诉你是否有这样一个元素。详细信息请参考API。 < p > < p > 然而,对于你的情况,你可能不想区分存储的null和"noSuchElement"。如果你不想允许__abc0,你可能更喜欢Hashtable。使用包装器库可能是手动处理的更好解决方案,这取决于应用程序的复杂性

为了完成答案(多亏了编辑函数,我一开始忘记把它放进去了!),最好的本机方法是将get放入final变量中,检查null并将其返回1。变量应该是final,因为它是不可变的。编译器可能不需要这个提示,但这样更清楚。

final HashMap map = generateRandomHashMap();
final Object key = fetchSomeKey();
final Integer i = map.get(key);
if (i != null) {
map.put(i + 1);
} else {
// do something
}

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

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

在您的方法中,最大的代价是必须搜索两次地图。在我这里,你只需要做一次。只需要获取值(如果没有,它将被初始化)并增加它。

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

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

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

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

如果您知道增量的数量将大大超过键的数量(在本例中为=words),请考虑使用int holder。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宝库

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

对于这类事情,查看谷歌集合库总是一个好主意。在这种情况下,多重集就可以了:

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>,所以你不会产生装箱成本。

部分测试结果

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

  • 我在这个问题中给出的“ContainsKey”方法
  • Aleksandar Dimitrov建议的“TestForNull”方法
  • Hank Gay建议的“AtomicLong”方法
  • 即鲁道夫提出的“宝藏”方法
  • phax.myopenid.com建议的“MutableInt”方法

方法

我是这么做的……

  1. 创建了5个类,除了下面所示的不同之处外,它们完全相同。每个类都必须执行我所介绍的场景的典型操作:打开一个10MB的文件并将其读入,然后对文件中的所有单词标记执行频率计数。因为这平均只花了3秒,所以我让它执行了10次频率计数(而不是I/O)。
  2. 对10次迭代的循环进行计时,但是而不是I/O操作并基本上使用Ian Darwin在Java烹饪书中的方法记录了所花费的总时间(以时钟秒为单位)。
  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);
}

AtomicLong

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

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

"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
}

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

谷歌集合HashMultiset:
-使用
非常优雅 —但占用CPU和内存

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

这样的方法将只计算哈希和索引一次, 然后我们可以对元素做我们想做的 (替换或更新值) < p >更优雅:< br > -取HashSet<Entry>
-扩展它,以便get(K)在需要时放置一个新的条目
- Entry可以是你自己的对象 ——> (new MyHashSet()).get(k).increment();

MutableInt方法的一个变体可能更快,如果有点hack,是使用一个单元素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);

谷歌番石榴是你的朋友…

...至少在某些情况下是这样。它们有漂亮的< em > AtomicLongMap < / em >。特别好,因为你在你的映射中处理作为值。

如。

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

也可以在值上增加多于1的值:

map.getAndAdd(word, 112L);

如果你正在使用Eclipse集合,你可以使用HashBag。在内存使用方面,这将是最有效的方法,而且在执行速度方面也会表现良好。

HashBagMutableObjectIntMap支持,它存储的是基本整数而不是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集合的提交者。

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
< p >时间\空间结果: < img src = " https://i.stack.imgur.com/nR5yp.png " width = " 600 " > < / p >
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(),您就已经知道在哪里放置(),因此不需要再次搜索。但是在哈希映射中搜索通常只需要很短的时间你可以忽略这个性能问题。

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

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

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

我不知道它有多高效,但下面的代码也可以工作。你需要在开头定义一个BiFunction。另外,你可以用这个方法做更多的增量。

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

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

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。

另一方面,如果你遇到多个线程更新一个公共和的情况,你可以看看LongAdder类。在高争用情况下,该类的期望吞吐量显著高于AtomicLong,代价是更高的空间消耗。

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

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

它的作用:

  • 如果关键不存在,则将1作为值
  • 否则将和1转换为链接到关键的值

更多信息在这里

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

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

如果你有

map.put(key, 1)

你会这么做

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

希望这能有所帮助!

很简单,只需使用Map.java中的内置函数,如下所示

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

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

final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
map.computeIfAbsent("foo", key -> new AtomicLong(0)).incrementAndGet();
我建议使用Java 8 Map::compute()。 它也考虑键不存在的情况。

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

使用流和getOrDefault进行计数:

String s = "abcdeff";
s.chars().mapToObj(c -> (char) c)
.forEach(c -> {
int count = countMap.getOrDefault(c, 0) + 1;
countMap.put(c, count);
});