Spec-Zone.ru › Kotlin 1.8

Коллекции в Java и Kotlin

Коллекции — это группы переменного числа элементов (возможно, нуля), которые важны для решаемой задачи и с которыми обычно выполняются операции. Это руководство объясняет и сравнивает концепции и операции с коллекциями в Java и Kotlin. Оно поможет вам перейти с Java на Kotlin и писать код на языке Kotlin аутентичным образом.

Первая часть этого руководства содержит краткий глоссарий операций над одними и теми же коллекциями в Java и Kotlin. Она разделена на одинаковые операции и операции, существующие только в Kotlin. Вторая часть руководства, начиная с Изменяемости, объясняет некоторые из различий, рассматривая конкретные случаи.

Для ознакомления с коллекциями см. Обзор коллекций или посмотрите это видео Себастьяна Айгнера, разработчика-евангелиста Kotlin.

Все приведенные ниже примеры используют только API стандартных библиотек Java и Kotlin.

Одинаковые операции в Java и Kotlin

В Kotlin есть много операций над коллекциями, которые выглядят точно так же, как их аналоги в Java.

Операции над списками, множествами, очередями и деками

Описание

Общие операции

Дополнительные альтернативы Kotlin

Добавить элемент или элементы

add(), addAll()

Используйте plusAssign(+=) оператор: collection += element, collection += anotherCollection.

Проверить, содержит ли коллекция элемент или элементы

contains(), containsAll()

Используйте in ключевое слово для вызова contains() в операторной форме: element in collection.

Проверить, пуста ли коллекция

isEmpty()

Используйте isNotEmpty() для проверки того, не пуста ли коллекция.

Удалить по определенному условию

removeIf()

Оставить только выбранные элементы

retainAll()

Удалить все элементы из коллекции

clear()

Получить поток из коллекции

stream()

Kotlin имеет свой собственный способ обработки потоков: последовательности и методы, такие как map() и filter().

Получить итератор из коллекции

iterator()

Операции над картами

Описание

Общие операции

Дополнительные альтернативы Kotlin

Добавить элемент или элементы

put(), putAll(), putIfAbsent()

В Kotlin присваивание map[key] = value ведет себя так же, как put(key, value). Также можно использовать plusAssign(+=) оператор: map += Pair(key, value) или map += anotherMap.

Заменить элемент или элементы

put(), replace(), replaceAll()

Используйте оператор индексирования map[key] = value вместо put() и replace().

Получить элемент

get()

Используйте оператор индексирования для получения элемента: map[index].

Проверить, содержит ли карта элемент или элементы

containsKey(), containsValue()

Используйте in ключевое слово для вызова contains() в операторной форме: element in map.

Проверить, пуста ли карта

isEmpty()

Используйте isNotEmpty() для проверки того, не пуста ли карта.

Удалить элемент

remove(key), remove(key, value)

Используйте minusAssign(-=) оператор: map -= key.

Удалить все элементы из карты

clear()

Получить поток из карты

stream() для элементов, ключей или значений

Операции, существующие только для списков

Описание

Общие операции

Дополнительные альтернативы Kotlin

Получить индекс элемента

indexOf()

Получить последний индекс элемента

lastIndexOf()

Получить элемент

get()

Используйте оператор индексирования для получения элемента: list[index].

Взять подсписок

subList()

Заменить элемент или элементы

set(), replaceAll()

Используйте оператор индексирования вместо set(): list[index] = value.

Операции, немного отличающиеся

Операции над любым типом коллекции

Описание

Java

Kotlin

Получение размера коллекции

size()

count(), size

Получение плоского доступа к элементам вложенной коллекции

collectionOfCollections.forEach(flatCollection::addAll) или collectionOfCollections.stream().flatMap().collect()

flatten() или flatMap()

Применение заданной функции к каждому элементу

stream().map().collect()

map()

Применение предоставленной операции к элементам коллекции последовательно и возврат накопленного результата

stream().reduce()

reduce(), fold()

Группировка элементов по классификатору и подсчёт их количества

stream().collect(Collectors.groupingBy(classifier, counting()))

eachCount()

Фильтрация по условию

stream().filter().collect()

filter()

Проверка того, удовлетворяют ли элементы коллекции условию

stream().noneMatch(), stream().anyMatch(), stream().allMatch()

none(), any(), all()

Сортировка элементов

stream().sorted().collect()

sorted()

Получение первых N элементов

stream().limit(N).collect()

