วิธีที่มีประสิทธิภาพมากที่สุดในการเพิ่มค่า Map ใน Java


377

ฉันหวังว่าคำถามนี้จะไม่ถือว่าเป็นพื้นฐานสำหรับฟอรัมนี้ แต่เราจะเห็น ฉันสงสัยว่าจะสร้างรหัสใหม่อีกครั้งเพื่อประสิทธิภาพที่ดีขึ้นซึ่งเริ่มขึ้นหลายครั้ง

สมมติว่าฉันกำลังสร้างรายการความถี่คำโดยใช้แผนที่ (อาจเป็น HashMap) โดยที่แต่ละคีย์คือสตริงที่มีคำที่ถูกนับและค่านั้นเป็นจำนวนเต็มที่เพิ่มขึ้นทุกครั้งที่พบโทเค็นของคำ

ใน Perl การเพิ่มค่าดังกล่าวอาจเป็นเรื่องง่าย:

$map{$word}++;

แต่ใน Java มันซับซ้อนกว่ามาก นี่คือวิธีที่ฉันกำลังทำอยู่:

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

หลักสูตรใดขึ้นอยู่กับฟีเจอร์ autoboxing ในเวอร์ชั่น Java ที่ใหม่กว่า ฉันสงสัยว่าคุณสามารถแนะนำวิธีที่มีประสิทธิภาพมากขึ้นในการเพิ่มมูลค่าดังกล่าวหรือไม่ มีเหตุผลด้านประสิทธิภาพที่ดีในการละทิ้งเฟรมเวิร์กของ Collections และใช้อย่างอื่นแทนหรือไม่?

อัปเดต: ฉันได้ทำการทดสอบคำตอบหลายข้อ ดูด้านล่าง


ฉันคิดว่ามันจะเหมือนกันสำหรับ java.util.Hashtable
jrudolph

2
แน่นอนถ้าจะเหมือนกันเพราะ Hashtable เป็น infact แผนที่
whiskysierra

Java 8: computeIfAbsent ตัวอย่าง: stackoverflow.com/a/37439971/1216775
akhil_mittal

คำตอบ:


366

ผลการทดสอบบางอย่าง

ฉันได้รับคำตอบที่ดีมากสำหรับคำถามนี้ - ขอบคุณผู้คน - ดังนั้นฉันจึงตัดสินใจทดสอบและหาวิธีที่เร็วที่สุด ห้าวิธีที่ฉันทดสอบคือ:

  • วิธีการ "containKey" ที่ฉันนำเสนอในคำถาม
  • วิธี "TestForNull" ที่แนะนำโดย Aleksandar Dimitrov
  • วิธีการ "AtomicLong" แนะนำโดย Hank Gay
  • วิธี "Trove" ที่แนะนำโดย jrudolph
  • วิธีการ "MutableInt" แนะนำโดย phax.myopenid.com

วิธี

นี่คือสิ่งที่ฉันทำ ...

  1. สร้างห้าคลาสที่เหมือนกันยกเว้นความแตกต่างที่แสดงด้านล่าง แต่ละชั้นจะต้องดำเนินการตามปกติของสถานการณ์ที่ฉันนำเสนอ: เปิดไฟล์ 10MB และอ่านมันจากนั้นทำการนับความถี่ของโทเค็นคำทั้งหมดในไฟล์ เนื่องจากใช้เวลาเฉลี่ยเพียง 3 วินาทีฉันจึงทำการนับความถี่ (ไม่ใช่ I / O) 10 ครั้ง
  2. หมดเวลาห่วง 10 ซ้ำ แต่ไม่ได้ที่ผมดำเนินการ / Oและบันทึกเวลาทั้งหมดที่นำ (วินาทีนาฬิกา) เป็นหลักโดยใช้วิธีเอียนดาร์วินใน 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 อาจจะน่าดึงดูดกว่าตัวอื่น ๆ (ฉันไม่แน่ใจจริงๆ) ฉันยังใช้ TestForNull ด้วยfinalตัวแปร แต่ความแตกต่างนั้นเล็กน้อย

