Kotlin - 콜렉션!
콜렉션
java
kotlin
콜렉션을 사용하며 immutable
콜렉션, mutable
콜렉션을 제공한다.
mutable
콜렉션은 함수형 구조 작성을 위해 반드시 필요한 클래스이다.
하지만 java
의 경우 immutable
콜렉션, mutable
콜렉션 모두 동일한 인터페이스(List, Map) 을 구현하며, add
, put
과 같은 함수 호출시 실행시간에 UnsupportedOperationException
을 발생시킨다.
반면 kotlin
은 컴파일 시점에서 mutable
메서드 사용시 오류를 발생시킨다.
Array
kotlin
에서 배열을 사용할땐 Array
클래스를 와 arrayOf
팩토리 메서드를 통해 배열을 생성한다.
java
배열과 동일하게 선언과 동시에 배열의 크기가 정해지며 크기 변경이 불가능하지만 내부 요소는 변경 가능한 콜렉션이다.
최적화를 위한 낮은 수준의 프로그래밍이 가능할 때만
Array
를 사용하고, 이외에는List
콜렉션 사용을 권장한다.
val array: Array<Any> = arrayOf(1, 2, "three")
array[0] = -1;
println(array.contentToString()) // [-1, 2, three]
null
로 채워진 배열 생성은 arrayOfNulls
메서드 사용
var nullArray: Array<String?> = arrayOfNulls<String>(5)
arrayOfNulls
의 경우 요소의 타입 정의 필수
Array
를 생성하는 함수는 이 외에도 오토박싱, 언박싱 비용을 방지할 수 있도록
기본 타입의 Array
를 생성하는 함수가 정의되어 있다.
booleanArrayOf
byteArrayOf
shortArrayOf
charArrayOf
intArrayOf
longArrayOf
floatArrayOf
doubleArrayOf
Array 생성자
public class Array<T> {
public inline constructor(size: Int, init: (Int) -> T)
...
}
Array
의 기본생성자는 한개뿐이다,
인자값으로 크기값과 람다함수(반환값이 Int
인) 2개를 전달받는다.
반복적인 패턴의 Array
를 만들 때 사용할만 한다.
var array = Array(5) {i -> (i * i).toString()}
println(array.contentToString()) // [0, 1, 4, 9, 16]
in 연산자를 사용하면 요소가 해당 콜렉션 안에 있는지 확인 가능하다.
앞으로 소개할 List, ArrayList 에서도 사용 가능
val array: Array<Any> = arrayOf(1, 2, "three")
println("three" in array) // true
withIndex
값과 인덱스를 가지고 있는 IndexedValue
클래스를 반환하는 withIndex
메서드를 사용,
그리고 구조분해 연산자를 사용하면 아래와 같이 깔끔한 형태로 (index, value)
를 같이 사용할 수 있다.
val strings = arrayOf("this", "is", "an", "array", "of", "strings")
for ((index, value) in strings.withIndex()) {
println("Index: $index, value: $value")
}
/*
Index: 0, value: this
Index: 1, value: is
Index: 2, value: an
Index: 3, value: array
Index: 4, value: of
Index: 5, value: strings
*/
List, MutableList, Set
List, MutableList
은 kotlin
의 표준 콜렉션으로 모두 인터페이스이다.
List
는 immutable 콜렉션, set add remove
같은 메서드가 정의되어 있지 않다.
MutableList
는 mutable 표준 콜렉션
val list: List<Int> = listOf(1, 2, 3)
// list[0] = -1
// list.add(4)
// 에러 발생, set 명령도 없음
var mlist: MutableList<Int> = mutableListOf(1, 2, 3)
mlist[0] = -1
mlist.add(4)
println(mlist) // [-1, 2, 3, 4]
// 형 지정을 자동화 하려면 제너릭 지정 필요
val emptyList1: MutableList<String> = mutableListOf()
val emptyList2 = mutableListOf<String>()
val emptySet1: MutableSet<String> = mutableSetOf()
val emptySet2 = mutableSetOf<String>()
요소에 접근할 때에는 get 메서드나 인덱스 연산자를 사용할 수 있다.
val list: List<Int> = listOf(1, 2, 3)
println(list.get(0)) // 1
println(list[0]) // 1
ArrayList
는MutableList
인터페이스를 구현한 클래스.
listOf
,mutableListOf
,arrayListOf
두 함수 모두ArrayList
클래스를 반환한다.
MutableList
,ArrayList
두 클래스간 큰 차이는 없지만 표준 콜렉션 타입인MutableList
사용을 권장한다.
또한kotlin
은 함수형 코딩을 지향함으로mutable
보다도immutable
콜렉션 사용을 권장한다.
Set
콜렉션도 Set, MutableSet
으로 나뉜다.
마찬가지로 둘다 인터페이스이고 내부 구현체는 LinkedHashSet
이다.
var set = setOf(1, 2, 3, 4, 5)
println(set) // [1, 2, 3, 4, 5]
var mset = mutableSetOf(1, 2, 3, 4, 5)
println(mset) // [1, 2, 3, 4, 5]
fold
배열 형태의 콜렉션을 순회하면서 연산하고 싶을때 사용, reduce
와 비슷한 역할.
public inline fun <R> IntArray.fold(initial: R, operation: (acc: R, Int) -> R): R { ... }
초기값과 순회하면서 처리할 함수를 지정한다.
fun main() {
val nums = intArrayOf(1, 2, 3, 4, 5)
val result = nums.fold(0) { acc, i -> acc + i }
println(result) // 15
}
reduce
fold
와 동일한 기능을 수행하지만 초기값을 배열의 첫번째 값으로 사용한다.
public inline fun IntArray.reduce(operation: (acc: Int, Int) -> Int): Int { ... }
fun main() {
val nums = intArrayOf(1, 2, 3, 4, 5)
val result = nums.reduce() { acc, i -> acc + i }
println(result) // 15
}
contains
콜렉션 내부에 요소가 있는지 확인하는 메서드
val fruits: List<String> = listOf("Apple", "Banana", "Grape")
println(fruits.contains("Apple")) //true
println("Apple" in fruits) //true
immutable, mutable 변환
List
콜렉션 뿐 아니라 앞으로 소개할 콜렉션에서도 immutable, mutable
콜렉션으로 나뉘며
immutable, mualbe
간 변환할 수 있는 메서드를 제공한다.
val list = listOf(1, 2, 3)
val mlist = mutableListOf(1, 2, 3)
val list2 = mlist.toList()
val mlist2 = list.toMutableList()
구조분해, 오버라이딩 함수
List
에 기본적으로 component1 ~ component5
함수가 정의되어 있는데
아래 사용처럼 구조분해 문법 사용이 가능하다.
val range: CharRange = 'a'..'g'
val (a, b, c, d, e) = range.toList();
println("$a $b $c $d $e") // a b c d e
// val (a1, b1, c1, d1, e1, f1) = range.toList(); 에러 발생 component6 가 없기 때문
kotlin
의 연잔사 오버라이딩을 통해 +, -
연산자로 새로운 콜렉션을 생성해서 요소를 추가하거나 삭제할 수 있다.
val fruits: List<String> = listOf("Apple", "Banana", "Grape")
var fruits2: List<String> = fruits + "orange"
println(fruits2) // [Apple, Banana, Grape, orange]
var fruits3: List<String> = fruits - "Apple"
println(fruits3) // [Banana, Grape]
Tuple
Pair
는 값이 2개인 튜플, Triple
값이 3개인 튜플, 둘다 kotlin
의 표준 콜렉션 클레스이다.
public data class Pair<out A, out B>(
public val first: A,
public val second: B
) : Serializable { ... }
public data class Triple<out A, out B, out C>(
public val first: A,
public val second: B,
public val third: C
) : Serializable { ... }
val p1: Pair<String, Int> = "a" to 1
val p2: Pair<String, Int> = Pair("b", 2)
val t1: Triple<String, String, String> = Triple("John", "Quincy", "Adams")
Pair
Triple
모두 구조분해 연산자를 제공한다.
아래처럼 값을 추출하면 좀더 간결하게 값을 할당할 수 있다.
fun getFullName() = Triple("John", "Quincy", "Adams")
fun main() {
val (one, two, three) = getFullName()
println("$one $two $three") // John Quincy Adams
val (first, _, middle) = getFullName()
println("$first $middle") // John Adams
val (only) = getFullName()
println("$only") // John
val full = getFullName()
println("$full") // (John, Quincy, Adams)
}
중간에 값이 빌 경우 언더바를 통해 생략할 수 있다.
Map
Map
는 immutable 콜렉션.
MutableMap
는 mutable 표준 콜렉션.
Pair
객체를 사용해서 생성할 수 있다.
val p1 = "a" to 1
val p2 = Pair("b", 2)
val map = mapOf(p1, p2, "c" to 3)
val mmap = mutableMapOf(p1, p2, "c" to 3)
println(map) // {a=1, b=2, c=3}
println(mmap) // {a=1, b=2, c=3}
get, set 메서드는 인덱스 연산자로 대체해서 사용할 수 있다.
val mmap = mutableMapOf("hello" to "world", "foo" to "bar")
println(mmap.get("hello")) // world
println(mmap["foo"]) // bar
mmap["baz"] = "qux"
println(mmap) // {hello=world, foo=bar, baz=qux}
associate, associateWith
콜렉션간의 결합을 통해 Map
콜렉션 생성이 가능하다.
var keys: CharRange = 'a'..'f'
val map: Map<Char, String> = keys.associate { it to it.toString().repeat(5).capitalize() }
println(map) // {a=Aaaaa, b=Bbbbb, c=Ccccc, d=Ddddd, e=Eeeee, f=Fffff}
var keys: CharRange = 'a'..'f'
val map: Map<Char, Pair<Char, String>> = keys.associateWith { it to it.toString().repeat(5).capitalize() }
println(map) // {a=(a, Aaaaa), b=(b, Bbbbb), c=(c, Ccccc), d=(d, Ddddd), e=(e, Eeeee), f=(f, Fffff)}
contains
key
, value
에 요소가 포함되어 있는지 검사하는 메서드를 제공한다.
val map = mapOf("hello" to "world", "foo" to "bar")
println(map.contains("hello")) // true, containsKey 와 동일
println(map.containsKey("hello")) // true
println(map.containsValue("bar")) // true
println("hello" in map) // true
구조분해, 오버라이딩 함수
Pair
콜렉션을 기반으로 반복문을 구성하는데
Pair
의 구조분해 문법과 같이 사용할 수 있다.
val mmap = mutableMapOf("hello" to "world", "foo" to "bar")
for (entry in mmap) {
println("${entry.key} : ${entry.value}")
}
for ((k, v) in mmap) {
println("$k : $v")
}
// hello : world
// foo : bar
List
콜렉션과 마찬가지로 Map
콜렉션에서도 오버라이딩 함수를 제공한다.
val map = mapOf("hello" to "world", "foo" to "bar")
var newMap1 = map + ("baz" to "quz")
println(newMap) // {hello=world, foo=bar, baz=quz}
var newMap2= map - "hello"
println(newMap2) // {foo=bar}
내부 반복자 메서드
kotlin
에서는 [filter, map]
과 같은 콜렉션을 쉽게 컨트롤할 수 있는 내부 반복자 메서드를 제공한다, 내부 반복자 메서드
는 즉시 처리되어 모든원소에 접근한다.
java
에선 성능상의 이유로 stream
을 통해 해당 함수를 호출시켰지만, kotlin
에선 편의성을 위해 바로 사용가능하다.
내부 반복자 메서드는 크기가 작은 콜렉션에서만 사용해야 한다.
chucked, windowed
콜렉션을 아래와 같이 윈도우 단위로 콜렉션을 분할하는 기능
val range: IntRange = 0..10
val chucked: List<List<Int>> = range.chunked(3)
println(chucked) // [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
사실 chucked
는 windowed
함수를 추상화 한 것
val range: IntRange = 0..10
// size, step, partialWindows
val chucked: List<List<Int>> = range.windowed(3, 3, true)
println(chucked) // [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10]]
size
: 각 윈도우 원소 수
step
: 전진할 윈도우 개수 (default 1)
partialWindows
: size 로 나누어 떨어지지 않을때 버림 여부 (deafult false 버림)
filter, map, joinToString
data class Product(
val name: String,
val price: Double,
val onSale: Boolean = false
)
fun generateList(): List<Product> {
val product1 = Product("product1", 2000.0, true)
val product2 = Product("product2", 1000.0, false)
val product3 = Product("product3", 3000.0, true)
return listOf(product1, product2, product3)
}
위 Product
클래스로 사용법 확인
fun main() {
var products: List<Product> = generateList();
var result: String = products
.filter { it.onSale }
.map { it.name }
.joinToString(separator = ", ")
println(result) // product1, product3
}
ifEmpty, ifBlank
fun generateList(): List<Product> {
val product1 = Product("product1", 2000.0, onSale = false)
val product2 = Product("product2", 1000.0, onSale = false)
val product3 = Product("product3", 3000.0, onSale = false)
return listOf(product1, product2, product3)
}
fun namesOfProductsOnSale1(products: List<Product>) =
products.filter { it.onSale }
.map { it.name }
.ifEmpty { listOf("none") }
.joinToString(separator = ", ")
fun namesOfProductsOnSale2(products: List<Product>) =
products.filter { it.onSale }
.map { it.name }
.joinToString(separator = ", ")
.ifEmpty { "none" }
fun main() {
var products: List<Product> = generateList();
var result1 = namesOfProductsOnSale1(products)
var result2 = namesOfProductsOnSale2(products)
println(result1) // none
println(result2) // none
}
sortedWith, compareBy
요소 정렬을 위한 스트림 메서드
fun main() {
var products: List<Product> = generateList();
products = products.sortedWith(compareBy({ it.price }, { it.name }))
println(products)
// [Product(name=product2, price=1000.0, onSale=false), Product(name=product1, price=2000.0, onSale=true), Product(name=product3, price=3000.0, onSale=true)]
}
filterIsInstance, filterIsInstanceTo
List<Any>
형식의 콜렉션을 필터링할 때에는 아래와 같은 문제가 발생할 수 있다.
fun main() {
val list: List<Any> = listOf("a", LocalDate.now(), 3, 1, 4, "b")
val strings: List<Any> = list.filter { it is String }
for (str in strings) {
str.length // error, Any 타입에는 length 가 없음
}
}
filterIsInstance
를 사용해 해당 요소만 뽑아내어 타입을 지정할 수 있다.
val list: List<Any> = listOf("a", LocalDate.now(), 3, 1, 4, "b")
val all: List<Any> = list.filterIsInstance<Any>()
val strings: List<String> = list.filterIsInstance<String>()
val ints: List<Int> = list.filterIsInstance<Int>()
val dates: List<LocalDate> = list.filterIsInstance(LocalDate::class.java)
사실 filterIsInstance
은 filterIsInstanceTo
함수를 구현한 함수이며
filterIsInstanceTo
를 사용하면 어떤 인스턴스에 해당 요소를 집어넣을지 지정 가능하다.
val list: List<Any> = listOf("a", LocalDate.now(), 3, 1, 4, "b")
val all: MutableList<Any> = list.filterIsInstanceTo(mutableListOf())
val strings: MutableList<String> = list.filterIsInstanceTo(mutableListOf<String>())
val ints: MutableList<Int> = list.filterIsInstanceTo(mutableListOf<Int>())
val dates: MutableList<LocalDate> = list.filterIsInstanceTo(mutableListOf<LocalDate>())
Iterable 클래스
기본적으로 콜렉션은 순회가능한 Iterable
클래스이다.
Iterable
인터페이스 구현
data class Player(val name: String)
class Team(
val name: String,
val players: MutableList<Player> = mutableListOf()
) : Iterable<Player> {
fun addPlayers(vararg people: Player) = players.addAll(people)
override fun iterator(): Iterator<Player> = players.iterator()
}
fun main() {
val team = Team("Warriors")
team.addPlayers(
Player("Curry"),
Player("Thompson")
)
for (player in team) {
println(player)
// Player(name=Curry)
// Player(name=Thompson)
}
}
다음과 같이 operator
키워드로 iterator
함수만 재정의하면 Iterator 구현 없이 for...each
문을 실행할 수 있다.
class Team(
val name: String,
val players: MutableList<Player> = mutableListOf()
) {
fun addPlayers(vararg people: Player) = players.addAll(people)
}
operator fun Team.iterator(): Iterator<Player> = players.iterator()
시퀀스
kotlin
시퀀스는 java stream
과 비슷한 기능으로 콜렉션의 성능 향상을 위한 wrapper 라 볼 수 있다.
콜렉션의 first
와 같은 내부 반복자 메서드를 사용할 경우 전체 요소를 순회하는 문제가 발생한다.
반면 asSequence()
함수를 사용하면 동일하게 조건을 효율적으로 빠져나올 수 있다.
심지어 실제 result2
를 참조하기 전까지는 시퀀스가 실행되지 않는다.
var result1: Int = (100..200)
.map { it * 2 }
.filter { println("filter1: $it"); it % 3 == 0 }
.first();
/*
filter1: 200
filter1: 201
... 201 ~ 399 까지 출력
filter1: 400
*/
var result2: Int = (100..200).asSequence()
.map { it * 2 }
.filter { println("filter2: $it"); it % 3 == 0 }
.firstOrNull();
println(result2) // result2 를 참조하지 않으면 시퀀스 실행 X
/*
filter2: 200
filter2: 202
filter2: 204
204
*/
시퀀스 연산은 중간연산과 최종연산으로 나뉘며 최종연산까지 작성하여야 전체파이프라인이 구성되며 시퀀스가 동작한다.
map
, filter
와 같은것을 중간연산,
first
, toList
같은함수를 최종연산이라 한다.
시퀀스 최종값이 비어있을경우
first
는 예외를 발생함으로firstOrNull
사용을 추천.
시퀀스 생성 - sequenceOf, asSequence, generateSequence
- 원소는
sequenceOf
iterable
객체는asSequence
val numSequence1 = sequenceOf(3, 1, 4, 1, 5, 9)
val numSequence2 = listOf(3, 1, 4, 1, 5, 9).asSequence()
generateSequence
는 초기값과 다음값을 생성할 nextFunction
함수를 인자로 받아 시퀀스를 생성한다.
nextFunction
에서 계속 값을 반환하면 무한시퀀스가 생성된다.
내부적으로 재귀형식으로 함수가 구성됨
public fun <T : Any> generateSequence(seed: T?, nextFunction: (T) -> T?): Sequence<T> =
if (seed == null)
EmptySequence
else
GeneratorSequence({ seed }, nextFunction)
단순이 1씩 증가시키는 함수를 nextFunction
으로 지정, take
최종연산으로 값을 출력한다.
val buffer = StringBuffer();
var joinResult = generateSequence(1) { it + 1 }
.take(10)
.joinTo(buffer, ",")
println(joinResult) // 1,2,3,4,5,6,7,8,9,10
아래처럼 탈출조건을 삽입해도 된다.
val buffer = StringBuffer();
generateSequence(1) { if (it > 10) null else it + 1 }
.joinTo(buffer, ",")
println(buffer) // 1,2,3,4,5,6,7,8,9,10,11
탈출한다 해도 해당요소까지 시퀀스에 포함됨으로 아래처럼 직관적인 takeWhile
문을 사용하는것을 권장
val buffer = StringBuffer();
generateSequence(1) { it + 1 }
.takeWhile { it < 10 }
.joinTo(buffer, ",")
println(buffer) // 1,2,3,4,5,6,7,8,9
시퀀스 생성 - sequence, yield
sequence
와 yield
함수를 사용해 시퀀스를 생성가능하다.
public fun <T> sequence(@BuilderInference block: suspend SequenceScope<T>.() -> Unit): Sequence<T> = Sequence { iterator(block) }
fun fibonacciSequence() = sequence {
var terms = Pair(0, 1)
while (true) {
yield(terms.first) // stop & return
terms = terms.second to terms.first + terms.second
}
}
fun main() {
val fibs = fibonacciSequence()
.take(10)
.toList()
println(fibs) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}
다른언어의 yield
와 마찬가지로 stop interrupt
를 발생시키며 값을 반환하는 함수
해당
yield
는Coroutine
의yield
가 아닌SequenceScope
내부에 정의된 함수임의 주의
public abstract class SequenceScope<in T> internal constructor() {
public abstract suspend fun yield(value: T)
public abstract suspend fun yieldAll(iterator: Iterator<T>)
}
rangeTo
범위클래스를 상속하면 ..
키워드를 사용해 범위타입 클래스인 ClosedRange
을 사용할 수 있다.
val range1: CharRange = 'a'..'f'
val range2: CharRange = 'a'.rangeTo('f')
범위클래스로 CharRange
외에도 IntRange
, LongRange
등이 있으며 모두 ClosedRange
를 구현하고 있다.
ClosedRange
의 contains
메서드를 보면 infix
연산자로 비교문만 정의하면
아래와 같이 in 키워드를 사용할 수 있다.
public interface ClosedRange<T: Comparable<T>> {
// The minimum value in the range.
public val start: T
// The maximum value in the range (inclusive).
public val endInclusive: T
// Checks whether the specified [value] belongs to the range.
public operator fun contains(value: T): Boolean = value >= start && value <= endInclusive
// Checks whether the range is empty.
public fun isEmpty(): Boolean = start > endInclusive
}
val startDate = LocalDate.now()
val midDate = startDate.plusDays(3)
val endDate = startDate.plusDays(5)
val overDate = startDate.plusDays(6)
val dateRange: ClosedRange<LocalDate> = startDate..endDate
println(startDate in dateRange) // true
println(midDate in dateRange) // true
println(endDate in dateRange) // true
println(overDate in dateRange) // false
Progression(수열, 연속)
ClosedRange<LocalDate>
타입이 수열처럼 연속된 값의 나열은 아니기 때문에 for...each
사용은 불가능하다.
public class CharRange(start: Char, endInclusive: Char) : CharProgression(start, endInclusive, 1), ClosedRange<Char> {...}
public class IntRange(start : Int, endInclusive: Int) : IntProgression(start, endInclusive, 1), ClosedRange<Int> { ... }
public class LongRange(start: Long, endInclusive: Long) : LongProgression(start, endInclusive, 1), ClosedRange<Long> { ... }
CharRange
, IntRange
, LongRange
처럼 Iterable
인터페이스를 구현한 CharProgression
, IntProgression
, LongProgression
과 같은 클래스를 상속받아야 한다.
class LocalDateProgression(
override val start: LocalDate,
override val endInclusive: LocalDate,
val step: Long = 1 // 1day
) : Iterable<LocalDate>, ClosedRange<LocalDate> {
override fun iterator(): Iterator<LocalDate> = LocalDateProgressionIterator(start, endInclusive, step)
infix fun step(days: Long) = LocalDateProgression(start, endInclusive, days)
internal class LocalDateProgressionIterator(
var current: LocalDate,
val endInclusive: LocalDate,
val step: Long
) : Iterator<LocalDate> {
override fun hasNext() = current <= endInclusive
override fun next(): LocalDate {
val next = current
current = current.plusDays(step)
return next
}
}
}
operator fun LocalDate.rangeTo(other: LocalDate) = LocalDateProgression(this, other, 1)
fun main() {
val startDate = LocalDate.now()
val midDate = startDate.plusDays(3)
val endDate = startDate.plusDays(5)
val overDate = startDate.plusDays(6)
val dateRange = startDate..endDate
for (date in dateRange) {
println(date)
// 2023-02-20
// 2023-02-21
// 2023-02-22
// 2023-02-23
// 2023-02-24
// 2023-02-25
}
}