Вступление
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(): возвращаетSetEntry<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. Кроме того, реализация оптимизирована для обработки довольно небольшого количества элементов, которые может содержатьMapTreeMap: В качестве реализацииSortedMapиNavigableMapинтерфейсов,TreeMapгарантирует , что элементы , добавленные к нему будет соблюдать определенный порядок (на основе ключа). Этот порядок будет либо естественным порядком ключей, либо тем, который обеспечиваетсяComparatorмы можем передать конструкторуTreeMapConcurrentHashMap: эта последняя реализация, скорее всего, такая же, какHashMap, ожидайте, что она обеспечивает безопасность потоков для операций обновления, что гарантируется интерфейсомConcurrentMap
Заключение
Фреймворк Java Collections - это фундаментальный фреймворк, который должен знать каждый Java-разработчик.
В этой статье мы говорили об интерфейсе Map Мы рассмотрели основные
операции с помощью HashMap а также несколько интересных расширений,
таких как SortedMap или ConcurrentMap .