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.