Найефективніший спосіб збільшення значення Карти на Java


377

Сподіваюсь, це питання не вважається занадто базовим для цього форуму, але ми побачимо. Мені цікаво, як перефактурувати якийсь код для кращої продуктивності, який запускається багато разів.

Скажімо, я створюю список частот слів, використовуючи Map (можливо HashMap), де кожен ключ - це рядок зі словом, що підраховується, а значення - це ціле число, що збільшується щоразу, коли знайдено знак слова.

У Perl збільшення такого значення було б тривіально простим:

$map{$word}++;

Але на Java це набагато складніше. Ось як я зараз це роблю:

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

Що, звичайно, покладається на функцію автобоксингу в новіших версіях Java. Цікаво, чи можете ви запропонувати більш ефективний спосіб збільшення такої цінності. Чи є навіть вагомі причини для того, щоб уникнути рамки колекцій та використовувати щось інше?

Оновлення: я зробив тест на кілька відповідей. Дивись нижче.


Я думаю, це було б те саме для java.util.Hashtable.
jrudolph

2
Звичайно, якщо це було б так само, оскільки Hashtable - це інфактна карта.
viskiysierra

Java 8: computeIfAbsent приклад: stackoverflow.com/a/37439971/1216775
akhil_mittal

Відповіді:


366

Деякі результати тесту

Я отримав багато хороших відповідей на це питання - дякую людям - тому вирішив пройти кілька тестів і з’ясувати, який метод насправді найшвидший. Я перевірив п'ять методів:

  • метод "ContainsKey", який я представив у запитанні
  • метод "TestForNull", запропонований Олександром Димитров
  • метод "AtomicLong", запропонований Генком Гей
  • метод "Trove", запропонований jrudolph
  • метод "MutableInt", запропонований phax.myopenid.com

Метод

Ось що я зробив ...

  1. створили п'ять класів, які були ідентичними, за винятком відмінностей, показаних нижче. Кожен клас повинен був виконати операцію, типову для представленого мною сценарію: відкрити файл 10 МБ і прочитати його, а потім виконати підрахунок частоти всіх символів слова у файлі. Оскільки це займало в середньому лише 3 секунди, я мав його виконувати підрахунок частоти (не введення / виводу) 10 разів.
  2. присвоїв цикл 10 ітерацій, але не операцію вводу / виводу, і записав загальний час (за годинникові секунди), по суті, використовуючи метод Яна Дарвіна в Java Cookbook .
  3. виконував усі п’ять тестів серійно, а потім робив це ще три рази.
  4. усереднено чотири результати для кожного методу.

Результати

Спочатку я представлю результати та код нижче для тих, хто цікавиться.

Метод ContainsKey був, як очікувалося, найбільш повільним, тому я дам швидкість кожного методу порівняно зі швидкістю цього методу.

  • Містить Ключ : 30.654 секунди (базовий рівень)
  • AtomicLong: 29,780 секунд (1,03 рази швидше)
  • TestForNull: 28.804 секунди (1,06 рази швидше)
  • Trove : 26.313 секунд (1,16 рази швидше)
  • MutableInt: 25,747 секунди (1,19 рази швидше)

Висновки

Здається, що лише метод MutableInt і метод Trove значно швидше, оскільки лише вони дають підвищення продуктивності більше ніж на 10%. Однак якщо нитка - це проблема, AtomicLong може бути привабливішим за інші (я не дуже впевнений). Я також запускав TestForNull зі finalзмінними, але різниця була незначною.

Зауважте, що я не переробляв використання пам'яті в різних сценаріях. Я був би радий почути від кого-небудь, хто має хорошу думку про те, як методи 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);

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

Trove

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), ви, ймовірно, отримаєте покращення цих результатів тесту.
Лі Колдвелл,

2
Я робив те саме нещодавно з підходом, схожим на MutableInt. Я радий почути, що це було оптимальне рішення (я просто припускав, що воно є, не роблячи ніяких тестів).
Кіп

Приємно чути, що ти швидший за мене, Кіп. ;-) Повідомте мене, якщо ви виявите якісь недоліки такого підходу.
Григорій