โปรดทราบว่าฉันไม่ได้ใช้หน่วยความจำประวัติในสถานการณ์ที่แตกต่างกัน ฉันยินดีที่จะได้ยินจากใครก็ตามที่มีความเข้าใจอย่างถ่องแท้เกี่ยวกับวิธีที่ 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();
}

3
เยี่ยมมากทำได้ดีมาก ความคิดเห็นเล็กน้อย - การเรียก putIfAbsent () ในรหัส AtomicLong จะยกตัวอย่าง AtomicLong ใหม่ (0) แม้ว่าจะมีอยู่ในแผนที่แล้วก็ตาม หากคุณปรับแต่งสิ่งนี้เพื่อใช้ถ้า (map.get (key) == null) แทนคุณอาจจะได้รับการปรับปรุงในผลการทดสอบเหล่านั้น
Leigh Caldwell

2
ฉันทำสิ่งเดียวกันเมื่อเร็ว ๆ นี้ด้วยวิธีการที่คล้ายกับ MutableInt ฉันดีใจที่ได้ยินว่ามันเป็นทางออกที่ดีที่สุด (ฉันแค่คิดว่ามันเป็นโดยไม่ต้องทำการทดสอบใด ๆ )
กีบ

ดีใจที่ได้ยินว่าคุณเร็วกว่าฉันคิป ;-) แจ้งให้เราทราบหากคุณค้นพบข้อเสียเปรียบใด ๆ
กอรี่

4
ในกรณีของอะตอมยาวจะไม่มีประสิทธิภาพมากกว่าในขั้นตอนเดียว (ดังนั้นคุณมีการดำเนินการที่แพงเพียง 1 แทนที่จะเป็น 2) "map.putIfAbsent (คำใหม่ AtomicLong (0)) incrementAndGet ();"
smartnut007

1
@gregory คุณพิจารณา Java 8 freq.compute(word, (key, count) -> count == null ? 1 : count + 1)หรือไม่ ภายในมันทำการค้นหาที่ถูกแฮ็กน้อยกว่าหนึ่งcontainsKeyมันน่าสนใจที่จะดูว่ามันเปรียบเทียบกับคนอื่นอย่างไรเพราะแลมบ์ดา
TWiStErRob

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); ทำ
russter

2
@Tiina Atomicity เป็นลักษณะเฉพาะของการใช้งาน cf เอกสาร : "การใช้งานเริ่มต้นทำให้ไม่มีการรับประกันเกี่ยวกับคุณสมบัติการซิงโครไนซ์หรือ atomicity ของวิธีนี้การใช้งานใด ๆ ที่ให้การค้ำประกันแบบอะตอมมิกส์จะต้องแทนที่วิธีนี้และบันทึกคุณสมบัติการทำงานพร้อมกันของมัน เฉพาะในกรณีที่ไม่มีค่า "
jensgram

2
สำหรับ groovy มันจะไม่ยอมรับInteger::sumในฐานะ BiFunction และไม่ชอบ @russter ตอบตามที่เขียนไว้ สิ่งนี้ใช้ได้กับฉันMap.merge(key, 1, { a, b -> a + b})
jookyone

2
@ รัสเตอร์ฉันรู้ว่าความคิดเห็นของคุณมากกว่าหนึ่งปีที่ผ่านมา แต่คุณจะจำได้ไหมว่าทำไมมันถึงไม่เหมาะกับคุณ? คุณได้รับข้อผิดพลาดในการรวบรวมหรือค่าไม่เพิ่มขึ้น?
พอล

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

ผลเวลา \ space:


2
ขอบคุณนี่เป็นประโยชน์จริงๆ มันจะยอดเยี่ยมเมื่อเพิ่ม Multiset ของ Guava (ตัวอย่างเช่น HashMultiset) ลงในเกณฑ์มาตรฐาน
cabad

34

