Які еквіваленти Java 8 Stream.collect доступні в стандартній бібліотеці Котліна?


181

У Java 8 є те, Stream.collectщо дозволяє агрегувати колекції. У Котліна це не існує аналогічно, як, можливо, як набір функцій розширення в stdlib. Але незрозуміло, що таке еквівалентність для різних випадків використання.

Наприклад, у верхній частині JavaDoc дляCollectors є приклади, написані для Java 8, і при перенесенні їх на Kolin ви не можете використовувати класи Java 8, коли в іншій версії JDK, тому, ймовірно, вони повинні писатись інакше.

Що стосується ресурсів в Інтернеті, що показують приклади колекцій Котліна, вони, як правило, банальні і насправді не порівнюються з тими ж випадками використання. Які хороші приклади справді відповідають таким випадкам, як задокументовано для Java 8 Stream.collect? Список є:

  • Накопичуйте імена до списку
  • Накопичуйте імена в TreeSet
  • Перетворіть елементи в рядки і з'єднайте їх, розділені комами
  • Розрахувати суму зарплати працівника
  • Групуйте співробітників за відділами
  • Розрахуйте суму заробітної плати за відділами
  • Розбийте учнів на проходження та провал

З деталями в JavaDoc, пов'язаному вище.

Примітка: це запитання навмисно написано та відповіло автором ( запитання із самовідказами ), так що ідіоматичні відповіді на поширені теми Котліна присутні в ТА. Також для уточнення деяких дійсно старих відповідей, написаних для алфавітів Котліна, не точних для поточного Котліна.


У випадках, коли у вас немає іншого вибору, крім використання collect(Collectors.toList())чи подібного, ви можете потрапити на цю проблему: stackoverflow.com/a/35722167/3679676 (проблема, з обхідними шляхами)
Jayson Minard

Відповіді:


257

У stdlib Котліна є функції для середнього, підрахунку, розрізнення, фільтрації, пошуку, групування, приєднання, картографування, хв, макс, розділення, зрізування, сортування, підсумовування, до / зі масивів, до / зі списків, до / з карт , об'єднання, взаємодія, всі функціональні парадигми тощо. Таким чином, ви можете використовувати їх для створення невеликих 1-лайнерів, і не потрібно використовувати складніший синтаксис Java 8.

Я думаю, що єдине, чого не вистачає у вбудованому Collectorsкласі Java 8, - це узагальнення (але в іншій відповіді на це питання - просте рішення) .

Одне, чого не вистачає в обох, - це груповий підрахунок, який видно в іншій відповіді на переповнення стека і також має просту відповідь. Ще один цікавий випадок - цей також із переповнення стека: ідіоматичний спосіб розподілити послідовність у три списки за допомогою Котліна . А якщо ви хочете створити щось на зразок Stream.collectіншої мети, перегляньте розділ Custom Stream.collect у Котліні

РЕДАКЦІЯ 11.08.2017: Додані операції збору з віконцем / вікнами в kotlin 1.2 M2, див. Https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


Завжди добре вивчити посилання API для kotlin.collections в цілому, перш ніж створювати нові функції, які там вже можуть існувати.

Ось кілька перетворень з Stream.collectприкладів Java 8 в еквівалент у Котліні:

Накопичуйте імена до списку

// Java:  
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
// Kotlin:
val list = people.map { it.name }  // toList() not needed

Перетворіть елементи в рядки і з'єднайте їх, розділені комами

// Java:
String joined = things.stream()
                       .map(Object::toString)
                       .collect(Collectors.joining(", "));
// Kotlin:
val joined = things.joinToString(", ")

Розрахувати суму зарплати працівника

// Java:
int total = employees.stream()
                      .collect(Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val total = employees.sumBy { it.salary }

Групуйте співробітників за відділами

// Java:
Map<Department, List<Employee>> byDept
     = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment));
// Kotlin:
val byDept = employees.groupBy { it.department }

Розрахуйте суму заробітної плати за відділами

// Java:
Map<Department, Integer> totalByDept
     = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                     Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}

Розбийте учнів на проходження та провал

// Java:
Map<Boolean, List<Student>> passingFailing =
     students.stream()
             .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));
// Kotlin:
val passingFailing = students.partition { it.grade >= PASS_THRESHOLD }

Імена чоловічих членів

// Java:
List<String> namesOfMaleMembers = roster
    .stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(p -> p.getName())
    .collect(Collectors.toList());