4
У випадку Atomic Long не було б більш ефективно зробити це за один крок (тому у вас є лише 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. Це атомний? або я повинен оточувати його синхронізованим?
Тіна

4
це, схоже, не спрацювало для мене, але все map.merge(key, 1, (a, b) -> a + b); ж
russter

2
Характеристики @Tiina Atomicity є специфічними для впровадження, пор. Документи : "Реалізація за замовчуванням не дає гарантій щодо властивостей синхронізації чи атомності цього методу. Будь-яка реалізація, що забезпечує гарантії атомності, повинна перекривати цей метод і документувати його паралельні властивості. Зокрема, всі реалізації підінтерфейсу ConcurrentMap повинні документувати, чи застосовується функція один раз атомічно, лише якщо значення немає ".
jensgram

2
Для groovy він не сприймає Integer::sumяк BiFunction, і не любив @russter відповідати так, як це було написано. Це працювало для менеMap.merge(key, 1, { a, b -> a + b})
jookyone

2
@russter, я знаю, що ваш коментар був більше року тому, але ви пам'ятаєте, чому це не працювало для вас? Ви отримали помилку компіляції або значення не збільшувалося?
Павло

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

Результати часу \ простір:


2
Дякую, це було дуже корисно. Було б круто додати мультисет Guava (наприклад, HashMultiset) до еталону.
кабад

34

Google Guava - ваш друг ...

... принаймні в деяких випадках. У них є цей приємний AtomicLongMap . Особливо приємно , тому що ви маєте справу з довжиною як значення в вашій картці.

Напр

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

Також можна додати до значення більше 1:

map.getAndAdd(word, 112L); 

7
AtomicLongMap#getAndAddприймає примітивний, longа не клас обгортки; немає сенсу робити new Long(). І AtomicLongMapє параметризованим типом; ви повинні були оголосити це як AtomicLongMap<String>.
Хелдер Перейра

32

@Hank Gay

Як продовження мого власного (досить марного) коментаря: Трове виглядає як шлях. Якщо з будь-якої причини ви хотіли дотримуватися стандартних 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 може повернути нульове значення, якщо вказаний ключ ще не асоційований із значенням всередині Map, тому я буду обережно використовувати повернене значення. docs.oracle.com/javase/8/docs/api/java/util/…
бумбур

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
  • Не покладаючись ні на яку бібліотеку
  • Легко зрозуміти, що саме відбувається (Не надто велика абстракція)

Нижня сторона:

  • Хеш-карту буде двічі шукати get () та put (). Тож це буде не найефективніший код.

Теоретично, коли ви телефонуєте get (), ви вже знаєте, куди поставити (), тому вам не доведеться шукати знову. Але пошук на хеш-карті зазвичай займає дуже мінімальний час, щоб ви могли ігнорувати цю проблему з продуктивністю.

Але якщо ви дуже серйозно ставитесь до проблеми, ви перфекціоніст, інший спосіб - використовувати метод злиття, це (мабуть) ефективніше, ніж попередній фрагмент коду, оскільки ви (теоретично) будете шукати карту лише один раз: (хоча цей код не очевидний з першого погляду, він короткий і виконавський)

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

Пропозиція: вам слід піклуватися про читабельність коду більше, ніж невеликий приріст продуктивності протягом більшої частини часу. Якщо перший фрагмент коду вам легше зрозуміти, використовуйте його. Але якщо ви зможете зрозуміти 2-й штраф, то ви також можете піти на це!


Метод getOfDefault недоступний у JAVA 7. Як я можу досягти цього в JAVA 7?
танві

1
Тоді вам, можливо, доведеться покладатися на інші відповіді. Це працює лише в Java 8.
off99555

1
+1 для рішення об’єднання, це буде найвищою функцією, оскільки вам потрібно заплатити лише 1 раз за розрахунок хеш-коду (у випадку, якщо карта, якою ви користуєтесь, належним чином підтримує метод), а не потенційно платити за неї 3 разів
Ferrybig

2
Використання методу умовиводу: map.merge (ключ, 1, ціле число :: сума)
earandap

25

Завжди корисно подивитися цю бібліотеку в колекціях Google . У цьому випадку Multiset виконає трюк:

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

Існують картоподібні методи ітерації ключів / записів і т. Д. Внутрішня реалізація в даний час використовує a HashMap<E, AtomicInteger>, тому ви не будете нести витрат на бокс.


Наведений вище відповідь повинен відображати корисну реакцію. Апі змінився з моменту публікації (3 роки тому :))
Стів

