5. Основы Kotlin. Ассоциативные массивы Maps и множества Sets

Вернуться к предыдущему разделу

Введение

В прошлом уроке мы познакомились с вами с различными структурами данных, которые предназначены для хранения упорядоченной последовательности элементов. Понятно, что это далеко не единственный тип структур данных, которые существуют, и сегодня мы с вами познакомимся с двумя новыми структурами данных.

Чтобы понять первую структуру данных — ассоциативный массив — далеко ходить не надо, достаточно вспомнить о такой штуке как толковый словарь. Он связывает элементы отношением “ключ”-“значение”: для определенных слов (ключей) он содержит их описание (значения), для всех остальных — не содержит ничего. Подобной структурой обладают, на самом деле, многие вещи: набор товаров с их ценами, список контактов в телефоне, рестораны и рейтинги, и т.д. Основная операция, которую они поддерживают, — это достать значение, соответствующее интересующему нас ключу, т.е. то, что вы делаете, когда ищете значение неизвестного вам слова в словаре.

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

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 в противном случае

Додати коментар