Der effizienteste Weg, um einen Map-Wert in Java zu erhöhen


377

Ich hoffe, diese Frage wird für dieses Forum nicht als zu grundlegend angesehen, aber wir werden sehen. Ich frage mich, wie ich Code für eine bessere Leistung umgestalten kann, die einige Male ausgeführt wird.

Angenommen, ich erstelle eine Worthäufigkeitsliste mit einer Map (wahrscheinlich einer HashMap), wobei jeder Schlüssel eine Zeichenfolge mit dem zu zählenden Wort ist und der Wert eine Ganzzahl ist, die jedes Mal erhöht wird, wenn ein Token des Wortes gefunden wird.

In Perl wäre es trivial einfach, einen solchen Wert zu erhöhen:

$map{$word}++;

In Java ist es jedoch viel komplizierter. Hier ist die Art, wie ich es gerade mache:

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

Was natürlich auf der Autoboxing-Funktion in den neueren Java-Versionen beruht. Ich frage mich, ob Sie einen effizienteren Weg vorschlagen können, um einen solchen Wert zu erhöhen. Gibt es überhaupt gute Leistungsgründe, um das Collections-Framework zu meiden und stattdessen etwas anderes zu verwenden?

Update: Ich habe einige der Antworten getestet. Siehe unten.


Ich denke, dass es für java.util.Hashtable dasselbe wäre.
jrudolph

2
Natürlich wäre das gleich, denn Hashtable ist eine Karte.
whiskeysierra

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

Antworten:


367

Einige Testergebnisse

Ich habe viele gute Antworten auf diese Frage erhalten - danke Leute -, also habe ich beschlossen, einige Tests durchzuführen und herauszufinden, welche Methode tatsächlich am schnellsten ist. Die fünf Methoden, die ich getestet habe, sind folgende:

  • die "ContainsKey" -Methode, die ich in der Frage vorgestellt habe
  • die von Aleksandar Dimitrov vorgeschlagene "TestForNull" -Methode
  • die von Hank Gay vorgeschlagene "AtomicLong" -Methode
  • die von jrudolph vorgeschlagene "Trove" -Methode
  • die von phax.myopenid.com vorgeschlagene "MutableInt" -Methode

Methode

Folgendes habe ich getan ...

  1. hat fünf Klassen erstellt, die bis auf die unten gezeigten Unterschiede identisch waren. Jede Klasse musste eine für das von mir vorgestellte Szenario typische Operation ausführen: Öffnen einer 10-MB-Datei und Einlesen sowie anschließende Häufigkeitszählung aller Wort-Token in der Datei. Da dies durchschnittlich nur 3 Sekunden dauerte, ließ ich die Frequenzzählung (nicht die E / A) 10 Mal durchführen.
  2. Die Schleife von 10 Iterationen, jedoch nicht die E / A-Operation, wurde zeitlich festgelegt und die Gesamtzeit (in Uhrsekunden) im Wesentlichen nach Ian Darwins Methode im Java-Kochbuch aufgezeichnet .
  3. führte alle fünf Tests in Serie durch und tat dies dann noch dreimal.
  4. gemittelt die vier Ergebnisse für jede Methode.

Ergebnisse

Ich werde zuerst die Ergebnisse und den folgenden Code für diejenigen präsentieren, die interessiert sind.

Die ContainsKey- Methode war erwartungsgemäß die langsamste, daher gebe ich die Geschwindigkeit jeder Methode im Vergleich zur Geschwindigkeit dieser Methode an.

  • Enthält Schlüssel : 30,654 Sekunden (Grundlinie)
  • AtomicLong: 29,780 Sekunden (1,03-mal so schnell)
  • TestForNull: 28,804 Sekunden (1,06-mal so schnell)
  • Fundgrube : 26,313 Sekunden (1,16-mal so schnell)
  • MutableInt: 25,747 Sekunden (1,19-mal so schnell)

Schlussfolgerungen

Es scheint, dass nur die MutableInt-Methode und die Trove-Methode wesentlich schneller sind, da nur sie eine Leistungssteigerung von mehr als 10% bewirken. Wenn Threading jedoch ein Problem darstellt, ist AtomicLong möglicherweise attraktiver als die anderen (ich bin mir nicht sicher). Ich habe auch TestForNull mit finalVariablen ausgeführt, aber der Unterschied war vernachlässigbar.