// Kotlin:
val namesOfMaleMembers = roster.filter { it.gender == Person.Sex.MALE }.map { it.name }

Назви груп членів у реєстрі за статтю

// Java:
Map<Person.Sex, List<String>> namesByGender =
      roster.stream().collect(
        Collectors.groupingBy(
            Person::getGender,                      
            Collectors.mapping(
                Person::getName,
                Collectors.toList())));
// Kotlin:
val namesByGender = roster.groupBy { it.gender }.mapValues { it.value.map { it.name } }   

Фільтр списку до іншого списку

// Java:
List<String> filtered = items.stream()
    .filter( item -> item.startsWith("o") )
    .collect(Collectors.toList());
// Kotlin:
val filtered = items.filter { it.startsWith('o') } 

Пошук списку найкоротших рядків

// Java:
String shortest = items.stream()
    .min(Comparator.comparing(item -> item.length()))
    .get();
// Kotlin:
val shortest = items.minBy { it.length }

Підрахунок елементів у списку після застосування фільтра

// Java:
long count = items.stream().filter( item -> item.startsWith("t")).count();
// Kotlin:
val count = items.filter { it.startsWith('t') }.size
// but better to not filter, but count with a predicate
val count = items.count { it.startsWith('t') }

і далі йде ... У всіх випадках не потрібно було спеціальної складки, зменшення чи іншої функціональності для імітації Stream.collect. Якщо у вас є додаткові випадки використання, додайте їх у коментарі, і ми можемо побачити!

Про лінь

Якщо ви хочете ліниво обробити ланцюжок, ви можете перетворитись на Sequenceвикористання asSequence()перед ланцюжком. Наприкінці ланцюга функцій зазвичай ви також закінчуєте Sequenceа. Потім ви можете використовувати toList(), toSet(), toMap()або який -небудь інший функції матеріалізуватиSequence в кінці.

// switch to and from lazy
val someList = items.asSequence().filter { ... }.take(10).map { ... }.toList()

// switch to lazy, but sorted() brings us out again at the end
val someList = items.asSequence().filter { ... }.take(10).map { ... }.sorted()

Чому немає типів?!?

Ви помітите, як у прикладах Котліна не вказані типи. Це пояснюється тим, що Котлін має повний висновок типу і повністю безпечний під час компіляції. Більше, ніж Java, оскільки вона також має зведені типи і може допомогти запобігти жахливий NPE. Отже, це в Котліні:

val someList = people.filter { it.age <= 30 }.map { it.name }

те саме, що:

val someList: List<String> = people.filter { it.age <= 30 }.map { it.name }

Оскільки Котлинський знає , що peopleє, і що people.ageце , Intотже , вираз фільтра допускає тільки порівняння до Int, і що people.nameце , Stringотже, mapкрок виробляє List<String>(тільки для читання Listз String).

Тепер, якщо peopleце можливо null, як List<People>?тоді, як:

val someList = people?.filter { it.age <= 30 }?.map { it.name }

Повертає a, List<String>?який потрібно було б перевірити на нуль ( або використовувати один з інших операторів Котліна для змінних значень. Дивіться цей ідіоматичний спосіб Котліна для обробки нульових значень, а також ідіоматичний спосіб обробки нульового або порожнього списку в Kotlin )

Дивитися також:


Чи є еквівалент паралельному JavaStream () у Котліні?
arnab

Відповідь про незмінні колекції та Котлін - це та сама відповідь для @arnab тут паралельно, існують інші бібліотеки, використовуйте їх: stackoverflow.com/a/34476880/3679676
Jayson Minard

2
@arnab Ви можете поглянути на підтримку Kotlin для Java 7/8 (зокрема, kotlinx-support-jdk8), яка була доступна на початку цього року: обговорити.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
робототехніка

Чи справді ідіоматично використовувати в одній заяві 3 різних посилання "it"?
герман

2
Це вподобання, у наведених вище зразках я робив їх короткими і лише надав локальну назву параметра, якщо це необхідно.
Джейсон Мінард

47

Для додаткових прикладів, ось усі зразки з навчального посібника Java 8 Stream, перетворені на Kotlin. Заголовок кожного прикладу походить із початкової статті:

Як працюють потоки

// Java:
List<String> myList = Arrays.asList("a1", "a2", "b1", "c2", "c1");

myList.stream()
      .filter(s -> s.startsWith("c"))
      .map(String::toUpperCase)
     .sorted()
     .forEach(System.out::println);

