5장 컬렉션

코틀린 쿡북 5장을 요약한 내용 입니다.

배열 다루기

코틀린에는 배열을 생성하고 싶을 경우, arrayOf 함수를 이용해 배열을 만들고 Array 클래스에 들어 있는 속성과 메소드를 이용할 수 있다.

코틀린에서 배열을 다루는 방법과 자바에서 배열을 다루는 방법은 서로 약간 다르다.

자바에서 배열 인스턴스화

String[] strings = new String[4];
strings[0] = "an";
strings[0] = "array";
strings[0] = "of";
strings[0] = "strings";

// 또는 더 쉽게
Strings = "an array of strings".split(" ");

코틀린에서 배열 인스턴스화

val strings = arrayOf("this", "is", "an", "array", "of", "strings")

// 또는 널로만 채워진 배열을 생성할 수 있다. 
val nummStringArray = arrayOfNulls<String>(5)

코틀린의 Array 클래스에는 public 생성자가 하나만 있다. 이 생성자는 다음의 두 인자를 받는다.

  • Int 타입의 size

  • init, 즉 (Int) → T 타입의 람다

Array 클래스 생성자의 두 번째 인자인 람다는 배열을 생성할 때 인덱스마다 호출된다.

val squares = Array(5) { i -> (i * i).toString() }

// results
// {"0", "1", "4", "9", "16"}

코틀린에는 오토박싱과 언박싱 비용을 방지할 수 있는 기본 타입을 나타내는 클래스가 있다. booleanArrayOf, byteArrayOf, shortAr, rayOf, charArrayOf, intArrayOf, longArrayOf, floatArrayOf, doubleArrayOf 등이 있다.

코틀린에는 명시적인 기본 타입은 없지만 값이 널 허용 값인 경우 생성된 바이트코드는 Integer와 Double 같은 자바 래퍼 클래스를 사용하고, 널 비허용 값인 경우 생성된 바이트코드는 int와 double 같은 기본 타입을 사용한다.

indices, withIndex

배열의 확장 메소드는 대부분 컬렉션에 있는 이름이 같은 확장 메소드와 동일하게 동작한다. 하지만 배열에는 두어 개의 고유한 확장 함수가 존재한다. 예를 들어 배열의 적법한 인덱스 값을 알고 싶다면 다음과 같이 indices 속성을 사용하자

val list = ArrayOf("Hello", "World", "!")
for (i in list.indicies) {
	println(list[i])
}

//results
// Hello
// World
// !

for-in 루프를 사용하지만 배열의 인덱스 값도 같이 사용하고 싶다면 withIndex 함수를 사용하자

val strings = arrayOf("this", "is", "an", "array", "of", "strings")

for((index, value) in strings.withIndex()) {
	println("index $index maps to $value")
}

컬렉션 생성하기

기본적으로 코틀린 컬렉션은 '불변'이다 그런 의미에서 컬렉션은 원소를 추가하거나 제거하는 메소드를 지원하지 않는다.

불변 컬렉션 생성

  • listOf

  • setOf

  • mapOf

var numList = listOf(3, 1, 4, 5, 9)
var numSet = setOf(3, 1, 4, 1, 5, 9)
// numSet.size == 5 -> true
var mapOf(1 to "one", 2 to "Two", 3 to "three")

컬렉션을 변경하는 메소드는 다음과 같은 팩토리 메소드에서 제공하는 '가변(mutable)' 인터페이스에 들어 있다.

가변 컬렉션 생성

  • mutableListOf

  • mutableSetOf

  • mutableMapOf

var numList = mutableListOf(3, 1, 4, 5, 9)
var numSet = mutavleSetOf(3, 1, 4, 1, 5, 9)
var mutableMapOf(1 to "one", 2 to "Two", 3 to "three")

가변 컬렉션에서 불변 컬렉션으로 변경

가변 컬렉션을 읽기 전용 버전으로 생성하는 방법은 두 가지이다.

  • List 타입의 래퍼런스를 리턴하는 toList 메소드 호출

  • List 타입 래퍼런스에 할당

// solution 1
val readOnlyNumList: List<Int> = mutableNums.toList();