Beachten Sie, dass ich die Speichernutzung in den verschiedenen Szenarien nicht profiliert habe. Ich würde mich freuen, von jedem zu hören, der gute Einblicke in die Auswirkungen der Methoden MutableInt und Trove auf die Speichernutzung hat.

Persönlich finde ich die MutableInt-Methode am attraktivsten, da keine Klassen von Drittanbietern geladen werden müssen. Wenn ich also keine Probleme damit entdecke, gehe ich wahrscheinlich so vor.

Der Code

Hier ist der entscheidende Code für jede Methode.

Enthält Schlüssel

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

Fundgrube

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
Tolle Arbeit, gut gemacht. Ein kleiner Kommentar - der Aufruf von putIfAbsent () im AtomicLong-Code instanziiert eine neue AtomicLong (0), selbst wenn sich eine bereits in der Karte befindet. Wenn Sie dies optimieren, um stattdessen if (map.get (key) == null) zu verwenden, werden Sie wahrscheinlich eine Verbesserung dieser Testergebnisse erhalten.
Leigh Caldwell

2
Ich habe das gleiche kürzlich mit einem ähnlichen Ansatz wie MutableInt gemacht. Ich bin froh zu hören, dass es die optimale Lösung war (ich habe nur angenommen, dass dies der Fall ist, ohne irgendwelche Tests durchzuführen).
Kip

Schön zu hören, dass du schneller bist als ich, Kip. ;-) Lass es mich wissen, wenn du irgendwelche Nachteile bei diesem Ansatz entdeckst.
Gregory

4
Im Fall von Atomic Long wäre es nicht effizienter, dies in einem Schritt zu tun (Sie haben also nur 1 teure Get-Operation anstelle von 2) "map.putIfAbsent (Wort, neues AtomicLong (0)). IncrementAndGet ();"
smartnut007

1
@gregory hast du Java 8's in Betracht gezogen freq.compute(word, (key, count) -> count == null ? 1 : count + 1)? Intern führt es eine weniger gehashte Suche durch als containsKey, es wäre interessant zu sehen, wie es sich aufgrund des Lambda im Vergleich zu anderen verhält.
TWiStErRob

255

Jetzt gibt es einen kürzeren Weg mit Java 8 Map::merge.

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

Was es macht:

  • Wenn der Schlüssel nicht vorhanden ist, geben Sie 1 ein als Wert ein
  • Andernfalls addieren Sie 1 zu dem mit dem Schlüssel verknüpften Wert

Weitere Informationen hier .


liebe immer Java 8. Ist das atomar? oder soll ich es mit einem synchronisierten umgeben?
Tiina

4
das schien für mich nicht zu funktionieren, map.merge(key, 1, (a, b) -> a + b); tat es aber
russter

2
@ Tiina Atomicity-Eigenschaften sind implementierungsspezifisch, vgl. Die Dokumente : "Die Standardimplementierung übernimmt keine Garantie für Synchronisations- oder Atomizitätseigenschaften dieser Methode. Jede Implementierung, die Atomizitätgarantien bietet, muss diese Methode überschreiben und ihre Parallelitätseigenschaften dokumentieren. Insbesondere müssen alle Implementierungen der Subschnittstelle ConcurrentMap dokumentieren, ob die Funktion einmal angewendet wird atomar nur, wenn der Wert nicht vorhanden ist. "
Jensgramm

2
Für Groovy würde es nicht Integer::sumals BiFunction akzeptiert und @russter mochte es nicht, so zu antworten, wie es geschrieben wurde. Das hat bei mir Map.merge(key, 1, { a, b -> a + b})
funktioniert

2
@ Russter, ich weiß, dein Kommentar war vor über einem Jahr, aber erinnerst du dich zufällig, warum es bei dir nicht funktioniert hat? Haben Sie einen Kompilierungsfehler erhalten oder wurde der Wert nicht erhöht?
Paul

44

