Overview

Java8 에서는 Collection 을 다루기 위해 Stream 을 사용합니다.

Kotlin 은 Collections 자체에서 filter, map 등의 여러 가지 API 를 제공하기 때문에 매번 .streams() 를 붙이지 않아도 사용 가능하다는 장점이 있습니다.

하지만 비슷해보이는 두 코드 사이에는 큰 차이점이 하나 있는데요.

바로 Lazy Evaluation 입니다.


1. Lazy Evaluation

Lazy Evaluation 이란 쉽게 말해서 필요하지 않은 연산을 하지 않는다 라고 이해할 수 있습니다.

어떤 로직이나 연산을 그 즉시 수행하지 않고 실제로 사용되기 전까지 미루는 걸 의미합니다.

반대의 의미인 Eager Evaluation 은 연산이 있으면 그때그때 수행하는 것을 의미합니다.

Java Stream 의 예시와 함께 보면 쉽게 이해할 수 있습니다.


2. Java Streams

Stream.of(1, 2, 3, 4, 5, 6)
        .filter(e -> {
            System.out.println("filter: " + e);
            return e < 3;
        })
        .map(e -> {
            System.out.println("map: " + e);
            return e * e;
        })
        .anyMatch(e -> {
            System.out.println("anyMatch: " + e);
            return e > 2;
        });
  • (1, 2, 3, 4, 5, 6) 의 숫자 묶음 존재
  • filter: 3 보다 작은 수만 추출
  • map: 제곱으로 변환
  • anyMatch: 2 보다 큰 수가 있는지 확인

조금 지저분해 보이지만 이해를 돕기 위해 연산 중간중간마다 print 문을 추가했습니다.

위 코드를 있는 그대로 나열하면 6 개의 숫자 묶음에서 3 보다 작은 수만 뽑아서 제곱한 뒤 그 중에서 2 보다 큰 수가 있는지 확인하는 겁니다.

위 코드의 결과값은 아래와 같습니다.


filter: 1
map: 1
anyMatch: 1
filter: 2
map: 2
anyMatch: 4

총 6 개의 숫자가 있었지만 실제로 연산된 것은 두 개 뿐입니다.

anyMatch 조건에 해당하는 숫자가 나오자 이후 숫자들은 볼 필요가 없다고 판단하여 전부 생략했습니다.

이게 바로 Lazy Evaluation (필요하지 않는 연산은 하지 않는다) 입니다.


3. Kotlin Collections

listOf(1, 2, 3, 4, 5, 6)
    .filter {
        println("filter: $it")
        it < 3
    }
    .map {
        println("map: $it")
        it * it
    }
    .any {
        println("any: $it")
        it > 2
    }

그럼 이제 같은 로직을 Kotlin 으로 작성해보았습니다.

위 로직을 실행하며 어떻게 될까요?


filter: 1
filter: 2
filter: 3
filter: 4
filter: 5
filter: 6
map: 1
map: 2
any: 1
any: 4

한 눈에 봐도 결과가 다른 것을 알 수 있습니다.

Kotlin Collections 는 매 연산마다 모든 원소에 대해서 수행합니다.

데이터의 양이 많으면 많을수록 성능 차이는 더욱 벌어질 겁니다.


4. Kotlin Sequences

Kotlin 에서도 Lazy Evaluation 을 수행하게 하는 방법이 있습니다.

바로 Sequence 를 사용하는 겁니다.

위 코드에서 한줄만 추가하면 됩니다.


listOf(1, 2, 3, 4, 5, 6)
    .asSequence()   // 이 부분을 추가해서 Sequence 로 변경
    .filter {
        println("filter: $it")
        it < 3
    }
    .map {
        println("map: $it")
        it * it
    }
    .any {
        println("any: $it")
        it > 2
    }

Collection 에서 수행하지 말고 asSequence() 를 사용해서 Sequence 로 변경한 뒤에 연산을 수행하면 됩니다.

