코틀린의 내부 반복자는 편리하고, 표현력이 강하고, 외부 반복자와 비교했을 때 복잡성을 낮춰준다. 하지만 가끔은 퍼포먼스가 안 좋을 수 있다.
이후에 보게 될 특정 상황에서, 내부 반복자는 외부 반복자와 비교해 봤을 때 연산을 약간 더 많이 한다.
콜렉션의 요소의 크기가 수백개 정도로 비교적 작은 경우엔 별로 영향을 못 미치고, 데이터 수천개 를 다루는 아주 큰 데이터의 콜렉션을 다루는 경우는 오버헤드가 이슈가 될 수 있다. → 이럴 때 코틀린의 시퀀스
시퀀스는 내부 반복자로 다른 대부분의 내부 반복자와 같아 보이지만 내부적으로 다르게 구현되어 있음. lazy 연산이 적용되기 때문
외부 반복자 vs 내부 반복자내부 반복자filter, map, reduceflatten(), flatMap()sortedBy() 정렬groupBy()지연 연산을 위한 시퀀스무한 시퀀스
외부 반복자 vs 내부 반복자
예제 1
val numbers = listOf(10, 12, 15, 17, 18, 19) // 외부 반복자 for (i in numbers) { if (i % 2 == 0) { print("$i, ") // 10, 12, 18 } } // 내부 반복자 numbers.filter { it % 2 == 0 } .forEach { print("%it, ") }
예제2
val doubled = mutableListOf<Int>() for (i in numbers) { if (i % 2 == 0) { doubled.add(i * 2) } } println(doubled)
- 위 코드에서 냄새가 나는 첫 번째 부분은 빈 뮤터블 리스트를 정의한 것
val doubled = numbers.filter { it % 2 == 0 } .map { it * 2} println(doubled)
내부 반복자
filter, map, reduce
[ Blog ] reduce와 fold
fold는 collection이 비어있을 경우가 있을 수 있을때 사용. 초기값을 제공
reduce는 초기값을 제공하지 않고 collection의 첫번째 엘리먼트를 초기값으로 사용
filter()와 map() 에 전달되는 람다는 파라미터가 하나
하지만 reduce() 에 전달되는 람다는 2개의 파라미터를 가진다
첫 번째는 누적 값이고, 두 번째는 원래 콜렉션의 요소. 람다의 결과는 새로운 누적 값이 된다
data class Person(val firstName: String, val age: Int) { } val people = listOf( Person("Sara", 12), Person("Jill", 51), Person("Paula", 23), Person("Paul", 25), Person("Mani", 12), Person("Jack", 70), Person("Sue", 10)) val result = people.filter { person -> person.age > 20 } .map { person -> person.firstName } .map { name -> name.toUpperCase() } .reduce { names, name -> "$names, $name" } println(result)
- 코틀린은
sum
,max
,joinToString
같은 여러 연산에 특화된 reduce 연산을 제공한다.
val totalAge = people.map { person -> person.age } .reduce { total, age -> total + age } println(totalAge) val totalAge2 = people.map { person -> person.age } .sum()
첫 번째와 마지막 가져오기
val result = people.filter { person -> person.age > 20 } .map { person -> person.firstName } .first() // 마지막 것은 last()
flatten(), flatMap()
flatten() 함수에 Iterable<Iterable<T>> 를 전달하면 모든 중첩된 반복 가능 객체가 탑레벨로 합쳐진 Iterable<T>를 리턴한다.
val familes = listOf( listOf(Person("Jack", 40), Person("Jill", 40)), listOf(Person("Eve", 18), Person("Adam", 18))) println(familes.size) // 2 println(familes.flatten().size) // 4
flatMap() 은 flatten 연산과 map 연산을 합치는 것이다. 실제 연산은 맵을 만든 다음에 플랫화 하는 연산
val namesAndReversed = people.map { person -> person.firstName } .map(String::toLowerCase) .map { name -> listOf(name, name.reversed())} .flatten() println(namesAndReversed.size) // 14 // 위와 동일한 결과 val namesAndReversed = people.map { person -> person.firstName } .map(String::toLowerCase) .flatMap { name -> listOf(name, name.reversed())} println(namesAndReversed.size) // 14
sortedBy() 정렬
- sortedBy()
- sortedByDescending()
val namesSortedByAge = people.filter { person -> person.age > 17 } .sortedBy { person -> person.age } .map { person -> person.firstName }
groupBy()
val groupBy1stLetter = people.groupBy { person -> person.firstName.first() } println(groupBy1stLetter) // {S=[Person(firstName=Sara, age=12), Person(firstName=Sue, age=10)], J=[...
- 위 예제에서는 firstName의 첫 글자가 같은 Person이 같은 버켓 혹은 그룹에 들어가게 된다.
- 연산의 결과는 Map<L, List<T>>. 람다의 결과값이 Map의 키의 타입을 결정
- 밸류의 타입은 List<T>이며, groupBy() 는 Iterable<T>에서 호출
- 위 예제에서 결과의 타입은 Map<String, List<Person>>
- Person을 그룹핑하는 대신에 이름만 그룹핑 할 수 있다
val namesBy1stLetter = people.groupBy({ person -> person.firstName.first() }) { person -> person.firstName } println(namesBy1stLetter) // {S = [Sara, Sue], J= [Jill, Jack], P= [Paula, Paul], ...
지연 연산을 위한 시퀀스
콜렉션이 작을 경우 퍼포먼스의 차이는 무시할 만큼 적기에, 이 때는 지연 연산 사용치 않는 것이 디버그하기 편하고 추론하기 쉽다.
하지만 콜렉션의 크기가 수백, 수 천 개의 요소가 있어서 크다면 시퀀스를 사용하면 중간 콜렉션을 만들 때 생기는 오버헤드를 제거하고 연산을 생략할 수 있다.
Java 와는 다르게 코틀린에서 filter(), map() 등의 메서드는 Stream<T> 에서만 사용 가능 한게 아니고 List<T> 같은 콜렉션에서 직접 사용 가능
- 코틀린에서 내부 반복자는 콜렉션 사이즈가 작을 때 사용해야 함
사이즈가 큰 콜렉션에서는 시퀀스
를 이용해 내부 반복자를 사용해야 함.
fun isAdult(person: Person): Boolean { println("isAdult calle for ${person.firstName}") return person.age > 17 } fun fetchFirstName(person:Person): String { println("fetchfirstName called for ${person.firstName}") return person.firstName val nameOfFirstAdult = people .filter(::isAdult) .map(::fetchFirstName) .first() println(nameOfFirstAdult) // 결과 isAdult called for Sara isAdult called for Jill isAdult called for Paula isAdult called for Paul isAdult called for Mani isAdult called for Jack isAdult called for Sue fetchFirstName called for Jill fetchFirstName called for Paula fetchFirstName called for Paul fetchFirstName called for Jack Jill val nameOfFirstAdult = people.asSequence() .filter(::isAdult) .map(::fetchFirstName) .first() println(nameOfFirstAdult) // 결과 isAdult called for Sara isAdult called for Jill fetchFirstName called for Jill Jill
filter() 메서드가 열심히 adults의 리스트를 생성하지 않고 시퀀스에서 호출을 하면 다른 시퀀스를 리턴
이와 유사하게 시퀀스에서 map() 을 호출하면 다른 시퀀스를 리턴
하지만 filter() 나 map() 에 전달된 람다는 아직 실행 x. first() 메서드가 호출될 때 지금까지 연기됐던 실행이 시작됨
무한 시퀀스
fun isPrime(n: Long) = n > 1 && (2 until n).none { i -> n % i == 0L } // tailrec은 StackOverflowError를 방지해준다. tailrec fun nextPrime(n: Long): Long = if (isPrime(n+1)) n + 1 else nextPrime(n + 1) // 5 부터 시작하는 무한한 소수의 시퀀스 // generateSequence는 지연 연산을 함. 우리가 값을 요청할 때 까지 nextPrime을 실행하지 않음 val primes = generateSequence(5, ::nextPrime) System.our.println(primes.take(6).toList()) // [ 5, 7, 11, 13, 17, 19]
- generateSequence() 는 첫 번째 파라미터로 시작하는 값을 받고, 함수를 두 번째 파라미터로 받는다
- 람다는 값을 받고 값을 리턴
- toList() 함수의 호출이 시퀀스의 값의 평가를 발생시킴. 하지만 take()가 주어진 숫자만큼의 값만 제공
재귀함수 nextPrime()을 작성하고 generateSequence()를 사용하는 대신 sequence() 함수를 사용할 수도 있다
val primes = sequence { var i: Long = 0 while (true) { i++ if (isPrime(i)) { yield(i) } } } println(primes.drop(2).take(6).toList()) // [5, 7, 11, 13,17, 19]
- sequence() 호출의 결과는 Sequence 인터페이스를 구현한 인스턴스
- 람다에 포함된 코드는 시퀀스에서 값의 요청이나 소비가 있을 때만 온디맨드로 실행됨