Eine kleine Recherche im Jahr 2016: https://github.com/leventov/java-word-count , Benchmark-Quellcode

Beste Ergebnisse pro Methode (kleiner ist besser):

                 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

Zeit \ Raum Ergebnisse:


2
Danke, das war wirklich hilfreich. Es wäre cool, Guavas Multiset (zum Beispiel HashMultiset) zum Benchmark hinzuzufügen.
Cabad

34

Google Guava ist dein Freund ...

... zumindest in einigen Fällen. Sie haben diese schöne AtomicLongMap . Besonders schön , weil man es zu tun hat lange als Wert in Ihrer Karte.

Z.B

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

Es ist auch möglich, dem Wert mehr als 1 hinzuzufügen:

map.getAndAdd(word, 112L); 

7
AtomicLongMap#getAndAddnimmt ein Grundelement longund nicht die Wrapper-Klasse; Es macht keinen Sinn, das zu tun new Long(). Und AtomicLongMapist ein parametrisierter Typ; du hättest es als deklarieren sollen AtomicLongMap<String>.
Helder Pereira

32

@Hank Gay

Als Folge meines eigenen (ziemlich nutzlosen) Kommentars: Trove scheint der richtige Weg zu sein. Wenn aus irgendeinem Grund Sie mit dem Standard - JDK halten wollte, ConcurrentMap und Atomic kann der Code ein machen winziges bisschen schöner, obwohl YMMV.

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

wird 1als Wert in der Karte für verlassen foo. Realistisch gesehen ist eine erhöhte Freundlichkeit beim Einfädeln alles, was dieser Ansatz empfehlen muss.


9
PutIfAbsent () gibt den Wert zurück. Es könnte eine große Verbesserung sein, den zurückgegebenen Wert in einer lokalen Variablen zu speichern und ihn zum Inkrementieren von AndGet () zu verwenden, anstatt get erneut aufzurufen.
smartnut007

putIfAbsent kann einen Nullwert zurückgeben, wenn der angegebene Schlüssel noch keinem Wert in Map zugeordnet ist. Daher würde ich den zurückgegebenen Wert vorsichtig verwenden. 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);

Und so erhöhen Sie einen Wert mit einfachem Code.

Vorteil:

  • Sie müssen keine neue Klasse hinzufügen oder ein anderes Konzept von veränderlichem int verwenden
  • Sich nicht auf eine Bibliothek verlassen
  • Leicht zu verstehen, was genau los ist (nicht zu viel Abstraktion)

Nachteil:

  • Die Hash-Map wird zweimal nach get () und put () durchsucht. Es wird also nicht der performanteste Code sein.

Theoretisch wissen Sie nach dem Aufruf von get () bereits, wo Sie put () platzieren müssen, sodass Sie nicht erneut suchen müssen. Das Suchen in einer Hash-Map dauert jedoch normalerweise nur sehr wenig Zeit, damit Sie dieses Leistungsproblem ignorieren können.

Wenn Sie das Problem jedoch sehr ernst nehmen, sind Sie ein Perfektionist. Eine andere Möglichkeit ist die Verwendung der Zusammenführungsmethode. Dies ist (wahrscheinlich) effizienter als das vorherige Codefragment, da Sie die Karte (theoretisch) nur einmal durchsuchen werden: (obwohl) Dieser Code ist auf den ersten Blick nicht ersichtlich, er ist kurz und performant.

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

Vorschlag: Sie sollten sich in den meisten Fällen mehr um die Lesbarkeit des Codes als um einen geringen Leistungsgewinn kümmern. Wenn das erste Code-Snippet für Sie leichter zu verstehen ist, verwenden Sie es. Aber wenn Sie in der Lage sind, die zweite gut zu verstehen, können Sie es auch versuchen!


Die Methode getOfDefault ist in JAVA 7 nicht verfügbar. Wie kann ich dies in JAVA 7 erreichen?
Tanvi

1
Möglicherweise müssen Sie sich dann auf andere Antworten verlassen. Dies funktioniert nur in Java 8.
off99555