take(N)

Получение элементов с предикатом

stream().takeWhile().collect()

takeWhile()

Пропуск первых N элементов

stream().skip(N).collect()

drop(N)

Пропуск элементов с предикатом

stream().dropWhile().collect()

dropWhile()

Создание словарей из элементов коллекции и определённых с ними связанных значений

stream().collect(toMap(keyMapper, valueMapper))

associate()

Для выполнения всех операций, перечисленных выше, над картами, вам сначала нужно получить entrySet карты.

Операции над списками

Описание

Java

Kotlin

Сортировка списка в естественном порядке

sort(null)

sort()

Сортировка списка в обратном порядке

sort(comparator)

sortDescending()

Удаление элемента из списка

remove(index), remove(element)

removeAt(index), remove(element) или collection -= element

Заполнение всех элементов списка определённым значением

Collections.fill()

fill()

Получение уникальных элементов из списка

stream().distinct().toList()

distinct()

Операции, отсутствующие в стандартной библиотеке Java

  • zip(), unzip() – преобразование коллекции.

  • aggregate() – группировка по условию.

  • takeLast(), takeLastWhile(), dropLast(), dropLastWhile() – взятие или удаление элементов по предикату.

  • slice(), chunked(), windowed() – получение частей коллекции.

  • Операторы «плюс» (+) и «минус» (-) – добавление или удаление элементов.

Если вы хотите углубиться в изучение zip(), chunked(), windowed() и некоторых других операций, посмотрите это видео Себастьяна Айгнера о продвинутых операциях над коллекциями в Kotlin:

Изменяемость

В Java существуют изменяемые коллекции:

// Java
// This list is mutable!
public List<Customer> getCustomers() { ... }

Частично изменяемые:

// Java
List<String> numbers = Arrays.asList("one", "two", "three", "four");
numbers.add("five"); // Fails in runtime with `UnsupportedOperationException`

И неизменяемые:

// Java
List<String> numbers = new LinkedList<>();
// This list is immutable!
List<String> immutableCollection = Collections.unmodifiableList(numbers);
immutableCollection.add("five"); // Fails in runtime with `UnsupportedOperationException`

Если вы напишете эти два последних фрагмента кода в IntelliJ IDEA, IDE предупредит вас о попытке изменения неизменяемого объекта. Этот код будет компилироваться и аварийно завершится во время выполнения с UnsupportedOperationException. Вы не можете определить, изменяема ли коллекция, посмотрев на её тип.

В отличие от Java, в Kotlin вы явно объявляете изменяемые или только для чтения коллекции в зависимости от ваших потребностей. Если вы попытаетесь изменить только для чтения коллекцию, код не будет компилироваться:

// Kotlin
val numbers = mutableListOf("one", "two", "three", "four")
numbers.add("five")            // This is OK
val immutableNumbers = listOf("one", "two")
//immutableNumbers.add("five") // Compilation error - Unresolved reference: add

Подробнее об неизменяемости на странице конвенций кодирования Kotlin.

Ковариантность

В Java вы не можете передать коллекцию с типом-потомком в функцию, которая принимает коллекцию базового типа. Например, если Rectangle расширяет Shape, вы не можете передать коллекцию элементов Rectangle в функцию, которая принимает коллекцию элементов Shape. Чтобы код был компилируемым, используйте тип ? extends Shape, чтобы функция могла принимать коллекции с любыми наследниками от Shape:

// Java
class Shape {}

class Rectangle extends Shape {}

public void doSthWithShapes(List<? extends Shape> shapes) {
/* If using just List<Shape>, the code won't compile when calling
this function with the List<Rectangle> as the argument as below */
}

public void main() {
    var rectangles = List.of(new Rectangle(), new Rectangle());
    doSthWithShapes(rectangles);
}

В Kotlin типы неизменяемых коллекций ковариантны. Это означает, что если класс Rectangle наследуется от класса Shape, вы можете использовать тип List<Rectangle> в любом месте, где требуется тип List<Shape>. Другими словами, типы коллекций имеют ту же связь подтипирования, что и типы элементов. Карты ковариантны по типу значения, но не по типу ключа. Изменяемые коллекции не ковариантны – это привело бы к сбоям во время выполнения.

// Kotlin
open class Shape(val name: String)

class Rectangle(private val rectangleName: String) : Shape(rectangleName)

fun doSthWithShapes(shapes: List<Shape>) {
    println("The shapes are: ${shapes.joinToString { it.name }}")
}

