본문 바로가기
language/Kotilin In Action

3주차 (6~8장)

by abstract.jiin 2025. 9. 1.

6장 컬렉션과 시퀀스

6.1 컬렉션에 대한 함수형 API

6.1.1 filter와 map

val numbers = listOf(1, 2, 3, 4, 5)

// filter: 조건에 맞는 것만 골라내기
val evenNumbers = numbers.filter { it % 2 == 0 }  // [2, 4]

// map: 각 원소를 다른 형태로 변환
val squares = numbers.map { it * it }  // [1, 4, 9, 16, 25]

6.1.2 reduce와 fold : 값들을 하나로 합치기

val numbers = listOf(1, 2, 3, 4)

// reduce: 첫 번째 값부터 시작해서 차례로 합치기
val sum = numbers.reduce { acc, element -> acc + element }  // 10

// fold: 시작값을 정해서 합치기
val sumWithStart = numbers.fold(0) { acc, element -> acc + element }  // 10

차이점: reduce는 빈 리스트에서 오류가 나지만, fold는 시작값이 있어서 안전

6.1.3 all, any, none, count, find

all, any, none : 조건 검사하기

val ages = listOf(25, 30, 35)

// all: 모든 원소가 조건을 만족하는가?
val allAdults = ages.all { it >= 18 }  // true

// any: 하나라도 조건을 만족하는가?
val hasElderly = ages.any { it >= 65 }  // false

// none: 조건을 만족하는 게 하나도 없는가?
val noChildren = ages.none { it < 18 }  // true

find, count : 찾기와 세기

val people = listOf(Person("Alice", 27), Person("Bob", 31))

// count: 조건에 맞는 개수 세기
val youngCount = people.count { it.age < 30 }  // 1

// find: 조건에 맞는 첫 번째 원소 찾기
val firstYoung = people.find { it.age < 30 }  // Person("Alice", 27)

6.1.4 partition : 리스트를 두 그룹으로 나누기

조건에 맞는 것과 안 맞는 것을 동시에 얻고 싶을 때

val people = listOf(
    Person("Alice", 26),
    Person("Bob", 29),
    Person("Carol", 31)
)

// 기존 방식: 두 번 반복
val young = people.filter { it.age <= 27 }
val old = people.filterNot { it.age <= 27 }

// partition 사용: 한 번에 처리
val (young, old) = people.partition { it.age <= 27 }
// young: [Alice(26)]
// old: [Bob(29), Carol(31)]

6.1.5 groupBy : 여러 그룹으로 분류하기

val people = listOf(
    Person("Alice", 31),
    Person("Bob", 29),
    Person("Carol", 31)
)

val groupedByAge = people.groupBy { it.age }
// {31=[Alice, Carol], 29=[Bob]}

// 문자열을 첫 글자로 분류
val words = listOf("apple", "apricot", "banana", "cantaloupe")
val grouped = words.groupBy { it.first() }
// {a=[apple, apricot], b=[banana], c=[cantaloupe]}

6.1.6 associate, associateWith, associateBy : 리스트를 맵으로 변환

associate - 키와 값을 모두 변환

val people = listOf(Person("Joe", 22), Person("Mary", 31))
val nameToAge = people.associate { it.name to it.age }
// {Joe=22, Mary=31}

associateWith - 원소를 키로, 람다 결과를 값으로

val personToAge = people.associateWith { it.age }
// {Person("Joe", 22)=22, Person("Mary", 31)=31}

associateBy - 람다 결과를 키로, 원소를 값으로

주의: 키가 중복되면 마지막 값이 이전 값을 덮음

val ageToPerson = people.associateBy { it.age }
// {22=Person("Joe", 22), 31=Person("Mary", 31)}

6.1.7 replaceAII, fill : 가변 컬렉션 함수들

val names = mutableListOf("Martin", "Samuel")

// replaceAll: 모든 원소를 변환
names.replaceAll { it.uppercase() }  // [MARTIN, SAMUEL]

// fill: 모든 원소를 같은 값으로
names.fill("(redacted)")  // [(redacted), (redacted)]

6.1.8 ifEmpty : 빈 컬렉션 처리

val empty = emptyList<String>()
val full = listOf("apple", "orange")

println(empty.ifEmpty { listOf("no", "values") })  // [no, values]
println(full.ifEmpty { listOf("no", "values") })   // [apple, orange]