1
+1 für die Zusammenführungslösung ist dies die Funktion mit der höchsten Leistung, da Sie nur 1 Mal für die Hashcode-Berechnung bezahlen müssen (falls die Karte, auf der Sie sie verwenden, die Methode ordnungsgemäß unterstützt), anstatt sie möglicherweise zu bezahlen 3 Zeiten
Ferrybig

2
Verwenden der Methode Inferenz: map.merge (Schlüssel, 1, Integer :: sum)
earandap

25

Es ist immer eine gute Idee, in der Google-Sammlungsbibliothek nach solchen Dingen zu suchen . In diesem Fall reicht ein Multiset aus :

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

Es gibt Map-ähnliche Methoden zum Durchlaufen von Schlüsseln / Einträgen usw. Intern verwendet die Implementierung derzeit a HashMap<E, AtomicInteger>, sodass keine Boxkosten anfallen.


Die obige Antwort muss die Antwort von tovares widerspiegeln. Die API hat sich seit ihrer Veröffentlichung geändert (vor 3 Jahren :))
Steve

Läuft die count()Methode auf einem Multiset in O (1) oder O (n) Zeit (Worstcase)? Die Dokumente sind in diesem Punkt unklar.
Adam Parkin

Mein Algorithmus für diese Art von Dingen: if (hasApacheLib (thing)) gibt apacheLib zurück; sonst wenn (hasOnGuava (Ding)) Guave zurückgibt. Normalerweise komme ich an diesen beiden Schritten nicht vorbei. :)
digao_mb

22

Sie sollten sich der Tatsache bewusst sein, dass Ihr ursprünglicher Versuch

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

enthält zwei potenziell teure Operationen auf einer Karte, nämlich containsKeyund get. Ersteres führt eine Operation aus, die möglicherweise der letzteren ziemlich ähnlich ist, sodass Sie dieselbe Arbeit zweimal ausführen !

Wenn Sie sich die API für Map ansehen, werden getOperationen normalerweise zurückgegeben, nullwenn die Map das angeforderte Element nicht enthält.

Beachten Sie, dass dies eine Lösung wie ergibt

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

gefährlich, da es NullPointerExceptions ergeben könnte . Sie sollten zuerst nachsehen null.

Beachten Sie auch , und dies ist sehr wichtig, dass HashMaps per Definition enthalten kannnulls . Also nullsagt nicht jeder zurückgegebene "es gibt kein solches Element". In dieser Hinsicht containsKeyverhält sich anders aus getin dir eigentlich sagen , ob es ein solches Element ist. Weitere Informationen finden Sie in der API.

In Ihrem Fall möchten Sie jedoch möglicherweise nicht zwischen einem gespeicherten nullund einem "noSuchElement" unterscheiden. Wenn Sie nulls nicht zulassen möchten, bevorzugen Sie möglicherweise a Hashtable. Die Verwendung einer Wrapper-Bibliothek, wie sie bereits in anderen Antworten vorgeschlagen wurde, ist je nach Komplexität Ihrer Anwendung möglicherweise eine bessere Lösung für die manuelle Behandlung.

Um die Antwort zu vervollständigen (und ich habe vergessen, dies zuerst dank der Bearbeitungsfunktion einzugeben!), Besteht der beste Weg, dies nativ zu tun, darin, geteine finalVariable einzugeben, nach einer zu suchen nullund putsie wieder mit a einzugeben 1. Die Variable sollte sein, finalweil sie sowieso unveränderlich ist. Der Compiler benötigt diesen Hinweis möglicherweise nicht, ist aber auf diese Weise klarer.

final HashMap map = generateRandomHashMap ();
final Object key = fetchSomeKey ();
letzte Ganzzahl i = map.get (Schlüssel);
if (i! = null) {
    map.put (i + 1);
} else {
    // etwas tun
}}

Wenn Sie sich nicht auf Autoboxen verlassen möchten, sollten Sie map.put(new Integer(1 + i.getValue()));stattdessen etwas Ähnliches sagen .


Um das Problem der anfänglichen nicht zugeordneten / null-Werte in groovy zu vermeiden, mache ich am Ende: count.put (key, (count.get (key) ?: 0) + 1) // übermäßig komplexe Version von ++
Joe Atzberger

