Cara paling efisien untuk meningkatkan nilai Peta di Jawa

377

Saya harap pertanyaan ini tidak dianggap terlalu mendasar untuk forum ini, tetapi kita lihat saja nanti. Saya bertanya-tanya bagaimana cara memperbaiki beberapa kode untuk kinerja yang lebih baik yang dijalankan beberapa kali.

Katakanlah saya sedang membuat daftar frekuensi kata, menggunakan Peta (mungkin HashMap), di mana setiap kunci adalah String dengan kata yang sedang dihitung dan nilainya adalah Integer yang bertambah setiap kali token kata ditemukan.

Dalam Perl, menambahkan nilai seperti itu akan mudah:

$map{$word}++;

Tetapi di Jawa, ini jauh lebih rumit. Di sini cara saya saat ini melakukannya:

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

Yang tentu saja bergantung pada fitur autoboxing di versi Java yang lebih baru. Saya ingin tahu apakah Anda dapat menyarankan cara yang lebih efisien untuk meningkatkan nilai seperti itu. Apakah ada alasan kinerja yang baik untuk menghindari kerangka kerja Koleksi dan menggunakan sesuatu yang lain sebagai gantinya?

Pembaruan: Saya telah melakukan tes beberapa jawaban. Lihat di bawah.

menyenangkan
sumber
Saya pikir itu akan sama untuk java.util.Hashtable.
jrudolph
2
Tentu saja jika akan sama, karena Hashtable adalah sebuah Map.
whiskeysierra
Java 8: computeIfAbsent contoh: stackoverflow.com/a/37439971/1216775
akhil_mittal

Jawaban:

367

Beberapa hasil tes

Saya mendapatkan banyak jawaban yang bagus untuk pertanyaan ini - terima kasih semuanya - jadi saya memutuskan untuk menjalankan beberapa tes dan mencari tahu metode mana yang sebenarnya tercepat. Lima metode yang saya uji adalah:

  • metode "ContainsKey" yang saya sajikan dalam pertanyaan
  • metode "TestForNull" yang disarankan oleh Aleksandar Dimitrov
  • metode "AtomicLong" yang disarankan oleh Hank Gay
  • metode "Trove" yang disarankan oleh jrudolph
  • metode "MutableInt" yang disarankan oleh phax.myopenid.com

metode

Inilah yang saya lakukan ...

  1. menciptakan lima kelas yang identik kecuali untuk perbedaan yang ditunjukkan di bawah ini. Setiap kelas harus melakukan operasi khas skenario yang saya sajikan: membuka file 10MB dan membacanya, lalu melakukan penghitungan frekuensi semua kata token dalam file. Karena ini mengambil rata-rata hanya 3 detik, saya sudah melakukan penghitungan frekuensi (bukan I / O) 10 kali.
  2. menghitung waktu loop 10 iterasi tetapi bukan operasi I / O dan mencatat total waktu yang diambil (dalam detik jam) pada dasarnya menggunakan metode Ian Darwin di Java Cookbook .
  3. melakukan semua lima tes secara seri, dan kemudian melakukan ini tiga kali lagi.
  4. rata-rata empat hasil untuk setiap metode.

Hasil

Saya akan mempresentasikan hasil pertama dan kode di bawah ini untuk mereka yang tertarik.

The ContainsKey metode itu, seperti yang diharapkan, paling lambat, jadi saya akan memberikan kecepatan setiap metode dibandingkan dengan kecepatan metode tersebut.

  • ContainsKey: 30,654 detik (garis dasar)
  • AtomicLong: 29,780 detik (1,03 kali lebih cepat)
  • TestForNull: 28,804 detik (1,06 kali lebih cepat)
  • Trove: 26,313 detik (1,16 kali lebih cepat)
  • MutableInt: 25,747 detik (1,19 kali lebih cepat)

Kesimpulan

