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.
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:
- API Reference for extension functions for Iterable
- API reference for extension functions for Array
- API reference for extension functions for List
- API reference for extension functions to Map
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: Różne rodzaje strumieni #2 Różne rodzaje strumieni #3 Różne rodzaje strumieni #4 Różne rodzaje strumieni #5 Różne rodzaje strumieni #6 Różne rodzaje strumieni #7 Ta sekcja samouczka Java 8 Stream jest taka sama dla Kotlin i Java. W Kotlinie, to zależy od rodzaju kolekcji, czy może być zużyty więcej niż jeden raz. A I w Javie, aby uzyskać to samo zachowanie: Dlatego w Kotlin dostawca danych decyduje, czy może zresetować i dostarczyć nowy iterator, czy nie. Jeśli jednak chcesz celowo ograniczyć Zbierz przykład # 5 (tak, pominąłem je już w drugiej odpowiedzi) I na marginesie, w Kotlinie możemy utworzyć proste klasy danych i utworzyć instancję danych testowych w następujący sposób: Zbierz przykład #6?.
w Kotlinie, w jaki sposób idiomatyczny sposób radzić sobie z wartościami nullable, odwoływać się do nich lub je konwertować[50]} {]}
// Java:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println);
// Kotlin:
sequenceOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
// Java:
IntStream.range(1, 4).forEach(System.out::println);
// Kotlin: (inclusive range)
(1..3).forEach(::println)
// 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)
// 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)
// 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)
// 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
Ponowne Wykorzystanie Strumieni
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
// 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
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
// 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))
Ok, bardziej interesująca sprawa dla Kotlina. Pierwsze błędne odpowiedzi do zbadania odmian tworzenia // 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
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.
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.
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 }
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