위 코드의 결과는 다음과 같습니다.


filter: 1
map: 1
any: 1
filter: 2
map: 2
any: 4

이제 불필요한 연산을 하지 않는 것을 볼 수 있습니다.


5. 왜 그럴까?

Lazy Evaluation 에 대해 좀더 설명하면 중간 단계 (intermediate step) 의 결과를 바로 리턴하냐 아니냐의 차이에 있습니다.

Kotlin Collections 은 매 연산을 수행할 때마다 결과 Collection 을 반환합니다.

이에 비해 Kotlin Sequences 또는 Java Streams 는 종료 (terminate) 함수가 호출되기 전까지는 연산을 수행하지 않습니다.

위에서 사용한 any() 함수 또한 종료 함수입니다.

이 차이를 쉽게 알려면 종료 함수가 없는 Sequences 를 사용해보면 됩니다.


5.1. Kotlin Sequences 의 Lazy Evaluation 확인

val sequence: Sequence<Int> = listOf(1, 2, 3)
    .asSequence()
    .filter {
        println("filter: $it")
        it < 2
    }
    .map {
        println("map: $it")
        it * it
    }

println("종료함수를 아직 호출하지 않음")
sequence.toList()

Sequences 는 매 함수의 결과로 Sequence 를 반환합니다.

그래서 최종적으로 Collection 으로 변환하려면 다시 toList() 를 호출해야 합니다.

toList() 역시 종료함수라서 호출되는 순간에 모든 연산이 수행됩니다.

Java Streams 의 collect(Collectors.toList()) 와 같다고 생각하시면 됩니다.


종료함수를 아직 호출하지 않음
filter: 1
map: 1
filter: 2
filter: 3

5.2. Kotlin Collections 의 Eager Evaluation 확인

val list: List<Int> = listOf(1, 2, 3)
    .filter {
        println("filter: $it")
        it < 2
    }
    .map {
        println("map: $it")
        it * it
    }

println("Collection 은 매번 List 를 반환하기 때문에 이미 연산됨")

Sequences 와 다르게 Collections 은 매 함수의 결과로 Collection 을 반환합니다.

사실상 매 함수가 모두 종료 함수라고 볼 수 있으며, 그래서 결과를 다음 단계로 넘기지 못하고 매번 전부 연산을 하는겁니다.


filter: 1
filter: 2
filter: 3
map: 1
Collection 은 매번 List 를 반환하기 때문에 이미 연산됨

Conclusion

이제 Kotlin Sequences 는 Lazy Evaluation 때문에 불필요한 연산을 생략한다는 점을 알았습니다.

하지만 Sequences 가 항상 좋은 것은 아닙니다.

Sequences by Kotlin Reference 를 보면 다음과 같은 문구가 있습니다.

So, the sequences let you avoid building results of intermediate steps, therefore improving the performance of the whole collection processing chain. However, the lazy nature of sequences adds some overhead which may be significant when processing smaller collections or doing simpler computations. Hence, you should consider both Sequence and Iterable and decide which one is better for your case.


요약하자면 Sequences 는 중간 단계의 결과를 생략하기 때문에 성능 향상이 되지만, 오버헤드가 있기 때문에 데이터가 적거나 연산이 단순한 컬렉션을 처리할 때는 오히려 안좋을 수가 있다고 합니다.

그러므로 각자 상황에 맞춰 적절한 방법을 선택하는게 가장 좋습니다.


Reference

'Language > Kotlin' 카테고리의 다른 글

Kotlin Enum  (0) 2021.10.06
Kotlin Collection Sorting (정렬)  (0) 2021.02.19
[Kotlin] Swap  (0) 2020.11.11
[Kotlin] For 문  (0) 2020.11.11
[Kotlin] String - drop, dropLast, dropWhile, dropLastWhile  (0) 2020.09.25

1. Overview

Java Enum 에 이어 Kotlin Enum 사용법에 대해서도 알아봅니다.

