В прошлом уроке мы познакомились с вами с различными структурами данных, которые предназначены для хранения упорядоченной последовательности элементов. Понятно, что это далеко не единственный тип структур данных, которые существуют, и сегодня мы с вами познакомимся с двумя новыми структурами данных.
Чтобы понять первую структуру данных — ассоциативный массив — далеко ходить не надо, достаточно вспомнить о такой штуке как толковый словарь. Он связывает элементы отношением «ключ»-«значение»: для определенных слов (ключей) он содержит их описание (значения), для всех остальных — не содержит ничего. Подобной структурой обладают, на самом деле, многие вещи: набор товаров с их ценами, список контактов в телефоне, рестораны и рейтинги, и т.д. Основная операция, которую они поддерживают, — это достать значение, соответствующее интересующему нас ключу, т.е. то, что вы делаете, когда ищете значение неизвестного вам слова в словаре.
Ассоциативный массив является обобщенным способом представить подобное отношение. Давайте на следующем примере посмотрим, как с ним можно работать. Представим, что нам необходимо посчитать стоимость нашего списка покупок для заданного набора товаров. Сделать это можно при помощи следующей функции.
fun shoppingListCost( shoppingList: List<String>, costs: Map<String, Double>): Double { var totalCost = 0.0 for (item in shoppingList) { val itemCost = costs[item] if (itemCost != null) { totalCost += itemCost } } return totalCost }
Что мы здесь видим? Наша функция принимает на вход список покупок: параметр shoppingList
типа List<String>
— и набор цен для товаров: параметр costs
типа Map<String, Double>
. Данный параметризованный тип Map<Key, Value>
и является типом ассоциативного массива, у которого типовой параметр Key
задает тип ключей, а Value
— тип значений. В нашем случае набор товаров с ценами имеет тип Map<String, Double>
, т.е. для названия товара содержит его цену в виде действительного числа.
Для того, чтобы считать общую стоимость выбранного набора товаров, мы заводим новую изменяемую переменную totalCost
, которая изначально равна нулю и которую мы возвращаем как результат в конце функции при помощи return
. После этого мы проходимся по списку покупок при помощи цикла for
и для каждой покупки пытаемся достать ее цену из нашего ассоциативного массива при помощи операции индексирования. В отличии от индексирования для списка, операция индексирования map[key]
для ассоциативного массива пытается достать элемент не по какому-то целочисленному индексу, а по ключу соответствующего типа — в нашем случае, по названию товара, т.е. строке.
А вот дальше мы знакомимся с такой очень интересной вещью как null
. Как мы отметили раньше, ассоциативный массив содержит пары «ключ»-«значение», однако для некоторых ключей соответствующего им значения может не быть. Вместе с тем, просто так вернуть «ничего» мы не можем. Как раз для таких ситуаций и необходим объект null
— операция индексирования для ассоциативного массива возвращает null
в случае, если для заданного ключа нет значения. После того, как мы проверили, что для товара есть его стоимость (itemCost != null
), мы добавляем ее к общей стоимости набора; в противном случае мы считаем, что данная покупка просто игнорируется.
Попробуем написать тесты для нашей функции.
@Test fun shoppingListCostTest() { val itemCosts = mapOf( "Хлеб" to 50.0, "Молоко" to 100.0 ) assertEquals( 150.0, shoppingListCost( listOf("Хлеб", "Молоко"), itemCosts ) ) assertEquals( 150.0, shoppingListCost( listOf("Хлеб", "Молоко", "Кефир"), itemCosts ) ) assertEquals( 0.0, shoppingListCost( listOf("Хлеб", "Молоко", "Кефир"), mapOf() ) ) }
Как видно из тестов, для создания ассоциативного массива может использоваться функция mapOf()
, которая принимает на вход набор пар «ключ»-«значение» типа Pair<A, B>
(в нашем случае, Pair<String, Double>
). Для создания пары можно использовать либо конструкцию Pair(a, b)
, либо запись a to b
, обе из которых создадут пару из a
и b
. Для того, чтобы обратиться к первому или второму элементу пары pair
, следует использовать запись pair.first
или pair.second
соответственно.
В нашем случае мы создаем пары из названия товара и его стоимости (хлеб за 50.0 и молоко за 100.0), после чего собираем из них ассоциативный массив. Затем мы проверяем три случая:
- Список покупок содержит хлеб и молоко, и общая стоимость должна быть равна 150.0
- Список покупок, кроме хлеба и молока, содержит еще кефир, но — так как его стоимости мы не знаем — мы его игнорируем, и общая стоимость все равно должна быть равна 150.0
- Для какого-то списка покупок с пустым ассоциативным массивом (мы не знаем ни одной цены товара) общая стоимость должна быть равна 0.0
В третьем случае мы создаем пустой ассоциативный массив при помощи функции mapOf()
без аргументов. Типовые параметры в данном случае компилятор Котлина понимает из того, какой тип должен быть у второго аргумента функции shoppingListCost
, поэтому их можно не указывать.
Распространённые операции над ассоциативными массивами
Рассмотрим основные операции, доступные над ассоциативными массивами.
map[key] / map.get(key)
возвращает значение для ключаkey
илиnull
в случае, если значения нетmap.size / map.count()
возвращает количество пар «ключ»-«значение» в ассоциативном массивеmap + pair
возвращает новый ассоциативный массив на основеmap
, в который добавлено (или изменено) значение ключа, соответствующее паре «ключ»-«значение» изpair
map - key
возвращает новый ассоциативный массив на основеmap
, из которого, наоборот, удалено значение ключаkey
map1 + map2
собирает два ассоциативных массива в один, причем пары «ключ»-«значение» изmap2
вытесняют значения изmap1
map - listOfKeys
возвращает новый ассоциативный массив на основеmap
, в котором нет ключей из спискаlistOfKeys
map.getOrDefault(key, defaultValue)
является расширенной версией операции индексирования. В случае, если вmap
есть значение для ключаkey
, данное выражение вернет его; если значения нет, то будет возвращено значение по умолчаниюdefaultValue
.key in map / map.contains(key) / map.containsKey(key)
возвращаетtrue
, еслиmap
содержит значение для ключаkey
иfalse
в противном случаеmap.containsValue(value)
возвращаетtrue
, еслиmap
содержит значениеvalue
для хотя бы одного ключа иfalse
в противном случае