// 문자열용 ifBlank도 있음 (공백 문자열이 완전히 빈 문자열과 다른 것을 표현하는 경우는 드물기 때문)
val blankName = "   "
println(blankName.ifBlank { "(unnamed)" })  // (unnamed)

6.1.9 chunked와 windowed : 컬렉션 나누기

windowed - 슬라이딩 윈도우 (겹치는 구간)

val temps = listOf(27.7, 29.8, 22.0, 35.5, 19.1)

// 3일씩 묶어서 평균 구하기
val averages = temps.windowed(3) { it.sum() / it.size }
// [26.5, 29.1, 25.5]

chunked - 고정 크기로 나누기 (겹치지 않음)

val chunked = temps.chunked(2)
// [[27.7, 29.8], [22.0, 35.5], [19.1]]  // 마지막은 크기가 작을 수 있어요

6.1.10 zip : 두 리스트 합치기

val names = listOf("Joe", "Mary", "Jamie")
val ages = listOf(22, 31, 44, 0)  // 더 긴 리스트

val pairs = names.zip(ages)
// [(Joe, 22), (Mary, 31), (Jamie, 44)]  // 짧은 쪽에 맞춰서 끝남

// Person 객체로 변환
val people = names.zip(ages) { name, age -> Person(name, age) }

// 중위 표기법도 가능
val pairs2 = names zip ages

6.1.11 flatMap과 flatten : 중첩된 컬렉션 펼치기

class Book(val title: String, val authors: List<String>)

val library = listOf(
    Book("Kotlin in Action", listOf("Isakova", "Elizarov")),
    Book("Atomic Kotlin", listOf("Eckel", "Isakova"))
)

// map 사용하면 중첩된 리스트가 됨
val nested = library.map { it.authors }
// [[Isakova, Elizarov], [Eckel, Isakova]]

// flatMap으로 평평하게 만들기
val allAuthors = library.flatMap { it.authors }
// [Isakova, Elizarov, Eckel, Isakova]

// 중복 제거
val uniqueAuthors = allAuthors.toSet()
// [Isakova, Elizarov, Eckel]

6.2 지연 계산 컬렉션 연산: 시퀀스 → 성능 최적화

시퀀스는 "필요할 때만, 필요한 것만" 계산하는 똑똑한 컬렉션. 큰 데이터를 다룰 때 성능 최적화하기 좋음.

일반 컬렉션 연산의 문제점:

val people = listOf(*/*수백만 명의 사람들*/*)

*// 이렇게 하면 중간에 리스트가 2개나 더 생성됨!*
val result = people
    .map { it.name }        *// 첫 번째 임시 리스트 생성*
    .filter { it.startsWith("A") }  *// 두 번째 임시 리스트 생성// 메모리 낭비 + 성능 저하*

시퀀스를 사용한 해결책:

val result = people
    .asSequence()           *// 시퀀스로 변환*
    .map { it.name }        *// 임시 컬렉션 생성 안 함!*
    .filter { it.startsWith("A") }
    .toList()              *// 마지막에 결과만 리스트로// 메모리 효율적 + 빠름*

.asSequence() 나중에 처리하겠다는 계획만 세워두고 toList()가 호출되면 그때서야 실제 작업 시작됨

중간 연산 vs 최종 연산

중간 연산 (Intermediate): 시퀀스를 반환, 지연 실행

  • map, filter, take, drop 등

최종 연산 (Terminal): 실제 결과를 반환, 실행 트리거

  • toList(), count(), find(), sum() 등
fun main() {
    listOf(1, 2, 3, 4)
        .asSequence()
        .map { 
            println("map($it)")  *// 이건 출력 안 됨!*
            it * it 
        }
        .filter { 
            println("filter($it)")  *// 이것도 출력 안 됨!*
            it % 2 == 0 
        }
    *// 아무것도 출력되지 않음 - 최종 연산이 없어서!*
    
    *// 최종 연산 추가하면:*
    listOf(1, 2, 3, 4)
        .asSequence()
        .map { 
            println("map($it)")  *// 이제 출력됨!*
            it * it 
        }
        .filter { 
            println("filter($it)")
            it % 2 == 0 
        }
        .toList()  *// 최종 연산 - 이때 실제 계산 시작!*
}

지연 계산

  1. 필요한 것만 계산
