고딩왕 코범석

FP 본문

Language & Framework/Kotlin

FP

고딩왕 코범석 2022. 8. 3. 22:42
반응형

Index

  1. 코틀린에서 배열과 컬렉션을 다루는 방법
    1. 배열
    2. 코틀린에서의 Collection - List, Set, Map
    3. 컬렉션의 null 가능성, 자바와 함께 사용하기
  2. 코틀린에서 다양한 함수를 다루는 방법
    1. 확장함수
    2. infix 함수
    3. inline 함수
    4. 지역함수
  3. 코틀린에서 람다를 다루는 방법
    1. 코틀린에서의 람다
    2. Closure
    3. try with resource
  4. 코틀린에서 컬렉션을 함수형으로 다루는 방법
    1. 다양한 컬렉션 처리 기능
    2. List를 Map으로
    3. 중첩된 컬렉션 처리

코틀린에서 배열과 컬렉션을 다루는 방법

배열

잘 사용하지 않는다.

val arr = arrayOf(100, 200)
val plusArr = arr.plus(300)
for ((i, value) in plusArr.withIndex()) {
    println("${i}, ${value}")
}

코틀린에서의 Collection - List, Set, Map

컬렉션을 만들어줄 때 불변인지, 가변인지를 설정해야한다.

https://user-images.githubusercontent.com/37062337/182325943-cc76ac61-fea7-4f76-9315-08892b40e6b5.png

코틀린에서는 Mutable 키워드가 붙은 인터페이스가 있다. 그럼 불변 컬렉션과 가변 컬렉션의 차이는 무엇일까?

  • 가변 컬렉션 : 컬렉션에 element를 추가, 삭제할 수 있다.
  • 불변 컬렉션 : 컬렉션에 element를 추가, 삭제할 수 없다.

유의 사항

  • 불변 컬렉션이라 하더라도 Reference Type인 Element의 필드는 바꿀 수 있다.
  • 즉, 컬렉션 자체가 변경되는게 불가능하고 컬렉션 내 객체의 조작은 가능하다.
fun main() {
    val numbers = listOf(100, 200)
        // 타입을 명시해줘야 함
    val emptyList = emptyList<Int>()
        // 만약 추론할 수 있다면 타입 생략 가능
    printNumbers(emptyList())
}

fun printNumbers(emptyList: List<Int>) {
    // logic
}

조작은 다음과 같이 한다.

fun main() {

        val numbers = listOf(100, 200)
    val i = numbers[0] // 배열처럼 접근
    println(i)

    for (number in numbers) {
        println(number)
    }

    for ((idx, number) in numbers.withIndex()) {
        println("$idx, $number")
    }
}

가변 리스트를 만들고 싶다면? mutableListOf()를 사용하며 ArrayList와 동일하다.

fun main() {
        val numbers = mutableListOf(100, 200)
        numbers.add(300)
}

우선, 불변으로 생성한 다음 필요에 따라 가변으로 바꾸는 것을 권장한다.

Set의 경우 자료구조의 차이점 외에는 List와 비슷하다. 가변 집합을 만들고 싶다면 mutableSetOf()을 사용하고 타입은 LinkedHashSet 이다.

Map은 다음과 같이 작성한다. 차이점이 있다면 map변수명[key] = valueput()을 호출할 수 있다.

toPair를 반환하는 중위호출 함수이다.

fun main() {
    val mutableMap = mutableMapOf<Int, String>()
    mutableMap[1] = "one"
    mutableMap[2] = "two"

        for (key in mutableMap.keys) {
        println(key)
        println(mutableMap[key])
    }

    for ((key, value) in mutableMap.entries) {
        println(key)
        println(value)
    }

    val map = mapOf(1 to "one", 2 to "two")
}

컬렉션의 null 가능성, 자바와 함께 사용하기

컬렉션을 다룰 때는 null 가능성에 대해 알아야한다.

  • List<Int?>
    • element가 Int형이나 null이 올 수 있다.
  • List<Int>?
    • element는 Int만 올 수 있고 List 자체가 null일 수 있다.
  • List<Int?>?
    • element가 Int형이나 null이 올 수 있고, List 자체가 null일 수 있다.

자바는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않는다. 코틀린에서 만든 불변 컬렉션을 자바 코드에서 가져온다면 자바 코드에서는 element를 삽입, 삭제할 수 있다.