// C1
// C2
// Kotlin:
val list = listOf("a1", "a2", "b1", "c2", "c1")
list.filter { it.startsWith('c') }.map (String::toUpperCase).sorted()
        .forEach (::println)

Різні види потоків №1

// Java:
Arrays.asList("a1", "a2", "a3")
    .stream()
    .findFirst()
    .ifPresent(System.out::println);    
// Kotlin:
listOf("a1", "a2", "a3").firstOrNull()?.apply(::println)

або створіть функцію розширення на String, що називається ifPresent:

// Kotlin:
inline fun String?.ifPresent(thenDo: (String)->Unit) = this?.apply { thenDo(this) }

// now use the new extension function:
listOf("a1", "a2", "a3").firstOrNull().ifPresent(::println)

Дивіться також: apply()функція

Дивіться також: Функції розширення

Дивіться також: ?.Оператор безпечного виклику та загальна зведеність: У Котліні ідіоматичний спосіб поводитися з змінними значеннями, посилаючись на них або перетворюючи їх

Різні види потоків №2

// Java:
Stream.of("a1", "a2", "a3")
    .findFirst()
    .ifPresent(System.out::println);    
// Kotlin:
sequenceOf("a1", "a2", "a3").firstOrNull()?.apply(::println)

Різні види потоків №3

// Java:
IntStream.range(1, 4).forEach(System.out::println);
// Kotlin:  (inclusive range)
(1..3).forEach(::println)

Різні види потоків №4

// Java:
Arrays.stream(new int[] {1, 2, 3})
    .map(n -> 2 * n + 1)
    .average()
    .ifPresent(System.out::println); // 5.0    
// Kotlin:
arrayOf(1,2,3).map { 2 * it + 1}.average().apply(::println)

Різні види потоків №5

// Java:
Stream.of("a1", "a2", "a3")
    .map(s -> s.substring(1))
    .mapToInt(Integer::parseInt)
    .max()
    .ifPresent(System.out::println);  // 3
// Kotlin:
sequenceOf("a1", "a2", "a3")
    .map { it.substring(1) }
    .map(String::toInt)
    .max().apply(::println)

Різні види потоків №6

// Java:
IntStream.range(1, 4)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);

// a1
// a2
// a3    
// Kotlin:  (inclusive range)
(1..3).map { "a$it" }.forEach(::println)

Різні види потоків №7

// Java:
Stream.of(1.0, 2.0, 3.0)
    .mapToInt(Double::intValue)
    .mapToObj(i -> "a" + i)
    .forEach(System.out::println);

// a1
// a2
// a3
// Kotlin:
sequenceOf(1.0, 2.0, 3.0).map(Double::toInt).map { "a$it" }.forEach(::println)

Чому варто замовляти питання

Цей розділ навчального посібника Java 8 є однаковим для Kotlin та Java.

Повторне використання потоків

У Котліні від типу збору залежить, чи можна його вживати більше одного разу. Кожен раз Sequenceгенерує новий ітератор, і якщо він не стверджує, що "використовувати лише один раз", він може скидатися до початку кожного разу, коли діяти. Тому, хоча в потоці Java 8 виходить з ладу, але працює в Котліні:

// Java:
Stream<String> stream =
Stream.of("d2", "a2", "b1", "b3", "c").filter(s -> s.startsWith("b"));

stream.anyMatch(s -> true);    // ok
stream.noneMatch(s -> true);   // exception
// Kotlin:  
val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }

stream.forEach(::println) // b1, b2

println("Any B ${stream.any { it.startsWith('b') }}") // Any B true
println("Any C ${stream.any { it.startsWith('c') }}") // Any C false

stream.forEach(::println) // b1, b2

І в Java, щоб отримати таку ж поведінку:

// Java:
Supplier<Stream<String>> streamSupplier =
    () -> Stream.of("d2", "a2", "b1", "b3", "c")
          .filter(s -> s.startsWith("a"));

streamSupplier.get().anyMatch(s -> true);   // ok
streamSupplier.get().noneMatch(s -> true);  // ok

Тому в Котліні постачальник даних вирішує, може він повернути назад і надати новий ітератор чи ні. Але якщо ви хочете навмисно обмежувати Sequenceразову ітерацію, ви можете використовувати constrainOnce()функцію для Sequenceнаступного:

val stream = listOf("d2", "a2", "b1", "b3", "c").asSequence().filter { it.startsWith('b' ) }
        .constrainOnce()

stream.forEach(::println) // b1, b2
stream.forEach(::println) // Error:java.lang.IllegalStateException: This sequence can be consumed only once. 

