Kotlin
Java 8 Stream-equivalenten
Zoeken…
Invoering
Kotlin biedt vele uitbreidingsmethoden voor collecties en iterables voor het toepassen van functionele bewerkingen. Een specifiek Sequence
maakt een luie compositie van verschillende van dergelijke bewerkingen mogelijk.
Opmerkingen
Over luiheid
Als u een ketting op een luie manier wilt verwerken, kunt u deze converteren naar een Sequence
met behulp van asSequence()
vóór de ketting. Aan het einde van de reeks functies krijg je meestal ook een Sequence
. Vervolgens kunt u toList()
, toSet()
, toMap()
of een andere functie gebruiken om de Sequence
aan het einde te materialiseren.
// 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()
Waarom zijn er geen typen?!?
U zult merken dat de Kotlin-voorbeelden de typen niet specificeren. Dit komt omdat Kotlin volledige type inferentie heeft en volledig type veilig is tijdens het compileren. Meer nog dan Java omdat het ook nulbare typen heeft en de gevreesde NPE kan helpen voorkomen. Dus dit in Kotlin:
val someList = people.filter { it.age <= 30 }.map { it.name }
is hetzelfde als:
val someList: List<String> = people.filter { it.age <= 30 }.map { it.name }
Omdat Kotlin weet wat people
is, en dat people.age
is Int
daarom het filter uitdrukking staat slechts vergelijking met een Int
en dat people.name
is een String
dus de map
stap produceert een List<String>
(alleen-lezen List
van String
).
Nu, als people
mogelijk null
, zoals in een List<People>?
vervolgens:
val someList = people?.filter { it.age <= 30 }?.map { it.name }
Retourneert een List<String>?
die null-gecontroleerd zou moeten zijn ( of gebruik een van de andere Kotlin-operatoren voor nulbare waarden, zie deze idiomatische manier van Kotlin om met nulbare waarden om te gaan en ook idiomatische manier om nulbare of lege lijst in Kotlin te behandelen )
Streams hergebruiken
In Kotlin hangt het van het type collectie af of deze meerdere keren kan worden geconsumeerd. Een Sequence
genereert elke keer een nieuwe iterator, en tenzij het "slechts eenmaal" beweert, kan het telkens naar de start worden gereset wanneer erop wordt gehandeld. Daarom is het volgende mislukt in Java 8-stream, maar werkt in 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
En in Java om hetzelfde gedrag te krijgen:
// 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
Daarom beslist de aanbieder van de gegevens in Kotlin of deze terug kan gaan en een nieuwe iterator kan leveren of niet. Maar als u opzettelijk een Sequence
wilt beperken tot eenmalige iteratie, kunt u de functie constrainOnce()
voor de Sequence
als volgt gebruiken:
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.
Zie ook:
- API Reference voor uitbreidingsfuncties voor Iterable
- API-referentie voor uitbreidingsfuncties voor Array
- API-referentie voor uitbreidingsfuncties voor Lijst
- API-referentie voor uitbreidingsfuncties naar Map
Verzamel namen in een lijst
// Java:
List<String> list = people.stream().map(Person::getName).collect(Collectors.toList());
// Kotlin:
val list = people.map { it.name } // toList() not needed
Converteer elementen naar strings en voeg ze samen, gescheiden door komma's
// Java:
String joined = things.stream()
.map(Object::toString)
.collect(Collectors.joining(", "));
// Kotlin:
val joined = things.joinToString() // ", " is used as separator, by default
Bereken som van salarissen van werknemer
// Java:
int total = employees.stream()
.collect(Collectors.summingInt(Employee::getSalary)));
// Kotlin:
val total = employees.sumBy { it.salary }
Groepsmedewerkers per afdeling
// Java:
Map<Department, List<Employee>> byDept
= employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
// Kotlin:
val byDept = employees.groupBy { it.department }
Som van lonen per afdeling berekenen
// 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 }}
Verdeel studenten in slagen en falen
// 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 }
Namen van mannelijke leden
// Java:
List<String> namesOfMaleMembersCollect = 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 }
Groepsnamen van leden op basis van geslacht
// 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 } }
Filter een lijst naar een andere lijst
// Java:
List<String> filtered = items.stream()
.filter( item -> item.startsWith("o") )
.collect(Collectors.toList());
// Kotlin:
val filtered = items.filter { item.startsWith('o') }
Een lijst vinden met de kortste reeks
// Java:
String shortest = items.stream()
.min(Comparator.comparing(item -> item.length()))
.get();
// Kotlin:
val shortest = items.minBy { it.length }
Verschillende soorten streams # 2 - lui met het eerste item als dit bestaat
// Java:
Stream.of("a1", "a2", "a3")
.findFirst()
.ifPresent(System.out::println);
// Kotlin:
sequenceOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
Verschillende soorten streams # 3 - itereer een reeks gehele getallen
// Java:
IntStream.range(1, 4).forEach(System.out::println);
// Kotlin: (inclusive range)
(1..3).forEach(::println)
Verschillende soorten streams # 4 - itereer een array, wijs de waarden toe, bereken het gemiddelde
// 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)
Verschillende soorten streams # 5 - ituleer lui een lijst met strings, wijs de waarden toe, converteer naar Int, vind max
// 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)
Verschillende soorten stromen # 6 - itereer lui een stroom Ints, breng de waarden in kaart, druk resultaten af
// 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)
Verschillende soorten streams # 7 - dubbel lui itereren, kaart naar Int, kaart naar String, print elk
// 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)
Items in een lijst tellen nadat het filter is toegepast
// 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') }
Hoe streams werken - filter, hoofdletters en sorteer vervolgens een lijst
// 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)
Verschillende soorten streams # 1 - enthousiast gebruikend eerste item als het bestaat
// Java:
Arrays.asList("a1", "a2", "a3")
.stream()
.findFirst()
.ifPresent(System.out::println);
// Kotlin:
listOf("a1", "a2", "a3").firstOrNull()?.apply(::println)
of maak een uitbreidingsfunctie op String met de naam 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)
Zie ook: de functie apply()
Zie ook: Uitbreidingsfuncties
Zie ook: ?.
Safe Call-operator en in het algemeen nullabiliteit: http://stackoverflow.com/questions/34498562/in-kotlin-what-is-the-idiomatic-way-to-deal-with-nullable-values-referencing-o/34498563 # 34498563
Verzamel voorbeeld # 5 - vind mensen van wettelijke leeftijd, uitvoer geformatteerde string
// 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.
En als een kanttekening, in Kotlin we kunnen eenvoudig creëren data klassen en de testgegevens instantiëren als volgt:
// 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))
Verzamel voorbeeld # 6 - groepeer mensen op leeftijd, afdrukleeftijd en namen samen
// 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, een interessantere zaak hier voor Kotlin. Eerst de verkeerde antwoorden om variaties van het maken van een Map
uit een verzameling / reeks te verkennen:
// 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>
En nu voor het juiste antwoord:
// 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!!
We moesten alleen de overeenkomende waarden samenvoegen om de lijsten samen te vouwen en een transformator te bieden om joinToString
te verplaatsen van de instantie Person
naar de Person.name
.
Verzamel voorbeeld # 7a - Kaartnamen, voeg samen met scheidingsteken
// Java (verbose):
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
// Java (concise)
String names = persons.stream().map(p -> p.name.toUpperCase()).collect(Collectors.joining(" | "));
// Kotlin:
val names = persons.map { it.name.toUpperCase() }.joinToString(" | ")
Verzamel voorbeeld # 7b - Verzamel met 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)
Maar het is beter om een uitbreidingsfunctie te maken, 2 die eigenlijk overeenkomt met stijlen in 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)) }
Nu hebt u twee manieren om de nieuwe functies voor summarizingInt
te gebruiken:
val stats2 = persons.map { it.age }.summarizingInt()
// or
val stats3 = persons.summarizingInt { it.age }
En al deze produceren dezelfde resultaten. We kunnen deze extensie ook maken om te werken aan Sequence
en voor geschikte primitieve types.