// solution 2
val readOnlyNumList: List<Int> = mutableNums

컬렉션에서 맵 만들기

키 리스트가 있을 경우 각가의 키와 생성한 값을 연관시켜서 맵을 만들고 싶을 경우 associate, associateWith 함수를 활용하라

val keys = 'a'..'f'
val map = keys.associate { it to it.toString().repeat(5).capitalize() }
println(map)

// results
// {a=Aaaaa, b=Bbbbb, c=Ccccc, d=Ddddd, e=Eeeee}

val map2 = keys.associateWith { it.toString().repeat(5).capitalize() }

컬렉션이 빈 경우 기본값 리턴하기

컬렉션이나 문자열이 비어 있는 경우에는 ifEmpty와 ifBlank 함수를 사용해 기본값을 리턴한다.

// 빈 컬렉션에 기본 리스트를 제공
fun onSaleProduct_ifEmptyCollection(products: list<Product>) = 
	products.filter { it.onSale }
		.map { it.name } 
		.ifEmpty { listOf("none") } // filter한 리스트가 비었을 경우
		.joinToString(separator = ", ")

// 빈 문자열에 기본 문자열을 제공
fun onSaleProduct_ifEmptyString(products: list<Product>) = 
	products.filter { it.onSale }
		.map { it.name }
		.joinToString(separator = ", ")
		.ifEmpty { "none" } // filter한 후에 결과 String이 비었을 경우

// 빈 문자일 경우 기본 문자열 제공
val blank = "    "
val blankOrDefault = blank.ifBlank { "default" }
println(blankOrDefault) // default

주어진 범위로 값 제한하기

값이 주어졌을 때, 주어진 값이 특정 범위 안에 들면 해당 값을 리턴하고 그렇지 않다면 범위의 최솟값 또는 최댓값을 리턴할 경우 kotlin.ranges를 사용한다.

@Test
fun `coerceIn given a range`() {
	val range = 3..8

	assertThat(5, `is`(5.coerceIn(range))) // true
	assertThat(range.start, `is`(1.coerceIn(range))) // true
	assertThat(range.endInclusive, `is`(9.coerceIn(range))) // true
}

@Test
fun `coerceIn given min and max`() {
	val min = 2
	val max = 6

	assertThat(5, `is`(5.coerceIn(min, max))) // true
	assertThat(min, `is`(1.coerceIn(min, max))) // true
	assertThat(max, `is`(9.coerceIn(min, max))) // true
}

컬렉션을 윈도우로 처리하기

  • chunked : 컬렉션을 같은 크기로 나눈다.

  • windowed : 정해진 간격으로 컬렉션을 따라 움직이는 블록을 사용할 수 있다.

