2025. 2. 13. 13:17ㆍKnowledge/Kotlin
오늘은 Kotlin 문법에 대해서 다뤄보려고 합니다.
저번에 코딩테스트를 한 번 해봤는데, Kotlin 문법을 까먹었더니 확장함수를 쓸 수가 없어서 너무 불편하더라고요, 그래서 이번 포스트를 작성하게 됐습니다.
먼저, 반복문에 대해서 알아보겠습니다.
반복문
1. 기본 for와 범위 연산자(..)
// N값이 5인 경우: 1, 2, 3, 4, 5
for (stage in 1..N) { }
- 끝 값을 포함합니다. (N값이 5인 경우: 1, 2, 3, 4, 5)
2. until
// N값이 5인 경우: 1, 2, 3, 4
for (stage in 1 until N) { }
- 끝 값을 포함하지 않습니다. (N값이 5인 경우: 1, 2, 3, 4)
- 배열의 사이즈만큼 반복할 때 유용해 보입니다.
3. forEach
(1..N).forEach { stage -> }
- continue와 break 사용이 불가능합니다.
- 코드가 간결해집니다.
- 함수형 프로그래밍에 적합합니다.
4. repeat
repeat(N) { index -> }
- continue와 break 사용이 불가능합니다.
- 0부터 시작하는 인덱스를 제공합니다.
- 배열의 크기 만큼만 반복합니다.
5. while
var stage = 1
while (stage <= N) {
stage++
}
- 조건에 따른 유연한 반복이 가능합니다.
- 무한 루프의 위험이 있습니다.
6. indices
for (i in stages.indices) {
val stage = stages[i]
}
- 0부터 시작하는 인덱스를 제공합니다.
- 배열의 크기 만큼만 반복합니다.
- repeat과 동일한 기능입니다.
7. withIndex
for ((index, stage) in stages.withIndex()) { }
- 인덱스와 값을 동시에 받을 수 있습니다.
8. forEachIndexed
stages.forEachIndexed { index, stage -> }
- 인덱스와 값을 동시에 받을 수 있습니다.
- forEach와 마찬가지로 함수형 프로그래밍에 적합합니다.
9. map과 range
(1..N).map { stage -> }
val stageArray: Array<Int> = arrayOf(6, 7, 4, 2)
val stageCopy = (1..stageArray.size).map { index ->
stageArray[index - 1] * 2
}
for((index, stage) in stageArray.withIndex()) {
println("[원본] index: $index, stage: $stage")
}
println("[복제] stageCopy: $stageCopy")
// [원본] index: 0, stage: 6
// [원본] index: 1, stage: 7
// [원본] index: 2, stage: 4
// [원본] index: 3, stage: 2
// [복제] stageCopy: [12, 14, 8, 4]
- 새로운 컬렉션을 생성합니다.
- 원본 데이터는 변경되지 않습니다.
10. do-while
var stage = 1
do {
println("Stage: $stage")
stage++
} while (stage <= N)
- while 조건과 관계 없이 최소 1번은 실행합니다.
- 반복 조건을 마지막에 체크합니다.
11. downTo
// 기본 사용법
for (i in N downTo 1) {
println(i) // N부터 1까지 역순으로 출력
}
// step 사용
for (i in N downTo 1 step 2) {
println(i) // 2칸씩 건너뛰며 역순으로
}
- 역순 반복을 원할 때 유용합니다.
- step으로 감소 간격 지정이 가능합니다.
- 끝 값을 포함합니다.
12. sequence
sequence {
for (stage in 1..N) {
yield(stage)
}
}
- 지연 평가로 메모리 효율적
- 체이닝 연산에 효과적
'sequence'는 추가적인 설명을 더 드리겠습니다.
* 일반적인 처리 방식 (map, filter 사용)
val numbers = listOf(1, 2, 3, 4, 5)
val result = numbers
.map { it * 2 } // [2, 4, 6, 8, 10] 새로운 리스트 생성
.filter { it > 5 } // [6, 8, 10] 또 새로운 리스트 생성
println(result) // [6, 8, 10]
* sequence 처리 방식
val numbers = listOf(1, 2, 3, 4, 5)
val result = numbers.asSequence()
.map { it * 2 } // 아직 계산하지 않음
.filter { it > 5 } // 아직 계산하지 않음
.toList() // 이때 실제로 계산
println(result) // [6, 8, 10]
* 예시
// 일반 처리: 과정 출력해보기
val numbers = listOf(1, 2, 3)
println("일반 처리 시작")
val result1 = numbers
.map {
println("map: $it")
it * 2
}
.filter {
println("filter: $it")
it > 3
}
println("결과: $result1")
// sequence 처리: 과정 출력해보기
println("\nsequence 처리 시작")
val result2 = numbers.asSequence()
.map {
println("map: $it")
it * 2
}
.filter {
println("filter: $it")
it > 3
}
.toList()
println("결과: $result2")
일반 처리 시작
map: 1
map: 2
map: 3
filter: 2
filter: 4
filter: 6
결과: [4, 6]
sequence 처리 시작
map: 1
filter: 2
map: 2
filter: 4
map: 3
filter: 6
결과: [4, 6]
자주 사용할 것 같은 반복문: 범위 연산자(..), until, indices, withIndex
이번엔 배열에 대해서 다뤄보겠습니다.
배열
1. Array와 arrayOf()
// Array: 크기가 고정된 배열
val arr1 = Array(5) { 0 } // [0, 0, 0, 0, 0]
val arr2 = Array(3) { it } // [0, 1, 2]
val arr3 = Array(5) { it * 2 } // [0, 2, 4, 6, 8]
// arrayOf(): 요소를 직접 지정해서 배열 생성
val arr4 = arrayOf(1, 2, 3) // [1, 2, 3]
- 크기가 고정됩니다.
- 인덱스로 접근합니다.
2. ArrayList와 arrayListOf()
// ArrayList: 동적 크기 리스트
val list1 = ArrayList<Int>() // 빈 리스트
list1.add(1) // 요소 추가 가능
// arrayListOf(): 초기 요소와 함께 ArrayList 생성
val list2 = arrayListOf(1, 2, 3) // [1, 2, 3]
- 크기가 동적으로 변합니다.
- 요소의 추가/삭제가 가능합니다.
3. mutableListOf()
val list = mutableListOf(1, 2, 3)
list.add(4) // 추가 가능
list[0] = 10 // 수정 가능
list.remove(2) // 삭제 가능
- 수정 가능한 리스트입니다.
- ArrayList와 비슷하지만 더 추상화된 인터페이스입니다.
좀 더 추가적인 설명을 드리겠습니다.
// ArrayList는 구체적인 구현체를 직접 사용
val arrayList = ArrayList<Int>()
arrayList.add(1)
arrayList.ensureCapacity(100) // ArrayList의 고유 메서드 사용 가능
// mutableListOf는 MutableList 인터페이스를 통해 사용
val mutableList = mutableListOf<Int>()
mutableList.add(1)
// mutableList.ensureCapacity(100) // 컴파일 에러, MutableList 인터페이스에 없는 메서드
- ArrayList는 실제 구현체를 직접 사용하므로 ArrayList의 모든 구체적인 메서드를 사용할 수 있습니다.
- mutableListOf는 MutableList 인터페이스를 통해 접근하므로 인터페이스에 정의된 메서드만 사용이 가능합니다.
4. IntArray (기본타입 배열)
// 크기와 초기값으로 생성
val arr1 = IntArray(5) // [0, 0, 0, 0, 0]
val arr2 = IntArray(5) { it } // [0, 1, 2, 3, 4]
// 직접 값 지정
val arr3 = intArrayOf(1, 2, 3) // [1, 2, 3]
- 기본 타입(int) 배열 (참고로 Double 등등 가능합니다. *DoubleArray)
- Array<Int>보다 성능이 좋음
성능 차이가 나는 이유에 대해서 좀 더 설명을 드리겠습니다.
* 메모리 사용
- IntArray: 순수하게 int 값만 저장 (4바이트 × 배열 크기)
- Array<Int>: 각 요소가 Integer 객체를 참조 (참조 8바이트 + 객체 오버헤드 12바이트 + int 값 4바이트) × 배열 크기
*접근 속도
- IntArray: 메모리에 직접 접근
- Array<Int>: 참조를 통해 접근
따라서 기본 타입(Int, Long, Double 등)을 다룰 때는 가능한 IntArray, LongArray, DoubleArray 등을 사용하는 것이 성능상 유리합니다!
* Array & arrayOf: 고정 배열에 적합
* ArrayList & arrayListOf: 추가, 삭제가 필요한 배열에 적합
* mutableListOf: 추가, 삭제, 수정이 필요한 배열에 적합
* IntArray: 기본 배열에 적합
이번엔 해시에 대해서 알아보겠습니다.
해시
1. HashMap
// HashMap 생성
val scores = hashMapOf<String, Int>() // 빈 HashMap
val fruits = hashMapOf(
"사과" to 1000,
"바나나" to 2000,
"딸기" to 3000
)
// 요소 추가/수정
scores["Kim"] = 95
scores["Lee"] = 88
// 값 가져오기
println(fruits["사과"]) // 1000
println(fruits.get("바나나")) // 2000
println(fruits.getOrDefault("망고", 0)) // 0
// 키/값 존재 여부 확인
println(fruits.containsKey("사과")) // true
println(fruits.containsValue(1000)) // true
// 요소 제거
fruits.remove("바나나")
// 순회하기
for ((key, value) in fruits) {
println("$key: $value")
}
- 키&값 쌍으로 데이터를 저장합니다.
* 각 요소에 대한 추가 정보를 저장해야하는 경우
* 키를 통한 빠른 검색이 필요한 경우
2. HashSet
// HashSet 생성
val numberSet = hashSetOf<Int>() // 빈 HashSet
val fruitsSet = hashSetOf("사과", "바나나", "딸기") // 초기값이 있는 HashSet
// 요소 추가
numberSet.add(1)
numberSet.add(2)
// 요소 존재 여부 확인
println(fruitsSet.contains("사과")) // true
// 요소 제거
fruitsSet.remove("바나나")
// 크기 확인
println(fruitsSet.size) // 2
- 중복 제거가 필요한 경우 매우 효율적입니다.
- 단일 값만 저장합니다.
* 단순히 고유한 값들의 집합이 필요한 경우
* 집합 연산(교집합, 합집합 등)이 필요한 경우
이번엔 정렬에 대해서 알아보겠습니다.
정렬
1. sorted / sortedBy & sort / sortBy
// sorted(): 오름차순 정렬 (새로운 리스트 반환)
val numbers = listOf(3, 1, 4, 1, 5, 9)
val sortedNumbers = numbers.sorted() // [1, 1, 3, 4, 5, 9]
// sortedDescending(): 내림차순 정렬
val descendingNumbers = numbers.sortedDescending() // [9, 5, 4, 3, 1, 1]
// sort(): 변경 가능한 리스트 직접 정렬
val mutableNumbers = mutableListOf(3, 1, 4, 1, 5, 9)
mutableNumbers.sort() // 리스트 자체가 정렬됨
// sortDescending(): 변경 가능한 리스트 내림차순 정렬
mutableNumbers.sortDescending()
data class Person(val name: String, val age: Int)
val people = listOf(
Person("Kim", 25),
Person("Lee", 20),
Person("Park", 30)
)
// sortedBy: 특정 프로퍼티로 정렬
val sortedByAge = people.sortedBy { it.age }
val sortedByName = people.sortedBy { it.name }
// sortedByDescending: 특정 프로퍼티로 내림차순 정렬
val sortedByAgeDesc = people.sortedByDescending { it.age }
// sortedWith: 여러 조건으로 정렬
val sortedWithComparator = people.sortedWith(
compareBy<Person> { it.age }.thenBy { it.name }
)
// compareBy로 복잡한 정렬 조건 만들기
val complexSort = people.sortedWith(
compareBy(
{ it.name.length }, // 먼저 이름 길이로 정렬
{ it.age }, // 그 다음 나이로 정렬
{ it.name } // 마지막으로 이름으로 정렬
)
)
// 널 값 처리
data class User(val name: String, val score: Int?)
val users = listOf(
User("Kim", 80),
User("Lee", null),
User("Park", 90)
)
// nullsLast: null 값을 마지막으로
val sortedWithNulls = users.sortedWith(
compareBy(nullsLast()) { it.score }
)
- sorted / sortedBy: 새로운 리스트를 생성하므로 메모리를 더 사용하지만, 원본 리스트를 보존합니다.
- sort / sortBy: 원본 리스트를 직접 수정하므로 메모리 효율적이지만, 원본이 변경됩니다.
2. compareBy
// 1. 단일 조건 compareBy
val people = listOf(
Person("Kim", 25),
Person("Lee", 20),
Person("Park", 30)
)
// 나이로 정렬
val sortedByAge = people.sortedWith(compareBy { it.age })
// 결과: [Person("Lee", 20), Person("Kim", 25), Person("Park", 30)]
// 이름으로 정렬
val sortedByName = people.sortedWith(compareBy { it.name })
// 결과: [Person("Kim", 25), Person("Lee", 20), Person("Park", 30)]
- compareBy: 정렬 기준을 만드는 함수입니다.
3. thenBy
data class Student(
val grade: Int, // 학년
val name: String, // 이름
val score: Int // 점수
)
val students = listOf(
Student(2, "Kim", 85),
Student(1, "Lee", 90),
Student(2, "Park", 85),
Student(1, "Choi", 95)
)
// 1. 학년 순으로 정렬하고,
// 2. 학년이 같다면 점수로 정렬하고,
// 3. 점수도 같다면 이름순으로 정렬
val sorted = students.sortedWith(
compareBy<Student> { it.grade } // 첫 번째 조건
.thenByDescending { it.score } // 두 번째 조건 (내림차순)
.thenBy { it.name } // 세 번째 조건
)
// 결과 출력
sorted.forEach { student ->
println("${student.grade}학년 ${student.name}: ${student.score}점")
}
- thenBy: 첫 번째 조건이 같을 때, 그 다음 조건으로 정렬하고 싶을 때 사용합니다.
4. sortWith
// 1. 기본적인 sortWith 사용
val numbers = mutableListOf(3, 1, 4, 1, 5, 9)
numbers.sortWith(compareBy { it }) // [1, 1, 3, 4, 5, 9]
// 2. 객체 정렬
data class Person(val name: String, val age: Int)
val people = mutableListOf(
Person("Kim", 25),
Person("Lee", 20),
Person("Park", 30)
)
// 나이순으로 정렬
people.sortWith(compareBy { it.age })
// 여러 조건으로 정렬 (나이 오름차순, 이름 내림차순)
people.sortWith(
compareBy<Person> { it.age }
.thenByDescending { it.name }
)
// null을 처음에 위치시키기
val nullsFirst = people.sortedWith(
compareBy(
nullsFirst() { it.age }, // null이 앞으로
{ it.name }
)
)
// null을 마지막에 위치시키기
val nullsLast = people.sortedWith(
compareBy(
nullsLast() { it.age }, // null이 뒤로
{ it.name }
)
)
- sortWith: 커스텀 비교자(Comparator)를 사용하여 MutableList를 정렬하는 함수입니다. 원본 리스트를 직접 정렬합니다.
마지막으로 확장 함수에 대해서 다루고 마치겠습니다.
확장 함수
1. 배열 관련 확장 함수
// toTypedArray() : 컬렉션을 배열로 변환
val list = listOf(1, 2, 3)
val array = list.toTypedArray() // Array<Int>
// toIntArray(), toDoubleArray() 등: 기본 타입 배열로 변환
val primitiveArray = list.toIntArray() // IntArray
// contentToString() : 배열의 내용을 문자열로 변환
println(array.contentToString()) // [1, 2, 3]
2. 컬렉션 관련 확장 함수
// getOrDefault() : 키가 없을 때 기본값 반환
val map = mapOf("a" to 1, "b" to 2)
val value = map.getOrDefault("c", 0) // 0
// getOrElse() : 키가 없을 때 람다 실행
val value2 = map.getOrElse("c") {
println("키를 찾을 수 없습니다")
0
}
// orEmpty() : null일 경우 빈 컬렉션 반환
val nullableList: List<Int>? = null
val safeList = nullableList.orEmpty() // 빈 리스트 반환
3. 변환 관련 확장 함수
// map : 각 요소를 변환
val numbers = listOf(1, 2, 3)
val doubled = numbers.map { it * 2 } // [2, 4, 6]
// mapNotNull : null을 제외하고 변환
val mixed = listOf(1, null, 2, null, 3)
val nonNull = mixed.mapNotNull { it } // [1, 2, 3]
// flatten : 중첩 컬렉션을 단일 레벨로
val nested = listOf(listOf(1, 2), listOf(3, 4))
val flat = nested.flatten() // [1, 2, 3, 4]
4. 필터링 관련 확장 함수
// filter : 조건에 맞는 요소만 선택
val numbers = listOf(1, 2, 3, 4, 5)
val evens = numbers.filter { it % 2 == 0 } // [2, 4]
// filterNot : 조건에 맞지 않는 요소 선택
val odds = numbers.filterNot { it % 2 == 0 } // [1, 3, 5]
// filterNotNull : null이 아닌 요소만 선택
val withNull = listOf(1, null, 2, null, 3)
val noNulls = withNull.filterNotNull() // [1, 2, 3]
5. 검색 관련 확장 함수
// find : 조건에 맞는 첫 요소 찾기
val numbers = listOf(1, 2, 3, 4, 5)
val firstEven = numbers.find { it % 2 == 0 } // 2
// firstOrNull : 첫 요소 또는 null
val firstBig = numbers.firstOrNull { it > 10 } // null
// count : 조건에 맞는 요소 개수
val evenCount = numbers.count { it % 2 == 0 } // 2
6. 문자열 관련 확장 함수
// capitalize : 첫 글자를 대문자로
val str = "hello"
println(str.capitalize()) // "Hello"
// trimMargin : 여러 줄 문자열의 마진 제거
val text = """
|First line
|Second line
""".trimMargin()
// split : 문자열 분할
val parts = "a,b,c".split(",") // [a, b, c]
// removeSurrounding : 앞뒤 문자열 제거
println("<<Hello>>".removeSurrounding("<<", ">>")) // Hello
// replaceFirst : 첫 번째 일치 항목만 교체
println("hello hello".replaceFirst("hello", "hi")) // hi hello
// lines : 줄 단위로 분리
val multiline = """
First
Second
Third
""".trimIndent()
println(multiline.lines()) // [First, Second, Third]
// padStart, padEnd : 문자열 패딩
println("123".padStart(5, '0')) // 00123
println("123".padEnd(5, '0')) // 12300
7. 컬렉션 처리 관련 확장 함수
// distinct : 중복 제거
val numbers = listOf(1, 1, 2, 2, 3)
println(numbers.distinct()) // [1, 2, 3]
// chunked : n개씩 묶기
val list = (1..7).toList()
println(list.chunked(3)) // [[1, 2, 3], [4, 5, 6], [7]]
// windowed : 슬라이딩 윈도우
println(list.windowed(3)) // [[1, 2, 3], [2, 3, 4], [3, 4, 5], [4, 5, 6], [5, 6, 7]]
// groupBy : 그룹화
val people = listOf(
Person("Kim", 20),
Person("Lee", 25),
Person("Park", 20)
)
val byAge = people.groupBy { it.age }
// associate : Map 생성
val mapped = people.associate { it.name to it.age }
// takeIf : 조건을 만족할 때만 값 반환
val number = 10
val evenNumber = number.takeIf { it % 2 == 0 } // 10
val oddNumber = number.takeIf { it % 2 == 1 } // null
8. 널 안전성 관련 확장 함수
// let : null이 아닐 때 코드 블록 실행
val nullableString: String? = "Hello"
nullableString?.let {
println(it.length)
}
// also : 객체를 반환하면서 부가 작업
val numbers = mutableListOf<Int>()
.also { println("생성된 리스트: $it") }
.also { it.add(1) }
.also { println("숫자 추가 후: $it") }
// run : 객체의 함수나 프로퍼티 접근
val str = "Hello"
val length = str.run {
uppercase().length
}
// apply : 객체 설정 후 반환
val person = Person().apply {
name = "Kim"
age = 25
}
9. 수치 관련 확장 함수
// coerceIn : 범위 내로 제한
val number = 5
println(number.coerceIn(0..10)) // 5
println(number.coerceIn(6..10)) // 6
println(number.coerceIn(0..4)) // 4
// rangeTo : 범위 생성
val range = 1.rangeTo(5) // 1..5
// step : 특정 간격으로 반복
(0..10 step 2).forEach { print(it) } // 0 2 4 6 8 10
10. 시퀀스 관련 확장 함수
// asSequence : 지연 평가 시퀀스로 변환
val result = listOf(1, 2, 3)
.asSequence()
.map { it * 2 }
.filter { it > 3 }
.toList()
// generateSequence : 시퀀스 생성
val fibonacci = generateSequence(Pair(0, 1)) {
Pair(it.second, it.first + it.second)
}.map { it.first }
println(fibonacci.take(6).toList()) // [0, 1, 1, 2, 3, 5]
이번 포스트는 여기서 마치겠습니다.
다음 포스트는, 이번 지식을 활용해서 저번에 풀었던 코딩테스트 문제를 다시 도전하는 글을 올리겠습니다~!
'Knowledge > Kotlin' 카테고리의 다른 글
[Kotlin] Flutter로 비교하는 Kotlin & Compose 지식 (0) | 2025.02.11 |
---|