2
Oder am einfachsten: zählt = [:]. WithDefault {0} // ++ entfernt
Joe Atzberger

18

Ein anderer Weg wäre das Erstellen einer veränderlichen Ganzzahl:

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

Dies bedeutet natürlich, dass ein zusätzliches Objekt erstellt wird, aber der Overhead im Vergleich zum Erstellen einer Ganzzahl (auch mit Integer.valueOf) sollte nicht so hoch sein.


5
Sie möchten das MutableInt nicht um 1 starten, wenn Sie es zum ersten Mal in die Karte einfügen?
Tom Hawtin - Tackline

5
Apaches Commons-Lang hat bereits eine MutableInt für Sie geschrieben.
SingleShot

11

Sie können die computeIfAbsent- Methode in Mapder in Java 8 bereitgestellten Schnittstelle verwenden .

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]

Die Methode computeIfAbsentprüft, ob der angegebene Schlüssel bereits einem Wert zugeordnet ist oder nicht. Wenn kein zugeordneter Wert vorhanden ist, wird versucht, seinen Wert mithilfe der angegebenen Zuordnungsfunktion zu berechnen. In jedem Fall wird der aktuelle (vorhandene oder berechnete) Wert zurückgegeben, der dem angegebenen Schlüssel zugeordnet ist, oder null, wenn der berechnete Wert null ist.

Nebenbei bemerkt , wenn Sie eine Situation haben, in der mehrere Threads eine gemeinsame Summe aktualisieren, können Sie einen Blick auf die LongAdder- Klasse werfen. Bei hohen Konflikten ist der erwartete Durchsatz dieser Klasse erheblich höher als AtomicLongauf Kosten eines höheren Speicherplatzverbrauchs.


Warum ConcurrentHashmap und AtomicLong?
ealeon

7

Die Speicherrotation kann hier ein Problem sein, da jedes Boxen eines int größer oder gleich 128 eine Objektzuweisung verursacht (siehe Integer.valueOf (int)). Obwohl der Garbage Collector sehr effizient mit kurzlebigen Objekten umgeht, leidet die Leistung bis zu einem gewissen Grad.

Wenn Sie wissen, dass die Anzahl der vorgenommenen Inkremente die Anzahl der Schlüssel (in diesem Fall = Wörter) weit übersteigt, sollten Sie stattdessen einen int-Halter verwenden. Phax hat bereits Code dafür vorgelegt. Hier ist es wieder mit zwei Änderungen (Inhaberklasse statisch gemacht und Anfangswert auf 1 gesetzt):

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

Wenn Sie extreme Leistung benötigen, suchen Sie nach einer Map-Implementierung, die direkt auf primitive Werttypen zugeschnitten ist. jrudolph erwähnte GNU Trove .

Ein guter Suchbegriff für dieses Thema ist übrigens "Histogramm".


5

Anstatt includesKey () aufzurufen, ist es schneller, map.get aufzurufen und zu überprüfen, ob der zurückgegebene Wert null ist oder nicht.

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

3

Sind Sie sicher, dass dies ein Engpass ist? Haben Sie eine Leistungsanalyse durchgeführt?

Verwenden Sie den NetBeans-Profiler (kostenlos und in NB 6.1 integriert), um Hotspots anzuzeigen.

Schließlich ist ein JVM-Upgrade (z. B. von 1,5 bis 1,6) häufig ein billiger Leistungssteigerer. Selbst ein Upgrade der Build-Nummer kann zu guten Leistungssteigerungen führen. Wenn Sie unter Windows ausgeführt werden und dies eine Serverklassenanwendung ist, verwenden Sie -server in der Befehlszeile, um die Server-Hotspot-JVM zu verwenden. Auf Linux- und Solaris-Computern wird dies automatisch erkannt.


3

Es gibt verschiedene Ansätze:

  1. Verwenden Sie einen Taschenalorithmus wie die in Google Collections enthaltenen Sätze.

  2. Erstellen Sie einen veränderlichen Container, den Sie in der Map verwenden können:


    class My{
        String word;
        int count;
    }

Und benutze put ("Wort", neues Mein ("Wort")); Anschließend können Sie überprüfen, ob es vorhanden ist, und beim Hinzufügen erhöhen.