@Test
internal fun chunked() {
	val range = 0..10

	val chuncked = range.chunked(3)
	assertThat(chunked, containts(listOf(0,1,2), listOf(3,4,5), 
		listOf(6,7,8), listOf(9,10)))

	assertThat(range.chunked(3) { it.sum() }, `is`(listOf(3,12,21,19)))
	assertThat(range.chunked(3) { it.average() }, `is`(listOf(1.0 ,4.0, 7.0, 9.5)))
@Test
fun windowed() {
	val range = 0..10

	assertThat(range.windowed(3, 3),
		contains(listOf(0,1,2), listOf(3,4,5), listOf(6,7,8)))

	assertThat(range.windowed(3, 3) { it.average() },
		contains(1.0, 4.0, 7.0))

	assertThat(range.windowed(3,1),
		contains(
			listOf(0,1,2), listOf(1,2,3), listOf(2,3,4),
			listOf(3,4,5), listOf(4,5,6), listOf(5,6,7),
			listOf(6,7,8), listOf(7,8,9), listOf(8,9,10)))

	assertThat(range.windowed(3, 1) { it.average() },
		contains(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0))
}

다수의 속성으로 정렬하기

컬렉션에서 클래스의 다수의 속성을 정렬하고 싶다면 sortedWith와 comparedBy 함수를 사용한다.

data class Golfer(val score: Int, val first: String, val last: String)
val golfers = listOf(
	Golfer(70, "Jack", "Nicklaus"),
	Golfer(68, "Tom", "Watson"),
	Golfer(68, "Budda", "Watson"),
	Golfer(70, "Tiger", "Woods"),
	Golfer(68, "Ty", "Webb"),
)

골프 선수를 점수로 정렬한 다음, 점수가 같은 선수를 성으로 정렬하고, 마지막으로 점수가 성이 같은 선수를 이름으로 정렬하려고 한다.

val sored = folfers.sortedWith(
	compareBy({it.score}, {it.last}, {it.first})
)

sorted.sorEach { println(it) }

// results
// Golfer(68, "Budda", "Watson")
// Golfer(68, "Tom", "Watson")
// Golfer(68, "Ty", "Webb")
// Golfer(70, "Jack", "Nicklaus")
// Golfer(70, "Tiger", "Woods")

sortBy와 sortWith 함수는 자신의 원소를 제자리에서 정렬하므로 변경 가능 컬렉션을 요구한다.

비교 후 같은 값일 경우에 새로운 비교를 적용하는 thenBy 함수를 사용해 Comparator를 생성할 수 있다.

val comparator = compareBy<Golfer>(Golfer::score)
	.thenBy(Golfer::last)
	.thenBy(Golfer::first)

golfers.wortedWith(comparator)
	.forEach(::println)

사용자 정의 이터레이터 정의하기

컬렉션을 감싼 클래스를 임의로 순회하고 싶을 경우 next와 hasNext 함수를 모두 구현한 이터레이터를 리턴하는 연산자 함수를 정의한다.

val team = Team("warriors")
team.addPlayers(Player("Curry"), Player("Thompson"),
	Player("Durant"), Player("Green"), Player("Cousins"))

for (player in team.playes) {
	println(player)
}

iterator라는 이름의 연산자 함수를 정의하면 이 코드를 더 간단하게 만들 수 있다.

operator fun Team.iterator() : Iterator<Player> = players.iterator()

for (player in team) {
	println(player)
}

사실 Team 클래스가 Iterable 인터페이스를 구현하도록 만든 이유는 Iterable 인터페이스에 추상 연산자 함수 iterator가 있기 때문이다.

class Team(val name: String,
	val players: MutableList<Player> = mutableListOf()) : Iterable<Player> {
	
	override fun iterator(): Iterator<Player> =
		players.iterator()

	...
}

이제 Team 클래스에서 Iterable의 모든 확장 함수를 사용 가능하다는 점을 제외하면 실행 결과가 같기 때문에 아래처럼 작성할 수 있다.

assertEquals("Cousins, Curry, Durant, Green, Thompson",
	team.map { it.name }.joinToString())

타입으로 컬렉션을 필터링하기

여러 타입이 섞여 있는 컬렉션에서 특정 타입의 원소로만 구성된 새 컬렉션을 생성할 경우 filterIsInstance 또는 filterIsInstanceTo 확장 함수를 사용해라

val list = listOf("a", LocalDate.now(), 3, 1, 4, "b")
val strings = list.filter { it is String }

for (s in strings) {
	s.length // 컴파일되지 않는다. 
}

컴파일 되지 않는 이유는 strings 변수의 추론 타입은 List<Any>이기 때문이다.

val list = listOf("a", LocalDate.now(), 3, 1, 4, "b")

val all = list.filterIsInstance<Any>()
val strings = list.filterIsInstance<String>()
val ints = list.filterIsInstance<Int>()
val dates = list.filterIsInstance(LocalDate::class.java)

filterIsInstance를 구현하면 filterIsInstanceTo 함수가 호출된다. 그러므로 filterIsInstanceTo 메소드를 바로 호출해도 된다.

val list = listOf("a", LocalDate.now(), 3, 1, 4, "b")

val all = list.filterIsInstanceTo(mutableListOf())
val strings = list.filterIsInstanceTo(mutableListOf<String>())
val ints = list.filterIsInstanceTo(mutableListOf<Int>())
val dates = list.filterIsInstanceTo(mutableListOf<LocalDate>())

범위를 수열로 만들기

Last updated