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() *// 최종 연산 - 이때 실제 계산 시작!*
}
지연 계산
- 필요한 것만 계산
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는 계산 안 함!*
}
- 연산 순서 최적화
일반적으로 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()
시퀀스 생성 방법들
- 기존 컬렉션에서
val list = listOf(1, 2, 3, 4, 5)
val sequence = list.asSequence()
- 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]*
- 실무 예시: 파일 경로 탐색
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 |