사용법과 이유에 대해서는 Java 에서 알아보았으니 간단하게 코드만 작성합니다.


2. 기본 사용법

enum class Day {
    MON, TUE, WED, THU, FRI, SAT, SUN
}

3. 필드값 추가

enum class Day(
    val label: String
) {
    MON("Monday"),
    TUE("Tuesday"),
    WED("Wednesday"),
    THU("Thursday"),
    FRI("Friday"),
    SAT("Saturday"),
    SUN("Sunday")
    ;
}

Kotlin 은 원래 ; 을 안쓰지만 Enum 클래스에 필드값을 추가하는 경우 마지막에 꼭 추가해야합니다.


4. 필드값 캐싱

enum class Day(
    val label: String
) {
    MON("Monday"),
    TUE("Tuesday"),
    WED("Wednesday"),
    THU("Thursday"),
    FRI("Friday"),
    SAT("Saturday"),
    SUN("Sunday")
    ;

    companion object {
        private val LABEL_CACHE: Map<String, Day> =
            values().associateBy { it.label }

        fun findByLabel(label: String) = LABEL_CACHE[label]
    }
}

5. 상수별 메소드 구현

enum class Operation {
    PLUS {
        override fun apply(x: Double, y: Double): Double {
            return x + y
        }
    },

    MINUS {
        override fun apply(x: Double, y: Double): Double {
            return x - y
        }
    },

    TIMES {
        override fun apply(x: Double, y: Double): Double {
            return x * y
        }
    },

    DIVIDE {
        override fun apply(x: Double, y: Double): Double {
            return x / y
        }
    };

    abstract fun apply(x: Double, y: Double): Double
}

Reference

1. Collection Sort

Kotlin 에서는 Collection 을 정렬하기 위한 여러가지 유틸리티들을 제공합니다.


1.1. Sort, Sorted

가장 쉬운 방법은 sort 메소드를 호출하는 겁니다.

기본적으로 오름차순으로 정렬합니다.

val list = mutableListOf(1, 2, 7, 6, 5, 6)
list.sort()
println(list)  // [1, 2, 5, 6, 6, 7]

sort 메소드는 해당 Collection 의 원소 위치가 변경됩니다.

기존 Collection 은 그대로 둔 채 새로운 Collection 으로 받길 원한다면 sorted 메소드를 사용해야 합니다.

sorted 메소드를 사용하면 기존 Collection 은 변하지 않습니다.

val list = mutableListOf(1, 2, 7, 6, 5, 6)
val sorted = list.sorted()
println(sorted)  // [1, 2, 5, 6, 6, 7]
println(list)    // [1, 2, 7, 6, 5, 6] (sorted 를 사용했기 때문에 변하지 않음)

내림차순으로 정렬하고 싶다면 sortByDescending 를 사용하거나 reverse 메소드를 사용하면 됩니다.

마찬가지로 sortedByDescending 를 사용하면 원래 Collection 의 변경 없이 내림차순으로 정렬된 값을 구할 수 있습니다.

// 1. sortByDescending 로 내림차순 정렬
list.sortByDescending { it }

val sorted = list.sortedByDescending { it }

// 2. reverse 사용해서 정렬 후 뒤집기
list.sort()
list.reverse()

val sorted = list.sorted().reversed()

1.2. SortBy

만약 Object 의 특정 Property 들을 기준으로 정렬하고 싶다면 sortBy 메소드를 사용하면 됩니다.

sortBy 메소드는 Object 를 받아서 Property 를 반환하는 Lamdba 식을 파라미터로 받습니다.

val list = mutableListOf(1 to "a", 2 to "b", 7 to "c", 6 to "d", 5 to "c", 6 to "e")
list.sortBy { it.second }
println(list)  // [(1, a), (2, b), (7, c), (5, c), (6, d), (6, e)]

sort 와 마찬가지로 기존 Collection 의 변경 없이 정렬된 값을 받고 싶다면 sortedBy 를 사용하면 됩니다.

