Kotlin을 배워보자 #4 collection, 예외처리, 확장함수

이영준·2023년 3월 16일
0

코틀린 문법

목록 보기
4/4

📌Collection

자주 사용하는 기본적인 자료구조를 모아놓은 프레임워크

🔑List

데이터 삭제, 저장마다 순서를 지키는 collection

  • 중복 허용
  • 기본적으로 list는 immutable
    • get만 가능, 인덱스 접근 위해 .get(index), [index] 모두 지원
    val fruits= listOf<String?>("apple", "banana", "kiwi", "peach", null)
// val fruits= listOf("apple", "banana", "kiwi", "peach") -> 타입 생략 가능
    println("fruits: $fruits")
    println("fruits.size: ${fruits.size}")
    println("fruits.get(2): ${fruits.get(2)}")
    println("fruits[3]: ${fruits[3]}")
    println("fruits.indexOf(\"peach\"): ${fruits.indexOf("peach")}")

immutable list

listOf와 달리 추가 및 삭제 가능을 위해 mutableList 사용


val fruits= mutableListOf<String>("apple", "banana", "kiwi", "peach")
    fruits.remove("apple")
    fruits.add("grape")
    println("fruits: $fruits")

    fruits.addAll(listOf("melon", "cherry"))
    println("fruits: $fruits")
    fruits.removeAt(3)
    println("fruits: $fruits")

🔑Set

순서 x, 중복 x
null 객체를 가질 수 있음
list와 같이 기본은 immutable, mutableSet 존재

@RequiresApi(Build.VERSION_CODES.N)
fun main() {
    val numbers = mutableSetOf<Int>(33, 22, 11, 1, 22, 3)
    println(numbers)
    numbers.add(100)
    numbers.remove(33)
    println(numbers)
    numbers.removeIf({ it < 10 }) // 10 이하의 숫자를 삭제
    println(numbers)
}

[33, 22, 11, 1, 3]
.
[22, 11, 1, 3, 100]
.
[22, 11, 100]

removeIf

는 filter처럼 조건에 따라 요소들을 제거한다.
removeIf는 api버전 N부터 추가되었으므로 @RequiresApi 처리를 해준다.

🔑Collection 의 자식으로서의 List, Set

List와 Set은 Collection을 상속받기 때문에 collection을 인자로 받는 함수 등에 사용할 수 있다.

fun main() {

    fun printAll(strings: Collection<String>) {
        for (s in strings) print("$s ")
        println()
    }

    val stringList = listOf("one", "two", "one")
    printAll(stringList)

    val stringSet = setOf("one", "two", "three")
    printAll(stringSet)
}

🔑Map

Map 선언

val numbersMap = mapOf<String, String>("1" to "one", "2" to "two", "3" to "three")
    println("numbersMap: $numbersMap")
    val numbersMap2 = mapOf(Pair("1", "one"), Pair("2", "two"), Pair("3", "three"))
    println("numbersMap2: $numbersMap2")

key, val 가져오기

println("numbersMap keys:${numbersMap3.keys}")
println("numbersMap values:${numbersMap3.values}")

keys는 set으로, values는 collection으로 반환

    val numbersMap = mutableMapOf<String, String>( "1" to "one", "2" to "two", "3" to "three" )
    println("numbersMap: $numbersMap")

    numbersMap.put("4", "four")
    numbersMap["5"] = "five"
    println("numbersMap: $numbersMap")

    numbersMap.remove("1")
    println("numbersMap: $numbersMap")

    numbersMap.clear()
    println("numbersMap: $numbersMap")

🔑Collection의 API들

filter, filterKeys, filterValues

조건에 해당하는 데이터만으로 콜렉션 재구성,
콜랙션에 변화를 주는 것이 아니라 리스트로 리턴하기 때문에 사용하기 위해서는 필터된 리스트를 따로 저장해야 한다.

fun filterTest() {
    /* 리스트에서 짝수만 뽑아내는 예제 */
    val list = listOf(1, 2, 3, 4)
    println(list.filter { it % 2 == 0 }) //짝수만 필터링


    /* 나이가 30살 이상인 사람만 뽑아내는 예제 */
    val people = listOf(Person("안드로이드", 29), Person("코틀린", 30))
    val filtered = people.filter { it.age >= 30 }
    filtered.forEach {
        println(it) // Person(name=코틀린, age=30)
    }
}