Чи працює count()метод на мультисеті в O (1) або O (n) час (найгірший)? Документи в цьому питанні незрозумілі.
Адам Паркін

Мій алгоритм такого роду: якщо (hasApacheLib (річ)) повернути apacheLib; інакше якщо (hasOnGuava (річ)) повернути гуаву. Зазвичай я не проходжу ці два кроки. :)
digao_mb

22

Ви повинні знати про те, що ваша перша спроба

int count = map.containsKey (слово)? map.get (слово): 0;

містить дві потенційно дорогі операції на карті, а саме containsKeyі get. Перший виконує операцію, яка може бути дуже схожою на останню, тому ви виконуєте ту саму роботу двічі !

Якщо ви подивитеся на API для Map, getоперації зазвичай повертаються, nullколи карта не містить запитуваного елемента.

Зауважте, що це дозволить зробити таке рішення

map.put (ключ, map.get (ключ) + 1);

небезпечно, оскільки може призвести до NullPointerExceptionс. Слід перевірити nullспочатку.

Також зауважте , і це дуже важливо, що HashMaps може містити nullsза визначенням. Тож не кожен повернений nullкаже, що "такого елемента немає". У цьому відношенні containsKeyповодиться інакше, ніж getнасправді говорити вам, чи є такий елемент. Для отримання детальної інформації зверніться до API.

Однак для вашого випадку, можливо, ви не хочете розрізняти збережений nullі "noSuchElement". Якщо ви не хочете дозволити nulls, ви можете віддати перевагу Hashtable. Використання бібліотеки для обгортки, як уже було запропоновано в інших відповідях, може бути кращим рішенням для ручного лікування, залежно від складності вашої заявки.

Для завершення відповіді (і я забув сказати, що спочатку, завдяки функції редагування!), Найкращий спосіб зробити це натисненим способом - це перейти getв finalзмінну, перевірити nullі putповернути її з а 1. Змінна повинна бути, finalтому що вона в будь-якому випадку незмінна. Цей компілятор може не потребувати цього підказу, але його зрозуміліший спосіб.

остаточна карта HashMap = generatorRandomHashMap ();
заключний ключ об’єкта = fetchSomeKey ();
final Integer i = map.get (ключ);
if (i! = null) {
    map.put (i + 1);
} else {
    // робити щось
}

Якщо ви не хочете покладатися на автобоксинг, вам слід сказати щось подібне map.put(new Integer(1 + i.getValue()));.


Щоб уникнути проблеми початкових неописаних / нульових значень у groovy, я закінчую: counts.put (ключ, (counts.get (ключ)?: 0) + 1) // надмірно складна версія ++
Джо Атцбергер

2
Або, найпростіше: counts = [:]. WithDefault {0} // ++ away
Джо Атцбергер

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 під час першого введення його на карту?
Том Хотін - тайклін

5
Commons-lang Apache вже написав для вас 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перевіряє, чи вказаний ключ вже пов'язаний зі значенням чи ні? Якщо немає пов'язаного значення, воно намагається обчислити його значення за допомогою заданої функції відображення. У будь-якому випадку він повертає поточне (існуюче або обчислене) значення, пов'язане із вказаним ключем, або null, якщо обчислене значення є null.

Зі сторони, якщо у вас є ситуація, коли кілька потоків оновлюють загальну суму, ви можете подивитися на клас LongAdder. Під великою суперечливістю очікувана пропускна здатність цього класу значно більша, ніж AtomicLongза рахунок більшого споживання місця.


чому паралельні Hashmap та 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();
}

Якщо вам потрібна надзвичайна ефективність, знайдіть реалізацію Map, яка безпосередньо налаштована на примітивні типи значень. jrudolph згадав GNU Trove .

До речі, хорошим пошуковим терміном для цього предмета є "гістограма".


5

