Jaki Strumień Java 8.Zbierz odpowiedniki są dostępne w standardowej bibliotece Kotlin?

W Javie 8 jest Stream.collect który umożliwia agregacje na kolekcjach. W Kotlinie nie istnieje to w ten sam sposób, poza tym może jako zbiór funkcji rozszerzeń w stdlib. Ale nie jest jasne, jakie są ekwiwalencje dla różnych przypadków użycia.

Na przykład na górze JavaDoc dla Collectors są przykładami napisanymi dla Javy 8, a przy ich przenoszeniu do Kolin nie można używać klas Java 8, gdy na innej wersji JDK, więc prawdopodobnie powinny być napisane inaczej.

Jeśli chodzi o zasoby online pokazujące przykłady kolekcji Kotlina, są one zazwyczaj trywialne i nie porównują się do tych samych przypadków użycia. Jakie są dobre przykłady, które naprawdę pasują do przypadków, takich jak udokumentowane dla Java 8 Stream.collect? Lista znajduje się:

  • gromadzić nazwy na listę
  • gromadzić nazwy w TreeSet
  • Konwertuj elementy na łańcuchy i łącz je, oddzielając przecinki
  • Oblicz sumę wynagrodzeń pracownika
  • grupa pracowników według działów
  • Oblicz sumę wynagrodzeń według działów
  • podział uczniów na zdające i obalające

Ze szczegółami w JavaDoc podlinkowanym powyżej.

Uwaga: to pytanie jest celowo napisane i odpowiedziane przez autora (Self-answer Questions), tak aby idiomatyczne odpowiedzi na najczęściej zadawane tematy Kotlina były obecne w SO. Również w celu wyjaśnienia niektórych naprawdę starych odpowiedzi napisanych dla Alf z Kotlina, które nie są dokładne dla obecnego Kotlina.

Author: Jayson Minard, 2016-01-06

4 answers

W Kotlin stdlib znajdują się funkcje: average, count, distinct, filtering, finding, Group, joining, mapping, min, max, partioning, slicing, sortowanie, sumowanie, to/from array, to/from lists, to/from maps, union, co-iteration, all the functional paradygmates, and more. Możesz więc używać ich do tworzenia małych 1-linerów i nie ma potrzeby używania bardziej skomplikowanej składni Java 8.

myślę, że jedyną rzeczą, której brakuje we wbudowanej klasie Java 8 Collectors jest podsumowanie (ale w inna odpowiedź na to pytanie jest prostym rozwiązaniem) .

Jednej rzeczy brakuje w obu jest porcjowanie według liczby, co widać w kolejna odpowiedź przepełnienia stosu i ma również prostą odpowiedź. Innym interesującym przypadkiem jest ten również ze Stack Overflow: Idiomatyczny sposób rozlewania sekwencji na trzy listy za pomocą Kotlina . Jeśli chcesz utworzyć coś w rodzaju Stream.collect w innym celu, zobacz strumień Niestandardowy.zbieraj w Kotlinie

Edycja 11.08.2017: w kotlinie o powierzchni 1,2 M2 dodano operacje kolekcji Chunked / windowed, zobacz https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


Zawsze dobrze jest zapoznać się z referencjami API dla Kotlina.zbiory jako całość przed utworzeniem nowych funkcji, które mogą już tam istnieć.

Oto kilka konwersji z przykładów Java 8 Stream.collect do odpowiednika w Kotlin:

Gromadź nazwy na Listę

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

Konwertuj elementy na łańcuchy i łącz je, oddzielając przecinkami

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

Oblicz sumę wynagrodzeń pracownika

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

Grupa pracowników według działów

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

Oblicz sumę wynagrodzeń według działów

// 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 }}

Podział uczniów na zdające i brak

// 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 }

Imiona męskich członków

// 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 }

Nazwa grupy członków w spisie według płci

// 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 } }   

Filtruj listę do innej listy

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

Znajdowanie najkrótszego ciągu listy

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

Zliczanie pozycji na liście po zastosowaniu filtra

// 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') }

I zaczyna się... We wszystkich przypadkach, nie specjalne krotnie, zmniejszyć, lub inne funkcjonalność była wymagana do naśladowania Stream.collect. Jeśli masz inne przypadki użycia, dodaj je w komentarzach, a zobaczymy!

O lenistwie

Jeśli chcesz leniwie przetwarzać łańcuch, możesz przekonwertować do Sequence używając asSequence() przed łańcuchem. Na końcu łańcucha funkcji zwykle kończy się Sequence. Wtedy możesz użyć toList(), toSet(), toMap() lub jakąś inną funkcję zmaterializującą Sequence na końcu.

// 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()

Dlaczego nie ma Typy?!?

Zauważysz, że przykłady Kotlina nie określają typów. Dzieje się tak dlatego, że Kotlin ma pełne wnioskowanie typu i jest całkowicie bezpieczny w czasie kompilacji. Bardziej niż Java, ponieważ ma również typy nullable i może pomóc zapobiec przerażającemu NPE. Więc to w Kotlinie:

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

Jest tym samym co:

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

Ponieważ Kotlin wie, czym jest people, a to people.age jest Int dlatego wyrażenie filtrujące pozwala tylko na porównanie do Int, a to people.name jest a String dlatego krok map tworzy A List<String> (tylko czytane List z String).

Teraz, jeśli people były ewentualnie null, jak-w List<People>? wtedy:

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

