표준 Kotlin 라이브러리에서 사용할 수있는 Java 8 Stream.collect는 무엇입니까?


180

Java 8에는 Stream.collect컬렉션에 대한 집계가 가능합니다. Kotlin에서는 stdlib의 확장 함수 모음 이외의 다른 방식으로 존재하지 않습니다. 그러나 다른 사용 사례에 해당하는 것이 무엇인지는 확실하지 않습니다.

예를 들어 JavaDocCollectors맨 위에는 Java 8 용으로 작성된 예제가 있으며이를 Kolin으로 이식 할 때 다른 JDK 버전에서는 Java 8 클래스를 사용할 수 없으므로 다르게 작성해야합니다.

Kotlin 컬렉션의 예를 보여주는 온라인 리소스 측면에서 볼 때 일반적으로 사소하고 동일한 사용 사례와 실제로 비교되지는 않습니다. Java 8 용으로 문서화 된 것과 실제로 일치하는 좋은 예는 무엇입니까 Stream.collect? 목록은 다음과 같습니다.

  • 이름을리스트에 축적
  • 이름을 TreeSet에 축적
  • 요소를 문자열로 변환하고 쉼표로 구분하여 연결
  • 직원의 급여 합계 계산
  • 부서별 그룹 직원
  • 부서별 급여 계산
  • 학생들을 합격과 불합격으로 나누십시오

위에 링크 된 JavaDoc에 세부 사항이 있습니다.

참고 : 이 질문은 저자 ( 자기 답변 질문 ) 가 의도적으로 작성하고 답변 하므로 일반적인 Kotlin 주제에 대한 관용적 답변이 SO에 있습니다. 또한 현재 코 틀린에 대해 정확하지 않은 코 틀린 알파에 대해 작성된 오래된 답변을 명확하게하기 위해.


당신은 선택의 여지가 있지만 사용하는 경우에는 collect(Collectors.toList())유사하거나, 당신은이 문제를 칠 수 있습니다 stackoverflow.com/a/35722167/3679676 (해결 방법의 문제)
제이슨 Minard

답변:


256

Kotlin stdlib에는 평균, 개수, 구별, 필터링, 찾기, 그룹화, 결합, 매핑, 최소, 최대, 파티셔닝, 슬라이싱, 정렬, 합산, 어레이 간 / 목록 간, 목록 간 /지도 간 기능이 있습니다. , 노조, 공동 반복, 모든 기능적 패러다임 등. 따라서 그것들을 사용하여 작은 1 라이너를 만들 수 있으며 더 복잡한 Java 8 구문을 사용할 필요가 없습니다.

내장 Java 8 Collectors클래스 에서 누락 된 유일한 것은 요약 이라고 생각합니다 (그러나이 질문에 대한 또 다른 대답 은 간단한 해결책입니다) .

둘 다에서 누락 된 한 가지는 다른 배치 오버플로 답변 에서 볼 수 있고 간단한 답변도있는 카운트 기준 배치 입니다. 또 다른 흥미로운 사례는 Stack Overflow : Kotlin을 사용하여 시퀀스를 세 개의 목록으로 유출하는 관용적 방법 입니다. Stream.collect다른 목적으로 무언가를 만들고 싶다면 Kotlin의 Custom Stream.collect를 참조하십시오.