Google Guavaเป็นเพื่อนของคุณ ...

... อย่างน้อยในบางกรณี พวกเขามีAtomicLongMap ที่ดีนี้ ดีมากโดยเฉพาะอย่างยิ่งเพราะคุณกำลังติดต่อกับตราบใดที่มูลค่าในแผนที่ของคุณ

เช่น

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

เป็นไปได้ที่จะเพิ่มมากกว่า 1 ค่า

map.getAndAdd(word, 112L); 

7
AtomicLongMap#getAndAddใช้แบบดั้งเดิมlongและไม่ใช่คลาส wrapper new Long()มีจุดใดในการทำ และAtomicLongMapเป็นประเภทพารามิเตอร์ AtomicLongMap<String>คุณควรจะมีการประกาศเป็น
Helder Pereira

32

@ แฮงค์เกย์

ติดตามความคิดเห็นของฉันเอง (ค่อนข้างไร้ประโยชน์): Trove ดูเหมือนหนทางที่จะไป ถ้าด้วยเหตุผลอะไรก็ตามที่คุณต้องการที่จะติดกับ JDK มาตรฐานConcurrentMapและAtomicLongสามารถทำให้โค้ดเล็ก ๆดีกว่าเล็กน้อย แต่ YMMV

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

จะปล่อยให้เป็นค่าที่อยู่ในแผนที่สำหรับ1 fooความเป็นมิตรที่เพิ่มมากขึ้นในการทำเกลียวเป็นสิ่งที่วิธีนี้มีเพื่อแนะนำ


9
putIfAbsent () ส่งคืนค่า มันอาจเป็นการปรับปรุงครั้งใหญ่ในการจัดเก็บค่าที่ส่งคืนในตัวแปรโลคัลและใช้เพื่อ incrementAndGet () แทนที่จะเรียกรับอีกครั้ง
smartnut007

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 ที่ไม่แน่นอน
  • ไม่ต้องพึ่งพาห้องสมุดใด ๆ
  • ง่ายต่อการเข้าใจสิ่งที่เกิดขึ้นอย่างแน่นอน (ไม่เป็นนามธรรมมากเกินไป)

Downside:

  • แผนที่แฮชจะถูกค้นหาสองครั้งเพื่อรับ () และใส่ () ดังนั้นมันจะไม่เป็นรหัสที่มีประสิทธิภาพมากที่สุด

ตามหลักวิชาการแล้วเมื่อคุณโทรหา get () คุณจะรู้ว่าจะใส่ที่ไหน () ดังนั้นคุณไม่ควรค้นหาอีกครั้ง แต่การค้นหาในแผนที่แฮชมักจะใช้เวลาน้อยมากที่คุณสามารถเพิกเฉยต่อปัญหาประสิทธิภาพการทำงานนี้ได้

แต่ถ้าคุณจริงจังกับปัญหามากคุณเป็นพวกชอบความสมบูรณ์แบบและอีกวิธีคือใช้วิธีการผสานนี่คือ (น่าจะ) มีประสิทธิภาพมากกว่าข้อมูลโค้ดก่อนหน้าเนื่องจากคุณจะค้นหาในทางทฤษฎีเพียงครั้งเดียว: (แม้ว่า รหัสนี้ไม่ชัดเจนตั้งแต่แรกเห็นมันสั้นและมีประสิทธิภาพ)

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

คำแนะนำ: คุณควรใส่ใจกับความสามารถในการอ่านโค้ดมากกว่าการเพิ่มประสิทธิภาพเพียงเล็กน้อยในเกือบตลอดเวลา หากข้อมูลโค้ดแรกง่ายกว่าที่คุณจะเข้าใจให้ใช้มัน แต่ถ้าคุณสามารถเข้าใจดีคนที่ 2 ได้คุณก็สามารถไปได้!


เมธอด getOfDefault ไม่พร้อมใช้งานใน JAVA 7 ฉันจะทำสิ่งนี้ใน JAVA 7 ได้อย่างไร
tanvi

