Как я могу избежать повторения кода, инициализирующего hashmap hashmap?

27

У каждого клиента есть идентификатор и множество счетов с датами, которые хранятся в виде Hashmap клиентов по идентификатору, хэш-карты счетов по дате:

HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.get(id);

if(allInvoices!=null){
    allInvoices.put(date, invoice);      //<---REPEATED CODE
}else{
    allInvoices = new HashMap<>();
    allInvoices.put(date, invoice);      //<---REPEATED CODE
    allInvoicesAllClients.put(id, allInvoices);
}

Решением Java кажется использовать getOrDefault:

HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.getOrDefault(
    id,
    new HashMap<LocalDateTime, Invoice> (){{  put(date, invoice); }}
);

Но если get не равен null, я по-прежнему хочу, чтобы put (дата, счет-фактура) выполнялся, а добавление данных в «allInvoicesAllClients» все еще необходимо. Так что, похоже, это не сильно помогает.

Эрнан Эш
источник
Если вы не можете гарантировать уникальность ключа, лучше всего, чтобы вторичная карта имела значение List <Invoice>, а не только Invoice.
Райан

Ответы:

39

Это отличный вариант использования для Map#computeIfAbsent. Ваш фрагмент по сути эквивалентен:

allInvoicesAllClients.computeIfAbsent(id, key -> new HashMap<>()).put(date, invoice);

Если idего нет в качестве ключа allInvoicesAllClients, он создаст отображение из idнового HashMapи вернет новое HashMap. Если idприсутствует в качестве ключа, то он вернет существующий HashMap.

Джейкоб Г.
источник
1
computeIfAbsent, выполняет get (id) (или пут, сопровождаемый get (id)), поэтому следующий шаг выполняется для исправления позиции (дата), правильного ответа.
Эрнан Эче
allInvoicesAllClients.computeIfAbsent(id, key -> Map.of(date, invoice))
Александр - Восстановить Монику
1
@ Alexander-ReinstateMonica Map.ofсоздает неизменяемое Map, которое я не уверен, что ОП хочет.
Джейкоб Г.
Будет ли этот код менее эффективным, чем тот, который изначально имел ОП? Спрашивая это, потому что я не знаком с тем, как Java обрабатывает лямбда-функции.
Цеконг Ху
16

computeIfAbsentэто отличное решение для этого конкретного случая. В общем, хотелось бы отметить следующее, так как никто еще не упомянул об этом:

«Внешний» hashmap просто хранит ссылку на «внутренний» hashmap, так что вы можете просто изменить порядок операций, чтобы избежать дублирования кода:

HashMap<LocalDateTime, Invoice> allInvoices = allInvoicesAllClients.get(id);

if (allInvoices == null) {           
    allInvoices = new HashMap<>();
    allInvoicesAllClients.put(id, allInvoices);
}

allInvoices.put(date, invoice);      // <--- no longer repeated
Heinzi
источник
Так мы делали это десятилетиями, пока Java 8 не появилась вместе со своим причудливым computeIfAbsent()методом!
Нил Бартлетт
1
Я все еще использую этот подход сегодня в языках, где реализация карты не предоставляет единственного метода get-or-put-and-return-if-absent. Стоит упомянуть, что это может быть лучшим решением на других языках, хотя этот вопрос специально помечен для Java 8.
Куинн Мортимер,
11

Вы почти никогда не должны использовать инициализацию карты "двойной скобкой".

{{  put(date, invoice); }}

В этом случае вы должны использовать computeIfAbsent

allInvoicesAllClients.computeIfAbsent(id, (k) -> new HashMap<>())
                     .put(date, allInvoices);

Если для этого идентификатора карты нет, вы ее вставите. Результатом будет существующая или вычисленная карта. Затем вы можете putразмещать элементы на этой карте, гарантируя, что она не будет нулевой.

Майкл
источник
1
Я не знаю, кто понизил голос, а не я, возможно, однострочный код сбивает с толку allInvoicesAllClients, потому что вы используете id вместо даты, я отредактирую ее
Hernán Eche
1
@ HernánEche Ах. Моя ошибка. Спасибо. Да, предложение idсделано, а также. Вы можете думать об computeIfAbsentусловном путе, если хотите. И это также возвращает значение
Майкл
« Вы никогда не должны использовать инициализацию карты с« двойной скобкой ». Почему? (Я не сомневаюсь, что вы правы; я прошу из искреннего любопытства.)
Хайнци
1
@ Heinzi Потому что это создает анонимный внутренний класс. Он содержит ссылку на класс, который объявил его, что, если вы выставите карту (например, через геттер), предотвратит сбор мусора окружающим классом. Кроме того, я нахожу, что это может сбивать с толку людей, менее знакомых с Java; блоки инициализатора почти никогда не используются, и их написание выглядит так, как будто оно {{ }}имеет особое значение, чего не происходит.
Майкл
1
@ Майкл: Имеет смысл, спасибо. Я полностью забыл, что анонимные внутренние классы всегда нестатичны (даже если они не нужны).
Хайнци
5

Это длиннее, чем другие ответы, но имхо гораздо более читабельно:

if(!allInvoicesAllClients.containsKey(id))
    allInvoicesAllClients.put(id, new HashMap<LocalDateTime, Invoice>());

allInvoicesAllClients.get(id).put(date, invoice);
волк
источник
3
Это может работать для HashMap, но общий подход не является оптимальным. Если это ConcurrentHashMaps, эти операции не являются атомарными. В таком случае проверка-то-действие приведет к условиям гонки. В любом случае, проголосовали противники.
Майкл
0

Здесь вы делаете две разные вещи: гарантируете HashMapсуществование и добавляете в него новую запись.

Существующий код обязательно вставляет новый элемент перед регистрацией хеш-карты, но в этом нет необходимости, потому что здесь нет необходимости HashMapв упорядочении. Ни один из вариантов не является потокобезопасным, поэтому вы ничего не теряете.

Итак, как предложил @Heinzi, вы можете просто разделить эти два шага.

Я также хотел бы разгрузить создание объекта HashMapдо allInvoicesAllClientsобъекта, чтобы getметод не мог вернуться null.

Это также уменьшает возможность гонок между отдельными потоками, которые могут как получить nullуказатели, так getи затем принять решение putо новом HashMapс одной записью - второй put, вероятно, отбросит первый, потеряв Invoiceобъект.

Саймон Рихтер
источник