(Kotlin) stream API / 시퀀스(Sequence)
- 기본 filter, map 등은 인라인 함수로 정의되어 있어 알아서 인라이닝되므로 성능 신경쓰지말고 그냥 사용하면 된다.
연쇄해서 사용하는 경우 연산의 순서가 성능에 영향을 줄 수 있기 때문에 고려해야 한다. 보통
filter()
를 먼저 적용해 추리는게 도움이 되고, 메서드 체이닝 시 앞쪽에서 부터 적용되기 때문에filter()
를 먼저 써주는게 좋다.- …고 하는데, 지연평가 기본적으로는 안되는게 맞다.
- collection에서 그냥 map, filter 사용 시 모든 원소에 대해 적용하고 다음으로 넘기는 방식.
- asSequence() 로 시퀀스 변경 후 map, filter 사용 시 lazy evaluation 됨.
- lazy eval && take, takeLast가 지원되는 Kotlin에서는 subList() 가 아니라 take 사용하는 것을 권장한다.
filter/map
1
2
3
4
5
>>> val people = listOf(Person("A", 25), Person("umbum", 25), Person("B", 19), Person("C", 23))
>>> println(people.filter({ it.age > 24 })) // 객체의 리스트를 반환
[Person@37c5cf61, Person@700a2863]
>>> println(people.filter({ it.age > 24 }).map({ it.name }))
[A, umbum]
all/any/count/find
1
2
3
4
5
6
7
8
9
10
11
>>> println(people.all({ it.age > 20 }))
false
>>> println(people.any({ it.age > 20 }))
true
>>> println(people.count({ it.age > 20})) // 갯수 세기는 이게 제일 효율적.
3
>>> println(people.any({ it.age == 25 }))
true
>>> println(people.find({ it.age == 25})) // 만족하는 원소 없으면 null 반환
Person@277a0b1e // 만족하는 원소 중 첫 번째 원소 반환.
groupBy
컬렉션의 모든 원소를 특성에 따라 여러 그룹으로 나누고 싶을 때. 아~~주 편함. e.g., 사람을 나이에 따라서 분류할 때.
1
2
>>> println(people.groupBy { it.age })
>>> println(people.groupBy { it.age > 20 })
associateBy
List
1
list.associateBy { it.coin /* key가 될 필드 */ }
flatMap ( map + flatten )
람다를 컬렉션의 모든 객체에 적용하고(map) 람다를 적용한 결과로 얻어지는 여러 리스트를 하나의 리스트로 합친다.(flatten)
1
2
3
4
5
6
>>> class Book(val title: String, val authors: List<String>)
>>> val books = listOf(Book("Tursday Next", listOf("Jasper Fforde")),
... Book("Mort", listOf("Terry Pratchett")),
... Book("Good Omens", listOf("Terry Pratchett", "Neil Gaiman")))
>>> println(books.flatMap({ it.authors }).toSet())
[Jasper Fforde, Terry Pratchett, Neil Gaiman]
* 여러 리스트를 하나의 리스트로 합치고만 싶을 때는 flatten()
을 따로 사용하면 된다.
시퀀스(Sequence) : 컬렉션의 크기가 큰 경우
컬렉션 함수를 그냥 사용하는 경우, 결과 컬렉션을 즉시 생성한다. 즉, 다음과 같이 연쇄해서 사용하는 경우 filter()
의 실행 결과(중간 계산 결과)를 새로운 컬렉션에 담고, 이 컬렉션에 대해 다시 map()
을 적용한 결과를 결과 컬렉션에 담아 반환하게 된다.
1
2
3
people
.filter({ it.age > 24 })
.map({ it.name })
컬렉션의 크기가 크다면 이렇게 중간 계산 결과를 매번 담는게 굉장히 비효율적이다. Sequence는 컬렉션 원소를 하나 씩 가져와서 .filter().map()
chain을 차례로 모두 적용한 후, 곧바로 결과 컬렉션에 저장하는 방식이다. 때문에 컬렉션 함수와 달리 중간 계산 결과를 담을 필요가 없어 원소가 많은 경우 성능이 눈에 띄게 좋아진다.
1
2
3
4
people.asSequence()
.filter({ it.age > 24 })
.map({ it.name })
.toList()
* Sequence
인터페이스 안에는 kt iterator
라는 단일 메소드가 존재(SAM)한다. 시퀀스의 원소는 필요할 때 계산된다. 지연 평가 된다는 뜻. 자바8 스트림과 같은 개념인데, 코틀린은 따로 옵션 안주면 Java 6 대응이므로 둘이 같지는 않다. 코틀린 스트림을 사용하면 안드로이드 등 예전 버전 자바를 사용하는 경우도 대응이 된다는 장점이 있고, 자바8 스트림을 사용하면 filter()/map()
등의 연산을 CPU에서 병렬적으로 실행한다는 장점이 있다.
Note )
시퀀스는 크기가 큰 컬렉션에 대해서만 사용하도록 한다.
원래filter()
등은inline
으로 선언된 함수이기 때문에 사용시 함수 본문이 인라이닝되어 추가 객체나 클래스를 생성하지 않는다.
그러나 시퀀스를 사용하게 되면, 중간 시퀀스가 람다를 필드에 저장하는 객체로 표현되며 최종 연산은 중간 시퀀스에 있는 여러 람다를 연쇄 호출하는 형태이기 때문에 시퀀스는 람다를 저장해 두어야 한다 ==> 람다를 인라인 할 수 없다.
그래서 작은 컬렉션에 대해서는 시퀀스를 사용하지 않는 편이 좋다.
시퀀스를 사용해야 할지 말지에 대한 판단 기준은 아래 참고.
https://typealias.com/guides/when-to-use-sequences/
https://medium.com/@ajalt/benchmarking-kotlin-sequences-e06d8bb4011c
중간 연산(=지연 연산)과 최종 연산
시퀀스에 대한 연산은 중간 연산(intermediate)과 최종 연산(terminal)으로 나뉜다. 중간 연산은 항상 지연 계산된다. 최종 연산의 그 원소가 필요할 때 그 원소에 대해서만 모든 연산이 순차적으로 적용되는 방식이기 때문에, 최종 연산이 없다면 아무것도 실행되지 않는다.
1
2
3
4
>>> listOf(1, 2, 3, 4).asSequence()
... .map({ print("map($it) "); it \* it })
... .filter({ print("filter($it) "); it % 2 == 0 })
kotlin.sequences.FilteringSequence@f9e69b
이렇게 최종 연산을 붙여주어야 한다.
1
2
3
4
5
>>> listOf(1, 2, 3, 4).asSequence()
... .map({ print("map($it) "); it \* it })
... .filter({ print("filter($it) "); it % 2 == 0 })
... .toList()
map(1) filter(1) map(2) filter(4) map(3) filter(9) map(4) filter(16) [4, 16]
시퀀스 만들기 : generator
시퀀스를 사용하는 일반적인 용례 중 하나는 객체의 조상으로 이루어진 시퀀스를 만드는 것이다.
1
2
3
4
5
>>> fun File.isInsideHiddenDirectory() =
... generateSequence(this, { it.parentFile }).any({ it.isHidden })
>>> val file = File("/umbum/.hidden/aaa")
>>> println(file.isInsideHiddenDirectory())
true