1
คุณอาจต้องพึ่งพาคำตอบอื่น ๆ แล้ว ใช้งานได้เฉพาะใน Java 8
off99555

1
+1 สำหรับการผสานนี่จะเป็นฟังก์ชั่นที่มีประสิทธิภาพสูงสุดเพราะคุณจะต้องจ่าย 1 ครั้งสำหรับการคำนวณ hashcode (ในกรณีที่แผนที่ที่คุณใช้อยู่รองรับวิธีการอย่างถูกต้อง) แทนที่จะจ่ายให้มัน 3 ครั้ง
Ferrybig

2
ใช้การอนุมานวิธี: map.merge (คีย์, 1, จำนวนเต็ม :: ผลรวม)
earandap

25

เป็นความคิดที่ดีที่จะดูห้องสมุด Google Collectionsสำหรับสิ่งนี้ ในกรณีนี้Multisetจะทำเคล็ดลับ:

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

มีวิธีที่คล้ายกับแผนที่สำหรับการวนซ้ำคีย์ / รายการ ฯลฯ ภายในการนำไปใช้งานในปัจจุบันจะHashMap<E, AtomicInteger>ทำให้คุณไม่ต้องเสียค่าใช้จ่ายในการชกมวย


ผู้ตอบคำถามข้างต้นจำเป็นต้องสะท้อนการตอบสนองต่อการตอบกลับ api มีการเปลี่ยนแปลงนับตั้งแต่มีการโพสต์ (3 ปีที่แล้ว :))
Steve

ไม่count()วิธีการวิ่ง MultiSet ใน O (1) หรือ O (n) เวลา (worstcase)? เอกสารไม่ชัดเจนในจุดนี้
Adam Parkin

อัลกอริทึมของฉันสำหรับสิ่งนี้: ถ้า (hasApacheLib (สิ่ง)) ส่งคืน apacheLib; มิฉะนั้นถ้า (hasOnGuava (สิ่งของ)) ส่งคืนฝรั่ง โดยปกติฉันจะไม่ผ่านสองขั้นตอนเหล่านี้ :)
digao_mb

22

คุณควรตระหนักถึงความจริงที่ว่าคุณพยายามครั้งแรก

int count = map.containKey (คำ)? map.get (คำ): 0;

มีสองการดำเนินงานอาจมีราคาแพงบนแผนที่คือและcontainsKey getอดีตดำเนินการที่อาจคล้ายกับหลังดังนั้นคุณจึงทำงานเหมือนเดิมสองครั้ง !

หากคุณดู API สำหรับแผนที่getการดำเนินงานมักจะกลับมาnullเมื่อแผนที่ไม่มีองค์ประกอบที่ร้องขอ

โปรดทราบว่าสิ่งนี้จะทำให้การแก้ปัญหาเช่น

map.put (คีย์ map.get (คีย์) + 1);

อันตรายเพราะมันอาจจะให้ผลผลิตNullPointerExceptions คุณควรตรวจสอบnullก่อน

นอกจากนี้ยังทราบและนี่คือสิ่งที่สำคัญมากที่HashMaps สามารถประกอบด้วยnullsโดยความหมาย ดังนั้นไม่ใช่ทุกคืนที่nullบอกว่า "ไม่มีองค์ประกอบดังกล่าว" ในแง่นี้containsKeyจะมีพฤติกรรมที่แตกต่างจากการgetบอกคุณว่าจริงหรือไม่มีองค์ประกอบดังกล่าวหรือไม่ อ้างถึง API สำหรับรายละเอียด

อย่างไรก็ตามสำหรับกรณีของคุณคุณอาจไม่ต้องการแยกแยะระหว่างการจัดเก็บnullและ "noSuchElement" หากคุณไม่ต้องการอนุญาตnullคุณอาจต้องการHashtableคุณอาจชอบ การใช้ไลบรารีแรปเปอร์ตามที่เสนอไว้แล้วในคำตอบอื่น ๆ อาจเป็นวิธีแก้ปัญหาที่ดีกว่าสำหรับการดูแลด้วยตนเองขึ้นอยู่กับความซับซ้อนของแอปพลิเคชันของคุณ