Zwraca List<String>?, które musiałyby być sprawdzone jako NULL (lub użyć jednego z innych operatorów Kotlina dla wartości nullable, zobacz ten Idiomatyczny sposób postępowania z wartościami nullable , a także Idiomatyczny sposób obsługi listy nullable lub pustej w Kotlinie)

Zobacz także:

 271
Author: Jayson Minard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-05-05 14:39:06

Aby uzyskać dodatkowe przykłady, oto wszystkie próbki z Java 8 Stream Tutorial przekonwertowane do Kotlina. W 2007 roku został wybrany do Izby Gmin.]}

Jak działają strumienie

// 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)

Różne rodzaje strumieni #1

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

Lub utwórz funkcję rozszerzenia na łańcuchu o nazwie 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)

Zobacz: apply() function

Zobacz też: rozszerzenie Funkcje

Zobacz: ?. w Kotlinie, w jaki sposób idiomatyczny sposób radzić sobie z wartościami nullable, odwoływać się do nich lub je konwertować[50]} {]}

Różne rodzaje strumieni #2

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

Różne rodzaje strumieni #3

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

Różne rodzaje strumieni #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)

Różne rodzaje strumieni #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)

Różne rodzaje strumieni #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)

Różne rodzaje strumieni #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)

Dlaczego Porządek Ma Znaczenie

Ta sekcja samouczka Java 8 Stream jest taka sama dla Kotlin i Java.

Ponowne Wykorzystanie Strumieni

W Kotlinie, to zależy od rodzaju kolekcji, czy może być zużyty więcej niż jeden raz. A Sequence generuje za każdym razem nowy iterator, chyba że stwierdzi " użyj tylko raz " może zresetować się do startu za każdym razem, gdy zostanie uruchomiony. Z tego powodu w Javie 8 stream nie działa, ale działa w Kotlinie:

// 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

I w Javie, aby uzyskać to samo zachowanie:

// 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

Dlatego w Kotlin dostawca danych decyduje, czy może zresetować i dostarczyć nowy iterator, czy nie. Jeśli jednak chcesz celowo ograniczyć Sequence do jednorazowej iteracji, możesz użyć constrainOnce() funkcji dla Sequence w następujący sposób:

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. 

Zaawansowane Operacje

Zbierz przykład # 5 (tak, pominąłem je już w drugiej odpowiedzi)

// 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.

I na marginesie, w Kotlinie możemy utworzyć proste klasy danych i utworzyć instancję danych testowych w następujący sposób:

// 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))

Zbierz przykład #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}    
Ok, bardziej interesująca sprawa dla Kotlina. Pierwsze błędne odpowiedzi do zbadania odmian tworzenia Map ze zbioru / sekwencji:

// 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>

A teraz za poprawną odpowiedź:

// 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!!

Po prostu musieliśmy połączyć pasujące wartości, aby zwijać listy i zapewnić transformator do jointToString, aby przejść z instancji Person do instancji Person.name.

Zbierz przykład #7

Ok, ten można łatwo zrobić bez niestandardowego Collector, więc rozwiążmy go w sposób Kotlina, a następnie wymyślmy Nowy przykład, który pokazuje, jak wykonać podobny proces dla Collector.summarizingInt, który nie istnieje natywnie w Kotlinie.

// 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(" | ")

on nie moja wina, że wybrali trywialny przykład!!! Ok, oto nowa metoda summarizingInt Dla Kotlina i dopasowana próbka:

Podsumowanie Przykład

// 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)
Jednak lepiej jest utworzyć funkcję rozszerzenia, 2 faktycznie dopasować style w Kotlin stdlib:
// 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)) }

Teraz masz dwa sposoby korzystania z nowych funkcji summarizingInt:

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

// or

val stats3 = persons.summarizingInt { it.age }

I wszystkie z nich dają takie same wyniki. Możemy również utworzyć to rozszerzenie do pracy na Sequence i dla odpowiednie prymitywne typy.

Dla Zabawy, porównaj kod Java JDK z kodem niestandardowym Kotlina wymaganym do implementacji tego podsumowania.

 47
Author: Jayson Minard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-25 16:17:58

Są przypadki, w których trudno jest uniknąć wywołania collect(Collectors.toList()) lub podobnego. W takich przypadkach można szybciej zmienić na odpowiednik Kotlina za pomocą funkcji rozszerzenia, takich jak:

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

Następnie możesz po prostu stream.toList() lub stream.asSequence() wrócić do API Kotlina. Przypadek taki jak Files.list(path) zmusza cię do Stream, gdy możesz go nie chcieć, a te rozszerzenia mogą pomóc ci wrócić do standardowych kolekcji i API Kotlin.

 3
Author: Jayson Minard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-03-01 13:30:16

Więcej o lenistwie

Weźmy przykładowe rozwiązanie dla "Oblicz sumę wynagrodzeń według działów" podaną przez Jaysona:

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

Aby zrobić to leniwe (tzn. uniknąć tworzenia mapy pośredniej w kroku groupBy), nie jest możliwe użycie asSequence(). Zamiast tego musimy użyć operacji groupingBy i fold:

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

Dla niektórych może to być nawet bardziej czytelne, ponieważ nie masz do czynienia z wpisami map: it.value część rozwiązania była dla mnie myląca również na najpierw.

Ponieważ jest to częsty przypadek i wolelibyśmy nie wypisywać fold za każdym razem, może lepiej podać ogólną sumBy funkcję na Grouping:

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

Abyśmy mogli po prostu napisać:

val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }
 2
Author: herman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-03-22 18:02:48