fun main() {
    val result = listOf(1, 2, 3, 4)
        .asSequence()
        .map { it * it }        *// 제곱 계산*
        .find { it > 3 }        *// 3보다 큰 첫 번째 찾기*
    
    println(result)  *// 4// 1*1=1(3보다 작음), 2*2=4(3보다 큼) -> 찾았으니 3, 4는 계산 안 함!*
}
  1. 연산 순서 최적화

일반적으로 map → filter 보다 filter → map 이 빠름

val people = listOf(
    Person("Alice", 29),
    Person("Bob", 31),
    Person("Charles", 31),
    Person("Dan", 21)
)

*// 비효율적: 모든 사람을 이름으로 변환 후 필터링*
people.asSequence()
    .map { it.name }           *// 4번 변환*
    .filter { it.length < 4 }  *// 4번 체크*
    .toList()

*// 효율적: 필터링 후 이름 변환*
people.asSequence()
    .filter { it.name.length < 4 }  *// 4번 체크*
    .map { it.name }                *// 2번만 변환 (Bob, Dan)*
    .toList()

시퀀스 생성 방법들

  1. 기존 컬렉션에서
val list = listOf(1, 2, 3, 4, 5)
val sequence = list.asSequence()
  1. generateSequence로 무한 시퀀스
*// 자연수 시퀀스*
val naturalNumbers = generateSequence(0) { it + 1 }

*// 0부터 100까지의 합*
val sum = naturalNumbers
    .takeWhile { it <= 100 }
    .sum()
println(sum)  *// 5050// 피보나치 수열*
val fibonacci = generateSequence(0 to 1) { (a, b) -> b to (a + b) }
    .map { it.first }
    .take(10)
    .toList()
println(fibonacci)  *// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]*
  1. 실무 예시: 파일 경로 탐색
import java.io.File

fun File.isInsideHiddenDirectory(): Boolean {
    return generateSequence(this) { it.parentFile }
        .any { it.isHidden }
}

fun main() {
    val file = File("/Users/user/.hiddenFolder/document.txt")
    println(file.isInsideHiddenDirectory())  *// true*
}
  • 작은 데이터에 시퀀스 사용, 여러 번 이터레이션(괜히 처음부터 다시 하게됨) , 최종 연산 꼭 챙기기

7장 널이 될 수 있는 값

7.1 NullPointerException 피하고 값이 없는 경우 처리: 널 가능성

  • NPE의 공포 (자바에서 흔히 겪는 상황)
  • 실제 사용자가 보는 메시지들
  • 코틀린의 해결책: 타입 시스템에 널 정보 포함
  • 런타임 → 컴파일 시점으로 문제 해결 시점 이동

7.2 널이 될 수 있는 타입으로 널이 될 수 있는 변수 명시

  • String vs String? 차이점 -물음표(?) 표기법
  • 널이 될 수 없는 타입이 기본, ?를 붙여야 널 허용
  • 널 가능 타입의 제약사항들:
    • 직접 메서드 호출 불가
    • 널 불가능 타입 변수에 대입 불가
    • 널 불가능 파라미터 함수에 전달 불가
class User(val name: String?, val email: String?)

fun sendWelcomeEmail(email: String) {  // 널 불가능 파라미터
    println("이메일을 $email 로 전송합니다")
}

fun processUser(user: User?) {
    // 이런 식으로 하면 모두 컴파일 에러
    // val userName = user.name              // 1. 직접 메서드 호출 불가
    // val userEmail: String = user.email    // 2. 널 불가능 변수에 대입 불가  
    // sendWelcomeEmail(user.email)          // 3. 널 불가능 파라미터에 전달 불가
    
    // 올바른 처리 방법들
    
    // 1. 안전한 접근
    val userName = user?.name ?: "익명 사용자"
    
    // 2. 이메일이 있을 때만 처리
    user?.email?.let { email ->
        sendWelcomeEmail(email)  // 이제 안전하게 전달 가능
    }
    
    // 3. 전체적인 null 체크
    if (user != null && user.email != null) {
        val safeEmail: String = user.email  // 스마트 캐스트로 안전
        sendWelcomeEmail(safeEmail)
    }
}

7.3 타입의 의미 자세히 살펴보기

  • 타입의 정의: 가능한 값의 집합 + 연산의 집합
  • 자바 String 타입의 문제점 (String 또는 null)
  • 코틀린의 타입 시스템이 널을 제대로 다루는 방법
  • Optional vs 코틀린 널 타입 (성능상 이점)

