PHM 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