เพื่อให้คำตอบเสร็จสมบูรณ์ (และฉันลืมที่จะใส่ในตอนแรกต้องขอบคุณฟังก์ชั่นการแก้ไข!) วิธีที่ดีที่สุดในการทำมันคือการ getเป็นfinalตัวแปรการตรวจสอบnullและมันกลับมาด้วยput 1ตัวแปรควรเป็นfinalเพราะมันไม่เปลี่ยนรูปต่อไป คอมไพเลอร์อาจไม่ต้องการคำใบ้นี้ แต่ชัดเจนกว่านั้น

แผนที่ HashMap สุดท้าย = generateRandomHashMap ();
คีย์อ็อบเจ็กต์สุดท้าย = fetchSomeKey ();
จำนวนเต็ม i = map.get สุดท้าย (คีย์);
ถ้า (i! = null) {
    map.put (i + 1);
} อื่น {
    // ทำอะไรสักอย่าง
}

หากคุณไม่ต้องการพึ่งพาการ autobox คุณควรพูดว่าชอบmap.put(new Integer(1 + i.getValue()));แทน


เพื่อหลีกเลี่ยงปัญหาของค่า unmapped / null เริ่มต้นใน groovy ฉันทำ: counts.put (key, (counts.get (key)?: 0) + 1) // เวอร์ชันที่ซับซ้อนมากเกินไปของ ++
Joe Atzberger

2
หรืออย่างง่ายที่สุด: counts = [:] withDefault {0} // ++ ออกไป
Joe Atzberger

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 ที่ 1 ในครั้งแรกที่คุณใส่ไว้ในแผนที่?
Tom Hawtin - tackline

5
Apache's Commons-lang มี MutableInt ที่เขียนขึ้นสำหรับคุณแล้ว
SingleShot

11

คุณสามารถทำให้การใช้งานของcomputeIfAbsentวิธีการในMapอินเตอร์เฟซที่ให้ไว้ในJava 8

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ตรวจสอบว่าคีย์ที่ระบุมีการเชื่อมโยงกับค่าหรือไม่? หากไม่มีค่าที่เกี่ยวข้องก็จะพยายามคำนวณค่าของมันโดยใช้ฟังก์ชั่นการทำแผนที่ที่กำหนด ไม่ว่าในกรณีใดมันจะคืนค่าปัจจุบัน (ที่มีอยู่หรือที่คำนวณแล้ว) ที่เชื่อมโยงกับคีย์ที่ระบุหรือเป็นโมฆะหากค่าที่คำนวณเป็นโมฆะ

ในบันทึกด้านข้างหากคุณมีสถานการณ์ที่หลายเธรดอัพเดตผลรวมทั่วไปคุณสามารถดูคลาส LongAdder ได้ภายใต้การแข่งขันที่สูงกว่าปริมาณงานที่คาดหวังของคลาสนี้จะสูงกว่าAtomicLongค่าใช้จ่ายของการใช้พื้นที่


ทำไม concurrentHashmap และ AtomicLong
ealeon

7

การหมุนหน่วยความจำอาจเป็นปัญหาที่นี่เนื่องจากการชกมวยของ int ที่มากกว่าหรือเท่ากับ 128 ทำให้การจัดสรรวัตถุ (ดู Integer.valueOf (int)) แม้ว่าตัวเก็บขยะจะจัดการกับวัตถุระยะสั้นได้อย่างมีประสิทธิภาพ แต่ประสิทธิภาพจะลดลงในระดับหนึ่ง

หากคุณรู้ว่าจำนวนที่เพิ่มขึ้นส่วนใหญ่จะมีจำนวนมากกว่าจำนวนคีย์ (= คำในกรณีนี้) ให้พิจารณาใช้ผู้ถือ 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();
}

