Jakie odpowiedniki Java 8 Stream.collect są dostępne w standardowej bibliotece Kotlin?


180

W Javie 8 istnieje Stream.collectmożliwość agregacji zbiorów. W Kotlinie nie istnieje to w ten sam sposób, poza tym, że może być zbiorem funkcji rozszerzenia w stdlib. Ale nie jest jasne, jakie są odpowiedniki dla różnych przypadków użycia.

Na przykład na górze JavaDocCollectors znajdują się przykłady napisane dla Java 8, a podczas przenoszenia ich do Kolina nie można używać klas Java 8 w innej wersji JDK, więc prawdopodobnie powinny być napisane inaczej.

Jeśli chodzi o zasoby online pokazujące przykłady kolekcji Kotlin, są one zwykle trywialne i tak naprawdę 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? Oto lista:

  • Gromadzić nazwy na liście
  • Zbierz nazwy w TreeSet
  • Konwertuj elementy na ciągi i łącz je, oddzielając je przecinkami
  • Oblicz sumę wynagrodzeń pracownika
  • Grupuj pracowników według działów
  • Oblicz sumę wynagrodzeń według działów
  • Podziel uczniów na zaliczenia i porażki

Szczegółowe informacje w JavaDoc znajdują się powyżej.

Uwaga: to pytanie zostało celowo napisane i udzielone przez autora ( pytania z odpowiedziami ), 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 alpha Kotlina, które nie są dokładne dla dzisiejszego Kotlina.


W przypadkach, w których nie masz innego wyboru, jak użyć collect(Collectors.toList())lub podobny, możesz trafić w ten problem: stackoverflow.com/a/35722167/3679676 (problem, z obejściem)
— Jayson Minard

Odpowiedzi:


256

W standardzie Kotlin znajdują się funkcje: uśredniania, liczenia, wyodrębniania, filtrowania, wyszukiwania, grupowania, łączenia, mapowania, min, maks., Partycjonowania, krojenia, sortowania, sumowania, do / z tablic, do / z list, do / z map , zjednoczenie, ko-iteracja, wszystkie paradygmaty funkcjonalne i wiele innych. Możesz więc używać ich do tworzenia małych 1-liniowych programów i nie trzeba używać bardziej skomplikowanej składni Java 8.

Myślę, że jedyną rzeczą, której brakuje we wbudowanej Collectorsklasie Java 8, jest podsumowanie (ale w innej odpowiedzi na to pytanie jest proste rozwiązanie) .

Jedną z rzeczy, której brakuje w obu przypadkach, jest grupowanie według liczby, co widać w innej odpowiedzi Przepełnienie stosu i ma również prostą odpowiedź. Innym interesującym przypadkiem jest również ten z przepełnienia stosu: Idiomatyczny sposób na rozlanie sekwencji na trzy listy za pomocą Kotlina . A jeśli chcesz stworzyć coś Stream.collectw innym celu, zobacz Custom Stream.collect w Kotlin

EDYCJA 11.08.2017: Operacje zbierania fragmentów / okien zostały dodane w kotlin 1.2 M2, patrz https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/


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

Oto niektóre konwersje z Stream.collectprzykładów Java 8 na odpowiednik w Kotlin:

Gromadzić nazwy na liście

// 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 ciągi i łącz je, oddzielając je 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 }

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

Podziel uczniów na zaliczenia i porażki

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

Nazwiska członków męskich

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

Grupuj nazwiska członków w wykazie 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 znaków na liście

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

Zliczanie elementów 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 tak dalej ... We wszystkich przypadkach naśladowanie nie wymagało specjalnego składania, zmniejszania ani innych funkcji Stream.collect. Jeśli masz dalsze przypadki użycia, dodaj je w komentarzach, a my zobaczymy!

O lenistwie

Jeśli chcesz leniwie przetworzyć łańcuch, możesz przekonwertować go na Sequenceusing asSequence()przed łańcuchem. Na końcu łańcucha funkcji zwykle kończy się również znakiem „ Sequencea”. Następnie można użyć toList(), toSet(), toMap()lub jakaś inna funkcja urzeczywistnienia Sequencena 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 żadnych typów?!?

Zauważysz, że przykłady Kotlin nie określają typów. Jest tak, ponieważ Kotlin ma pełne wnioskowanie o typach i jest całkowicie bezpieczny podczas kompilacji. Bardziej niż Java, ponieważ ma również typy zerowalne i może pomóc zapobiec przerażającemu NPE. To w Kotlinie:

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

jest taki sam jak:

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

Kotlin bo nie wie, co peoplejest, i to people.agejest Intzatem wyrazem filtr pozwala tylko porównania do Int, a people.nameto Stringdlatego mapkrok wytwarza List<String>(tylko do odczytu Listz String).

Teraz, jeśli peopleto możliwe null, jako List<People>?:

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

Zwraca wartość List<String>?, która wymagałaby sprawdzenia wartości null ( lub użyj jednego z innych operatorów Kotlin dla wartości zerowalnych, zobacz ten idiomatyczny sposób Kotlina, aby radzić sobie z wartościami zerowalnymi, a także Idiomatyczny sposób obsługi wartości zerowej lub pustej listy w Kotlinie )

Zobacz też:


Czy w Kotlin istnieje odpowiednik JavaStartallelStream ()?
— arnab