7.4 안전한 호출 연산자로 null 검사와 메서드 호출 합치기: ?.

  • ?. 연산자 기본 사용법
  • str?.uppercase() ≡ if (str != null) str.uppercase() else null
  • 프로퍼티 접근에서도 사용 가능
  • 안전한 호출 체이닝: person?.company?.address?.city
  • 결과 타입도 널 가능 타입

기본 동작:

*// 전통적인 방식*
if (str != null) str.uppercase() else null

*// 코틀린 방식*
str?.uppercase()  *// 같은 결과, 훨씬 간결!*

실제 사용 예시:

class Employee(val name: String, val manager: Employee?)

fun managerName(employee: Employee): String? {
    return employee.manager?.name  *// manager가 null이면 null 반환*
}

val ceo = Employee("대표님", null)
val developer = Employee("개발자", ceo)

println(managerName(developer))  *// "대표님"*
println(managerName(ceo))        *// null*

체이닝도 가능:

class Address(val streetAddress: String, val country: String)
class Company(val name: String, val address: Address?)
class Person(val name: String, val company: Company?)

fun Person.countryName(): String? {
    return this.company?.address?.country  *// 3단계 체이닝!*
}

val person = Person("김철수", null)
println(person.countryName())  *// null*

7.5 엘비스 연산자로 null에 대한 기본값 제공: ?:

  • ?: 연산자 (엘비스 프레슬리 헤어스타일 닮음)
  • name ?: "guest" - null일 때 기본값 제공
  • 안전한 호출과 함께: s?.length ?: 0
  • return/throw도 가능: ?: throw Exception()
  • 전제조건 검사에 유용

기본 사용법:

fun greet(name: String?) {
    val recipient = name ?: "guest"  *// name이 null이면 "guest" 사용*
    println("Hello, $recipient!")
}

greet("김철수")  *// "Hello, 김철수!"*
greet(null)    *// "Hello, guest!"*

안전한 호출과 함께:

fun strLenSafe(s: String?): Int = s?.length ?: 0

println(strLenSafe("hello"))  *// 5*
println(strLenSafe(null))     *// 0*

return/throw도 가능:

fun printShippingLabel(person: Person) {
    val address = person.company?.address 
        ?: throw IllegalArgumentException("주소가 없습니다")  *// 예외 던지기*
    
    println(address.streetAddress)
}

7.6 예외를 발생시키지 않고 안전하게 타입을 캐스트하기: as?

  • as? 연산자 - 캐스트 실패 시 null 반환
  • 기존 as 연산자의 문제점 (ClassCastException)
  • 엘비스 연산자와 함께 사용하는 패턴
  • equals 구현에서의 활용 예시

기존 방식:

if (obj is String) {
    val str = obj as String  *// 안전하지만 번거로움*
}

코틀린 방식:

val str: String? = obj as? String  *// 실패하면 null*

equals 구현에서 자주 사용:

class Person(val firstName: String, val lastName: String) {
    override fun equals(other: Any?): Boolean {
        val otherPerson = other as? Person ?: return false
        
        return otherPerson.firstName == firstName && 
               otherPerson.lastName == lastName
    }
}

7.7 널 아님 단언: !!

  • !! 연산자 - 강제로 널 불가능 타입으로 변환
  • "나는 이 값이 null이 아님을 보장한다"
  • 실제 null이면 NPE 발생
fun ignoreNulls(str: String?) {
    val strNotNull: String = str!!  // "내가 보장한다, null 아니야!"
    println(strNotNull.length)
}

ignoreNulls(null)  // 💥 KotlinNullPointerException!
  • 언제 사용하는가? (액션 클래스 예시)
  • 주의사항: 한 줄에 여러 !! 사용 금지
// 나쁜 예: 한 줄에 여러 !! 사용
person.company!!.address!!.country

// 좋은 예: 안전한 호출 체이닝
person.company?.address?.country
  • 무례한 기호를 의도적으로 선택한 이유

7.8 let 함수

  • let 함수의 기본 개념
  • 안전한 호출과 함께 사용: email?.let { sendEmail(it) }
  • 널 가능 값을 널 불가능 함수에 전달하는 패턴
  • 긴 식의 결과를 처리할 때 유용
  • 여러 값 체크 시 중첩 let보다는 일반 if 사용
  • 영역 함수들 비교 (with, apply, let, run, also)

널 가능 값을 널 불가능 함수에 전달:

fun sendEmail(email: String) {
    println(" $email 로 이메일 전송")
}

