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