Tampaknya hanya metode MutableInt dan metode Trove yang secara signifikan lebih cepat, hanya mereka yang memberikan peningkatan kinerja lebih dari 10%. Namun, jika threading adalah masalah, AtomicLong mungkin lebih menarik daripada yang lain (saya tidak begitu yakin). Saya juga menjalankan TestForNull dengan finalvariabel, tetapi perbedaannya dapat diabaikan.

Perhatikan bahwa saya belum membuat profil penggunaan memori dalam berbagai skenario. Saya akan senang mendengar dari siapa pun yang memiliki wawasan yang baik tentang bagaimana metode MutableInt dan Trove akan mempengaruhi penggunaan memori.

Secara pribadi, saya menemukan metode MutableInt yang paling menarik, karena tidak perlu memuat kelas pihak ketiga. Jadi, kecuali saya menemukan masalah dengan itu, itulah cara saya kemungkinan besar pergi.

Kode

Berikut adalah kode penting dari setiap metode.

Berisi kunci

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

Harta karun

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();
}
menyenangkan
sumber
3
Kerja bagus, bagus sekali. Sebuah komentar kecil - panggilan putIfAbsent () dalam kode AtomicLong akan membuat Instantiate AtomicLong baru (0) bahkan jika sudah ada di peta. Jika Anda mengubah ini untuk digunakan if (map.get (key) == null) sebagai gantinya, Anda mungkin akan mendapatkan peningkatan dalam hasil tes tersebut.
Leigh Caldwell
2
Saya melakukan hal yang sama baru-baru ini dengan pendekatan yang mirip dengan MutableInt. Saya senang mendengar itu adalah solusi optimal (saya hanya berasumsi, tanpa melakukan tes).
Kip
Senang mendengar bahwa Anda lebih cepat dari saya, Kip. ;-) Beritahu saya jika Anda menemukan kekurangan untuk pendekatan itu.
gregory
4
Dalam Atomic Long case bukankah lebih efisien untuk melakukannya dalam satu langkah (sehingga Anda hanya memiliki 1 operasi yang mahal alih-alih 2) "map.putIfAbsent (kata, AtomicLong baru (0)). IncrementAndGet ();"
smartnut007
1
@gregory apakah Anda mempertimbangkan Java 8 freq.compute(word, (key, count) -> count == null ? 1 : count + 1)? Secara internal ia melakukan pencarian yang kurang hash daripada containsKey, akan menarik untuk melihat bagaimana membandingkannya dengan yang lain, karena lambda.
TWiStErRob
255

Sekarang ada cara yang lebih pendek dengan Java 8 menggunakan Map::merge.

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

Apa fungsinya:

  • jika kunci tidak ada, masukkan 1 sebagai nilai
  • jika tidak, jumlahkan 1 ke nilai yang dikaitkan dengan kunci

Informasi lebih lanjut di sini .

LE GALL Benoît
sumber
selalu cinta java 8. Apakah atom ini? atau haruskah saya mengelilinginya dengan sinkronisasi?
Tiina
4
ini sepertinya tidak berhasil bagi saya tetapi map.merge(key, 1, (a, b) -> a + b); berhasil
russter
2
Karakteristik @Tiina Atomicity spesifik untuk implementasi, lih. dokumen : "Implementasi default tidak memberikan jaminan tentang sinkronisasi atau sifat-sifat atomisitas dari metode ini. Setiap implementasi yang memberikan jaminan atomitas harus mengesampingkan metode ini dan mendokumentasikan properti konkurensinya. Secara khusus, semua implementasi dari subinterface ConcurrentMap harus mendokumentasikan apakah fungsi tersebut diterapkan sekali secara atomis hanya jika nilainya tidak ada. "
jensgram
2
Untuk asyik, itu tidak akan menerima Integer::sumsebagai BiFunction, dan tidak suka @russter menjawab cara itu ditulis. Ini berhasil untuk sayaMap.merge(key, 1, { a, b -> a + b})
jookyone
2
@russter, saya tahu komentar Anda lebih dari setahun yang lalu, tetapi apakah Anda ingat mengapa itu tidak berhasil untuk Anda? Apakah Anda mendapatkan kesalahan kompilasi atau nilainya tidak bertambah?
Paul
44