Vermeiden Sie es, Ihre eigene Lösung mithilfe von Listen zu erstellen, da Ihre Leistung stinkt, wenn Sie in Innerloops suchen und sortieren. Die erste HashMap-Lösung ist eigentlich ziemlich schnell, aber eine solche wie die in Google Collections ist wahrscheinlich besser.

Das Zählen von Wörtern mit Google Collections sieht ungefähr so ​​aus:



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


Die Verwendung des HashMultiset ist sehr elegant, da ein Taschenalgorithmus genau das ist, was Sie zum Zählen von Wörtern benötigen.


3

Ich denke, Ihre Lösung wäre der Standardweg, aber - wie Sie selbst bemerkt haben - wahrscheinlich nicht der schnellste Weg.

Sie können sich GNU Trove ansehen . Das ist eine Bibliothek, die alle Arten von schnellen primitiven Sammlungen enthält. Ihr Beispiel würde eine TObjectIntHashMap verwenden, die eine Methode adjustOrPutValue hat, die genau das tut, was Sie wollen.


Der Link zu TObjectIntHashMap ist unterbrochen. Dies ist der richtige Link: trove4j.sourceforge.net/javadocs/gnu/trove/map/…
Erel Segal-Halevi

Danke, Erel, ich habe den Link repariert.
Jrudolph

3

Eine Variation des MutableInt-Ansatzes, die bei einem Hack sogar noch schneller sein kann, ist die Verwendung eines Int-Arrays mit einem Element:

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

Es wäre interessant, wenn Sie Ihre Leistungstests mit dieser Variante wiederholen könnten. Es könnte das schnellste sein.


Bearbeiten: Das obige Muster hat für mich gut funktioniert, aber schließlich habe ich Troves Sammlungen verwendet, um die Speichergröße in einigen sehr großen Karten, die ich erstellt habe, zu reduzieren - und als Bonus war es auch schneller.

Eine wirklich nette Funktion ist, dass die TObjectIntHashMapKlasse einen einzelnen adjustOrPutValueAufruf hat, der, abhängig davon, ob dieser Schlüssel bereits einen Wert enthält, entweder einen Anfangswert setzt oder den vorhandenen Wert erhöht. Dies ist perfekt zum Inkrementieren:

TObjectIntHashMap<String> map = new TObjectIntHashMap<String>();
...
map.adjustOrPutValue(key, 1, 1);

3

Google Collections HashMultiset:
- Sehr elegant zu bedienen
- verbrauchen aber CPU und Speicher

Am besten wäre eine Methode wie: Entry<K,V> getOrPut(K); (elegant und kostengünstig)

Eine solche Methode berechnet Hash und Index nur einmal, und dann können wir mit dem Eintrag tun, was wir wollen (entweder den Wert ersetzen oder aktualisieren).

Eleganter:
- Nehmen Sie eine HashSet<Entry>
- erweitern Sie sie so, dass Sie get(K)bei Bedarf einen neuen Eintrag einfügen.
- Der Eintrag kann Ihr eigenes Objekt sein.
->(new MyHashSet()).get(k).increment();


3

Ganz einfach, verwenden Sie einfach die eingebaute Funktion Map.javawie folgt

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

Dies erhöht den Wert nicht, sondern setzt nur den aktuellen Wert oder 0, wenn dem Schlüssel kein Wert zugewiesen wurde.
Siegi

Sie können den Wert ++erhöhen um ... OMG, es ist so einfach. @siegi
Sudoz

Für den Datensatz: Funktioniert ++nirgendwo in diesem Ausdruck, da eine Variable als Operand benötigt wird, aber nur Werte vorhanden sind. Ihre Hinzufügung von + 1Arbeiten. Jetzt ist Ihre Lösung dieselbe wie in der Antwort von off99555 .
Siegi

2

"put" muss "get" (um sicherzustellen, dass kein doppelter Schlüssel vorhanden ist).
Machen Sie also direkt einen "Put"
und wenn es einen vorherigen Wert gab, dann machen Sie einen Zusatz:

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
}

