표준 Kotlin 라이브러리에서 사용할 수있는 Java 8 Stream.collect는 무엇입니까? 비교되지는 않습니다. Java 8 용으로 문서화 된

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

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

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

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

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

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



답변

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 또는 빈 목록 을 처리하는 관용적 방법 참조 )

또한보십시오:


답변

추가 예제는 다음과 같이 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 사용자 정의 코드를 비교하십시오 .


답변

전화 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로 다시 전환 할 수 있습니다.


답변

게으름에 대한 추가 정보

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 }


답변