Sebuah penelitian kecil pada tahun 2016: https://github.com/leventov/java-word-count , kode sumber patokan

Hasil terbaik per metode (lebih kecil lebih baik):

                 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

Waktu \ ruang hasil:

leventov
sumber
2
Terima kasih, ini sangat membantu. Akan lebih baik untuk menambahkan Multiset Guava (misalnya, HashMultiset) ke patokan.
cabad
34

Google Jambu adalah teman Anda ...

... setidaknya dalam beberapa kasus. Mereka memiliki AtomicLongMap yang bagus ini . Terutama baik karena Anda berurusan dengan lama sebagai nilai di peta Anda.

Misalnya

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

Juga dimungkinkan untuk menambahkan lebih dari 1 ke nilai:

map.getAndAdd(word, 112L); 
H6.
sumber
7
AtomicLongMap#getAndAddmengambil kelas primitif longdan bukan kelas pembungkus; tidak ada gunanya melakukan new Long(). Dan AtomicLongMapmerupakan tipe parameter; Anda seharusnya menyatakannya sebagai AtomicLongMap<String>.
Helder Pereira
32

@Hank Gay

Sebagai tindak lanjut dari komentar saya (yang agak tidak berguna): Trove terlihat seperti cara untuk pergi. Jika, untuk alasan apapun, Anda ingin tetap dengan JDK standar, ConcurrentMap dan AtomicLong dapat membuat kode kecil sedikit lebih bagus, meskipun YMMV.

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

akan meninggalkan 1nilai di peta untuk foo. Secara realistis, peningkatan keramahan terhadap threading adalah semua yang harus direkomendasikan oleh pendekatan ini.

Hank Gay
sumber
9
PutIfAbsent () mengembalikan nilai. Ini bisa menjadi perbaikan besar untuk menyimpan nilai yang dikembalikan dalam variabel lokal dan menggunakannya untuk incrementAndGet () daripada panggilan get lagi.
smartnut007
putIfAbsent dapat mengembalikan nilai nol jika kunci yang ditentukan belum dikaitkan dengan nilai di dalam Peta jadi saya akan berhati-hati untuk menggunakan nilai yang dikembalikan. 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);

Dan itulah bagaimana Anda menambah nilai dengan kode sederhana.

Manfaat:

  • Tidak perlu menambahkan kelas baru atau menggunakan konsep lain dari int bisa berubah
  • Tidak mengandalkan perpustakaan apa pun
  • Mudah dimengerti apa yang sebenarnya terjadi (Tidak terlalu banyak abstraksi)

Kelemahan:

  • Peta hash akan dicari dua kali untuk mendapatkan () dan meletakkan (). Jadi itu bukan kode yang paling performant.

Secara teoritis, setelah Anda memanggil get (), Anda sudah tahu ke mana harus meletakkan (), jadi Anda tidak perlu mencari lagi. Tetapi mencari di peta hash biasanya membutuhkan waktu yang sangat minimal sehingga Anda dapat mengabaikan masalah kinerja ini.

Tetapi jika Anda sangat serius tentang masalah ini, Anda perfeksionis, cara lain adalah menggunakan metode penggabungan, ini (mungkin) lebih efisien daripada potongan kode sebelumnya karena Anda akan (secara teoritis) mencari peta hanya sekali: (meskipun kode ini tidak jelas dari pandangan pertama, pendek dan performan)

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

Saran: Anda harus lebih memperhatikan pembacaan kode lebih dari sedikit peningkatan kinerja di sebagian besar waktu. Jika potongan kode pertama lebih mudah untuk Anda pahami maka gunakanlah. Tetapi jika Anda dapat memahami yang ke-2 baik-baik saja maka Anda juga bisa melakukannya!