Odpowiedź na niezmienne kolekcje i Kotlin to ta sama odpowiedź na @arnab tutaj równolegle, istnieją inne biblioteki, użyj ich: stackoverflow.com/a/34476880/3679676
— Jayson Minard

2
@arnab Być może warto przyjrzeć się obsłudze Kotlin dla funkcji Java 7/8 (w szczególności kotlinx-support-jdk8), która została udostępniona na początku tego roku: omów.kotlinlang.org
— roborative

Czy naprawdę idiomatyczne jest stosowanie 3 różnych odniesień „it” w jednej instrukcji?
— herman

2
Jest to preferowane, w powyższych przykładach utrzymywałem je krótkie i podając tylko lokalną nazwę parametru, jeśli to konieczne.
— Jayson Minard

47

Aby uzyskać dodatkowe przykłady, oto wszystkie przykłady z samouczka strumienia Java 8 przekonwertowanego na Kotlin. Tytuł każdego przykładu pochodzi z artykułu źródłowego:

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 także: apply()funkcja

Zobacz także: Funkcje rozszerzeń

Zobacz także: ?.operator Safe Call i ogólnie zerowalność : w Kotlin, co jest idiomatycznym sposobem radzenia sobie z wartościami zerowymi, odwoływania się do nich lub konwertowania ich

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 warto się zamawiać?

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

Ponowne użycie strumieni

W Kotlinie zależy od rodzaju zbioru, czy można go spożywać więcej niż jeden raz. A Sequencegeneruje nowy iterator za każdym razem i jeśli nie zapewni „użyj tylko raz”, może zresetować do początku za każdym razem, gdy jest podejmowane działanie. Dlatego, mimo że w strumieniu Java 8 nie działa, ale działa w Kotlin:

// 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ć takie 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. Ale jeśli chcesz celowo ograniczyć Sequenceiterację do jednego czasu, możesz użyć constrainOnce()funkcji Sequencew 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 te, które już znalazły się w innej 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.

Na marginesie, w Kotlin możemy tworzyć proste klasy danych i tworzyć 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ący przypadek tutaj dla Kotlina. Najpierw złe odpowiedzi, aby zbadać warianty tworzenia Mapzbioru / 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 poprawna 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!!

Musieliśmy tylko połączyć pasujące wartości, aby zwinąć listy i zapewnić transformator jointToStringdo przenoszenia z Personinstancji do Person.name.

Zbierz przykład 7

Ok, można to łatwo zrobić bez niestandardowego rozwiązania Collector, więc rozwiążmy to na Kotlin, a następnie wymyślmy nowy przykład, który pokazuje, jak wykonać podobny proces, dla Collector.summarizingIntktórego w Kotlinie nie ma natywnej metody.

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

To nie moja wina, że ​​wybrali trywialny przykład !!! Ok, oto nowa summarizingIntmetoda dla Kotlina i pasująca próbka:

Przykład podsumowania

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

Ale lepiej jest utworzyć funkcję rozszerzenia, w rzeczywistości 2, aby 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 summarizingIntfunkcji:

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

// or

val stats3 = persons.summarizingInt { it.age }

Wszystkie dają te same wyniki. Możemy również utworzyć to rozszerzenie do pracy Sequencei dla odpowiednich typów pierwotnych.

Dla zabawy porównaj kod JDK Java i niestandardowy kod Kotlin wymagany do zaimplementowania tego podsumowania.


W strumieniu 5 nie ma plusów, aby użyć dwóch map zamiast jednej .map { it.substring(1).toInt() }: jak wiadomo, dobrze wywnioskowany typ to moc kotlin.
— Michele d'Amico,

to prawda, ale nie ma też wady (dla porównania trzymałem je osobno)
— Jayson Minard

Ale kod Java można łatwo ustawić równolegle, więc w wielu przypadkach lepiej byłoby wywołać kod strumienia Java z Kotlin.
— Howard Lovatt

@HowardLovatt istnieje wiele przypadków, w których równoległość nie jest dobrym rozwiązaniem, szczególnie w ciężkich, współbieżnych środowiskach, w których już jesteś w puli wątków. Założę się, że przeciętny przypadek użycia NIE jest równoległy i jest to rzadki przypadek. Ale oczywiście zawsze masz możliwość korzystania z klas Java według własnego uznania, i tak naprawdę to nie było celem tego pytania i odpowiedzi.
— Jayson Minard

3

W niektórych przypadkach trudno uniknąć połączeń telefonicznych collect(Collectors.toList())lub podobnych. W takich przypadkach można szybciej zmienić na ekwiwalent Kotlin za pomocą funkcji rozszerzeń, 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 interfejsu API Kotlin. Przypadek taki jak Files.list(path)zmusza cię do Streammomentu, w którym możesz go nie chcieć, a te rozszerzenia mogą pomóc Ci wrócić do standardowych kolekcji i interfejsu API Kotlin.


2

Więcej o lenistwie

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

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

Aby uczynić to leniwym (tj. Uniknąć tworzenia mapy pośredniej w groupBykroku), nie można jej użyć asSequence(). Zamiast tego musimy używać groupingByi foldobsługiwać:

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

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

Ponieważ jest to częsty przypadek i za foldkażdym razem wolimy nie wypisywać , lepiej może być zapewnienie ogólnej sumByfunkcji 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 }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.