ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 4. 코틀린에서의 FP
    Kotlin/자바 개발자를 위한 코틀린 입문 2023. 7. 26. 19:20

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

    1. 배열

    - 사실 배열은 잘 사용하지 않는다.

     

    for (i in array.indices) {
        println("${i} ${array[i]}")
    }

    - array.indices 는 0부터 마지막 index까지의 Range이다.

     

    for ((idx, value) in array.withIndex()) {
        println("$idx $value")
    }

    - withIndex() 를 사용하면, 인덱스와 값을 한 번에 가져올 수 있다.

     

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

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

        ㆍ가변(Mutable) 컬렉션 : 컬렉션에 element를 추가, 삭제할 수 있다.

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

    - 불변 컬렉션은 Collection을 만들자마자 Collection.unmodifiableList() 등을 붙여준다!

    - 불변 컬렉션이라 하더라도 Reference Type인 Element의  필드는 바꿀 수 있다.

     

    - Kotlin은 불변/가변을 지정해 주어야 한다느 사실을 기억하자!

     

    List

    val numbers = listOf(100, 200)
    val emptyList = emptyList<Int>()

    - listOf를 통해 '불변 리스트'를 만든다.

    - emptyList<타입>()

    fun main() {
        printNumbers(emptyList())
    }
    
    private fun printNumbers(numbers: List<Int>) {
    
    }

    - 타입을 추론할 수 있다면 생략 가능하다.

     

    - 가변리스트를 만들고 싶다면?

    // 가변 리스트
    val numbers2 = mutableListOf(100, 200)
    numbers2.add(300)

     

    - 기본 구현체는 ArrayList이고 기타 사용법은 Java와 동일하다.

     

    * 간단한 TIP

     : 우선 불변 리스트를 만들고, 꼭 필요한 경우 가변 리스트로 바꾸자!

     

    Set

    - 집합은 List와 다르게 순서가 없고, 같은 element는 하나만 존재할 수 있다.

    - 자료구조적 의미만 제외하면 모든 기능이 List와 비슷하다.

    // 3. Set
    val numbers3 = setOf(100, 200)
    
    // For Each
    for (number in numbers3) {
        println(number)
    }
    
    // 전통적인 For 문
    for ((index, number) in numbers3.withIndex()) {
        println("$index $number")
    }
    
    // 가변집합을 만들고 싶다면?
    val numbers4 = mutableSetOf(100, 200)

    - 기본 구현체는 LinkedHashSet 이다.

     

    Map

    - Kotlin도 MutableMap을 만들어 넣을 수도 있고, 정적 팩토리 메소드를 바로 활용할 수도 있다.

    val oldMap = mutableMapOf<Int, String>()
    oldMap[1] = "MONDAY"
    oldMap[2] = "TUESDAY"
    
    mapOf(1 to "MONDAY", 2 to "TUESDAY")

    - 타입을 추론할 수 없어, 타입을 지정해주었다.

    - 가변 Map 이기 때문에 (key, value)를 넣을 수 있다.

    - Java처럼 put을 쓸 수도 있고, map[key] = value를 쓸 수도 있다.

    - mapOf(key to value)를 사용해 불변 map을 만들 수 있다.

     

    3. 컬렉션의 null 가능성, Java와 함께 사용하기

    - List<Int?> : 리스트에 null이 들어갈 수 있지만, 리스트는 절대 null이 아님

    - List<Int>? : 리스트에는 null이 들어갈 수 없지만, 리스트는 null일 수 있음

    - List<Int?>? : 리스트에 null이 들어갈 수도 있고, 리스트가 null일 수도 있음

    - ? 위치에 따라 null 가능성 의미가 달라지므로 차이를 잘 이해해야한다.


    - Java는 읽기 전용 컬렉션과 변경 가능 컬렉션을 구분하지 않는다.

    - Java는 nullable 타입과 non-nullable 타입을 구분하지 않는다.

    - Kotlin 쪽의 컬렉션이 Java에서 호출되면 컬렉션 내용이 변할 수 있음을 감안해야 한다.

    - 코틀린 쪽에서 Collections.unmodifiableXXX() 를 활용하면 변경 자체를 막을 수는 있다!

     

    - Kotlin에서 Java 컬렉션을 가져다 사용할 때 플랫폼 타입을 신경써야 한다.

    - Java 코드를 보며, 맥락을 확인하고 Java 코드를 가져오는 지점을 wrapping 한다.

     

    정리

    - 배열의 사용법이 약간 다르다!

    - 코틀린에서는 컬렉션을 만들 때도 불변/가변을 지정해야 한다.

    - List, Set, Map 에 대한 사용법이 변경, 확장되었다.

    - Java와 Kotlin 코드를 섞어 컬렉션을 사용할 때에는 주의해야 한다.

        ㆍJava에서 Kotlin 컬렉션을 가져갈 때는 불변 컬렉션을 수정할 수도 있고, non-nullable 컬렉션에 null을 넣을 수도 있다.

        ㆍKotlin에서 Java 컬렉션을 가져갈 때는 플랫폼타입을 주의해야 한다.

     

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

    1. 확장함수

    - 어떤 클래스안에 있는 메소드처럼 호출할 수 있지만, 함수는 밖에 만들 수 있게 하자!

    fun String.lastChar(): Char {
        return this[this.length - 1]
    }

    - String 클래스를 확장

    - 함수 안에서는 this를 통해 인스턴스에 접근 가능하다!

    fun 확장할려는클래스(수신객체타입).함수이름(파라미터): 리턴타입 {
        // this(수신객체)를 이용해 실제 클래스 안의 값에 접근
    }

    - 확장함수는 클래스에 있는 private 또는 protected 멤버를 가져올 수 없다!!

     

    public class Person {
    
        private final String firstName;
        private final String lastName;
        private int age;
    
        public Person(String firstName, String lastName, int age) {
            this.firstName = firstName;
            this.lastName = lastName;
            this.age = age;
        }
    
        public int nextYearAge() {
            System.out.println("멤버 함수");
            return this.age + 1;
        }
    
    }
    fun Person.nextYearAge(): Int {
        println("확장 함수")
        return this.age + 1
    }
    
    fun main() {
        val person = Person("A", "B",100)
        println(person.nextYearAge())   // 멤버 함수
    }

    - 멤버함수가 우선적으로 호출된다! 

    - 확장함수를 만들었지만, 다른 기능의 똑같은 멤버함수가 생기면?! 오류가 발생할 수 있다.

     

    val train: Train = Train()
    train.isExpensive() // Train의 확장함수
    
    val srt1: Train = Srt()
    srt1.isExpensive()  // Train의 확장함수
    
    val srt2: Srt = Srt()
    srt2.isExpensive()  // Srt의 확장함수

    - 해당 변수의 현재 타입. 즉, 정적인 타입에 의해 어떤 확장함수가 호출될지 결정된다.

     

    확장함수 중간정리

    1. 확장함수는 원본 클래스의 private, protected 멤버 접근이 안된다!

    2. 멤버함수, 확장함수 중 멤버함수에 우선권이 있다!

    3. 확장함수는 현재 타입을 기준으로 호출된다.

     

    - Java에서는 Kotlin 확장함수를 가져다 사용할 수 있나?!

    public static void main(String[] args) {
        StringUtilsKt.lastChar("ABC");
    }

    - 정적 메소드를 부르는 것처럼 사용 가능하다.

     

    - 확장함수 라는 개념은 확장 프로퍼티와도 연결

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

    - 확장 프로퍼티의 원리는 확장함수 + custom getter와 동일하다!

     

    2. infix 함수

    - 중위함수, 함수를 호출하는 새로운 방법!!

    - downTo, step 도 함수이다! ( 중위 호출 함수 )

    - 변수.함수이름(argument) 대신 변수 함수이름 argument

    fun Int.add(other: Int): Int {
        return this + other
    }
    
    infix fun Int.add2(other: Int): Int {
        return this + other
    }
    
    fun main() {
        3.add(4)
        3.add2(4)
        3 add2 4
    }

    - Infix는 멤버함수에도 붙일 수 있다!!

     

    3. inline 함수

    - 함수가 호출되는 대신, 함수를 호출한 지점에 함수 본문을 그대로 복붙하고 싶은 경우!

    - 함수를 파라미터로 전달할 때에 오버헤드를 줄일 수 있다.

    - 하지만 inline 함수의 사용은 성능 측정과 함께 신중하게 사용되어야 한다.

     

    4. 지역함수

    - 함수안에 함수를 선언할 수 있다.

    fun createPerson(firstName: String, lastName: String): Person {
    //    if(firstName.isEmpty()) {
    //        throw IllegalArgumentException("firstName은 비어있을 수 없습니다! 현재 값 : $firstName")
    //    }
    //    if(lastName.isEmpty()) {
    //        throw IllegalArgumentException("lastName 비어있을 수 없습니다! 현재 값 : $lastName")
    //    }
        fun validateName(name: String, fieldName: String) {
            if (name.isEmpty()) {
                throw IllegalArgumentException("$firstName 은 비어있을 수 없습니다! 현재값 : $name")
            }
        }
        validateName(firstName, "firstName")
        validateName(lastName, "lastName")
        
        return Person(firstName, lastName, 1)
    }

    - 함수로 추출하면 좋을 것 같은데, 이 함수를 지금 함수 내에서만 사용하고 싶을 때

    - 하지만 depth가 깊어지기도 하고, 코드가 그렇게 깔끔하지 않다..

     

    정리

    - Java 코드가 있는 상황에서, Kotlin 코드로 추가 기능 개발을 하기 위해 확장함수와 확장프로퍼티가 등장했다.

    - 확장함수는 원본 클래스의 private, protected 멤버 접근이 안된다!

    - 멤버함수, 확장함수 중 멤버함수에 우선권이 있다!

    - 확장함수는 현재 타입을 기준으로 호출된다!

    - Java에서는 static 함수를 쓰는 것처럼 Kotlin의 확장함수를 쓸 수 있다.

    - 함수 호출 방식을 바꿔주는 infix 함수가 존재한다.

    - 함수를 복사-붙여넣기 하는 inline 함수가 존재한다.

    - Kotlin에서는 함수 안에 함수를 선언할 수 있고, 지역함수라고 부른다.

     

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

    1. Java에서 람다를 다루기 위한 노력

    - java에서는 함수는 변수에 할당되거나 파라미터로 전달할 수 없다. ( 2급 시민 )

     

    2. 코틀린에서의 람다

    - Java와는 근본적으로 다른 한 가지가 있다.

    - 코틀린에서는 함수가 그 자체로 값이 될 수 있다. 변수에 할당할수도, 파라미터로 넘길 수도 있다. (1급 시민)

    // 람다를 만드는 방법 1
    val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
        return fruit.name == "사과"
    }
    
    // 람다를 만드는 방법2
    val isApple2: (Fruit) -> Boolean = { fruit: Fruit -> fruit.name == "사과" }
    
    // 람다를 직접 호출하는 방법1
    isApple(fruit[0])
    // 람다를 직접 호출하는 방법2
    isApple.invoke(fruit[0])

    - 함수의 타입: (파라미터 타입...) -> 반환타입

     

    private fun filterFruits(
        fruits: List<Fruit>, filter: (Fruit) -> Boolean
    ): List<Fruit> {
        val results = mutableListOf<Fruit>()
        for (fruit in fruits) {
            if(filter(fruit)) {
                results.add(fruit)
            }
        }
    
        return results
    }
    fun main() {
        val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
            return fruit.name == "사과"
        }
        filterFruits(fruits, isApple)
        
        filterFruits(fruits) { it.name == "사과" }
        filterFruits(fruits) { fruit ->
          println("사과")
          fruit.name == "사과"
        } 
    }

    - 마지막 파라미터가 함수인 경우, 소괄호 밖에 람다 사용가능

    - 람다를 작성할때, 람다의 파라미터를 it으로 직접 참조할 수 있다.

    - 람다를 여러줄 작성할 수 있고, 마지막 줄의 결과가 람다의 반환값이다.

     

    3. Closure

    String targetFruitName = "바나나";
    targetFruitName = "수박";
    filterFruits(fruits, (fruit) -> targetFruitName.equals(fruit.getName()));
    // Variable used in lambda expression should be final or effectively final

    - Java에서는 람다를 쓸 때 사용할 수 있는 변수에 제약이 있다

     

    var targetFruitName = "바나나"
    targetFruitName = "수박"
    filterFruits(fruits) { it.name == targetFruitName }

    - 코틀린에서는 아무런 문제 없이 동작한다!

    - 코틀린에서는 람다가 시작하는 지점에 참조하고 있는 변수들을 모두 포획하여 그 정보를 가지고 있다.

    - 이렇게 해야만, 람다를 진정한 일급 시민으로 간주할 수 있다.

        ㆍ이 데이터 구조를 Closure라고 부른다

    4. 다시 try with resources

    - use 구현체

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

        ㆍCloseable 구현체에 대한 확장함수이다.

        ㆍInline 함수이다.

        ㆍ람다를 받게 만들어진 함수이다.

    fun readFile(path: String) {
        BufferedReader(FileReader(path)).use { reader ->
            println(reader.readLine())
        }
    }

     

    정리

    - 함수는 Java에서는 2급시민이지만, 코트린에서는 1급 시민이다.

        ㆍ때문에, 함수 자체를 변수에 넣을 수도 있고, 파라미터로 전달할 수도 있다.

    - 코틀린에서 함수 타입은 (파라미터 타입, ...) -> 반환타입 이었다.

    - 코틀린에서 람다는 두 가지 방법으로 만들 수 있고, {} 방법이 더 많이 사용된다.

    - 함수를 호출하며, 마지막 파라미터인 람다를 쓸 때는 소괄호 밖으로 람다를 뺄 수 있다.

        ㆍit을 쓰기보다는 화살표를 쓰는 방법을 더 추천한다. ( it 데이터가 어떤 데이터인지 모를 수 도 있다. )

    - 람다의 마지막 expression 결과는 람다의 반환 값이다.

    - 코트린에서는 Closure를 사용하여 non-final 변수도 람다에서 사용할 수 있다.

     

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

    1. 필터와 맵

    - filter / filterIndexed / map / mapIndexed / mapNotNull

     

    2. 다양한 컬렉션 처리 기능

    - all : 조건을 모두 만족하면 true 그렇지 않으면 false

    // all
    val isAllApple = fruits.all {fruit -> fruit.name == "사과" }

     

    - none : 조건을 모두 불만족하면 true 그렇지 않으면 false

    // none
    val isNoApple = fruits.none {fruit -> fruit.name == "사과" }

     

    - any : 조건을 하나라도 만족하면 true 그렇지 않으면 false

    // any
    val isNoApple2 = fruits.any {fruit -> fruit.factoryPrice >= 10_000 }

     

    // count
    val fruitCount = fruits.count()
    
    // sortedBy : (오름차순) 정렬을 한다.
    val fruitSort1 = fruits.sortedBy { fruit -> fruit.currentPrice }
    
    // sortedByDescending : (내림차순) 정렬을 한다
    val fruitSort2 = fruits.sortedByDescending { fruit -> fruit.currentPrice }
    
    // distinctBy : 변형된 값을 기준으로 중복을 제거한다.
    val distinctFruitNames = fruits.distinctBy { fruit: Fruit -> fruit.name }
        .map { fruit: Fruit -> fruit.name }
    
    // first : 첫번째 값을 가져온다 (무조건 null이 아니어야함)
    fruits.first()
    
    // firstOrNull : 첫번째 값 또는 null을 가져온다
    fruits.firstOrNull()
    
    // last : 마지막 값을 가져온다 (무조건 null이 아니어야함)
    fruits.last()
    
    // lastOrNull : 첫번째 값 또는 null을 가져온다
    fruits.lastOrNull()

    3. List를 Map으로

    - groupBy / associateBy

    // 과일이름 -> List<과일> 의 Map
    val map: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
    
    // id -> 과일 의 Map
    val map2: Map<Long, Fruit> = fruits.associateBy { fruit -> fruit.id }
    
    // 과일이름 -> List<출고가> Map
    val map3: Map<String, List<Long>> = fruits.groupBy({fruit -> fruit.name}, {fruit -> fruit.factoryPrice})
    
    // id -> 출고가 Map
    val map4: Map<Long, Long> = fruits.associateBy({fruit -> fruit.id}, {fruit -> fruit.factoryPrice})
    
    
    val map5: Map<String, List<Fruit>> = fruits.groupBy { fruit -> fruit.name }
        .filter { (key, value) -> key == "사과" }

    4. 중첩된 컬렉션 처리

    - flatMap

    val samePriceFruits = fruitsInList.flatMap { list ->
        list.filter { fruit -> fruit.factoryPrice == fruit.currentPrice }
    }

     

    data class Fruit(
        val id: Long,
        val name: String,
        val factoryPrice: Long,
        val currentPrice: Long,
    ) {
        fun nullOrValue(): Unit? {
            TODO("Not yet implemented")
        }
    
        val isSamePrice: Boolean
            get() = factoryPrice == currentPrice
    }
    
    
    val List<Fruit>.samePriceFilter: List<Fruit>
        get() = this.filter(Fruit::isSamePrice)
    
    
    fun main() {
        val samePriceFruits2 = fruitsInList.flatMap { list -> list.samePriceFilter }
    }

    - flatten

    // List<List<Fruit>>를 List<Fruit>로 변경
    fruitsInList.flatten()

     

    정리

    - filter / filterIndexed

    - map / mapIndexed / mapNotNull

    - all / none / any

    - count / sortedBy / sortedByDescending / distinct

    - first / firstOrNull / last / lastOrNull

    - groupBy / associateBy

    - flatMap / flatten

    댓글

Designed by Tistory.