off99555
sumber
Metode getOfDefault tidak tersedia di JAVA 7. Bagaimana saya bisa mencapainya di JAVA 7?
tanvi
1
Anda mungkin harus mengandalkan jawaban lain. Ini hanya berfungsi di Java 8.
off99555
1
+1 untuk solusi gabungan, ini akan menjadi fungsi berperforma tertinggi karena Anda hanya perlu membayar 1 kali untuk perhitungan kode hash (dalam kasus Peta yang Anda gunakan pada metode yang didukung dengan benar), daripada berpotensi membayarnya 3 kali
Ferrybig
2
Menggunakan inferensi metode: map.merge (kunci, 1, Integer :: sum)
earandap
25

Itu selalu merupakan ide yang baik untuk melihat Perpustakaan Koleksi Google untuk hal semacam ini. Dalam hal ini Multiset akan melakukan trik:

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

Ada metode seperti Peta untuk mengulangi kunci / entri, dll. Secara internal implementasi saat ini menggunakan a HashMap<E, AtomicInteger>, sehingga Anda tidak akan dikenakan biaya tinju.

Chris Nokleberg
sumber
Penjawab di atas perlu merefleksikan respons respons. Api telah berubah sejak diposkan (3 tahun yang lalu :))
Steve
Apakah count()metode pada multiset berjalan dalam waktu O (1) atau O (n) (terburuk)? Dokumen tidak jelas tentang hal ini.
Adam Parkin
Algoritma saya untuk hal semacam ini: if (hasApacheLib (thing)) return apacheLib; lain jika (hasOnGuava (benda)) mengembalikan jambu biji. Biasanya saya tidak bisa melewati dua langkah ini. :)
digao_mb
22

Anda harus menyadari fakta bahwa upaya awal Anda

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

mengandung dua operasi yang berpotensi mahal pada peta, yaitu containsKeydan get. Yang pertama melakukan operasi yang berpotensi sangat mirip dengan yang terakhir, jadi Anda melakukan pekerjaan yang sama dua kali !

Jika Anda melihat API untuk Peta, getoperasi biasanya kembali nullketika peta tidak mengandung elemen yang diminta.

Perhatikan bahwa ini akan membuat solusi seperti

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

berbahaya, karena mungkin menghasilkan NullPointerExceptions. Anda harus memeriksa nulldulu.

Juga perhatikan , dan ini sangat penting, bahwa HashMaps dapat mengandung nullsdengan definisi. Jadi tidak setiap kembali nullmengatakan "tidak ada elemen seperti itu". Dalam hal ini, containsKeyberperilaku berbeda dari getdalam benar-benar memberitahu Anda apakah ada elemen seperti itu. Lihat API untuk detailnya.

Namun, untuk kasus Anda, Anda mungkin tidak ingin membedakan antara yang tersimpan nulldan "noSuchElement". Jika Anda tidak ingin mengizinkan null, Anda mungkin memilih a Hashtable. Menggunakan perpustakaan pembungkus seperti yang sudah diusulkan dalam jawaban lain mungkin merupakan solusi yang lebih baik untuk perawatan manual, tergantung pada kompleksitas aplikasi Anda.

Untuk menyelesaikan jawaban (dan saya lupa memasukkannya pada awalnya, berkat fungsi edit!), Cara terbaik untuk melakukannya secara asli, adalah ke getdalam finalvariabel, periksa nulldan putkembali dengan a 1. Variabelnya harus finalkarena tetap tidak berubah. Kompilator mungkin tidak memerlukan petunjuk ini, tetapi lebih jelas seperti itu.

peta HashMap akhir = menghasilkanRandomHashMap ();
kunci Objek akhir = fetchSomeKey ();
Integer akhir i = map.get (kunci);
if (i! = null) {
    map.put (i +1);
} lain {
    // lakukan sesuatu
}

Jika Anda tidak ingin mengandalkan autoboxing, Anda harus mengatakan sesuatu seperti itu map.put(new Integer(1 + i.getValue()));.

Aleksandar Dimitrov
sumber
Untuk menghindari masalah nilai awal yang tidak dipetakan / null di groovy, saya akhirnya melakukan: counts.put (key, (counts.get (key)?: 0) +1) // versi ++ yang terlalu rumit ++
Joe Atzberger
2
Atau, paling sederhana: counts = [:]. WithDefault {0} // ++ away
Joe Atzberger
18

