코틀린 완벽 가이드 책과 코드스피츠 유튜브 스터디 영상 내용을 정리하거나 생각을 정리한 문서 (인용 태그를 제외한 모든 텍스트들은 스터디 내용을 필사하거나, 책에서 정리한 내용입니다.)

출쳐 : 코틀린완벽가이드, 코드스피츠 유튜브 영상

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 메서드 사용
      • 효율적임

성능관점에서 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}
}