val userEmail: String? = "user@example.com"

*// 전통적 방식*
if (userEmail != null) {
    sendEmail(userEmail)
}

*// let 사용*
userEmail?.let { sendEmail(it) }  *// 더 간결!// null인 경우*
val nullEmail: String? = null
nullEmail?.let { sendEmail(it) }  *// 실행되지 않음*

복잡한 식에서 유용:

*// 전통적 방식*
val person = getBestPerson()
if (person != null) {
    sendEmail(person.email)
}

*// let 사용*
getBestPerson()?.let { sendEmail(it.email) }

7.9 직접 초기화하지 않는 널이 아닌 타입: 지연 초기화 프로퍼티

  • lateinit 키워드
  • 프레임워크 연동 시 필요성 (Android, JUnit)
  • var에만 사용 가능 (val 불가)
  • 초기화 전 접근 시 UninitializedPropertyAccessException
  • DI 프레임워크와의 호환성

문제 상황:

class MyTest {
    private var service: MyService? = null  *// null로 초기화해야 함*
    
    @BeforeAll 
    fun setup() {
        service = MyService()
    }
    
    @Test 
    fun test() {
        service!!.doSomething()  *// !! 사용해야 함*
    }
}

해결책: lateinit 사용

class MyTest {
    private lateinit var service: MyService  *// null 체크 불필요!*
    
    @BeforeAll 
    fun setup() {
        service = MyService()
    }
    
    @Test 
    fun test() {
        service.doSomething()  *// 바로 사용 가능!*
    }
}

주의사항:

  • var에만 사용 가능 (val 불가능)
  • 초기화 전 접근하면 UninitializedPropertyAccessException 발생

7.10 안전한 호출 연산자 없이 타입 확장: 널이 될 수 있는 타입에 대한 확장

  • 널 가능 타입에 대한 확장 함수 정의
  • String?.isNullOrBlank() 예시
    • isEmptyOrNull나 isBlankOrNull 메서드가 있다.
  • 안전한 호출 없이도 호출 가능
  • this가 null일 수 있음을 명시적 처리
  • let 함수와의 차이점
    • let 함수도 널이 될 수 있는 타입의 값에 대해 호출할 수 있지만 let은 this가 null인지 검사하지 않는다. → 확장함수는 String?에 함수를 붙이면 null도 호출 가능하지만 직접 null 체크를 해줘야 한다. let 은 ****null이든 아니든 람다를 실행하며, it으로 값(null 가능)을 전달한다. ?.let을 써야만 null을 건너뛴다.

직접 확장 함수 정의:

fun String?.isNullOrBlank(): Boolean {
    return this == null || this.isBlank()  *// this가 null일 수 있음!*
}

*// 사용*
fun verifyInput(input: String?) {
    if (input.isNullOrBlank()) {  *// 안전한 호출 불필요!*
        println("입력값을 확인해주세요")
    }
}

verifyInput(null)   *// "입력값을 확인해주세요"*
verifyInput("   ")  *// "입력값을 확인해주세요"*
verifyInput("OK")   *// 아무것도 출력 안 함*

7.11 타입 파라미터의 널 가능성

  • 타입 파라미터는 기본적으로 널 가능
  • <T>는 실제로는 <T: Any?>
  • 널 불가능하게 만들려면 상계 지정: <T: Any>
  • 물음표 규칙의 유일한 예외
// 타입 파라미터는 기본적으로 널 가능
fun <T> printHashCode(t: T) {
    println(t?.hashCode())  // 안전한 호출 필요
}

printHashCode(null)  // null (T는 Any?로 추론)

// 널 불가능하게 만들려면 상계 지정
fun <T : Any> printHashCode(t: T) {  // T는 널 불가능
    println(t.hashCode())  // 안전한 호출 불필요
}

  • 상계를 지정하다 = 타입 파라미터(T)*가 가질 수 있는 타입의 최대 범위(상한, upper bound)를 제한한다, Any는 코틀린에서 null이 될 수 없는 최상위 타입이므로, 이제 T는 절대 null이 아님

7.12 널 가능성과 자바

  • 자바 상호운용성 문제
  • 어노테이션 인식: @Nullable, @NotNull

플랫폼 타입 (String!)

*// 자바 코드*
public String getName() { return name; }  *// 널 가능성 불명// 코틀린에서 사용*
val person = JavaPerson()
val name = person.name  *// String! (플랫폼 타입)// 둘 다 가능*
val safeName: String? = person.name    *// 안전하게 처리*
val unsafeName: String = person.name   *// 위험하지만 허용*