Wenn die Zählung bei 0 beginnt, addieren Sie 1: (oder andere Werte ...)

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
}

Hinweis: Dieser Code ist nicht threadsicher. Verwenden Sie es zum Erstellen und verwenden Sie dann die Karte, um sie nicht gleichzeitig zu aktualisieren.

Optimierung: Behalten Sie in einer Schleife den alten Wert bei, um der neue Wert der nächsten Schleife zu werden.

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

Die verschiedenen primitiven Wrapper sind z. B. Integerunveränderlich, so dass es wirklich keine präzisere Möglichkeit gibt, das zu tun, was Sie verlangen, es sei denn, Sie können es mit etwas wie AtomicLong tun . Ich kann das in einer Minute ausprobieren und aktualisieren. BTW, Hashtable ist ein Teil des Collections Framework .


1

Ich würde Apache Collections Lazy Map verwenden (um Werte auf 0 zu initialisieren) und MutableIntegers von Apache Lang als Werte in dieser Map verwenden.

Die größten Kosten entstehen, wenn Sie die Karte in Ihrer Methode zweimal durchsuchen müssen. In meinem musst du es nur einmal machen. Holen Sie sich einfach den Wert (er wird initialisiert, wenn er nicht vorhanden ist) und erhöhen Sie ihn.


1

Die Datenstruktur der Functional Java- Bibliothek TreeMapverfügt über eine updateMethode im neuesten Trunk Head:

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

Anwendungsbeispiel:

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

Dieses Programm gibt "2" aus.


1

@Vilmantas Baranauskas: In Bezug auf diese Antwort würde ich kommentieren, wenn ich die Wiederholungspunkte hätte, aber ich nicht. Ich wollte beachten, dass die dort definierte Counter-Klasse NICHT threadsicher ist, da es nicht ausreicht, nur inc () zu synchronisieren, ohne value () zu synchronisieren. Bei anderen Threads, die value () aufrufen, wird der Wert nur dann garantiert angezeigt, wenn eine Beziehung hergestellt wurde, bevor mit der Aktualisierung eine Beziehung hergestellt wurde.


Wenn Sie auf die Antwort einer anderen Person verweisen möchten, verwenden Sie oben @ [Benutzername], z. B. @Vilmantas Baranauskas <Inhalt geht hier>
Hank Gay

Ich habe diese Änderung vorgenommen, um sie zu bereinigen.
Alex Miller

1

Ich weiß nicht, wie effizient es ist, aber der folgende Code funktioniert auch. Sie müssen BiFunctionam Anfang einen definieren . Außerdem können Sie mit dieser Methode mehr als nur inkrementieren.

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

Ausgabe ist

3
1

1

Wenn Sie Eclipse-Sammlungen verwenden , können Sie a verwenden HashBag. Dies ist der effizienteste Ansatz in Bezug auf die Speichernutzung und eine gute Leistung in Bezug auf die Ausführungsgeschwindigkeit.

HashBagwird von a unterstützt, MutableObjectIntMapdas primitive Ints anstelle von CounterObjekten speichert . Dies reduziert den Speicheraufwand und verbessert die Ausführungsgeschwindigkeit.

HashBag bietet die API, die Sie benötigen, da es sich um eine handelt Collection damit auch die Anzahl der Vorkommen eines Elements abfragen können.

Hier ist ein Beispiel aus der 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"));

Hinweis: Ich bin ein Committer für Eclipse-Sammlungen.


1

Ich schlage vor, Java 8 Map :: compute () zu verwenden. Es wird auch der Fall berücksichtigt, in dem kein Schlüssel vorhanden ist.

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

mymap.merge(key, 1, Integer::sum)?
Det

-2

Da viele Leute in Java-Themen nach Groovy-Antworten suchen, können Sie dies in Groovy folgendermaßen tun:

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

Der einfache und einfache Weg in Java 8 ist der folgende:

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

-3

Ich hoffe, ich verstehe Ihre Frage richtig. Ich komme von Python zu Java, damit ich mich in Ihren Kampf einfühlen kann.

Wenn Sie haben

map.put(key, 1)

Du würdest

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

Hoffe das hilft!

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.