Вступление
Java Collections Framework - это фундаментальная и важная среда, которую любой сильный Java-разработчик должен знать как свои пять пальцев.
Коллекция в Java определяется как группа или набор отдельных объектов, которые действуют как единый объект.
В Java существует множество классов коллекций, и все они расширяют
интерфейсы java.util.Collection
и java.util.Map
Эти классы в
основном предлагают разные способы составить коллекцию объектов в одном
объекте.
Коллекции Java - это структура, которая обеспечивает множество операций над коллекцией - поиск, сортировку, вставку, манипулирование, удаление и т. Д.
Это третья часть серии статей о Java Collections:
- Интерфейс списка
- Установленный интерфейс
- Интерфейс карты ( вы здесь )
- Интерфейсы Queue и Deque
Списки и наборы ограничений
Прежде всего, давайте обсудим ограничения List
и Set
. Они
предоставляют множество функций для добавления, удаления и проверки
наличия элементов, а также механизмы итерации. Но когда дело доходит до
получения определенных предметов, они не очень удобны.
Интерфейс Set
не предоставляет никаких средств для получения
определенного объекта, поскольку он неупорядочен. А List
просто
предоставляет возможность извлекать элементы по их индексу.
К сожалению, индексы не всегда говорят сами за себя и поэтому не имеют большого значения.
Карты
Вот где появляется интерфейс java.util.Map
Map
связывает элементы с
ключами, что позволяет нам извлекать элементы по этим ключам. Такие
ассоциации несут гораздо больше смысла, чем привязка индекса к элементу.
Map
- это общий интерфейс двух типов: один для ключей, а другой - для
значений. Поэтому, если бы мы хотели объявить Map
хранящую количество
слов в тексте, мы бы написали:
Map<String, Integer> wordsCount;
Такая Map
использует String
качестве ключа и Integer
качестве
значения.
Добавление элементов
Давайте теперь погрузимся в Map
, начиная с добавления элементов. Есть
несколько способов добавить элементы на Map
, наиболее
распространенным из которых является метод put()
:
Map<String, Integer> wordsCount = new HashMap<>();
wordsCount.put("the", 153);
Примечание. Помимо связывания значения с ключом, метод put()
также
возвращает ранее связанное значение, если оно есть, и null
противном
случае.
Но что, если мы хотим добавить элемент, только если с его ключом ничего
не связано? Затем у нас есть несколько возможностей, первая из которых -
проверить наличие ключа с помощью метода containsKey()
if (!wordsCount.containsKey("the")) {
wordsCount.put("the", 150);
}
Благодаря containsKey()
мы можем проверить, связан ли уже элемент с
ключом the
и добавить значение, только если нет.
Однако это немного многословно, особенно учитывая, что есть два других
варианта. Прежде всего, давайте посмотрим на самый древний метод
putIfAbsent()
:
wordsCount.putIfAbsent("the", 150);
Этот вызов метода дает тот же результат, что и предыдущий, но с использованием только одной строки.
Теперь давайте посмотрим на второй вариант. Начиная с Java 8, существует
другой метод, похожий на putIfAbsent()
- computeIfAbsent()
.
Он работает примерно так же, как и предыдущий, но принимает лямбда-функцию вместо прямого значения, что дает нам возможность создать экземпляр значения только в том случае, если к ключу еще ничего не прикреплено.
Аргумент функции является ключом, если от него зависит создание значения. Итак, чтобы добиться того же результата, что и с предыдущими методами, нам нужно будет сделать:
wordsCount.computeIfAbsent("the", key -> 3 + 150);
Это обеспечит тот же результат , как и раньше, только он не будет
вычислять значение 153 , если другое значение уже связано с ключом the
.
Примечание. Этот метод особенно полезен, когда значение сложно создать или если метод вызывается часто, и мы хотим избежать создания слишком большого количества объектов.
Получение элементов
До сих пор мы учились размещать элементы на Map
, но как насчет их
получения?
Для этого мы используем метод get()
:
wordsCount.get("the");
Этот код вернет количество слов слова the
.
Если ни одно значение не соответствует данному ключу, get()
возвращает
null
. Однако мы можем избежать этого, используя метод
getOrDefault()
:
wordsCount.getOrDefault("duck", 0);
Примечание. Здесь, если с ключом ничего не связано, мы вернем 0
вместо null
.
Теперь это для получения по одному элементу за раз, используя его ключ.
Посмотрим, как получить все элементы. Интерфейс Map
предлагает три
метода для достижения этой цели:
entrySet()
: возвращаетSet
Entry<K, V>
который представляет собой пары ключ / значение, представляющие элементы карты.keySet()
: возвращаетSet
ключей картыvalues()
: возвращаетSet
значений карты
Удаление элементов
Теперь, когда мы знаем, как размещать и извлекать элементы из карты, давайте посмотрим, как удалить некоторые из них!
Сначала давайте посмотрим, как удалить элемент по его ключу. Для этого
мы воспользуемся remove()
, который принимает ключ в качестве
параметра:
wordsCount.remove("the");
Метод удалит элемент и вернет связанное значение, если таковое имеется,
в противном случае он ничего не сделает и вернет null
.
Метод remove()
имеет перегруженную версию, принимающую также значение.
Его цель - удалить запись, только если она имеет тот же ключ и значение,
что и указанные в параметрах:
wordsCount.remove("the", 153);
Этот вызов удалит запись, связанную со словом the
только если
соответствующее значение равно 153
, в противном случае он ничего не
сделает.
Этот метод не возвращает Object
, а возвращает boolean
указывающее,
был ли элемент удален или нет.
Итерация по элементам
Мы не можем говорить о коллекции Java, не объясняя, как ее перебирать.
Мы увидим два способа перебора элементов Map
.
Первый - это for-each
, который мы можем использовать в entrySet()
:
for (Entry<String, Integer> wordCount: wordsCount.entrySet()) {
System.out.println(wordCount.getKey() + " appears " + wordCount.getValue() + " times");
}
До Java 8 это был стандартный способ перебора Map
. К счастью для нас,
в Java 8 появился менее подробный способ: метод forEach()
который
принимает BiConsumer<K, V>
:
wordsCount.forEach((word, count) -> System.out.println(word + " appears " + count + " times"));
Поскольку некоторые могут быть не знакомы с функциональным интерфейсом,
BiConsumer
- он принимает два аргумента и не возвращает никакого
значения. В нашем случае мы передаем word
и его count
, которые
затем распечатываются с помощью лямбда-выражения.
Этот код очень лаконичен и его легче читать, чем предыдущий.
Проверка наличия элемента
Хотя у нас уже был обзор того, как проверить наличие элемента на Map
,
давайте поговорим о возможных способах достижения этого.
Прежде всего, есть метод containsKey()
, который мы уже использовали и
который возвращает boolean
значение, сообщающее нам, соответствует ли
элемент заданному ключу или нет. Но есть также метод containsValue()
который проверяет наличие определенного значения.
Давайте представим Map
представляющую результаты игроков в игре и
первой, кто выиграл 150 побед, тогда мы могли бы использовать метод
containsValue()
чтобы определить, выиграл ли игрок игру или нет:
Map<String, Integer> playersScores = new HashMap<>();
playersScores.put("James", 0);
playersScores.put("John", 0);
while (!playersScores.containsValue(150)) {
// Game taking place
}
System.out.println("We have a winner!");
Получение размера и проверка на пустоту
Теперь, что касается List
и Set
, есть операции по подсчету
количества элементов.
Эти операции - size()
, который возвращает количество элементов Map
, и isEmpty()
, который возвращает boolean
, указывающее, содержит
ли Map
какой-либо элемент или нет:
Map<String, Integer> map = new HashMap<>();
map.put("One", 1);
map.put("Two", 2);
System.out.println(map.size());
System.out.println(map.isEmpty());
Результат:
2
false
SortedMap
Мы рассмотрели основные операции, которые мы можем реализовать на Map
помощью реализации HashMap
Но есть и другие унаследованные от него
интерфейсы карты, которые предлагают новые функции и делают контракты
более строгими.
Первое, о чем мы узнаем, - это SortedMap
, который гарантирует, что
записи карты будут поддерживать определенный порядок на основе его
ключей.
Кроме того, этот интерфейс предлагает функции, использующие преимущества
поддерживаемого порядка, такие как firstKey()
и lastKey()
.
Давайте повторно воспользуемся нашим первым примером, но на этот раз
SortedMap
SortedMap<String, Integer> wordsCount = new TreeMap<>();
wordsCount.put("the", 150);
wordsCount.put("ball", 2);
wordsCount.put("duck", 4);
System.out.println(wordsCount.firstKey());
System.out.println(wordsCount.lastKey());
Поскольку порядок по умолчанию является естественным, это приведет к следующему результату:
ball
the
Если вы хотите настроить критерии порядка, вы можете определить
собственный Comparator
в конструкторе TreeMap
Определив Comparator
, мы можем сравнивать ключи (не полные записи
карты) и сортировать их на основе них, а не значений:
SortedMap<String, Integer> wordsCount =
new TreeMap<String, Integer>(new Comparator<String>() {
@Override
public int compare(String e1, String e2) {
return e2.compareTo(e1);
}
});
wordsCount.put("the", 150);
wordsCount.put("ball", 2);
wordsCount.put("duck", 4);
System.out.println(wordsCount.firstKey());
System.out.println(wordsCount.lastKey());
Поскольку порядок обратный, теперь вывод будет:
the
ball
NavigableMap
NavigableMap
интерфейс является продолжением SortedMap
интерфейса и
добавляет методы , позволяющие перемещаться по карте более легко найти
записи ниже или выше определенного ключа.
Например, метод lowerEntry()
возвращает запись с наибольшим ключом,
который строго меньше заданного ключа:
Взяв карту из предыдущего примера:
SortedMap<String, Integer> wordsCount = new TreeMap<>();
wordsCount.put("the", 150);
wordsCount.put("ball", 2);
wordsCount.put("duck", 4);
System.out.println(wordsCount.lowerEntry("duck"));
Результатом будет:
ball
ConcurrentMap
Наконец, последнее Map
мы рассмотрим, - это ConcurrentMap
, которое
делает контракт Map
более строгим, обеспечивая его потокобезопасность,
которая может использоваться в многопоточном контексте, не опасаясь, что
содержимое карты будет быть непоследовательным.
Это достигается за счет синхронизации
операций обновления, таких как put()
и remove()
.
Реализации
Теперь давайте посмотрим на реализации различных интерфейсов Map
Мы не
будем рассматривать их все, а только основные:
HashMap
: это реализация, которую мы использовали чаще всего с самого начала, и она наиболее проста, поскольку предлагает простое сопоставлениеnull
ключами и значениями. Это прямая реализацияMap
и поэтому не гарантирует ни порядка элементов, ни потоковой безопасности.EnumMap
: реализация, которая принимаетenum
в качестве ключей карты. Следовательно, количество элементов вMap
ограничено количеством константenum
. Кроме того, реализация оптимизирована для обработки довольно небольшого количества элементов, которые может содержатьMap
TreeMap
: В качестве реализацииSortedMap
иNavigableMap
интерфейсов,TreeMap
гарантирует , что элементы , добавленные к нему будет соблюдать определенный порядок (на основе ключа). Этот порядок будет либо естественным порядком ключей, либо тем, который обеспечиваетсяComparator
мы можем передать конструкторуTreeMap
ConcurrentHashMap
: эта последняя реализация, скорее всего, такая же, какHashMap
, ожидайте, что она обеспечивает безопасность потоков для операций обновления, что гарантируется интерфейсомConcurrentMap
Заключение
Фреймворк Java Collections - это фундаментальный фреймворк, который должен знать каждый Java-разработчик.
В этой статье мы говорили об интерфейсе Map
Мы рассмотрели основные
операции с помощью HashMap
а также несколько интересных расширений,
таких как SortedMap
или ConcurrentMap
.