어노테이션 인식

*// 자바 코드*
@Nullable String getName()     *// 코틀린: String?*
@NotNull String getEmail()     *// 코틀린: String*

7.12.1 플랫폼 타입

  • String! 표기법
  • 널 가능성을 알 수 없는 자바 타입
  • 프로그래머 책임으로 처리
  • 왜 모든 자바 타입을 널 가능으로 하지 않았나?
  • IDE에서 플랫폼 타입 표시

7.12.2 상속

  • 자바 인터페이스를 코틀린에서 구현할 때
  • 파라미터를 널 가능/불가능 중 선택 가능
  • 널 불가능 선택 시 컴파일러의 자동 널 체크
  • 자바 코드가 null 전달 시 예외 발생

8장 기본 타입,컬렉션,배열

8.1 원시 타입과 기본 타입

8.1.1 코틀린은 원시 타입과 래퍼 타입을 구분하지 않음 : 자바보다 간단해짐

자바의 문제점: 언제 뭘 써야 할지 헷갈림

*// Java*
int primitive = 1;        *// 원시 타입*
Integer wrapper = 1;      *// 래퍼 타입*
Collection<Integer> list; *// 컬렉션에는 래퍼 타입만 사용 가능*

코틀린의 해결책: 하나의 int로 모든 곳에서 사용

*// Kotlin - 하나의 타입으로 통일!*
val i: Int = 1
val list: List<Int> = listOf(1, 2, 3)

*// 메서드 호출도 가능*
fun showProgress(progress: Int) {
    val percent = progress.coerceIn(0, 100)  *// Int에서 직접 메서드 호출!*
    println("We're $percent% done!")
}

내부 동작:

  • 대부분의 경우: 코틀린 Int → 자바 int (원시 타입)
  • 제네릭 사용 시: 코틀린 Int → 자바 Integer (래퍼 타입)

8.1.2 부호 없는 숫자 타입

*// 부호 없는 타입들*
val ubyte: UByte = 255u      *// 0~255*
val ushort: UShort = 65535u  *// 0~65535*  
val uint: UInt = 4000000000u *// 0~약 40억*
val ulong: ULong = 18446744073709551615u *// 0~약 1844경// 주의: 일반적인 용도가 아닌 특별한 경우에만 사용// 비트/바이트 수준 작업, 픽셀 조작, 바이너리 데이터 등*

8.1.3 널이 될 수 있는 기본 타입

data class Person(val name: String, val age: Int? = null)

fun isOlderThan(person1: Person, person2: Person): Boolean? {
    *// 널이 될 수 있는 Int는 직접 비교 불가능*
    if (person1.age == null || person2.age == null) {
        return null  *// 나이를 모르면 비교 불가능*
    }
    return person1.age > person2.age  *// null 체크 후 비교 가능*
}

*// 내부적으로 Int?는 java.lang.Integer로 저장됨*

8.1.4 수 변환 - 자동 변환 없음!

자바와의 차이점:

*// 자바처럼 자동 변환 안 됨*
val i = 1
val l: Long = i  *// 컴파일 에러!명시적 변환 필요*
val i = 1
val l: Long = i.toLong()  *// OK 모든 변환 함수들*
val byte = i.toByte()
val short = i.toShort()
val long = i.toLong()
val float = i.toFloat()
val double = i.toDouble()
val char = i.toChar()

왜 자동 변환을 허용하지 않을까?

*// 박스 타입 비교의 함정을 방지*
val x = 1 // Int 타입
val list = listOf(1L, 2L, 3L)  *// Long 타입 요소 리스트
//false가 나올 것 (예상과 다름)

//자동 변환이 있다면: x in list 했겠지만, 명시적 변환으로 명확하게 해야함*
x.toLong() in list  *// true*

8.1.5 Any와 Any? - 코틀린 타입 계층의 뿌리

모든 타입의 조상

*// Any = 자바의 Object와 비슷*
val answer: Any = 42        *// 원시 타입도 Any에 대입 가능 (자동 박싱)*
val items: Any = listOf(1, 2, 3)

*// Any는 널이 될 수 없음// val nullableAny: Any = null  // 컴파일 에러!// 널을 포함하려면 Any? 사용*
val nullableAny: Any? = null  *// OK// Any에는 기본 메서드들이 있음*
println(answer.toString())   *// "42"*
println(answer.hashCode())   *// 42*
println(answer.equals(42))   *// true*

