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

Изменяемый ассоциативный массив

Как и в случае со списками, обычный ассоциативный массив (или Map) нельзя изменить; если вы хотите иметь такую возможность, то следует использовать изменяемый ассоциативный массив (или MutableMap) типа MutableMap<Key, Value>. Аналогично List и MutableListMutableMap расширяет Map, т.е. объект MutableMap может использоваться везде, где нужен Map, — в подобных местах вы просто не будете использовать его возможности по модификации.

MutableMap предоставляет следующие основные возможности по модификации.

  • map[key] = value изменяет имеющееся значение для заданного ключа или добавляет новую пару “ключ”-“значение” в случае, если ключ key не был связан в map
  • map.remove(key) удаляет пару, связанную с ключом key

Давайте, как и раньше, попробуем изучить возможности MutableMap на следующем примере: из телефонной книги (набора пар “ФИО”-“телефон”) следует убрать все записи, не относящиеся к заданному коду страны. Сделать это можно, например, следующим образом.

fun filterByCountryCode(
        phoneBook: MutableMap<String, String>,
        countryCode: String) {
    val namesToRemove = mutableListOf<String>()

    for ((name, phone) in phoneBook) {
        if (!phone.startsWith(countryCode)) {
            namesToRemove.add(name)
        }
    }

    for (name in namesToRemove) {
        phoneBook.remove(name)
    }
}

Данную функцию можно разбить на две логические части. Первым делом мы проходимся по всем записям в нашей записной книжке и отбираем те из них, у которых телефон начинается с кода, отличного от заданного в параметре countryCode. Для этого мы используем функцию str.startsWith(prefix), которая возвращает true в случае, если строка str начинается со строки prefix, и false, если это не так. Все имена, которые следует удалить, мы записываем в изменяемый список namesToRemove.

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

fun filterByCountryCode(
        phoneBook: MutableMap<String, String>,
        countryCode: String) {
    val namesToRemove = mutableListOf<String>()

    for (entry in phoneBook) {
        val (name, phone) = entry
        if (!phone.startsWith(countryCode)) {
            namesToRemove.add(name)
        }
    }

    for (name in namesToRemove) {
        phoneBook.remove(name)
    }
}
fun filterByCountryCode(
        phoneBook: MutableMap<String, String>,
        countryCode: String) {
    val namesToRemove = mutableListOf<String>()

    for (entry in phoneBook) {
        val name = entry.key
        val phone = entry.value
        if (!phone.startsWith(countryCode)) {
            namesToRemove.add(name)
        }
    }

    for (name in namesToRemove) {
        phoneBook.remove(name)
    }
}

После того, как мы собрали все имена, которые следует удалить, мы это и делаем при помощи функции map.remove(key). В итоге, после вызова нашей функции с побочным эффектом в переданном MutableMap останутся только записи, у которых номер телефона начинается с нужного нам кода страны.

Тесты для нашей функции выглядят следующим образом.

@Test
@Tag("Example")
fun filterByCountryCode() {
    val phoneBook = mutableMapOf(
            "Quagmire" to "+1-800-555-0143",
            "Adam's Ribs" to "+82-000-555-2960",
            "Pharmakon Industries" to "+1-800-555-6321"
    )

    filterByCountryCode(phoneBook, "+1")
    assertEquals(2, phoneBook.size)

    filterByCountryCode(phoneBook, "+1")
    assertEquals(2, phoneBook.size)

    filterByCountryCode(phoneBook, "+999")
    assertEquals(0, phoneBook.size)
}

Сперва мы при помощи функции mutableMapOf создаем MutableMap, в котором есть три записи, две для кода +1 и одна для кода +82. Затем мы пробуем отфильтровать записи по указанным кодам, корректность же проверяем, сравнивая получившийся размер MutableMap с ожидаемым. Более правильно, конечно же, было бы сравнивать обновленное значение с эталонным MutableMap, однако инициализация эталона занимает дополнительные 3-4 строчки для каждой проверки, поэтому мы немножечко схитрили таким образом. Если запустить наши тесты, то мы увидим, что они успешно проходят. При желании вы можете попробовать модифицировать тесты так, чтобы сравнивать результат с эталоном, и посмотреть, изменится ли результат тестирования.

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

fun filterByCountryCode(
        phoneBook: MutableMap<String, String>,
        countryCode: String) {
    for ((name, phone) in phoneBook) {
        if (!phone.startsWith(countryCode)) {
            phoneBook.remove(name)
        }
    }
}

Код стал короче и понятнее, вот только при попытке запустить тесты они упадут с ошибкой java.util.ConcurrentModificationException. Название ошибки тонко намекает нам, в чем проблема, — мы пытаемся перебирать элементы структуры данных и одновременно изменять эту самую структуру данных. В этом мы подобны дровосеку, который решил забраться на сук и срубить его таким образом, — “ну а что такого, удобно же!” К сожалению, как и в жизни, в программировании подобные чудеса не работают — очень многие структуры данных (в том числе, и MutableMap) не позволяют вам одновременно перебирать и изменять свои элементы. Именно поэтому наша реализация состояла из двух отдельных частей: мы сперва собрали те элементы, что требуется удалить, а потом их удалили.

Распространённые операции над изменяемым ассоциативными массивами

Рассмотрим основные операции, доступные над изменяемыми ассоциативными массивами.

  • map.clear() удаляет все записи из данного MutableMap
  • map[key] = value / map.put(key, value) добавляет или изменяет соответствующую пару “ключ”-“значение”
  • map.putAll(otherMap) добавляет в MutableMap map все пары из otherMap, в случае одинаковых ключей значения из otherMap перезаписывают значения из map
  • map.remove(key) удаляет пару для ключа key

Преобразования между Map и MutableMap

Аналогично обычным и изменяемым спискам, обычные и изменяемые ассоциативные массивы могут быть преобразованы друг в друга при помощи функций конвертации mutableMap.toMap() или map.toMutableMap(). Каждая из функций создает новый объект на основе имеющегося — значения остаются одни и те же, но тип у нового объекта будет соответствовать типу для фукции конвертации.

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