11.08.2017 편집 : 청크 / 창 수집 작업이 kotlin 1.2 M2에 추가되었습니다 ( https://blog.jetbrains.com/kotlin/2017/08/kotlin-1-2-m2-is-out/ 참조)


이미 존재하는 새로운 함수를 만들기 전에 항상 kotlin.collections에 대한 API 참조 를 탐색하는 것이 좋습니다 .

다음은 Java 8 Stream.collect예제에서 Kotlin의 해당 예제로 변환 한 것입니다 .

이름을리스트에 축적

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

요소를 문자열로 변환하고 쉼표로 구분하여 연결

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

직원의 급여 합계 계산

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

부서별 그룹 직원

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

부서별 급여 계산

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

학생들을 합격과 불합격으로 나누십시오

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

남성 회원의 이름

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

성별 별 명단 회원의 그룹 이름

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

다른 목록으로 목록 필터링

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

가장 짧은 문자열 목록 찾기

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

필터가 적용된 후 목록에서 항목 수 계산

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

모든 경우에 특별한 모방, 축소 또는 기타 기능이 필요하지 않았습니다 Stream.collect. 더 많은 유스 케이스가 있으면 주석에 추가하면 볼 수 있습니다!

게으름에 대해

당신이 게으른 프로세스 체인을하려는 경우에 변환 할 수 있습니다 Sequence사용하여 asSequence()체인 전에. 기능 체인의 끝에서 보통 당신은 결국으로 끝납니다 Sequence. 그럼 당신은 사용할 수 있습니다 toList(), toSet(), toMap()또는 다른 기능은을 실현하기 위해 Sequence마지막에.

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

왜 타입이 없습니까?!?

Kotlin 예제는 유형을 지정하지 않습니다. 이것은 Kotlin이 전체 형식 유추를 가지며 컴파일 타임에 완전히 형식 안전하기 때문입니다. Java보다 null이 가능하며 nullable 유형도 있으며 두려워하는 NPE를 방지 할 수 있습니다. 그래서 이것은 코 틀린에서 :

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

와 같다:

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

코 틀린를 알고 있으므로 people, 그리고 그 people.age이고 Int따라서 필터 표현식만을 비교를 허용 Int하고, 즉, people.nameA는 String따라서 map단계는 생성 List<String>(읽기 전용 ListString).

이제 경우 people였다 가능 null, AS-에서 List<People>?다음 :

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

List<String>?null 검사가 필요한을 반환합니다 ( 또는 nullable 값에 다른 Kotlin 연산자 중 하나를 사용하십시오. null이 가능한 값 을 처리하는Kotlin 관용적 방법Kotlin에서 nullable 또는 빈 목록 을 처리하는 관용적 방법 참조 )

또한보십시오:


Kotlin에 Java8의 parallelStream ()과 동등한 것이 있습니까?
arnab

불변 컬렉션과 Kotlin에 대한 대답은 @arnab에 대한 동일한 대답입니다. 여기에 다른 라이브러리가 있습니다. stackoverflow.com/a/34476880/3679676
Jayson Minard

2
@arnab 당신은 올해 초에 이용할 수있게했다 그 (특히, kotlinx 지원-jdk8에) 자바 7/8 기능에 대한 코 틀린 지원을보고 할 수 있습니다 : discuss.kotlinlang.org/t/jdk7-8-features-in -kotlin-1-0 / 1625
roborative

한 문장에서 3 개의 다른 "it"참조를 사용하는 것이 실제로 관용적입니까?
herman

2
선호하는 것입니다. 위의 샘플에서는 짧게 유지하고 필요한 경우 매개 변수의 로컬 이름 만 제공했습니다.
Jayson Minard

47

추가 예제는 다음과 같이 Java 8 Stream Tutorial의 모든 샘플 을 Kotlin으로 변환 한 것입니다. 각 예제의 제목은 소스 기사에서 파생됩니다.

스트림 작동 방식

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

다른 종류의 스트림 # 1

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

또는 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)

참조 : apply()기능

참조 : 확장 기능

참고 : ?.Safe Call 연산자 및 일반적으로 null 허용 여부 : Kotlin에서는 nullable 값을 처리하거나 참조하거나 변환하는 관용적 방법이 무엇입니까

다른 종류의 스트림 # 2

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

다른 종류의 스트림 # 3

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

다른 종류의 스트림 # 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)

다른 종류의 스트림 # 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)

다른 종류의 스트림 # 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)

다른 종류의 스트림 # 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)

주문이 중요한 이유

Java 8 스트림 학습서의이 섹션은 Kotlin 및 Java에서 동일합니다.

스트림 재사용

Kotlin에서는 컬렉션 유형에 따라 두 번 이상 사용할 수 있는지 여부에 따라 다릅니다. A Sequence는 매번 새로운 반복자를 생성하며, "한 번만 사용"한다고 주장하지 않는 한, 실행될 때마다 시작으로 재설정 할 수 있습니다. 따라서 Java 8 스트림에서는 다음이 실패하지만 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

Java에서 동일한 동작을 얻으려면

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

따라서 Kotlin에서 데이터 제공자는 다시 재설정하고 새 반복자를 제공 할 수 있는지 여부를 결정합니다. 그러나 의도적으로 Sequence일회성 반복을 제한 하려면 다음과 같은 constrainOnce()기능을 사용할 수 있습니다 Sequence.

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. 

고급 작업