8.1.6 Unit 타입 - 코틀린의 void

자바 void 보다 똑똑

*// 기본 사용법*
fun doSomething(): Unit {
    println("작업 수행")
    *// return Unit (암시적)*
}

fun doSomething2() {  *// Unit 생략 가능*
    println("작업 수행")
}

*// Unit의 진짜 장점: 제네릭에서 타입 인자로 사용 가능*
interface Processor<T> {
    fun process(): T
}

class NoResultProcessor : Processor<Unit> {
    override fun process() {  *// Unit을 반환하지만 명시할 필요 없음*
        println("처리 완료")
        *// return Unit (컴파일러가 자동 추가)*
    }
}

8.1.7 Nothing 타입 - 절대 반환되지 않는 함수

*// 예외를 던지는 함수*
fun fail(message: String): Nothing {
    throw IllegalStateException(message)
}

*// 무한 루프*
fun infiniteLoop(): Nothing {
    while (true) {
        println("무한 실행")
    }
}

*// 실용적 사용: 엘비스 연산자와 함께*
val address = company.address ?: fail("주소가 없습니다")
println(address.city)  *// 컴파일러가 address가 null이 아님을 인식!*

8.2 컬렉션과 배열

8.2.1 널이 될 수 있는 값의 컬렉션 vs 널이 될 수 있는 컬렉션

*// 1. 널이 될 수 있는 값들의 리스트 (리스트 자체는 null 아님)*
val listOfNullableInts: List<Int?> = listOf(1, null, 3, null)

*// 2. 널이 될 수 있는 리스트 (리스트 자체가 null일 수 있음)*
val nullableListOfInts: List<Int>? = null

*// 3. 둘 다 가능한 경우*
val nullableListOfNullableInts: List<Int?>? = null

*// 실제 처리 예시*
fun readNumbers(text: String): List<Int?> {
    return text.lineSequence()
        .map { it.toIntOrNull() }  *// 파싱 실패하면 null*
        .toList()
}

fun processNumbers(numbers: List<Int?>) {
    *// null 값 처리*
    for (number in numbers) {
        if (number != null) {
            println("유효한 숫자: $number")
        } else {
            println("유효하지 않은 숫자")
        }
    }
    
    *// 또는 filterNotNull 사용*
    val validNumbers = numbers.filterNotNull()  *// List<Int> 타입으로 변환!*
    println("유효한 숫자들의 합: ${validNumbers.sum()}")
}

8.2.2 읽기 전용 vs 변경 가능한 컬렉션

*// 읽기 전용 인터페이스*
val readOnlyList: List<String> = listOf("a", "b", "c")
*// readOnlyList.add("d"): 컴파일 에러! add 메서드 없음// 변경 가능한 인터페이스*  
val mutableList: MutableList<String> = mutableListOf("a", "b", "c")
mutableList.add("d")  *// OK : 함수 파라미터로 구분*
fun <T> copyElements(
    source: Collection<T>,        *// 읽기만 함*
    target: MutableCollection<T>  *// 변경 가능*
) {
    for (item in source) {
        target.add(item)
    }
}

*// 주의: 읽기 전용이라고 해서 실제로 불변인 것은 아님!*
val readOnlyView: List<String> = mutableList  *// 같은 객체를 가리킴*
mutableList.add("e")  *// mutableList를 통해 변경*
println(readOnlyView.size)  *// 4 (변경 반영됨!)*

8.2.3 코틀린 컬렉션과 자바의 관계

*// 코틀린 컬렉션은 자바 컬렉션의 인터페이스를 그대로 사용, 읽기 전용/변경 가능으로 구분, 컬렉션 생성 함수들*
val readOnlyList = listOf(1, 2, 3)           *// List<Int>*
val mutableList = mutableListOf(1, 2, 3)     *// MutableList<Int>*
val arrayList = arrayListOf(1, 2, 3)         *// ArrayList<Int>   : 자바*

val readOnlySet = setOf("a", "b")             *// Set<String>*
val mutableSet = mutableSetOf("a", "b")       *// MutableSet<String>*
val hashSet = hashSetOf("a", "b")             *// HashSet<String>  : 자바*

val readOnlyMap = mapOf("key" to "value")     *// Map<String, String>*
val mutableMap = mutableMapOf("key" to "value") *// MutableMap<String, String>*