그리고 내림차순을 지원하는 sortByDescending 도 있습니다.


1.3. SortWith

sortWith 메소드를 사용하면 여러 가지 조건을 섞어서 정렬할 수 있습니다.

sortWith 메소드는 Comparator 를 파라미터로 받습니다.

(Kotlin 에서 Comparator 를 생성하는 여러가지 방법은 다음 챕터에서 다룹니다)

val list = mutableListOf(1 to "a", 2 to "b", 7 to "c", 6 to "d", 5 to "c", 6 to "e")
list.sortWith(compareBy({it.second}, {it.first}))
println(list)  // [(1, a), (2, b), (5, c), (7, c), (6, d), (6, e)]

위 Collection 은 it.second(문자) 로 먼저 정렬된 후에 it.first(숫자) 로 정렬됩니다.

그리고 역시 sortedWith 메소드가 존재하며, 역순으로 정렬할때는 reverse 를 사용하거나 Comparator 를 반대로 수정하면 됩니다.


2. Comparison

Kotlin 은 Comparator 를 만들기 위해 kotlin.comparisons 라는 유용한 패키지를 제공합니다.

이 챕터에서는 아래 컨텐츠를 다룹니다.

  • Comparator creation
  • Handling of null values
  • Comparator rules extension

2.1. Comparator Creation

Kotlin 은 Comparator 를 생성하는 여러 팩토리 메서드를 제공합니다.


2.1.1. naturalOrder

가장 간단한 생성 메서드는 naturalOrder() 입니다.

아무런 파라미터를 필요로 하지 않으며 오름차순을 기본으로 합니다.

val ascComparator = naturalOrder<Long>()

2.1.2. compareBy

여러 개의 속성을 사용하고 싶다면 compareBy 메소드를 사용하면 됩니다.

파라미터로는 Comparable 를 리턴하는 정렬 규칙을 여러 개 사용할 수 있습니다.

그럼 넘겨진 규칙들은 순차적으로 호출 되며 원소들을 정렬합니다.

만약 먼저 나온 규칙에서 원소의 우열이 가려져 정렬 처리가 되었다면 뒤의 규칙들은 확인하지 않습니다.

val complexComparator = compareBy<Pair<Int, String?>>({it.first}, {it.second})

위 코드에서 it.first 값을 사용해 먼저 비교를 하고 값이 같은 경우에만 it.second 비교까지 이루어집니다.


2.1.3. Comparator

간단하게 new Comparator 를 선언해서 만들 수도 있습니다.

자바와 마찬가지로 두 원소에 대한 비교 조건을 넣어줘야 합니다.

val caomparator = Comparator<Int> { a, b -> a.compareTo(b) }

2.2. Handling of null Values

정렬하려는 Collection 이 null 값을 갖고 있을 수도 있습니다.

nullsFirst 또는 nullsLast 와 함께 Comparator 를 사용하면 null 값을 가장 처음 또는 가장 마지막에 위치하도록 설정할 수 있습니다.

val list = mutableListOf(4, null, 1, -2, 3)

list.sortWith(nullsFirst())  // [null, -2, 1, 3, 4]

list.sortWith(nullsLast())  // [-2, 1, 3, 4, null]

list.sortWith(nullsFirst(reverseOrder()))  // [null, 4, 3, 1, -2]

list.sortWith(nullsLast(compareBy { it }))  // [-2, 1, 3, 4, null]

2.3. Comparator Rules Extension

Comparator 오브젝트는 추가적인 정렬 규칙과 혼합되거나 확장할 수 있습니다.

kotlin.comparable 패키지에 있는 then 키워드를 활용하면 됩니다.

첫 번째 비교의 결과가 동일할 때만 두번째 비교가 이루어집니다.

val students = mutableListOf(21 to "Helen", 21 to "Tom", 20 to "Jim")

val ageComparator = compareBy<Pair<Int, String?>> {it.first}
val ageAndNameComparator = ageComparator.thenByDescending {it.second}