예제 # 5를 수집하십시오 (예, 이미 다른 답변에있는 것을 건너 뛰었습니다)

// 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 에서는 다음과 같이 간단한 데이터 클래스를 만들고 테스트 데이터를 인스턴스화 할 수 있습니다 .

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

예제 # 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}    

좋아, Kotlin에 대한 더 흥미로운 사례입니다. 먼저 Map컬렉션 / 시퀀스에서 생성의 변형을 탐색하는 잘못된 답변 :

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

그리고 지금 정답 :

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

일치하는 값을 조인하여 목록을 축소하고 인스턴스 jointToString에서로 이동 하는 변환기를 제공 Person해야했습니다 Person.name.

예제 # 7 수집

좋아, 이것은 사용자 정의없이 쉽게 수행 할 수 있으므로 CollectorKotlin 방식으로 해결 한 다음 Collector.summarizingIntKotlin에 기본적으로 존재하지 않는 유사한 프로세스를 수행하는 방법을 보여주는 새로운 예제를 고안해 보겠습니다 .

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

그들이 사소한 예를 고른 것은 내 잘못이 아닙니다 !!! 다음은 summarizingIntKotlin 의 새로운 방법과 일치하는 샘플입니다.

SummaryInt 예제

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

그러나 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)) }

이제 새로운 summarizingInt기능 을 사용하는 두 가지 방법이 있습니다 .

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

// or

val stats3 = persons.summarizingInt { it.age }

그리고이 모든 것들이 같은 결과를 낳습니다. 또한이 기본 확장을 작성 Sequence하여 적절한 기본 유형에 대해 작업하고 수행 할 수 있습니다.

재미 를 위해이 요약을 구현하는 데 필요한 Java JDK 코드와 Kotlin 사용자 정의 코드를 비교하십시오 .


스트림 5에는 하나 대신 두 개의 맵을 사용하는 플러스가 없습니다 .map { it.substring(1).toInt() }. 잘 알려진 유추 유형은 kotlin power 중 하나입니다.
Michele d' Amico

사실,하지만 단점 중 하나가 없다 (비교를 위해 나는 그들이 분리 유지)
제이슨 Minard

그러나 Java 코드는 쉽게 병렬로 만들 수 있으므로 많은 경우 Kotlin에서 Java 스트림 코드를 호출하는 것이 좋습니다.
Howard Lovatt

@HowardLovatt 병렬 스레드가 진행되지 않는 경우가 많으며, 특히 스레드 풀에 이미있는 동시 동시 환경에서는 더욱 그렇습니다. 나는 평균 사용 사례가 병렬이 아니라고 내기하고 있으며 드문 경우입니다. 그러나 물론 Java 클래스를 사용할 수있는 옵션이 항상 있습니다.이 질문과 답변의 목적은 아닙니다.
Jayson Minard

3

전화 collect(Collectors.toList())또는 이와 유사한 것을 피하기 어려운 경우가 있습니다 . 이 경우 다음과 같은 확장 기능을 사용하여 Kotlin에 해당하는 것으로보다 빠르게 변경할 수 있습니다.

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

그런 다음 간단히 stream.toList()또는 stream.asSequence()Kotlin API로 다시 이동할 수 있습니다 . 원치 않는 상황에 Files.list(path)빠지게하는 등의 사례가 Stream있으며 이러한 확장을 통해 표준 컬렉션 및 Kotlin API로 다시 전환 할 수 있습니다.


2

게으름에 대한 추가 정보

Jayson이 제공 한 "부서별 급여 계산"에 대한 예제 솔루션을 살펴 보겠습니다.

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

이것을 게으르게하기 위해 (즉, groupBy단계 에서 중간 맵을 생성하지 않기 위해 )을 사용할 수 없습니다 asSequence(). 대신, 우리는 다음을 사용 groupingBy하고 fold운영 해야합니다 :

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

일부 사람들에게는 맵 항목을 다루지 않기 때문에 더 읽기 쉽습니다. it.value솔루션 의 일부가 처음에는 나에게 혼란 스러웠습니다.

이것은 일반적인 경우이며 fold매번 작성하지 않는 것이 좋으므로 다음과 같은 일반적인 sumBy기능을 제공하는 것이 좋습니다 Grouping.

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

다음과 같이 간단하게 작성할 수 있습니다.

val totalByDept = employees.groupingBy { it.dept }.sumBy { it.salary }
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.