В Data Geekery мы любим Java. И так как мы действительно входим в свободный API jOOQ и запросы DSL , мы абсолютно взволнованы тем, что Java 8 принесет в нашу экосистему. Мы пару раз писали о приятных вкусностях Java 8 , и теперь мы чувствуем, что пришло время начать новую серию блогов,…
Ява 8 Пятница
Каждую пятницу мы показываем вам пару замечательных новых функций Java 8 в виде учебника, которые используют лямбда-выражения, методы расширения и другие замечательные вещи. Вы найдете исходный код на GitHub .
Java 8 Goodie: Улучшения карт
В предыдущих постах мы уже имели дело с парой новых функций Streams, например, при сортировке . Большинство улучшений API действительно являются частью нового Streams API . Но несколько хороших методов были добавлены java.util.List
и, самое главное, к java.util.Map
. Если вы хотите получить краткий обзор дополнений к функциям, перейдите в JDK8 Javadoc и нажмите на новую вкладку «Методы по умолчанию»:
По причинам обратной совместимости все новые методы, добавленные в интерфейсы Java, фактически являются методами по умолчанию. Итак, у нас есть пара интересных новых дополнений!
методы compute ()
Часто мы выбираем значение с карты, производим на нем некоторые вычисления и помещаем его обратно в карту. Это может быть многословно и трудно понять правильно, если задействован параллелизм. С Java 8, мы можем пропускать BiFunction
к новому compute()
, computeIfAbsent()
или computeIfPresent()
методам и имеем Map
осуществление обработку семантики замены значения.
В следующем примере показано, как это работает:
// We'll be using this simple map // Unfortunately, still no map literals in Java 8.. Map<String, Integer> map = newHashMap<>(); map.put("A", 1); map.put("B", 2); map.put("C", 3); // Compute a new value for the existing key System.out.println(map.compute("A", (k, v) -> v == null? 42: v + 41)); System.out.println(map); // This will add a new (key, value) pair System.out.println(map.compute("X", (k, v) -> v == null? 42: v + 41)); System.out.println(map);
Вывод вышеуказанной программы такой:
42 {A=42, B=2, C=3} 42 {A=42, B=2, C=3, X=42}
Это действительно полезно для ConcurrentHashMap
, который поставляется со следующей гарантией:
Весь вызов метода выполняется атомарно. Некоторые попытки обновления этой карты другими потоками могут быть заблокированы во время вычислений, поэтому вычисления должны быть короткими и простыми и не должны пытаться обновить какие-либо другие отображения этой Карты.
метод forEach ()
Это действительно хорошая новость, которая позволяет вам передавать ссылку на метод или лямбду для получения пар (ключ, значение) одну за другой. Тривиальный пример будет следующим:
map.forEach((k, v) -> System.out.println(k + "="+ v));
Его вывод:
A=1 B=2 C=3
метод слияния ()
Теперь этот действительно не так легко понять. Javadoc использует этот пример здесь:
map.merge(key, msg, String::concat)
Учитывая следующий договор:
Если указанный ключ еще не связан со значением или связан с нулем, связывает его с данным значением. В противном случае заменяет значение результатами данной функции переназначения или удаляет, если результат равен нулю.
Итак, приведенный выше код переводится в следующую элементарную операцию:
String value = map.get(key); if(value == null) map.put(key, msg); else map.put(key, value.concat(msg));
Это, конечно, не повседневная функциональность, и она могла просто просочиться из реализации в API верхнего уровня. Кроме того, если карта уже содержит null
(так что null
значения в порядке) и ваши remappingFunction
возвраты null
, то запись удаляется. Это довольно неожиданно. Рассмотрим следующую программу:
map.put("X", null); System.out.println(map.merge( "X", null, (v1, v2) -> null)); System.out.println(map);
Его вывод:
null {A=1, B=2, C=3}
Обновление: сначала я написал приведенный выше код в JDK 8, сборка 116. В сборке 129 все снова полностью изменилось. Во-первых, передаваемое значение merge()
не может быть null
. Во- вторых. null
значения обрабатываются merge()
как отсутствующие значения. Для получения того же результата мы напишем:
map.put("X", 1); System.out.println(map.merge( "X", 1, (v1, v2) -> null)); System.out.println(map);
Таким merge()
образом, эта операция удалила значение с карты. Это , вероятно , хорошо , потому что семантика «слияние» часто является комбинация INSERT
, UPDATE
и DELETE
если мы используем SQL-Speak. И несколько разумный способ указать, что значение должно быть удалено, — это возврат null
из такой функции.
Но карта может содержать null
значения, которые никогда не могут быть вставлены в карту с помощью merge()
.
getOrDefault ()
Это ежу понятно. Правильно? Правильно! Неправильно!
К сожалению, есть два типа карт. Те, кто поддерживает null
ключи и / или значения, и те, кто не поддерживает nulls
. Хотя предыдущий merge()
метод не различал карту, не содержащую ключ, и карту, содержащую ключ со null
значением, этот новый getOrDefault()
возвращает значение по умолчанию, только если ключ не содержится. Это не защитит вас от NullPointerException
:
map.put("X", null); try{ System.out.println(map.getOrDefault("X", 21) + 21); } catch(NullPointerException nope) { nope.printStackTrace(); }
Это довольно облом. В общем, можно сказать, что API Map стал еще более сложным по отношению к нулям.
Тривиальные дополнения
Есть еще несколько способов, как putIfAbsent()
(извлеченных из ConcurrentHashMap
, remove()
(с ключом и аргументами значения), replace()
.
Вывод
В общем, можно сказать, что многие атомарные операции перешли на API-интерфейс верхнего уровня, что хорошо. Но опять же, ранее существовавшая путаница, связанная с семантикой null
в картах, углубилась. Термины «присутствует» или «отсутствует», «содержит», «по умолчанию» не обязательно помогают прояснить эти вещи, что на удивление противоречит правилам поддержания API согласованным и, что наиболее важно, регулярным . Таким образом, в качестве потребителя этого API, в идеале, вы должны хранить null
карты как ключи, так и значения!
На следующей неделе в этой серии блогов мы рассмотрим, как Java 8 позволит вам очень легко определять локальную область транзакций, так что следите за обновлениями!