Замість того, щоб викликати міститьKey (), швидше просто зателефонувати на 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 і це додаток для серверного класу, використовуйте -server у командному рядку, щоб використовувати JVM Server Hotspot. На машинах Linux і Solaris це автоматично виявлено.


3

Існує пара підходів:

  1. Використовуйте алоритм "Мішок", як набори, що містяться в колекціях Google.

  2. Створіть змінний контейнер, який ви можете використовувати на карті:


    class My{
        String word;
        int count;
    }

І використовуйте put ("слово", новий My ("Word")); Потім ви можете перевірити, чи існує він і збільшується при додаванні.

Уникайте прокрутки власного рішення за допомогою списків, тому що якщо ви будете шукати і сортувати внутрішній цикл, ваша ефективність буде смердючою. Перше рішення 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, який має метод prilagodOrPutValue, який робить саме те, що ви хочете.


Посилання на TObjectIntHashMap порушено. Це правильне посилання: trove4j.sourceforge.net/javadocs/gnu/trove/map/…
Segal-Halevi

Спасибі, Ереле, я зафіксував посилання.
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 HashMultiset:
- досить елегантний у використанні
- але споживає процесор і пам’ять

Найкраще було б мати такий метод, як: 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

"put" need "get" (щоб не було дублікату ключа).
Тож безпосередньо зробіть "put",
а якщо було попереднє значення, то зробіть додавання:

Map map = new HashMap ();

MutableInt newValue = new MutableInt (1); // default = inc
MutableInt oldValue = map.put (key, newValue);
if (oldValue != null) {
  newValue.add(oldValue); // old + inc
}

Якщо число починається з 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 . Я можу це зробити за хвилину і оновити. До речі, Hashtable є частиною Рамок колекцій .


1

Я б використовував Apache Collections Lazy Map (для ініціалізації значень до 0) і використовував MutableIntegers з Apache Lang як значення на цій карті.

Найбільша вартість полягає у тому, щоб у вашому методі двічі підняти карту. У моєму ви повинні це зробити лише один раз. Просто отримайте значення (воно буде ініціалізовано, якщо воно відсутнє) та збільште його.


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


1

@Vilmantas Baranauskas: Щодо цієї відповіді, я б прокоментував, якби у мене були пункти повторення, але я цього не роблю. Я хотів би зазначити, що визначений там Counter клас не є безпечним для потоків, оскільки його недостатньо просто синхронізувати inc () без синхронізуючого значення (). Інші потоки, що викликають значення (), не гарантують, що вони побачать це значення, якщо не буде встановлено зв'язок перед оновленням.


Якщо ви хочете посилатися на чиюсь відповідь, використовуйте @ [Ім'я користувача] вгорі, наприклад, @Vilmantas Baranauskas <Вміст йде сюди>
Hank Gay

Я вніс цю модифікацію, щоб очистити її.
Алекс Міллер

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 Collection , ви можете використовувати a HashBag. Це буде найефективнішим підходом з точки зору використання пам’яті, а також з точки зору швидкості виконання.

HashBagпідтримується тим, MutableObjectIntMapщо зберігає примітивні вставки замість Counterоб'єктів. Це зменшує накладні витрати на пам'ять та покращує швидкість виконання.

HashBag надає API, який вам знадобиться, оскільки він є Collection також дозволяє запитувати кількість входжень товару.

Ось приклад із колекцій Eclipse Kata .

MutableBag<String> bag =
  HashBag.newBagWith("one", "two", "two", "three", "three", "three");

Assert.assertEquals(3, bag.occurrencesOf("three"));

bag.add("one");
Assert.assertEquals(2, bag.occurrencesOf("one"));

bag.addOccurrences("one", 4);
Assert.assertEquals(6, bag.occurrencesOf("one"));

Примітка. Я є членом колекції Eclipse.


1

Я пропоную використовувати Java 8 Map :: compute (). Він розглядає випадок, коли ключ теж не існує.

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

mymap.merge(key, 1, Integer::sum)?
Дет

-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

Сподіваюся, що я правильно розумію ваше запитання, я приходжу на Яву з Python, щоб я міг співпереживати вашій боротьбі.

якщо у вас є

map.put(key, 1)

ти зробив би

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

Сподіваюся, це допомагає!

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.