또 자바는 null 가능성에 대해 판단하지 않는다. 코틀린에서 List를 선언하고 자바에서 가져와 리스트에 null을 넣고 반환할 수 있다는 것이다.

이를 감안하여 방어 로직을 작성하거나 코틀린에서 Collections.unmodifiableXXX()를 활용해야한다.

코틀린에서 자바 코드를 가져올 때는 플랫폼 타입에도 신경써야한다. 예를 들어 자바의 List를 가져온다면 코틀린에서는 List<Int?>인지 List<Int>?인지 List<Int?>?인지 알 수 없다. 이 때는 자바 코드를 보며 맥락을 파악하고 자바 코드를 가져오는 지점에 대해 Wrapping하는 것이 좋다.

위로

코틀린에서 다양한 함수를 다루는 방법

확장함수

코틀린은 자바와 100%호환되는 것을 목표로 하고 있다. 이에 대한 고민 중 하나가 자바 코드위에 코틀린 코드를 덧붙이는 것인데, 이를 해결하기 위해 ‘클래스안에 메서드처럼 호출할 수 있지만 함수는 밖에서 만들 수 있게 하자’ 라는 개념이 나온다.

// 함수 앞에 타입은 수신객체 타입
fun String.lastChar(): Char {
    // this는 수신객체라고 한다.
    return this[this.length - 1]
}

그렇다면 ‘확장함수가 public이고 확장함수의 수신객체 클래스 내부의 private 함수를 가져오면 캡슐화가 깨지지 않나?’ 라는 생각이 들 수 있다.

  • 확장함수는 애초에 private, protected 멤버를 가져올 수 없도록 했다.

멤버함수와 확장함수의 시그니처가 같다면 누구를 호출할까?

  • 멤버함수를 우선적으로 호출한다.
  • 확장함수를 만들었지만 다른 기능의 똑같은 시그니처를 가진 멤버함수가 생긴다면 오류가 발생할 수 있으니 유의해야한다.

확장함수가 오버라이드된다면?

  • 해당 변수의 현재 타입에 의해 어떤 확장함수가 호출할지 결정된다.

자바에서는 코틀린의 확장함수를 어떻게 호출?

  • 정적 메서드를 호출하는 것 처럼 사용 가능하다.

확장 프로퍼티

확장 프로퍼티의 원리는 확장함수 개념과 custom getter 개념을 합친 것과 같다.

val String.lastChar: Char
    get() = this[this.length - 1]

infix 함수

중위함수는 함수를 호출하는 새로운 방법이다. ex) downTo, step

변수랑 매개변수가 각각 하나씩만 있을 경우 ‘변수 함수명 argument’ 방식으로 정의할 수 있다.

확장함수, 멤버함수에도 infix 키워드를 붙일 수 있다.

fun main() {
    val a = 19
    println(a.add(2)) // 21
    println(a add 2)  // 21
}

infix fun Int.add(other: Int) = this + other

inline 함수

함수가 호출되는 대신 함수를 호출한 지점에 함수 본문을 그대로 복사하고 싶은 경우 사용한다.

함수를 파라미터로 전달할 때 오버헤드를 줄일 수 있어 사용한다. 하지만 성능 측정과 함께 신중히 사용해야한다.

inline fun Int.add(other: Int) = this + other

지역함수

함수안의 함수를 의미한다. 아래는 기존의 자바 코드를 코틀린의 지역함수로 변경한 코드이다.

함수 내에서 중복 로직을 함수로 추출하면 좋을 것 같은데 추출한 함수를 함수 내에서만 사용 가능하도록 하고싶을 때 사용하면 좋다.

// firstName, lastName 검증 로직 중복
fun createUser(firstName: String, lastName: String): User {

    if (firstName.isEmpty()) {
        throw Exception()
    }

    if (lastName.isEmpty()) {
        throw Exception()
    }

    return Person(firstName, lastName)
}

// 지역함수를 통해 중복 제거
fun createUser(firstName: String, lastName: String): User {

    fun validate(name: String) {
        if(name.isEmpty()) {
            throw Exception()
        }
    }
    validate(firstName)
    validate(lastName)

    return Person(firstName, lastName)
}

위로

코틀린에서 람다를 다루는 방법

코틀린에서의 람다