fun main() {
    val rectangles = listOf(Rectangle("rhombus"), Rectangle("parallelepiped"))
    doSthWithShapes(rectangles)
}

Подробнее о типах коллекций здесь.

Диапазоны и прогрессии

В Kotlin вы можете создавать интервалы, используя диапазоны. Например, Version(1, 11)..Version(1, 30) включает все версии от 1.11 до 1.30. Вы можете проверить, находится ли ваша версия в диапазоне, используя оператор in: Version(0, 9) in versionRange.

В Java вам нужно вручную проверить, подходит ли Version к обоим границам:

// Java
class Version implements Comparable<Version> {

    int major;
    int minor;

    Version(int major, int minor) {
        this.major = major;
        this.minor = minor;
    }

    @Override
    public int compareTo(Version o) {
        if (this.major != o.major) {
            return this.major - o.major;
        }
        return this.minor - o.minor;
    }
}

public void compareVersions() {
    var minVersion = new Version(1, 11);
    var maxVersion = new Version(1, 31);

   System.out.println(
           versionIsInRange(new Version(0, 9), minVersion, maxVersion));
   System.out.println(
           versionIsInRange(new Version(1, 20), minVersion, maxVersion));
}

public Boolean versionIsInRange(Version versionToCheck, Version minVersion, 
                                Version maxVersion) {
    return versionToCheck.compareTo(minVersion) >= 0 
            && versionToCheck.compareTo(maxVersion) <= 0;
}

В Kotlin вы работаете с диапазоном как с целым объектом. Вам не нужно создавать две переменные и сравнивать Version с ними:

// Kotlin
class Version(val major: Int, val minor: Int): Comparable<Version> {
    override fun compareTo(other: Version): Int {
        if (this.major != other.major) {
            return this.major - other.major
        }
        return this.minor - other.minor
    }
}

fun main() {
    val versionRange = Version(1, 11)..Version(1, 30)

    println(Version(0, 9) in versionRange)
    println(Version(1, 20) in versionRange)
}

Как только вам нужно исключить одну из границ, например, проверить, является ли версия больше или равна (>=) минимальной версии и меньше (<) максимальной версии, эти включительные диапазоны не помогут.