Розширені операції

Зберіть приклад №5 (так, я пропустив їх уже в іншій відповіді)

// Java:
String phrase = persons
        .stream()
        .filter(p -> p.age >= 18)
        .map(p -> p.name)
        .collect(Collectors.joining(" and ", "In Germany ", " are of legal age."));

    System.out.println(phrase);
    // In Germany Max and Peter and Pamela are of legal age.    
// Kotlin:
val phrase = persons.filter { it.age >= 18 }.map { it.name }
        .joinToString(" and ", "In Germany ", " are of legal age.")

println(phrase)
// In Germany Max and Peter and Pamela are of legal age.

І як зауваження, у Котліні ми можемо створити прості класи даних та інстанціювати дані тесту наступним чином:

// Kotlin:
// data class has equals, hashcode, toString, and copy methods automagically
data class Person(val name: String, val age: Int) 

val persons = listOf(Person("Tod", 5), Person("Max", 33), 
                     Person("Frank", 13), Person("Peter", 80),
                     Person("Pamela", 18))

Зберіть приклад №6

// Java:
Map<Integer, String> map = persons
        .stream()
        .collect(Collectors.toMap(
                p -> p.age,
                p -> p.name,
                (name1, name2) -> name1 + ";" + name2));

System.out.println(map);
// {18=Max, 23=Peter;Pamela, 12=David}    

Гаразд, більш цікавий випадок тут для Котліна. Спочатку неправильні відповіді, щоб вивчити варіанти створення Mapколекції / послідовності:

// Kotlin:
val map1 = persons.map { it.age to it.name }.toMap()
println(map1)
// output: {18=Max, 23=Pamela, 12=David} 
// Result: duplicates overridden, no exception similar to Java 8

val map2 = persons.toMap({ it.age }, { it.name })
println(map2)
// output: {18=Max, 23=Pamela, 12=David} 
// Result: same as above, more verbose, duplicates overridden

val map3 = persons.toMapBy { it.age }
println(map3)
// output: {18=Person(name=Max, age=18), 23=Person(name=Pamela, age=23), 12=Person(name=David, age=12)}
// Result: duplicates overridden again

val map4 = persons.groupBy { it.age }
println(map4)
// output: {18=[Person(name=Max, age=18)], 23=[Person(name=Peter, age=23), Person(name=Pamela, age=23)], 12=[Person(name=David, age=12)]}
// Result: closer, but now have a Map<Int, List<Person>> instead of Map<Int, String>

val map5 = persons.groupBy { it.age }.mapValues { it.value.map { it.name } }
println(map5)
// output: {18=[Max], 23=[Peter, Pamela], 12=[David]}
// Result: closer, but now have a Map<Int, List<String>> instead of Map<Int, String>

А тепер для правильної відповіді:

// Kotlin:
val map6 = persons.groupBy { it.age }.mapValues { it.value.joinToString(";") { it.name } }

println(map6)
// output: {18=Max, 23=Peter;Pamela, 12=David}
// Result: YAY!!

Нам просто потрібно було приєднати відповідні значення для згортання списків та надання трансформатору jointToStringдля переходу від Personекземпляра доPerson.name .

Зберіть приклад №7

Гаразд, це легко зробити без спеціального користування Collector, тому давайте вирішимо його як у Котліні, а потім надумаємо новий приклад, який показує, як зробити подібний процес, Collector.summarizingIntякий, як правило, не існує в Котліні.

// Java:
Collector<Person, StringJoiner, String> personNameCollector =
Collector.of(
        () -> new StringJoiner(" | "),          // supplier
        (j, p) -> j.add(p.name.toUpperCase()),  // accumulator
        (j1, j2) -> j1.merge(j2),               // combiner
        StringJoiner::toString);                // finisher

String names = persons
        .stream()
        .collect(personNameCollector);

System.out.println(names);  // MAX | PETER | PAMELA | DAVID    
// Kotlin:
val names = persons.map { it.name.toUpperCase() }.joinToString(" | ")

Не я винен, що вони обрали тривіальний приклад !!! Гаразд, ось новийsummarizingInt метод для Котліна та відповідний зразок:

Приклад підсумовування

// Java:
IntSummaryStatistics ageSummary =
    persons.stream()
           .collect(Collectors.summarizingInt(p -> p.age));

System.out.println(ageSummary);
// IntSummaryStatistics{count=4, sum=76, min=12, average=19.000000, max=23}    
// Kotlin:

// something to hold the stats...
data class SummaryStatisticsInt(var count: Int = 0,  
                                var sum: Int = 0, 
                                var min: Int = Int.MAX_VALUE, 
                                var max: Int = Int.MIN_VALUE, 
                                var avg: Double = 0.0) {
    fun accumulate(newInt: Int): SummaryStatisticsInt {
        count++
        sum += newInt
        min = min.coerceAtMost(newInt)
        max = max.coerceAtLeast(newInt)
        avg = sum.toDouble() / count
        return this
    }
}

// Now manually doing a fold, since Stream.collect is really just a fold
val stats = persons.fold(SummaryStatisticsInt()) { stats, person -> stats.accumulate(person.age) }

println(stats)
// output: SummaryStatisticsInt(count=4, sum=76, min=12, max=23, avg=19.0)

Але краще створити функцію розширення, 2 насправді, щоб відповідати стилям у stlinli Kotlin:

// Kotlin:
inline fun Collection<Int>.summarizingInt(): SummaryStatisticsInt
        = this.fold(SummaryStatisticsInt()) { stats, num -> stats.accumulate(num) }

inline fun <T: Any> Collection<T>.summarizingInt(transform: (T)->Int): SummaryStatisticsInt =
        this.fold(SummaryStatisticsInt()) { stats, item -> stats.accumulate(transform(item)) }

Тепер у вас є два способи використання нових summarizingIntфункцій:

val stats2 = persons.map { it.age }.summarizingInt()

// or

val stats3 = persons.summarizingInt { it.age }

І все це дає однакові результати. Ми також можемо створити це розширення для роботи над Sequenceвідповідними примітивними типами.

Для розваги порівняйте код Java JDK з спеціальним кодом Котліна, необхідним для здійснення цього узагальнення.


У потоці 5 немає жодного плюсу використовувати дві карти замість однієї .map { it.substring(1).toInt() }: як відомо, добре виведений тип - це один з котлінських потужностей.
Мікеле д'Аміко

правда, але немає і недоліків (для порівняння я тримав їх окремо)
Jayson Minard

Але код Java легко можна зробити паралельним, тому в багатьох випадках вам краще зателефонувати на потіковий код Java з Kotlin.
Говард Ловатт

@HowardLovatt є багато випадків, коли паралель - це не шлях, особливо у важких одночасних умовах, коли ти вже знаходиться в пулі ниток. Б'юсь об заклад, що середній випадок використання НЕ паралельний, і це рідкісний випадок. Але звичайно, у вас завжди є можливість використовувати класи Java, як вважаєте за потрібне, і нічого з цього насправді не було метою цього питання та відповіді.
Джейсон Мінард

3

Є випадки, коли важко уникнути дзвінків collect(Collectors.toList())чи подібних. У цих випадках ви можете швидше перейти на еквівалент Котліна, використовуючи функції розширення, такі як:

fun <T: Any> Stream<T>.toList(): List<T> = this.collect(Collectors.toList<T>())
fun <T: Any> Stream<T>.asSequence(): Sequence<T> = this.iterator().asSequence()

Тоді ви можете просто stream.toList()або stream.asSequence()повернутися назад в API Kotlin. Такий випадок, як Files.list(path)змушує вас перетворитись на те, Streamколи ви цього не хочете, і ці розширення можуть допомогти вам повернутися до стандартних колекцій та API Kotlin.


2

Більше про лінь

Візьмемо для прикладу рішення для "Обчислити суму заробітної плати за відділом", задану Джейсоном:

val totalByDept = employees.groupBy { it.dept }.mapValues { it.value.sumBy { it.salary }}

Для того, щоб зробити це ледачим (тобто уникнути створення проміжної карти на groupByкроці), використовувати це неможливо asSequence(). Натомість ми повинні використовувати groupingByта foldпрацювати:

val totalByDept = employees.groupingBy { it.dept }.fold(0) { acc, e -> acc + e.salary }

Деяким людям це може бути навіть читабельніше, оскільки ви не маєте справу з записами на карті: it.valueчастина цього рішення спочатку також бентежила мене.

Оскільки це звичайний випадок, і ми вважаємо за краще не виписувати foldкожен раз, можливо, краще просто надати загальну sumByфункцію на Grouping:

public inline fun <T, K> Grouping<T, K>.sumBy(
        selector: (T) -> Int
): Map<K, Int> = 
        fold(0) { acc, element -> acc + selector(element) }

щоб ми могли просто написати:

val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.