หากคุณต้องการประสิทธิภาพที่ยอดเยี่ยมให้มองหาการใช้งานแผนที่ซึ่งปรับให้ตรงกับประเภทของค่าดั้งเดิม jrudolph พูดถึงGNU TroveTrove

อย่างไรก็ตามคำค้นหาที่ดีสำหรับหัวข้อนี้คือ "ฮิสโตแกรม"


5

แทนการเรียก containKey () มันเร็วกว่าเพียงแค่เรียก map.get และตรวจสอบว่าค่าที่ส่งคืนเป็นโมฆะหรือไม่

    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 และนี่เป็นแอปพลิเคชันคลาสเซิร์ฟเวอร์ให้ใช้ - เซิร์ฟเวอร์บนบรรทัดคำสั่งเพื่อใช้เซิร์ฟเวอร์ Hotspot JVM บนเครื่อง Linux และ Solaris สิ่งนี้จะตรวจสอบโดยอัตโนมัติ


3

มีสองวิธี:

  1. ใช้กระเป๋าตามระยะเวลาเช่นชุดที่มีอยู่ใน Google Collections

  2. สร้างคอนเทนเนอร์ที่ไม่แน่นอนซึ่งคุณสามารถใช้ในแผนที่:


    class My{
        String word;
        int count;
    }

และใช้คำสั่ง put ("word", new My ("Word")); จากนั้นคุณสามารถตรวจสอบว่ามันมีอยู่และเพิ่มขึ้นเมื่อมีการเพิ่ม

หลีกเลี่ยงการนำเสนอโซลูชันของคุณเองด้วยการใช้รายการเพราะถ้าคุณได้รับการค้นหาและเรียงลำดับ Innerloop ประสิทธิภาพของคุณจะเหม็น วิธีการแก้ปัญหา HashMap แรกค่อนข้างเร็วจริง ๆ แต่วิธีที่เหมาะสมที่พบใน Google Collections น่าจะดีกว่า

การนับคำโดยใช้ Google Collections มีลักษณะดังนี้:



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


การใช้ HashMultiset นั้นค่อนข้างสง่างามเพราะกระเป๋าอัลกอริทึมเป็นสิ่งที่คุณต้องการเมื่อนับจำนวนคำ


3

ฉันคิดว่าวิธีแก้ปัญหาของคุณจะเป็นวิธีมาตรฐาน แต่เมื่อคุณจดบันทึกตัวเองมันอาจไม่ใช่วิธีที่เร็วที่สุด

คุณอาจจะดูGNU Trove นั่นคือห้องสมุดที่มีคอลเล็กชันดั้งเดิมที่รวดเร็วทุกประเภท ตัวอย่างของคุณจะใช้TObjectIntHashMapซึ่งมีวิธีการปรับ ORPutValue ซึ่งทำสิ่งที่คุณต้องการ


ลิงก์ไปที่ TObjectIntHashMap ใช้งานไม่ได้ นี่คือลิงค์ที่ถูกต้อง: trove4j.sourceforge.net/javadocs/gnu/trove/map/ ......
Erel Segal-Halevi

ขอบคุณ Erel ฉันแก้ไขลิงก์แล้ว
jrudolph

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)รายการใหม่ถ้าจำเป็น
- รายการอาจเป็นวัตถุของคุณเอง
->(new MyHashSet()).get(k).increment();


3

ค่อนข้างง่ายเพียงใช้ฟังก์ชั่นในตัวMap.javaดังต่อไปนี้

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

สิ่งนี้ไม่ได้เป็นการเพิ่มค่า แต่เพียงแค่ตั้งค่าปัจจุบันหรือ 0 หากไม่มีการกำหนดค่าให้กับคีย์
siegi

คุณสามารถเพิ่มค่าโดย++... OMG มันง่ายมาก @siegi
sudoz