END_OF_DOCUMENT_MARKER ```

Сравнение по нескольким критериям

В Java для сравнения объектов по нескольким критериям можно использовать функции comparing() и thenComparingX() из интерфейса Comparator. Например, для сравнения людей по имени и возрасту:

class Person implements Comparable<Person> {
    String name;
    int age;

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return this.name + " " + age;
    }
}

public void comparePersons() {
    var persons = List.of(new Person("Jack", 35), new Person("David", 30), 
            new Person("Jack", 25));
    System.out.println(persons.stream().sorted(Comparator
            .comparing(Person::getName)
            .thenComparingInt(Person::getAge)).collect(toList()));
}

В Kotlin просто перечислите поля, по которым хотите сравнить:

data class Person(
    val name: String,
    val age: Int
)

fun main() {
    val persons = listOf(Person("Jack", 35), Person("David", 30), 
        Person("Jack", 25))
    println(persons.sortedWith(compareBy(Person::name, Person::age)))
}

Последовательности

В Java можно сгенерировать последовательность чисел таким образом:

// Java
int sum = IntStream.iterate(1, e -> e + 3)
    .limit(10).sum();
System.out.println(sum); // Prints 145

В Kotlin используйте последовательности. Многоступенчатая обработка последовательностей выполняется лениво, когда это возможно — фактическое вычисление происходит только тогда, когда запрашивается результат всей цепочки обработки.

fun main() {
//sampleStart
    // Kotlin
    val sum = generateSequence(1) {
        it + 3
    }.take(10).sum()
    println(sum) // Prints 145
//sampleEnd
}

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

Удаление элементов из списка

В Java функция remove() принимает индекс удаляемого элемента.

При удалении целочисленного элемента используйте функцию Integer.valueOf() в качестве аргумента для функции remove():

// Java
public void remove() {
    var numbers = new ArrayList<>();
    numbers.add(1);
    numbers.add(2);
    numbers.add(3);
    numbers.add(1);
    numbers.remove(1); // This removes by index
    System.out.println(numbers); // [1, 3, 1]
    numbers.remove(Integer.valueOf(1));
    System.out.println(numbers); // [3, 1]
}

В Kotlin есть два типа удаления элементов: по индексу с помощью removeAt() и по значению с помощью remove().

fun main() {
//sampleStart
    // Kotlin
    val numbers = mutableListOf(1, 2, 3, 1)
    numbers.removeAt(0)
    println(numbers) // [2, 3, 1]
    numbers.remove(1)
    println(numbers) // [2, 3]
//sampleEnd
}

Проход по карте

В Java можно пройти по карте с помощью forEach:

// Java
numbers.forEach((k,v) -> System.out.println("Key = " + k + ", Value = " + v));

В Kotlin используйте цикл for или forEach, аналогично forEach Java, для прохода по карте:

// Kotlin
for ((k, v) in numbers) {
    println("Key = $k, Value = $v")
}
// Or
numbers.forEach { (k, v) -> println("Key = $k, Value = $v") }

Получение первого и последнего элементов потенциально пустой коллекции

В Java можно безопасно получить первый и последний элементы, проверив размер коллекции и используя индексы:

// Java
var list = new ArrayList<>();
//...
if (list.size() > 0) {
    System.out.println(list.get(0));
    System.out.println(list.get(list.size() - 1));
}

Также можно использовать функции getFirst() и getLast() для Deque и его наследников:

// Java
var deque = new ArrayDeque<>();
//...
if (deque.size() > 0) {
    System.out.println(deque.getFirst());
    System.out.println(deque.getLast());
}

В Kotlin существуют специальные функции firstOrNull() и lastOrNull(). Используя Elvis operator, вы можете сразу выполнить дальнейшие действия в зависимости от результата функции. Например, firstOrNull():

// Kotlin
val emails = listOf<String>() // Might be empty
val theOldestEmail = emails.firstOrNull() ?: ""
val theFreshestEmail = emails.lastOrNull() ?: ""

Создание множества из списка

В Java для создания Set из List можно использовать функцию Set.copyOf:

// Java
public void listToSet() {
    var sourceList = List.of(1, 2, 3, 1);
    var copySet = Set.copyOf(sourceList);
    System.out.println(copySet);
}

В Kotlin используйте функцию toSet():

fun main() {
//sampleStart
    // Kotlin
    val sourceList = listOf(1, 2, 3, 1)
    val copySet = sourceList.toSet()
    println(copySet)
//sampleEnd
}

Группировка элементов

В Java элементы можно сгруппировать с помощью функции Collectors groupingBy():

// Java
public void analyzeLogs() {
    var requests = List.of(
        new Request("https://kotlinlang.org/docs/home.html", 200),
        new Request("https://kotlinlang.org/docs/home.html", 400),
        new Request("https://kotlinlang.org/docs/comparison-to-java.html", 200)
    );
    var urlsAndRequests = requests.stream().collect(
            Collectors.groupingBy(Request::getUrl));
    System.out.println(urlsAndRequests);
}

В Kotlin используйте функцию groupBy():

class Request(
    val url: String,
    val responseCode: Int
)

fun main() {
//sampleStart
    // Kotlin
    val requests = listOf(
        Request("https://kotlinlang.org/docs/home.html", 200),
        Request("https://kotlinlang.org/docs/home.html", 400),
        Request("https://kotlinlang.org/docs/comparison-to-java.html", 200)
    )
    println(requests.groupBy(Request::url))
//sampleEnd
}

Фильтрация элементов

В Java для фильтрации элементов из коллекции необходимо использовать Stream API. Stream API имеет операции intermediate и terminal. filter() — это промежуточная операция, которая возвращает поток. Для получения коллекции в качестве результата необходимо использовать терминальную операцию, например collect(). Например, чтобы оставить только те пары, ключи которых заканчиваются на 1, а значения больше, чем 10:

// Java
public void filterEndsWith() {
    var numbers = Map.of("key1", 1, "key2", 2, "key3", 3, "key11", 11);
    var filteredNumbers = numbers.entrySet().stream()
        .filter(entry -> entry.getKey().endsWith("1") && entry.getValue() > 10)
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    System.out.println(filteredNumbers);
}

В Kotlin фильтрация встроена в коллекции, и filter() возвращает тот же тип коллекции, что и отфильтрованная. Поэтому всё, что вам нужно написать, это filter() и его предикат:

fun main() {
//sampleStart
    // Kotlin
    val numbers = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)
    val filteredNumbers = numbers.filter { (key, value) -> key.endsWith("1") && value > 10 }
    println(filteredNumbers)
//sampleEnd
}

Дополнительную информацию о фильтрации карт можно найти здесь.

Фильтрация элементов по типу

В Java для фильтрации элементов по типу и выполнения действий над ними необходимо проверять их типы с помощью оператора instanceof и затем выполнить приведение типов:

// Java
public void objectIsInstance() {
    var numbers = new ArrayList<>();
    numbers.add(null);
    numbers.add(1);
    numbers.add("two");
    numbers.add(3.0);
    numbers.add("four");
    System.out.println("All String elements in upper case:");
    numbers.stream().filter(it -> it instanceof String)
        .forEach( it -> System.out.println(((String) it).toUpperCase()));
}

В Kotlin просто вызывайте filterIsInstance<NEEDED_TYPE>() на вашей коллекции, и приведение типов выполняется с помощью умных приведений типов:

// Kotlin
fun main() {
//sampleStart
    // Kotlin
    val numbers = listOf(null, 1, "two", 3.0, "four")
    println("All String elements in upper case:")
    numbers.filterIsInstance<String>().forEach {
        println(it.uppercase())
    }
//sampleEnd
}

Тестирование предикатов

Некоторые задачи требуют проверки того, удовлетворяют ли все, ни один или любые элементы условию. В Java эти проверки можно выполнить с помощью функций Stream API allMatch(), noneMatch() и anyMatch():

// Java
public void testPredicates() {
    var numbers = List.of("one", "two", "three", "four");
    System.out.println(numbers.stream().noneMatch(it -> it.endsWith("e"))); // false
    System.out.println(numbers.stream().anyMatch(it -> it.endsWith("e"))); // true
    System.out.println(numbers.stream().allMatch(it -> it.endsWith("e"))); // false
}

В Kotlin для каждого объекта Iterable доступны расширяющие функции none(), any() и all():

fun main() {
//sampleStart
// Kotlin
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.none { it.endsWith("e") })
    println(numbers.any { it.endsWith("e") })
    println(numbers.all { it.endsWith("e") })
//sampleEnd
}

Дополнительную информацию о тестировании предикатов можно найти здесь.

Операции преобразования коллекций

Объединение элементов

В Java можно создать пары из элементов с одинаковыми позициями в двух коллекциях, одновременно итерируя по ним:

// Java
public void zip() {
    var colors = List.of("red", "brown");
    var animals = List.of("fox", "bear", "wolf");

    for (int i = 0; i < Math.min(colors.size(), animals.size()); i++) {
        String animal = animals.get(i);
        System.out.println("The " + animal.substring(0, 1).toUpperCase()
               + animal.substring(1) + " is " + colors.get(i));
   }
}

Если вам нужно сделать что-то более сложное, чем просто вывести пары элементов в выходные данные, вы можете использовать Records. В приведенном выше примере запись будет record AnimalDescription(String animal, String color) {}.

В Kotlin используйте функцию zip() для того же:

fun main() {
//sampleStart
    // Kotlin
    val colors = listOf("red", "brown")
    val animals = listOf("fox", "bear", "wolf")

    println(colors.zip(animals) { color, animal -> 
        "The ${animal.replaceFirstChar { it.uppercase() }} is $color" })
//sampleEnd
}

zip() возвращает список объектов Pair.

Если у коллекций разный размер, результатом zip() является меньший размер. Последние элементы большей коллекции не включаются в результат.

Связывание элементов

В Java можно использовать Stream API для связывания элементов с характеристиками:

// Java
public void associate() {
    var numbers = List.of("one", "two", "three", "four");
    var wordAndLength = numbers.stream()
        .collect(toMap(number -> number, String::length));
    System.out.println(wordAndLength);
}

В Kotlin используйте функцию associate():

fun main() {
//sampleStart
    // Kotlin
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.associateWith { it.length })
//sampleEnd
}

Что дальше?

  • Посетите Kotlin Koans – выполните упражнения, чтобы изучить синтаксис Kotlin. Каждое упражнение создаётся как не проходящий юнит-тест, и ваша задача сделать его проходящим.

  • Изучите другие Kotlin idioms.

  • Узнайте, как преобразовать существующий код Java в Kotlin с помощью конвертера Java в Kotlin.

  • Изучите коллекции в Kotlin.

Если у вас есть любимый idiom, поделитесь им, отправив pull request.

Последнее изменение: 10 января 2023
Строки в Java и Kotlin Необязательность в Java и Kotlin

© 2010–2023 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/java-to-kotlin-collections-guide.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API