Cara lain akan membuat integer yang bisa berubah:

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

tentu saja ini menyiratkan membuat objek tambahan tetapi overhead dibandingkan dengan membuat Integer (bahkan dengan Integer.valueOf) seharusnya tidak terlalu banyak.

Philip Helger
sumber
5
Anda tidak ingin memulai MutableInt pada 1 saat pertama kali Anda meletakkannya di peta?
Tom Hawtin - tackline
5
Commons-lang Apache memiliki MutableInt yang sudah ditulis untuk Anda.
SingleShot
11

Anda dapat menggunakan metode computeIfAbsent di Mapantarmuka yang disediakan di 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]

Metode ini computeIfAbsentmemeriksa apakah kunci yang ditentukan sudah dikaitkan dengan nilai atau tidak? Jika tidak ada nilai terkait maka ia mencoba menghitung nilainya menggunakan fungsi pemetaan yang diberikan. Dalam setiap kasus itu mengembalikan nilai saat ini (yang ada atau dihitung) terkait dengan kunci yang ditentukan, atau nol jika nilai yang dihitung adalah nol.

Di samping catatan jika Anda memiliki situasi di mana beberapa utas memperbarui jumlah umum Anda dapat melihat pada kelas LongAdder. Di bawah pertikaian tinggi, throughput yang diharapkan dari kelas ini secara signifikan lebih tinggi daripada AtomicLong, dengan mengorbankan konsumsi ruang yang lebih tinggi.

akhil_mittal
sumber
mengapa concurrentHashmap dan AtomicLong?
ealeon
7

Rotasi memori dapat menjadi masalah di sini, karena setiap tinju int yang lebih besar dari atau sama dengan 128 menyebabkan alokasi objek (lihat Integer.valueOf (int)). Meskipun pengumpul sampah sangat efisien menangani benda-benda berumur pendek, kinerja akan sedikit menurun.

Jika Anda tahu bahwa jumlah peningkatan yang dilakukan sebagian besar akan melebihi jumlah kunci (= kata dalam hal ini), pertimbangkan menggunakan int holder sebagai gantinya. Phax sudah menyajikan kode untuk ini. Ini dia lagi, dengan dua perubahan (kelas pemegang dibuat statis dan nilai awal diatur ke 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();
}

Jika Anda membutuhkan kinerja ekstrem, cari implementasi Peta yang langsung disesuaikan dengan tipe nilai primitif. jrudolph menyebut GNU Trove .

Omong-omong, istilah pencarian yang bagus untuk subjek ini adalah "histogram".

tembakan
sumber
5

Alih-alih memanggil containKey () lebih cepat hanya untuk memanggil map.get dan periksa apakah nilai yang dikembalikan adalah nol atau tidak.

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

Apakah Anda yakin ini adalah hambatan? Sudahkah Anda melakukan analisis kinerja?

Coba gunakan profiler NetBeans (gratis dan dibangun di NB 6.1) untuk melihat hotspot.

Akhirnya, peningkatan JVM (katakanlah dari 1,5-> 1,6) seringkali merupakan penambah kinerja yang murah. Bahkan peningkatan jumlah build dapat memberikan peningkatan kinerja yang baik. Jika Anda menjalankan pada Windows dan ini adalah aplikasi kelas server, gunakan -server pada baris perintah untuk menggunakan Server Hotspot JVM. Pada mesin Linux dan Solaris ini terdeteksi secara otomatis.


sumber
3

Ada beberapa pendekatan:

  1. Gunakan aloritma Bag seperti set yang terkandung dalam Google Collections.

  2. Buat wadah yang bisa berubah yang dapat Anda gunakan di Peta:


    class My{
        String word;
        int count;
    }

Dan gunakan put ("word", new My ("Word")); Kemudian Anda dapat memeriksa apakah ada dan bertambah saat menambahkan.

Hindari menggulung solusi Anda sendiri menggunakan daftar, karena jika Anda mencari dan menyortir innerloop, kinerja Anda akan berbau busuk. Solusi HashMap pertama sebenarnya cukup cepat, tetapi yang tepat seperti yang ditemukan di Google Collections mungkin lebih baik.

