[Kotlin] 문법 - 반복문 & 배열 & 해시 & 정렬

2025. 2. 13. 13:17Knowledge/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