코틀린에서는 함수가 그 자체로 값이 될 수 있다. 즉, 변수로 할당될 수 있고 파라미터로 넘길 수도 있다.

// 이름 없는 함수를 작성해 변수에 할당
val isApple = fun(fruit: Fruit) = fruit.name == "사과"

// 중괄호와 화살표를 통해 간결하게 선언
val isApple2 = {
    fruit: Fruit -> fruit.name == "사과"
}

isApple.invoke(fruits[0])
isApple2(fruits[0])

// 함수의 타입 표기 가능 (파라미터타입) -> 리턴타입
val isApple2: (Fruit) -> Boolean = {
    fruit: Fruit -> fruit.name == "사과"
}

함수의 마지막 파라미터가 함수이며 함수 파라미터가 한개일 경우 아래와 같이 간단하게 작성 가능하다.

fun main() {
    val fruits = listOf(
        Fruit("사과", 1_000L),
        Fruit("사과", 2_000L),
        Fruit("바나나", 1_000L),
        Fruit("바나나", 2_000L),
        Fruit("바나나", 3_000L),
        Fruit("파인애플", 3_000L),
        Fruit("파인애플", 2_000L),
    )

        // 컴파일을 통해 추론이 가능하여 it로 간단하게 작성 가능
    val filterFruits = filterFruits(fruits) { it.name == "사과" }
    for (fruit in filterFruits) {
        println(fruit)
    }
}

private fun filterFruits(
    fruits: List<Fruit>,
    nameFilter: (Fruit) -> Boolean,
): List<Fruit> {
    val filteredFruits = mutableListOf<Fruit>()
    for (fruit in fruits) {
        if (nameFilter(fruit)) {
            filteredFruits.add(fruit)
        }
    }
    return filteredFruits
}

Closure

자바에서는 다음 코드는 컴파일 에러가 발생한다.

String targetFruitName = "바나나";
targetFruitName = "수박";
filterFruits(fruits, (fruit) -> targetFruitName.equals(fruit.getName()));

Variable used in lambda expression should be final or effectively final

자바에서는 람다를 쓸 때 사용할 수 있는 변수에 제약이 있다. 하지만 코틀린에서는 아무 문제 없이 동작한다. 왜 가능할까?

  • 코틀린에서는 람다가 시작하는 지점에 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고 있다.
  • 이렇게 해야 람다를 일급 시민으로 간주할 수 있는데, 이러한 데이터 구조를 **Closure**라 부른다.

try with resource (use 확장함수 다시보기)

코틀린의 use 확장함수를 살펴보자.

public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {...}

먼저 Closeable 구현체에 대한 확장함수이며 인라인 함수임을 알 수 있다. 매개변수에는 block이라는 이름을 가진 함수(람다)를 받고 있다.

위로

코틀린에서 컬렉션을 함수형으로 다루는 방법

다양한 컬렉션 처리 기능

  • all : 조건을 모두 만족하면 true, 그렇지 않으면 false
  • none : 조건을 모두 만족하지 않으면 true, 하나라도 만족하면 false
  • any : 조건을 하나라도 만족하면 true, 그렇지 않으면 false
  • count : size와 같다.
  • sortedBy, sortedByDescending : 정렬과 역순 정렬
  • distinctBy : 람다의 리턴 값
  • first, last : 처음 혹은 마지막 값을 가져온다. (null이 아니어야하고 비어있는 컬렉션이 비어있을 경우 예외 발생)
  • firstOrNull, lastOrNull : 컬렉션이 비어도 가져올 수 있음

List를 Map으로

  • groupBy : 해당 함수의 람다로 넘긴 필드가 key, 만들어진 key에 해당하는 객체가 value 리스트에 속하게 된다. (그룹핑)
  • associateBy : groupBy와 달리 단일 객체만 들어가게 된다. 만약 같은 key를 가진 객체들이 여러개 있을 경우 가장 나중의 객체가 들어간다.

중첩된 컬렉션 처리

  • flatMap : 중첩된 리스트를 단일 리스트로 변경해준다. 람다가 중첩된다.
  • flatten : flatMap과 동일하나 단일 리스트로만 변경이 가능하다.

위로

반응형

'Language & Framework > Kotlin' 카테고리의 다른 글

특성  (0) 2022.08.05
OOP  (0) 2022.08.02
코드 제어  (0) 2022.07.30
변수, 타입, 연산자  (0) 2022.07.30