我希望这个问题对于这个论坛来说不算太基础,但我们会看到。我想知道如何重构一些代码以获得更好的性能,这些代码已经运行了很多次。
假设我正在使用Map(可能是HashMap)创建一个单词频率列表,其中每个键都是一个字符串,其中包含要计数的单词,并且值是一个整数,每当该单词的标记为找到。
在Perl中,递增这样的值很简单:
$map{$word}++;
但在Java中,它要复杂得多。这就是我目前正在做的方式:
int count = map.containsKey(word) ? map.get(word) : 0;
map.put(word, count + 1);
当然,这取决于较新Java版本中的自动装箱功能。我想知道你是否可以提出一种更有效的方法来增加这样的价值。是否有良好的性能原因可以避开Collections框架并使用其他东西?
更新:我已经对几个答案进行了测试。见下文。
答案 0 :(得分:342)
我已经为这个问题得到了很多好的答案 - 谢谢大家 - 所以我决定运行一些测试并找出哪种方法实际上最快。我测试的五种方法是:
这就是我做的......
我将首先展示结果,并为感兴趣的人提供以下代码。
正如预期的那样, ContainsKey 方法是最慢的,因此我会将每种方法的速度与该方法的速度进行比较。
似乎只有MutableInt方法和Trove方法明显更快,因为只有它们的性能提升超过10%。但是,如果线程是一个问题,AtomicLong可能比其他人更有吸引力(我不太确定)。我还使用final
变量运行TestForNull,但差别可以忽略不计。
请注意,我没有在不同的场景中分析内存使用情况。我很高兴听到任何人对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();
}
答案 1 :(得分:174)
好的,可能是一个老问题,但Java 8有一个较短的方法:
Map.merge(key, 1, Integer::sum)
它的作用:如果 key 不存在,则将 1 作为值,否则将 sum 1 添加到链接到的值键的。 更多信息here
答案 2 :(得分:42)
2016年的一项小型研究:https://github.com/leventov/java-word-count,benchmark source code
每种方法的最佳结果(越小越好):
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
时间\空间结果:
答案 3 :(得分:32)
......至少在某些情况下。他们有这个不错的AtomicLongMap。特别好,因为您在地图中处理 long 作为值。
E.g。
AtomicLongMap<String> map = AtomicLongMap.create();
[...]
map.getAndIncrement(word);
还可以在值中添加多于1:
map.getAndAdd(word, 112L);
答案 4 :(得分:31)
@Hank Gay
作为我自己(相当无用的)评论的后续行动:Trove看起来像是要走的路。无论出于何种原因,如果您希望坚持使用标准JDK,ConcurrentMap和AtomicLong可以使代码 tiny 位更好,但是YMMV。
final ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();
map.putIfAbsent("foo", new AtomicLong(0));
map.get("foo").incrementAndGet();
将1
作为foo
地图中的值。实际上,增加对线程的友好性就是这种方法必须推荐它。
答案 5 :(得分:25)
查看Google Collections Library这类事情总是一个好主意。在这种情况下,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>
,因此您不会招致拳击费用。
答案 6 :(得分:21)
你应该知道你原来的尝试
int count = map.containsKey(word) ? map.get(word) : 0;
在地图上包含两个可能很昂贵的操作,即containsKey
和get
。前者执行的操作可能与后者非常相似,所以你做的工作两次!
如果您查看地图API,get
操作通常会在地图不包含所请求的元素时返回null
。
请注意,这将构成类似
的解决方案map.put( key, map.get(key) + 1 );
危险,因为它可能会产生NullPointerException
s。您应首先检查null
。
另请注意,这非常重要,HashMap
s 可以包含nulls
的定义。因此,并非所有返回的null
都说“没有这样的元素”。在这方面,containsKey
在get
实际告诉您是否存在此类元素时,与null
的行为不同。有关详细信息,请参阅API。
但是,对于您的情况,您可能不想区分存储的null
和“noSuchElement”。如果您不想允许Hashtable
,则可能更喜欢get
。使用其他答案中已经提出的包装库可能是手动处理的更好解决方案,具体取决于应用程序的复杂程度。
要完成答案(我忘了先把它放进去,多亏了编辑功能!),本地做的最好方法是将final
变成null
变量,使用put
检查1
和final
。变量应该是map.put(new Integer(1 + i.getValue()));
,因为它无论如何都是不可变的。编译器可能不需要这个提示,但它更清晰。
final HashMap map = generateRandomHashMap(); final Object key = fetchSomeKey(); final Integer i = map.get(key); if (i != null) { map.put(i + 1); } else { // do something }
如果你不想依赖自动装箱,你应该说{{1}}之类的东西。
答案 7 :(得分:20)
Map<String, Integer> map = new HashMap<>();
String key = "a random key";
int count = map.getOrDefault(key, 0);
map.put(key, count + 1);
这就是你用简单代码增加值的方法。
好处:
另一种方法是使用合并方法,但这对于增加值来说太多了。
map.merge(key, 1, (a,b) -> a+b);
建议:在大多数情况下,您应该关注代码可读性而不是性能提升。
答案 8 :(得分: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)相比,开销不应该那么多。
答案 9 :(得分:9)
您可以在 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,则返回null。
另一方面,如果您遇到多个线程更新公共总和的情况,您可以查看LongAdder类。在高争用情况下,此类的预期吞吐量明显高于AtomicLong
,以牺牲更高的空间消耗为代价。
答案 10 :(得分:7)
内存轮换可能是一个问题,因为每次装入大于或等于128的int会导致对象分配(请参阅Integer.valueOf(int))。虽然垃圾收集器可以非常有效地处理短期对象,但性能会受到一定程度的影响。
如果您知道所做的增量数量将大大超过密钥数量(在这种情况下为单词),请考虑使用int holder。 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。
顺便说一下,这个主题的一个好的搜索词是“直方图”。
答案 11 :(得分:5)
不是调用containsKey(),而是调用map.get并检查返回的值是否为null更快。
Integer count = map.get(word);
if(count == null){
count = 0;
}
map.put(word, count + 1);
答案 12 :(得分:3)
你确定这是一个瓶颈吗?你做过任何性能分析吗?
尝试使用NetBeans探查器(它是免费的并内置于NB 6.1中)来查看热点。
最后,JVM升级(例如1.5-> 1.6)通常是一种廉价的性能助推器。即使是内部版本号的升级也可以提供良好的性能提升。如果您在Windows上运行并且这是服务器类应用程序,请在命令行上使用-server来使用Server Hotspot JVM。在Linux和Solaris计算机上,这是自动检测的。
答案 13 :(得分:3)
Google Collections HashMultiset:
- 使用相当优雅
- 但消耗CPU和内存
最好的方法是:Entry<K,V> getOrPut(K);
(优雅,低成本)
这样的方法只计算一次哈希和索引, 然后我们可以用条目做我们想要的 (替换或更新值)。
更优雅:
- 拿一个HashSet<Entry>
- 扩展它以便get(K)
在需要时添加新条目
- 进入可能是你自己的对象
- &GT; (new MyHashSet()).get(k).increment();
答案 14 :(得分: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);
答案 15 :(得分:3)
有几种方法:
使用与Google系列中包含的套装类似的行李算法。
创建可在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是非常好的,因为在计算单词时,你只需要一个包算法。
答案 16 :(得分:3)
我认为您的解决方案将是标准方式,但是 - 正如您自己所说 - 它可能不是最快的方式。
您可以查看GNU Trove。这是一个包含各种快速原始集合的库。您的示例将使用TObjectIntHashMap,其中有一个方法adjustOrPutValue,它可以完全按照您的意愿执行。
答案 17 :(得分:2)
“put”需要“get”(确保没有重复的密钥)
所以直接做“放”,
如果有先前的值,则进行添加:
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
}
}
答案 18 :(得分:1)
各种原始包装器,例如Integer
是不可变的,因此实际上没有更简洁的方法来执行你所要求的,除非你能用{{3}这样做}}。我可以在一分钟内完成并更新。顺便说一句,AtomicLong 是Hashtable的一部分。
答案 19 :(得分:1)
非常简单,只需按照以下步骤使用Map.java
中的内置函数
map.put(key, map.getOrDefault(key, 0) + 1);
答案 20 :(得分:1)
我不知道它的效率如何,但下面的代码也可以。你需要在开头定义一个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
答案 21 :(得分:1)
如果您使用Eclipse Collections,则可以使用HashBag
。就内存使用而言,它将是最有效的方法,并且在执行速度方面也表现良好。
HashBag
由MutableObjectIntMap
支持,该Counter
存储原始整数而不是HashBag
个对象。这可以减少内存开销并提高执行速度。
Collection
提供了您需要的API,因为它是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 Kata的示例。
{{1}}
注意:我是Eclipse Collections的提交者。
答案 22 :(得分:1)
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”。
答案 23 :(得分:1)
答案 24 :(得分:1)
我将使用Apache Collections Lazy Map(将值初始化为0)并使用Apache Lang中的MutableIntegers作为该映射中的值。
最大的成本是必须在方法中两次搜索地图。在我的,你只需要做一次。只需获取值(如果不存在,它将被初始化)并递增它。
答案 25 :(得分:0)
我建议使用Java 8 Map :: compute()。 它也考虑不存在密钥的情况。
Map.compute(num, (k, v) -> (v == null) ? 1 : v + 1);
答案 26 :(得分:0)
使用流和 getOrDefault
进行计数:
String s = "abcdeff";
s.chars().mapToObj(c -> (char) c)
.forEach(c -> {
int count = countMap.getOrDefault(c, 0) + 1;
countMap.put(c, count);
});
答案 27 :(得分:-1)
由于很多人都在搜索Groovy答案的Java主题,所以这里是如何在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}
答案 28 :(得分:-1)
希望我能正确理解您的问题,我正从Python进入Java,因此我可以同情您的努力。
如果有
map.put(key, 1)
你会做
map.put(key, map.get(key) + 1)
希望这会有所帮助!
答案 29 :(得分:-1)
java 8中的简单方法如下:
var costsfinalised=sourcevalues.map(costs);
//Logger.log();
Logger.log(costsfinalised);
destinationsheet.getRange(2,7,costsfinalised.length,1).setValues(costsfinalised);