สำหรับเร็กคอร์ด: ++ไม่ทำงานที่ใดก็ได้ในนิพจน์นี้เนื่องจากต้องการตัวแปรเป็นตัวถูกดำเนินการ แต่มีเพียงค่า นอกเหนือจากการ+ 1ทำงานของคุณ ตอนนี้การแก้ปัญหาของคุณเป็นเช่นเดียวกับในคำตอบ off99555s
siegi

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
}

หากการนับเริ่มต้นที่ 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

ห่อดั้งเดิมต่างๆเช่นIntegerจะไม่เปลี่ยนรูปเพื่อให้มีจริงๆไม่ได้เป็นวิธีที่รัดกุมมากขึ้นที่จะทำสิ่งที่คุณขอยกเว้นกรณีที่คุณสามารถทำมันกับสิ่งที่ต้องการAtomicLong ฉันสามารถให้เวลาในหนึ่งนาทีและอัปเดต BTW, Hashtable เป็นส่วนหนึ่งของคอลเลกชันกรอบ


1

ฉันจะใช้ Apache Collections Lazy Map (เพื่อเริ่มต้นค่าเป็น 0) และใช้ MutableIntegers จาก Apache Lang เป็นค่าในแผนที่นั้น

ค่าใช้จ่ายที่ใหญ่ที่สุดคือการค้นหาแผนที่สองครั้งในวิธีการของคุณ ในเหมืองคุณต้องทำเพียงครั้งเดียว เพิ่งได้รับค่า (มันจะได้รับการเริ่มต้นถ้าขาด) และเพิ่มขึ้น


1

โครงสร้างข้อมูลไลบรารีของFunctional JavaTreeMapมีupdateวิธีการในส่วนของ trunk ล่าสุด:

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: เกี่ยวกับคำตอบนี้ฉันจะแสดงความคิดเห็นถ้าฉันมีคะแนนตัวแทน แต่ฉันไม่ ฉันต้องการที่จะทราบว่าระดับของตัวนับที่กำหนดไว้นั้นไม่มีความปลอดภัยของเธรดเนื่องจากมันไม่เพียงพอที่จะซิงโครไนซ์ inc () โดยไม่มีค่าการซิงโครไนซ์ () เธรดการโทรค่าอื่น ๆ () ไม่รับประกันว่าจะเห็นค่าเว้นแต่จะมีการสร้างความสัมพันธ์ก่อนเกิดขึ้นกับการอัพเดท


หากคุณต้องการอ้างอิงคำตอบของใครบางคนให้ใช้ @ [ชื่อผู้ใช้] ที่ด้านบนเช่น @Vilmantas Baranauskas <เนื้อหาไปที่นี่>
แฮงค์เกย์

ฉันทำการดัดแปลงนั้นเพื่อทำความสะอาด
Alex Miller

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

1

หากคุณกำลังใช้Eclipse คอลเลกชัน , HashBagคุณสามารถใช้ มันจะเป็นวิธีที่มีประสิทธิภาพมากที่สุดในแง่ของการใช้หน่วยความจำและจะทำงานได้ดีในแง่ของความเร็วในการดำเนินการ

HashBagได้รับการสนับสนุนโดยMutableObjectIntMapที่เก็บดั้งเดิมดั้งเดิมแทนCounterวัตถุ สิ่งนี้จะช่วยลดโอเวอร์เฮดของหน่วยความจำและเพิ่มความเร็วในการประมวลผล

HashBagให้ API ที่คุณต้องการเนื่องจากเป็นตัวCollectionช่วยให้คุณสามารถค้นหาจำนวนรายการที่เกิดขึ้นได้

นี่คือตัวอย่างจากคราสคอลเลกชันกะตะ

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



-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

หวังว่าฉันจะเข้าใจคำถามของคุณถูกต้องฉันมาที่ Java จาก Python เพื่อที่ฉันจะได้เห็นอกเห็นใจในการต่อสู้ของคุณ

ถ้าคุณมี

map.put(key, 1)

คุณจะทำ

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

หวังว่านี่จะช่วยได้!

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.