코틀린 완벽 가이드 책과 코드스피츠 유튜브 스터디 영상 내용을 정리하거나 생각을 정리한 문서 (인용 태그를 제외한 모든 텍스트들은 스터디 내용을 필사하거나, 책에서 정리한 내용입니다.)
출쳐 : 코틀린완벽가이드, 코드스피츠 유튜브 영상
7장 컬렉션과 I/O 자세히 알아보기
Iterable
“일반적으로 즉시(eager) 계산되는 상태가 있는(stateful) 컬렉션”
그 외에는 java와 동일
Comparable과 Comparator
compareTo() : 자바와 동일, 수신객체 인스턴스가 상대방 인스턴수보다 크면 양수, 같으면 0
compareBy() : 비교 가능 객체를 제공
컬렉션 생성
~Of() 로 간단하게 생성 가능
val list = mutableListOf("1", "2")
val mutableMap = mutableMapOf(10 to "Ten")
known element인 시퀀스를 만드는 sequenceOf()가 있으며 기존 컬렉션으로부터 얻는 asSequence()
sequenceOf(1,2,3).iterator().next()
listOf(1,2,3).asSequence().iterator().next()
제네레이터 함수를 바탕으로 시퀀스를 만드는 것도 지원
val numbers = generateSequence{ readLine()?.toIntOrNull() }
val evens = generateSequence(10) { if (it >= 2) it -2 else null }
yield(), yieldAll() 도 지원
val numbers = sequence {
yield(0)
yieldAll(listOf(1,2,3))
}
컬렉션 접근
first(), last(), firstOrNull(), lastOrNull() : 첫(마지막)원소 접근과 nullsafe
elementAt() : get()의 일반화된 버전, 다만 random access list가 아닌 컬렉션에서는 접근이 n복잡도를 가짐
컬렉션 조건 검사
all() : 모든 원소가 조건절을 만족하면 true
none() : all과 반대
any() : 하나라도 만족하는지 여부
컬렉션 집계
count() : 갯수 반환인데, 오버로딩된 버전이 있다.
listOf(1,2,3,4).count { it < 0 }
average(), sum() : 평균, 합계값 반환
minWithOrNull(), maxWithOrNull()
minByOrNull(), maxByOrNull()
class Person(
val firstName: String,
val familyName: String,
val age: Int
) {
val fullName get() = "$firstName $familyName"
}
val FULL_NAME_COMPARATOR = Comparator<Person> { p1, p2 ->
p1.fullName.compareTo(p2.fullName)
}
fun main() {
val persons = sequenceOf(
Person("John", "Doe", 24),
Person("Hello", "Doe", 24),
Person("Jane", "Doe", 26),
Person("Eli", "Doe", 25),
)
println(persons.minByOrNull { it.firstName })
println(persons.maxByOrNull { it.firstName })
println(persons.minByOrNull { it.age })
println(persons.minWithOrNull(FULL_NAME_COMPARATOR))
println(persons.maxWithOrNull(FULL_NAME_COMPARATOR))
}
joinToString()도 따로 있음, 추가적으로 빌더(appendable)을 반환받고싶으면 그냥 joinTo()사용
reduce(), fold() : 첫 집계값을 이용하기 위해서
하위 컬렉션 추출
slice, subList, take, drop 등이 있음.
slice() 함수는 지정된 인덱스 범위의 요소들로 새로운 리스트를 생성
- 원본 리스트를 변경하지 않고 새로운 리스트를 반환.
- IntRange나 List를 인자로 받아 특정 인덱스의 요소들을 선택할 수 있음.
val numbers = listOf(10, 20, 30, 40, 50, 60, 70)
val slicedNumbers = numbers.slice(1..4)
println(slicedNumbers) // [20, 30, 40, 50]
subList() 함수는 원본 리스트의 지정된 범위의 뷰를 반환
- 원본 리스트의 변경 사항이 subList에도 반영됨.
- 메모리 효율적이며, 구조적 변경이 필요한 경우에 적합.
val numbers = mutableListOf(10, 20, 30, 40, 50, 60, 70)
val sublistNumbers = numbers.subList(2, 5)
println(sublistNumbers) // [30, 40, 50]
sublistNumbers[0] = 99
println(numbers) // [10, 20, 99, 40, 50, 60, 70]
take() 함수는 컬렉션의 앞에서부터 지정된 수만큼의 요소를 가져와 새로운 리스트를 생성
- 원본 컬렉션을 변경하지 않음.
- takeLast()를 사용하면 뒤에서부터 요소를 가져올 수 있음.
val numbers = (0..10).toList() println(numbers.take(3)) // 출력: [0, 1, 2]
println(numbers.takeLast(3)) // 출력: [8, 9, 10]
drop() 은 take와 동일하지만 반대 (건너뜀)
List
- 리스트 원소 바꾸는 방법
- var, 읽기전용 리스트, +- 연산자 사용
- 자유도가 높음
- val, 가변 리스트, MutableList 메서드 사용
- 효율적임
- var, 읽기전용 리스트, +- 연산자 사용
성능관점에서 Array, IntArray(기본형 배열)
| 항목 | Array | IntArray | List |
|---|---|---|---|
| 타입 | Array (객체 타입 배열) | Primitive Array (int 배열) | List (인터페이스) |
| 저장 방식 | 객체 타입(Int 객체들) | 기본형(int) 저장, 더 가볍고 빠름 | 객체 타입(Int) 저장 |
| 메모리 사용 | 큼 (박싱(Boxing) 발생) | 작음 (No 박싱) | 큼 (박싱 발생) |
| 크기 변경 | 불가 | 불가 | 불변(listOf()) or 가변(mutableListOf()) |
| 주요 생성 방법 | arrayOf(1, 2, 3) | intArrayOf(1, 2, 3) | listOf(1, 2, 3) |
| 성능 | 보통 | 가장 빠름 | 가장 느릴 수 있음 |
| 메서드 지원 | 배열 기본 연산 (get, set, size) | 배열 기본 연산 (get, set, size) | 다양한 컬렉션 연산 (filter, map, groupBy 등) |
정렬
자바와 별 차이 없음
코틀린의 타입 시스템
- 선요약
- 클래스는 객체를 생성하는 템플릿, 타입은 객체에 기대하는 바와 기능을 정의
- 모든 클래스는 두개의 타입, 즉 널 가능한 타입과 널 가능하지 않은 타입을 생성
- 널 가능한 타입은 널 가능하지 않은 타입의 슈퍼타입
- 모든 타입의 슈퍼타입은 Any?
- 모든 타입의 서브타입은 Nothing
- null의 타입은 Nothing?
- Nothing을 결과 타입으로 선언한 표현식이 있다면 코틀린 컴파일러는 해당 표현식 뒤에 오는 코드는 도달 불가능하다고 이해한다.
Nothing -> Nothing?
↑ ↑
Dog -> Dog?
↑ ↑
Animal -> Animal?
↑ ↑
Any -> Any?
Nothing 타입이란?
- Nothing은 모든 타입의 하위 타입(subtype)이다.
- 실제 값을 가질 수 없는 타입이다. 즉, 어떤 값도 Nothing 타입의 인스턴스가 될 수 없다.
- 주로 “정상적으로 끝나지 않는 코드”에 쓰인다.
- 예: 무조건 예외를 던지거나, 무한 루프에 빠지는 함수의 반환 타입.
throw가 Nothing인 이유
- throw는 절대 정상적으로 값을 반환하지 않는다.
- 따라서 throw 표현식의 타입은 Nothing이다.
- Nothing은 모든 타입의 하위 타입이기 때문에, 어떤 타입이 필요한 곳에서도 throw를 집어넣을 수 있다.
- 즉, “값이 필요한 곳”에도 throw를 쓸 수 있는 이유가 바로 Nothing 때문이다.
fun getLength(str: String?): Int {
return str?.length ?: throw IllegalArgumentException("str is null")
}
이 코드의 의미:
- str?.length 결과가 null이면,
- throw를 실행한다.
- throw는 Nothing이므로 Int 타입이 필요한 ?: 오른쪽 자리에 둘 수 있다. 코드 흐름 설명:
- str?.length : Int? 타입
- ?: : 좌항이 null이면 우항으로 대체
- 우항인 throw는 타입이 Nothing
- Nothing은 Int의 서브타입으로 간주되어 문제 없음
결론: throw가 Nothing 타입이기 때문에 ?: 우항에 올 수 있다.
제네릭
- 널 불가능 표기
& Any
fun <T> T.orThrow(): T & Any = this ?: throw Error()
- 제약조건
: Type
class ListAdapter<T: ItemAdapter>(){} // ItemAdapter의 서브타입만
// 두 개 이상의 경우
fun <T> pet(animal: T) where T : Animal, T: GoodTempered {}
// 주로 Iterable, Comparable같은 것들이나, 널체크를 위한 Any도 제약조건으로 자주 쓰임
- 스타프로젝션 : 구체적인 타입 인수를 지정하고 싶지 않을 때 (소거 관련)
a is List<*>
// Any? 타입 인수와 혼동하면 안됨
fun main() {
val l1: MutableList<Any?> = mutableListOf("A")
val r1 = l1.first() // r1의 타입은 Any?입니다.
l1.add("B") // 기대되는 인수 타입은 Any?입니다.
val l2: MutableList<*> = mutableListOf("A")
val r2 = l2.first() // r2의 타입은 Any?입니다.
// l2.add("B") // 에러 발생
// 기대되는 인수 타입이 Nothing이므로 인수로 어떠한 값도 사용할 수 없습니다.
}
- 언더스코어 연산자
- 여러개의 타입 인수 중 하나만 지정하고 나머지는 컴파일러가 유추하게 하고 싶을 때
- 이때 언더스코어를 쓰면 컴파일러가 유추해야 하는 타입 인수를 지정함.
// reified는 런타임에 타입정보를 소거시키지 않고 남기도록 하는 키워드
// 그래서 인라인이어야함
// 참고로 이 코드상에서도, 현실적으로도 굳이 * 안쓰고 Any 써도 됨
inline fun <K, reified V> Map<K, *>.filterValueIsInstance(): Map<K, V> =
filter { it.value is V } as Map<K, V>
fun main() {
val props = mapOf( // Map<K, Any>
"name" to "Alex",
"age" to 25,
"city" to "New York"
)
val strProps = props.filterValueIsInstance<_, String>()
println(strProps) // {name=Alex, city=New York}
}