// [(20, Jim), (21, Tom), (21, Helen)]
println(students.sortedWith(ageAndNameComparator))

위 코드는 나이가 어린 순으로 먼저 정렬하고 나이가 같으면 이름을 알파벳 역순으로 정렬합니다.


Reference

'Language > Kotlin' 카테고리의 다른 글

Kotlin Collections 와 Sequences 의 차이점 (feat. Java Stream)  (1) 2022.01.27
Kotlin Enum  (0) 2021.10.06
[Kotlin] Swap  (0) 2020.11.11
[Kotlin] For 문  (0) 2020.11.11
[Kotlin] String - drop, dropLast, dropWhile, dropLastWhile  (0) 2020.09.25

Swap

Python 만큼은 아니지만 Kotlin 에서도 문법을 활용하여 값의 Swap 을 쉽게 짤 수 있다.

var a = 1
var b = 2

a = b.also { b = a }

println(a) // print 2
println(b) // print 1

For

Kotlin 에서의 for 문은 자바와 마찬가지로 iterator 를 사용합니다.

따라서 iterator(), next(), hasNext() 을 제공하는 모든 컬렉션에서 for 문을 사용할 수 있습니다.

기본 for 문은 아래와 같습니다.

for (item in collection) {
    print(item)
}

특정 범위 (range) 를 지정해서 사용할 수도 있습니다.

.. 을 사용하면 마지막 숫자까지 포함하until 을 사용하면 마지막 숫자는 포함하지 않습니다.

// 1, 2, 3
for (i in 1..3) {
    println(i)
}

// 1, 2
for (i in 1 until 3) {
    println(i)
}

downTo 를 이용하면 감소하는 for 문을 만들 수 있고 step 을 사용하면 증가되는 양을 수정할 수 있습니다.

// 6, 4, 2, 0
for (i in 6 downTo 0 step 2) {
    println(i)
}

배열을 순회할 때는 index 만 뽑아내거나 (index, value) 형태로 순회 가능합니다.

for (i in array.indices) {
    println(array[i])
}
for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

컬렉션 타입에서는 forEach, forEachIndexed 를 사용하여 람다식으로 표현할 수 있습니다.

// 1, 2, 3
listOf(1, 2, 3).forEach { println(it) }

/**
 * index: 0, value: 4
 * index: 1, value: 5
 * index: 2, value: 6
 */
listOf(4, 5, 6).forEachIndexed { index, value -> 
    println("index: $index, value: $value")
}

drop, dropLast, dropWhile, dropLastWhile

drop 은 문자열의 앞이나 뒷부분을 자를 때 사용된다.

내부적으로는 substring 으로 구현되어 있다.

  • drop : 앞에서부터 n 개의 문자를 제거한 String 을 반환
  • dropLast: 뒤에서부터 n 개의 문자를 제거
  • dropWhile, dropLastWhile: 조건을 만족하지 않는 문자가 나올때까지 제거


/* definition */
fun String.drop(n: Int): String

fun String.dropLast(n: Int): String

fun String.dropWhile(
  predicate: (Char) -> Boolean
): String

fun String.dropLastWhile(
  predicate: (Char) -> Boolean
): String


/* exmaple */
val string = "<<<First Grade>>>"

println(string.drop(6)) // st Grade>>>
println(string.dropLast(6)) // <<<First Gr
println(string.dropWhile { !it.isLetter() }) // First Grade>>>
println(string.dropLastWhile { !it.isLetter() }) // <<<First Grade


Reference

String

'Language > Kotlin' 카테고리의 다른 글

Kotlin Collections 와 Sequences 의 차이점 (feat. Java Stream)  (1) 2022.01.27
Kotlin Enum  (0) 2021.10.06
Kotlin Collection Sorting (정렬)  (0) 2021.02.19
[Kotlin] Swap  (0) 2020.11.11
[Kotlin] For 문  (0) 2020.11.11

+ Recent posts