자바와의 상호운용 시 주의점: 자바 코드가 코틀린의 읽기 전용 컬렉션을 변경할 수 있음..

fun printUppercase(list: List<String>) {  *// 읽기 전용으로 선언-> 자바 메서드가 이를 변경할 수 있음*
    JavaUtils.uppercaseAll(list)  *// 실제로 list를 변경함*
    println(list.first())  *// 변경된 값이 출력됨*
}

8.2.4 자바 컬렉션은 플랫폼 타입

자바에서 온 컬렉션은 플랫폼 타입 (List<String>!) → 변경 가능성에 대해 알 수 없음

  • 컬렉션이 null일수 있는가?
  • 컬렉션 원소가 null일 수 있는가?
  • 메서드가 컬렉션을 변경할 수 있는가?
*/* Java */
// interface FileProcessor {
//     void process(List<String> lines);
// }

// 코틀린에서 구현할 때 선택사항들:*
class FileProcessor1 : FileProcessor {
    override fun process(lines: List<String>) {    *// 읽기 전용, null 불가능
    // lines는 변경하지 않음*
    }
}

class FileProcessor2 : FileProcessor {
    override fun process(lines: MutableList<String?>) {  *// 변경 가능, null 가능*
        lines.add(null)  *// 가능*
        lines.removeAt(0)  *// 가능*
    }
}

8.2.5 배열

*// 다양한 배열 생성 방법*
val arrayOf = arrayOf(1, 2, 3)                    *// [1, 2, 3]*
val arrayOfNulls = arrayOfNulls<String>(3)        *// [null, null, null]*
val arrayWithLambda = Array(5) { i -> i * i }     *// [0, 1, 4, 9, 16]

// 문자 배열 예시*
val letters = Array(26) { i -> ('a' + i).toString() }
println(letters.joinToString(""))  *// abcdefghijklmnopqrstuvwxyz

// 컬렉션 → 배열 변환*
val list = listOf("a", "b", "c")
val array = list.toTypedArray()
println("%s/%s/%s".format(*array))  *// 스프레드 연산자 * 사용

// 원시 타입 배열 (박싱하지 않음)*
val intArray = IntArray(5)                    *// [0, 0, 0, 0, 0]*
val intArrayOf = intArrayOf(1, 2, 3, 4, 5)   *// [1, 2, 3, 4, 5]*
val squares = IntArray(5) { i -> (i + 1) * (i + 1) }  *// [1, 4, 9, 16, 25]

// 다른 원시 타입 배열들*
val byteArray = ByteArray(10)
val charArray = CharArray(26) { ('a' + it) }
val booleanArray = BooleanArray(5) { it % 2 == 0 }

*// 배열도 컬렉션처럼 사용 가능*
val numbers = intArrayOf(1, 2, 3, 4, 5)
println(numbers.filter { it > 3 })    *// [4, 5] (리스트로 반환)*
println(numbers.map { it * 2 })       *// [2, 4, 6, 8, 10] (리스트로 반환)

// 배열 순회*
numbers.forEachIndexed { index, element ->
    println("[$index] = $element")
}

1. 타입 변환 실수 방지

*// 실수하기 쉬운 코드*
val list = listOf(1L, 2L, 3L)
val x = 1
*// println(x in list)  // false! (예상과 다름), 명확한 코드*
println(x.toLong() in list)  *// true*

2. 컬렉션 선택 기준

*// 읽기만 할 때*
fun displayItems(items: List<String>) { */* 읽기만 */* }

*// 변경해야 할 때*
fun addItems(items: MutableList<String>) { */* 변경 가능 */* }

*// 성능이 중요한 대용량 숫자 데이터*
val largeNumbers = IntArray(1_000_000) { it }  *// 원시 타입 배열 사용*

3. 널 처리 패턴

*// 널이 될 수 있는 값들을 안전하게 처리*
fun processNumbers(numbers: List<Int?>) {
    val validNumbers = numbers.filterNotNull()
    if (validNumbers.isNotEmpty()) {
        println("평균: ${validNumbers.average()}")
    }
}

'language > Kotilin In Action' 카테고리의 다른 글

6주차 (13장)  (2) 2025.09.01
5주차(11~12장)  (1) 2025.09.01
4주차(9~10장)  (1) 2025.09.01
2주차 (4~5장)  (1) 2025.09.01
1주차 (1~3장)  (0) 2025.09.01