Menghitung kata menggunakan Google Collections, terlihat seperti ini:



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


Menggunakan HashMultiset cukup elegan, karena bag-algoritme hanya yang Anda butuhkan saat menghitung kata.

tovare
sumber
3

Saya pikir solusi Anda akan menjadi cara standar, tetapi - seperti yang Anda catat sendiri - itu mungkin bukan cara tercepat yang mungkin.

Anda dapat melihat GNU Trove . Itu adalah perpustakaan yang berisi segala macam Koleksi primitif cepat. Contoh Anda akan menggunakan TObjectIntHashMap yang memiliki metode sesuaikanOrPutValue yang melakukan persis apa yang Anda inginkan.

jrudolph
sumber
Tautan ke TObjectIntHashMap rusak. Ini adalah tautan yang benar: trove4j.sourceforge.net/javadocs/gnu/trove/map/…
Erel Segal-Halevi
Terima kasih, Erel, saya memperbaiki tautannya.
jrudolph
3

Variasi pada pendekatan MutableInt yang mungkin lebih cepat, jika sedikit peretasan, adalah dengan menggunakan array int elemen tunggal:

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

Akan menarik jika Anda dapat menjalankan kembali tes kinerja Anda dengan variasi ini. Mungkin yang tercepat.


Sunting: Pola di atas bekerja dengan baik untuk saya, tetapi akhirnya saya berubah untuk menggunakan koleksi Trove untuk mengurangi ukuran memori di beberapa peta yang sangat besar yang saya buat - dan sebagai bonus itu juga lebih cepat.

Salah satu fitur yang sangat bagus adalah bahwa TObjectIntHashMapkelas memiliki satu adjustOrPutValuepanggilan itu, tergantung pada apakah sudah ada nilai pada kunci itu, apakah akan memasukkan nilai awal atau menambah nilai yang ada. Ini sempurna untuk menambah:

TObjectIntHashMap<String> map = new TObjectIntHashMap<String>();
...
map.adjustOrPutValue(key, 1, 1);
Eamonn O'Brien-Strain
sumber
3

Google Collections HashMultiset:
- cukup elegan untuk digunakan
- tetapi mengonsumsi CPU dan memori

Yang terbaik adalah memiliki metode seperti: Entry<K,V> getOrPut(K); (elegan, dan biaya rendah)

Metode seperti itu akan menghitung hash dan indeks hanya sekali, dan kemudian kita bisa melakukan apa yang kita inginkan dengan entri (baik mengganti atau memperbarui nilainya).

Lebih elegan:
- ambil a HashSet<Entry>
- rentangkan sehingga get(K)letakkan Entri baru jika diperlukan
- Entri bisa menjadi objek Anda sendiri.
->(new MyHashSet()).get(k).increment();

felis leo
sumber
3

Cukup sederhana, cukup gunakan fungsi Map.javabawaan sebagai berikut

map.put(key, map.getOrDefault(key, 0) + 1);
sudoz
sumber
Ini tidak menambah nilai, itu hanya menetapkan nilai saat ini atau 0 jika tidak ada nilai yang diberikan pada kunci.
siegi
Anda dapat menambah nilainya dengan ++... OMG, ini sangat sederhana. @siegi
sudoz
Sebagai catatan: ++tidak bekerja di mana saja dalam ekspresi ini karena variabel diperlukan sebagai operan tetapi hanya ada nilai. Penambahan Anda + 1bekerja meskipun. Sekarang solusi Anda sama dengan jawaban off99555s .
siegi
2

"put" need "get" (untuk memastikan tidak ada kunci duplikat).
Jadi langsung lakukan "put",
dan jika ada nilai sebelumnya, maka lakukan penambahan:

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
}

Jika hitungan dimulai dari 0, maka tambahkan 1: (atau nilai lainnya ...)

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
}

Perhatikan: Kode ini tidak aman untuk thread. Gunakan untuk membangun lalu gunakan peta, bukan untuk memperbaruinya secara bersamaan.