[2, 4]
Person(name=코틀린, age=30)

Map의 key, values를 필터를 주기 위해 filterkeys, filtervalues도 쓸 수 있다.

map, mapKeys, mapValues

각각의 원소를 변환하는 기능을 하여 원본 리스트와 원소개수가 같은 새 컬렉션을 반환한다.

    // 각 원소의 제곱으로 모인 리스트를 만드는 map 예제
    val list = listOf(1, 2, 3, 4)
    println(list.map { it * it }) //제곱 만들기 (1x1, 2x2, 3x3, 4x4)

    // 사람 리스트 -> 이름 리스트 변환
    val people = listOf(Person("안드로이드", 29), Person("코틀린", 30))
    val mapped = people.map { it -> it.name }
    mapped.forEach {
        println(it)
    }
    // 안드로이드
    // 코틀린

[1, 4, 9, 16]
안드로이드
코틀린

mapKeys, mapValues는 Map 객체의 key, value에 map을 쓰기 위해 사용한다.

    val numbers = mapOf(1 to "zero", 2 to "one")
    // key는 제곱수로, value는 대문자로 변환
    val newMap = numbers.mapValues { it.value.uppercase() }.mapKeys { it.key * it.key }
    newMap.forEach {
        println("${it.key} - ${it.value}")

1 - ZERO
4 - ONE

all

조건을 모두 만족하는지 Boolean을 반환한다.

val under30 = { p: Person -> p.age < 30 }

    //모든 원소가 만족하는지 판단하려면 all 함수를 사용
    val people3 = listOf(Person("안드로이드", 25), Person("코틀린", 33))
    println(people3.all(under30))  //false

false

any

조건을 하나라도 만족하는지 Boolean을 반환한다.

val under30 = { p: Person -> p.age < 30 }
 //하나라도 만족하는 원소가 있는지 판단하려면 any 함수를 사용
    println(people3.any(under30)) //true

true

count

조건에 만족하는 개수를 반환한다.

    val under30 = { p: Person -> p.age < 30 }

    val people5 = listOf(Person("안드로이드", 25), Person("코틀린", 33))
    println(people5.count(under30)) // 1

1

find

조건에 만족하면 그 만족한 첫번째 값을 반환하고 찾는 것을 멈춘다.
찾지 못하면 null을 반환한다.

    val under40 = { p: Person -> p.age < 40 }
    val people7 = listOf(Person("안드로이드", 25), Person("코틀린", 33))
    println(people7.find(under40)) //Person(name=안드로이드, age=25)

    val under10 = { p: Person -> p.age < 10 }
    println(people7.find(under10)) // null

Person(name=안드로이드, age=25)
null

groupBy

데이터를 특정 조건으로 묶어 그 조건이 키가 되고 묶은 값들이 value인 Map 을 반환한다.

    val people8 = listOf(
        Person("안드로이드", 25),
        Person("코틀린", 30),
        Person("자바", 30)
    )
    val map = people8.groupBy { it.age }
    map.forEach {
        println("나이: ${it.key}, 구성원: ${it.value}")
    }

나이: 25, 구성원: [Person(name=안드로이드, age=25)]
나이: 30, 구성원: [Person(name=코틀린, age=30), Person(name=자바, age=30)]

flatMap

리스트의 요소들을 붙여주어 처리할 때 쓰는 함수
List<T>List<T>들의 리스트의 요소들을 합쳐 새로운 리스트로 만든다.

    val strings = listOf("abc", "def")
    val newMap = strings.flatMap{
        it.toList()
    }
    println(newMap) //[a, b, c, d, e, f]

    val nestedList = listOf(listOf("abc", "def"), listOf("hij", "klm"))
    val newMap2 = nestedList.flatMap{ it.toList() }
    println(newMap2) //[abc, def, hij, klm]


    println(nestedList.flatMap{ it })
    val newMap3 = nestedList.flatMap{ it }.flatMap { it.toList() }
    println(newMap3) //[a, b, c, d, e, f, h, i, j, k, l, m]

[a, b, c, d, e, f][abc, def, hij, klm]
[abc, def, hij, klm][a, b, c, d, e, f, h, i, j, k, l, m]

flatten

리스트 안에 여러개의 리스트가 포함된 경우 하나의 리스트로 반환한다.

val strings = listOf(listOf("abc", "def"), listOf("hij", "klm"))
    println(strings.flatten()) //[abc, def, hij, klm]

[abc, def, hij, klm]

📌예외처리

코틀린은 자바와 달리 Checked Exception 개념이 없기 때문에
try~catch는 선택이고 로직에서 Exception을 처리하기를 권장함

fun readNumber(reader: BufferedReader): Int? {

    try {
        print("1 -->")
        val line = reader.readLine()
        return Integer.parseInt(line)
    } catch (e: NumberFormatException) {
        print("2 -->")
        e.printStackTrace()
    } finally {
        print("3 --> ")
        reader.close()
    }
    return null
}

fun main() {
    var reader = BufferedReader(StringReader("239"))
    print(readNumber(reader)) // 1 -->3 --> 239
    println()
    var reader2 = BufferedReader(StringReader("abc"))
    print(readNumber(reader2)) // 1 -->2 -->3 --> null
    println()

}

🔑use

Closeable 인터페이스가 구현된 클래스에 한해 use 사용 가능
내부 구현을 보면 Exception 여부에 상관없이 close() 호출을 보장

📌확장함수

어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만, 그 클래스의 밖에 선언된 함수

예를 들어 String이나 Int에 새로운 메소드를 추가하고 싶으면

fun String.메소드명() : 리턴타입 {메소드 구현} 형식으로 구현하면 된다.


람다식으로도 선언할 수 있는데 이를 lambda with receiver라고 부른다.
Int. 과 같이 추가할 클래스에 .을 붙이고 람다식을 구현한다.

fun stringExtensionTest(){
    // 문자열 확장 함수
    fun String.lastChar(): Char {
        return this.get(this.length - 1)
    }

    println("Hello World".lastChar()) // d

    // 확장함수 추가 예제. lambda로 선언. lambda with receiver라고 부름.
    val sum2 : Int.() -> Int = {
        println("sum2 : this :${this.javaClass.name}")
        this.plus(5)
    }

    println(100.sum2())

    //----------------------------------
    val stringToInt: String.() -> Int = {
        println("String 확장함수 : $this")
        toInt()
    }

    println("123".stringToInt().javaClass.name)
}

sum2 : this :int
105
String 확장함수 : 123

확장함수는 각 클래스에 정적 메서드로 생성되기 때문에 virtual method invocation이 발생하지 않는다.

fun staticExtension(){
    open class MyView {
        open fun click() = println("View clicked")
    }
    class MyButton: MyView() {
        override fun click() = println("Button clicked")
    }

    // 기능 확장
    fun MyView.showOff() = println("I'm a view!")
    fun MyButton.showOff() = println("I'm a button!")
    // 부모 타입으로 자식 객체 생성
    val view: MyView = MyButton()
    view.click() //Button clicked
    view.showOff() // I'm a view!
}

위 예시에서 click()이라는 멤버함수는 자식 클래스가 오버라이딩하게 되면 자식클래스 구현체의 click는 오버라이딩된 메소드가 불리지만
확장함수는 static 영역의 정적 메소드로 생성되므로 MyView의 showOff를 가져온다.

🔑제네릭 확장함수

제네릭 클래스도 확장함수를 추가할 수 있다.

fun List<Int>.getHigherThan(num: Int): List<Int> {
    val result = arrayListOf<Int>()
    for (item in this) {
        if (item > num) {
            result.add(item)
        }
    }

    return result
}

fun main() {
    val numbers: List<Int> = listOf(1, 2, 3, 4, 5, 6)
    val filtered = numbers.getHigherThan(3).toString()
    println(filtered)
}

[4, 5, 6]

🔑상속 활용한 확장함수

class NewList : ArrayList<Int>() {
    fun getHigherThan(num: Int): List<Int> {
        val result = arrayListOf<Int>()
        this.forEach { it->
            if (it > num) {
                result.add(it)
            }
        }
        return result
    }
}

fun main() {
    val numbers1 = NewList()
    numbers1.addAll(listOf(1,2,3,4,5,6))
    val filtered = numbers1.getHigherThan(3).toString()
    println(filtered)
}

📌문자열 관련 기능

val heroes = """
    |D.Va
    |Lucio
    |Mercy
    |Soldier: 76
    """.trimMargin()
    

""" 으로 감싸면 모든 문자를 자유롭게 사용 가능
각줄의 시작을 구문할 문자 앞을 공백으로 채우고 trimMargin()을 사용하면

D.Va
Lucio
Mercy
Soldier: 76

    val heroes = """
    D.va
   Lucio
    Mercy
    Soldier: 76
    """.trimIndent()

공백을 단순히 제거하는 trimIndent()를 사용할수도 있다.

joinToString

val items = listOf("D.Va", "Lucio", "Mercy", "Soldier: 76")
    println(items.joinToString()) // "D.Va, Lucio, Mercy, Soldier: 76" 출력
    println(items.joinToString("-")) // "D.Va-Lucio-Mercy-Soldier: 76" 출력

String을 합쳐준다. 인자를 넣으면 사이에 그 인자를 넣어준다.

val items2 = listOf(HeroName("Hana", "Song"), HeroName("Jack", "Morrison"))
    println(items2.joinToString { "${it.firstName} ${it.lastName}" }) 
    // "Hana Song, Jack Morrison" 출력

람다식으로 문자열로 변환하는 규칙을 추가로 지정하여 사용할 수도 있다. (joinToString의 마지막 인자가 transform 인데 여기에 문자열을 다르게 변환할 형식을 넣을 수 있다.)

📌연산자 오버로딩

자바는 연산자 오버로딩이 없으나 코틀린은 이를 지원한다.

대표적으로
plus, minues, times, div(/), rem(%)

data class Point(val x: Int, val y: Int) {
    operator fun plus(other: Point): Point {  //"plus" 라는 연산자 함수를 정의합니다.
        return Point(x + other.x, y + other.y)
    }
}


fun main() {
    val point1 = Point(10, 20)
    val point2 = Point(30, 40)
    println(point1 + point2)
    println(point1.plus(point2))
    }

간단하게 확장 함수로 그때그때 구현할 수도 있다

/* 연산자를 확장 함수로 구현하기 */
operator fun Point.plus(other: Point): Point {
    return Point(x + other.x, y + other.y)
}

/* 두 피연산자의 타입이 서로 다른 연산자 정의하기 */
operator fun Point.times(scale: Double): Point {
    return Point((x * scale).toInt(), (y * scale).toInt())
}

/* 단항 연산자 정의하기 */
operator fun Point.unaryMinus(): Point {  //단항연산자는 파라미터가 없습니다.
    return Point(-x, -y)  //각 좌표에 -(음수)를 취한 좌표를 반환
}

오해하지 말아햐 하는 것이 .plus, .times, .unaryMinus를 오버라이딩 한 것이지 새로운 함수명을 만들면
+, *, -를 쓸 때 이들이 불리지 않을것이다.

확장함수로 오버라이딩한 연산자 사용

    //times test
    val point = Point(10, 20)
    println("times 호출 : ${point * 1.5}") // times 호출

    //복합연산자
    var point3 = Point(1, 2)
    point3 += Point(3, 4)  // point = point + Point(3, 4)와 동일
    println("복합연산자 : ${point3}")  // 복합연산자 : Point(x=4, y=6)

    // 단항연산자 unaryMinus
    val point6 = Point(10, 20)
    println("단항 연산자(unaryMinus): ${-point6}") //// 단항 연산자(unaryMinus): Point(x=-10, y=-20)

times 호출 : Point(x=15, y=30)
복합연산자 : Point(x=4, y=6)
단항 연산자(unaryMinus): Point(x=-10, y=-20)

🔑비교 연산자 오버로딩 compareTo()

자바와 개념이 같은데 compareValuesBy를 사용하면 여러 기준으로 비교하기가 보다 더 수월하다.

compareValuesBy(비교대상 A, 비교대상 B, 비교값들 ...)

/* 비교연산자 overloading :compareTo */
data class Person55( val firstName: String, val lastName: String ) : Comparable<Person55> {

    override fun compareTo(other: Person55): Int {
        // 첫번째 비교에서 비교가 되면 리턴, 비교가 안되면 다음값 비교
        return compareValuesBy(
            this, other,
            Person55::lastName, Person55::firstName
        )
    }
}
profile
컴퓨터와 교육 그사이 어딘가

0개의 댓글