Optimasi: Dalam satu lingkaran, pertahankan nilai lama untuk menjadi nilai baru dari loop berikutnya.

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
  }
}
felis leo
sumber
1

Berbagai pembungkus primitif, misalnya, Integertidak berubah sehingga benar-benar tidak ada cara yang lebih ringkas untuk melakukan apa yang Anda minta kecuali Anda dapat melakukannya dengan sesuatu seperti AtomicLong . Saya bisa mencobanya sebentar lagi dan memperbarui. BTW, Hashtable adalah bagian dari Collections Framework .

Hank Gay
sumber
1

Saya akan menggunakan Apache Collections Lazy Map (untuk menginisialisasi nilai ke 0) dan menggunakan MutableIntegers dari Apache Lang sebagai nilai di peta itu.

Biaya terbesar adalah harus menyisir peta dua kali dalam metode Anda. Di tangan saya, Anda harus melakukannya sekali saja. Dapatkan saja nilainya (akan diinisialisasi jika tidak ada) dan tambahkan.

jb.
sumber
1

Itu Fungsional Java perpustakaan TreeMapdatastructure memiliki updatemetode dalam kepala batang terbaru:

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

Contoh penggunaan:

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

Program ini mencetak "2".

Apocalisp
sumber
1

@Vantantas Baranauskas: Mengenai jawaban ini, saya akan berkomentar jika saya memiliki poin rep, tapi saya tidak. Saya ingin mencatat bahwa kelas Counter didefinisikan TIDAK ada thread-safe karena tidak cukup hanya menyinkronkan inc () tanpa nilai sinkronisasi (). Nilai panggilan utas lainnya () tidak dijamin untuk melihat nilainya kecuali hubungan yang terjadi sebelum hubungan telah terjadi dengan pembaruan.

Alex Miller
sumber
Jika Anda ingin merujuk jawaban seseorang, gunakan @ [Nama pengguna] di bagian atas, misalnya, @Vilmantas Baranauskas <Konten ada di sini>
Hank Gay
Saya membuat modifikasi itu untuk membersihkannya.
Alex Miller
1

Saya tidak tahu seberapa efisien itu tetapi kode di bawah ini berfungsi juga. Anda harus mendefinisikan BiFunctiondi awal. Plus, Anda dapat membuat lebih dari sekadar peningkatan dengan metode ini.

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

output adalah

3
1
MGoksu
sumber
1

Jika Anda menggunakan Eclipse Collections , Anda dapat menggunakan a HashBag. Ini akan menjadi pendekatan yang paling efisien dalam hal penggunaan memori dan juga akan bekerja dengan baik dalam hal kecepatan eksekusi.

HashBagdidukung oleh MutableObjectIntMapyang menyimpan int primitif bukan Counterobjek. Ini mengurangi overhead memori dan meningkatkan kecepatan eksekusi.

HashBag menyediakan API yang Anda perlukan karena itu a Collection yang juga memungkinkan Anda untuk menanyakan jumlah kemunculan suatu item.

Berikut adalah contoh dari 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"));

Catatan: Saya pengendara untuk Eclipse Collections.

Craig P. Motlin
sumber
1

Saya sarankan untuk menggunakan Java 8 Map :: compute (). Itu mempertimbangkan kasus ketika kunci tidak ada juga.

Map.compute(num, (k, v) -> (v == null) ? 1 : v + 1);
Eugene Chung
sumber
mymap.merge(key, 1, Integer::sum)?
Det
-2

Karena banyak orang mencari topik Java untuk jawaban Groovy, berikut ini cara melakukannya di 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}
Keith
sumber
-2

Cara sederhana dan mudah di java 8 adalah sebagai berikut:

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

Semoga saya mengerti pertanyaan Anda dengan benar, saya datang ke Jawa dari Python sehingga saya bisa berempati dengan perjuangan Anda.

jika Anda memiliki

map.put(key, 1)

kamu akan lakukan

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

Semoga ini membantu!

ggaugler
sumber