Backend/코틀린

[Kotlin] Collection, 함수, 람다, 기타 ..

지수쓰 2023. 3. 26. 22:15
반응형

출처 - 인프런 자바개발자를 위한 코틀린 입문 강의

https://www.inflearn.com/course/java-to-kotlin

 

 

CH15. 배열과 컬렉션

배열

val array = arrayOf(100, 200)
​
for (i in array.indices) {
  println("${i} ${array[i]}")
}
​
for ((idx, value) in array.withIndex()) {
  println("${idx} ${value}")
}
​
array.plus(300)
  • 인덱스 가져오기 : array.indices
  • 인덱스랑 값 같이 가져오기 : array. withIndex()
  • 배열에 값 추가하기 : array.plus

Collection

  • 코틀린은 불변, 가변을 지정해주어야한다. (기본은 불변)
    • 가변컬렉션 : 컬렉션에 element 추가, 삭제 가능
      • listOf, setOf, mapOf
    • 불변컬렉션 : 컬렉션에 element 추가, 삭제 불가
      • mutableListOf, mutableSetOf ,mutableMapOf
  • 불변이라고 하더라도 reference타입 안에 있는 필드는 바꿀 수 있다.
// 선언 
// 기본적으로는 불변
val numbers = listOf(100, 200) 
val emptyList = emptyList<Int>() // 비어있고 타입 추론이 불가능하다면 <>에 타입을 적어주어야함
​
// 가변리스트
val numbers = mutableListOf(100, 200)
numbers.add(300)
​
// 가져오기
numbers.get(0)
numbers[0]
​
// for문
for (number in numbers) {
  println(number)
}
​
for ((idx, value) in numbers.withIndex()) {
  
}

map

val oldMap = mutableMapOf<Int, String>() // 타입 지정
oldMap[1] = "MONDAY"
oldMap[2] = "TUSEDAY"
​
mapOf(1 to "MONDAY", 2 to "TUESTDAY") // to라는 중위호출 사용
​
for (key in oldMap.keys) {
  println(key)
  println(oldMap[key])
}
​
for ((key,value)) in oldMap.entries) {
  println(key)
  println(value)
}

컬렉션의 null가능성

  • List<Int?>
    • 리스트에 null이 들어갈 수 있지만 리스트는 not null
  • List<Int>?
    • 리스트에 null이 들어갈 수 없지만 리스트는 nullable
  • List<Int?>?
    • 둘다 nullable

 

Java와 같이 사용하는 Collection

  • 자바에서 코틀린의 불변 컬렉션을 가져와서 구분없이 리스트에 값을 추가하고 다시 코틀린에 돌려줄 수 있음.
  • 아니면 코틀린 쪽에서 자바의 Collection.unmodifiableXX이런거로 사용해서 방어를 할 수도 있을것

 


 

 

CH16. 함수

확장함수

//EX. String클래스 안에 있는 것 같은 확장함수 
​
fun main() {
  val str = "ABC"
  println(str.lastChar())
}
​
fun String.lastChar(): Char {
  return this[this.length-1]
}
  • String클래스를 확장하므로 String. 으로 쓰고 (수신객체 타입) this (수신 객체) 를 통해 실제 클래스 안의 값에 접근함
  • 확장함수는 원본 클래스의 private, protected에 접근하지 못함
  • 원래 있는 클래스와 같은 시그니처 메서드를 호출하면 멤버함수(원본)가 호출된다.
    • 확장함수를 만들고 나중에 멤버함수에 같은 메서드가 생긴다면 오류가 생길 수 있음 (주의)
  • 오버라이드 된 확장함수
    • val tran: Tran = Train()
      train.isExpensive() // Train의 확장함수
      ​
      val srt1: Train = Srt()
      srt1.isExpensive() // Train의 확장함수
      ​
      val srt2: Srt = Srt()
      srt2.isExpensive() // Srt의 확장함수
    • 해당 변수의 현재 타입(즉, 정적인 타입)에 의해 어떤 확장함수를 호출할지 결정됨
  • 확장프로퍼티
    • 확장 함수와 비슷하게 프로퍼티도 만들 수 있음(ex. custom getter)

infix함수(중위함수) : 변수 {함수이름} argument

  • 함수를 호출하는 새로운 방법
fun Int.add(other: Int): Int {
  return this + other
}
​
infix fun Int.add2(other: Int):Int {
  return this + other
}
​
// 3.add(4)
// 3.add2(4)
3 add2 4

inline함수

  • 함수가 호출되는 대신 함수가 호출하는 지점에 본문을 그대로 복사하는것
    • 바이트코드로 봤을 때 알 수 있음
  • 함수를 파라미터로 전달할 때 오버헤드를 줄일 수 있다.
    • 성능측정과 함께 신중하게 사용되어야 한다.
fun main() {
  3.add(4)
}
​
inline fun Int.add(other: Int): Int {
  return this + other
}

지역함수

  • 함수 안에 함수를 선언할 수 있다.
  • 함수로 추출하면 좋을 것 같은데 이 함수를 지금 함수에서만 사용하고 싶을 때
    • 단 depth가 깊어지기도 하고 그렇게 깔끔하진 않을 수도 있음...
fun createPerson(~~): Person {
  fun validateName(name: String, fieldName: String) {
    if (name.isEmpty) {
      throw IllegalArgumentException()
    }
  }
  validateName(firstName, "firstName")
  validateName(lastName, "lastName")
  
  return Person(firstName, lastName, 1)
}

 

 

 


 

 

CH17. 람다

코틀린에서의 람다

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

// 람다 생성 방법 
val isApple: (Fruit) -> Boolean = fun(fruit: Fruit): Boolean {
  return fruit.name == "사과"
}

val isApple2: (Fruit) -> Boolean = { fruit: Fruit -> fruit.name == "사과"}

// 람다 호출 방법
isApple(fruits[0])
isApple.invoke(fruits[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
}
// 다 가능 
filterFruits(fruits, isApple)
filterFruits(fruits, isApple2)

// 코틀린 스럽게 바꿔보기 
filterFruits(fruits, { fruit: Fruit -> fruit.name == "사과"})
filterFruits(fruits) { fruit: Fruit -> fruit.name == "사과"} // 1
filterFruits(fruits) { fruit -> fruit.name == "사과"} // 2 
filterFruits(fruits) { it.name == "사과" } //3

1. 소괄호 밖으로 중괄호 람다 빼기

함수(파라미터1, 파라미터2, ..., {중괄호파라미터})

  • 함수(파라미터1, 파라미터2, ...) {중괄호파라미터} 이렇게 쓸 수 있다.
    • 마지막 파라미터가 중괄호로 되어있으면 소괄호 밖으로 중괄호를 빼면 함수 호출할 때 중괄호가 가장 마지막 파라미터로 들어감
  • 람다는 여러줄 작성할 수 있고 마지막 줄의 결과가 람다의 반환값임

2. 타입추론 가능한거 타입 빼기

3. 파라미터가 한개인 경우 익명함수에서 it 이라고 쓸 수 있음

  • 그래도 it 말고 이름 써주는걸 추천한다고 함

Closure

  • 자바
    • String targetFruitName = "바나나";
      targetFruitName = "수박";
      filterFruits(fruits, (fruit) -> targetFruitName.equals(fruit.getname()));
      // 자바에서는 final 아닌 변수를 람다에서 쓰는것에서 오류 발생시킴
  • 코틀린
    • var targetFruitName = "바나나"
      targetFruitName = "수박"
      filterFruits(fruits) { it.name == targetFruitName} // 됨
    • 코틀린에서는 람다가 시작하는 지점에 참조하는 모든 변수들을 모두 포획하여 가지고 있음. 이렇게 해야만 람다를 진정한 일급시민으로 간주할 수 있기 때문
    • 이 데이터 구조를 Closure라고 부른다.

try with resources

public inline fun <T : Closeable?, R> T.use(block: (T) -> R):R {
  
} 
// Cloesable 구현체 T에 대한 확장함수 
// inline함수
// 람다를 받게 만들어진 함수

CH18. 컬렉션을 함수형으로 다루는 방법

필터와 맵

filter

val apples = fruits.filter { fruit -> fruit.name == "사과"}

filterIndexed

val apples = fruits.filterIndexed { idx, fruit -> 
 println(idx)
 fruit.name == "사과"
}

map

val applePrices = fruits.filter { fruit -> fruit.name == "사과"}
.map { fruit -> fruit.currentPrice }

mapIndexed

val applePrices = fruits.filter { fruit -> fruit.name == "사과"}
.mapIndexed { idx, fruit ->
  println(idx)
  fruit.currentPrice 
}

mapNotNull

val values = fruits.filter { fruit -> fruit.name == "사과"}
  .mapNotNull { fruit -> fruit.nullOrValue() }

다양한 컬렉션 처리 기능

  • all : 조건을 모두 만족하면 true 그렇지 않으면 false
    • val isAllApple = fruits.all { fruit -> fruit.name == "사과" }
  • none : 조건을 모두 불만족하면 true 그렇지않으면 false
  • any : 조건을 하나라도 만족하면 true 그렇지 않으면 false
  • count : 개수를 센다
  • sortedBy : (오름차순) 정렬을 한다.
  • sortedByDescending : 내림차순
  • distincyBy : 변형된 값을 기준으로 중복을 제거한다.
  • first : 첫번째 값을 가져온다. (null이 무조건 아니어야함)
  • firstOrNull : 첫번째 값 또는 null을 가져온다.
  • last , lastOrNull

List를 Map으로

  • groupBy
    • 과일이름 (Key) -> List<과일> (value) Map
      • val map: Map<String, List<Fruit>> = fruits.groupBy {fruit -> fruit.name}
    • 과일이름 (key) -> List<출고가> (value) Map
      • val map: Map<String, List<Long>> = fruits
        .groupBy( {fruit -> fruit.name}, fruit -> fruit.factoryPrice})
  • associateBy : id를 통해서 중복되지 않는 키를 가지고 뭔가 매핑해야할 때
    • id (key) -> 과일(Value)
      • val map: Map<Long, Fruit> = fruits.associateBy { fruit -> fruit.id }	
    • id -> 출고가
      • val map: Map<Long, Long> = fruits
        .associateBy({ fruit -> fruit.id}, {fruit -> fruit.factoryPrice})

중첩된 컬렉션 처리

val fruitsInList: List<List<Fruit>> = listOf(
 listOf(),
 listOf(),
  ...
)
  • flatMap
    • 출고가와 현재가가 동일한 과일을 고를 때
      • val samePriceFruits = fruitsInList.flatMap {
          list ->
            list.filter {fruit->fruit.factoryPrice == fruit.currentPrice}
        }
      • 리팩토링
      • val samePriceFruits = fruitsInList.flatMap {list -> list.samePriceFilter}
        ​
        // List에 대한 확장함수 만들어서 호출 
        val List<Fruit>.samePriceFilter: List<Fruit>
          get() = this.filter(Fruit::isSamePrice)
  • flatten
    • 중첩된 컬렉션 해제

 

 


 

 

CH19. 코틀린 이모저모

Type Alias 긴 이름의 클래스 혹은 함수타입 네이밍

typealias FruitFilter = (Fruit) -> Boolean

fun filterFruits(fruits: List<Fruit>, filter: FruitFilter) {}

as import : 클래스, 함수 import할 때 이름을 바꾸는 기능

import ~~~.printHelloWorld as printHelloWorldA
import ~~~.printHelloWorld as printHelloWolrdB

fun main() {
  printHelloWorldA()
  printHelloWorldB()
}

구조분해 : 복합적인 값을 분해하여 여러 변수를 한 번에 초기화하는것

data class Person(
  val name: String,
  val age: Int
)
val person = Person("강지수", 27)
val (name, age) = person

println("이름 : ${name}, 나이 : ${age}")
  • Data Class는 component{N}이란 함수를 자동으로 만들어준다.
    • // 구조분해를 풀어보면 이렇게 됨. name, age를 인식하는 것은 아님 
      val name = person.compoent1()
      val age = person.component2()
  • Data Class가 아닌 경우 componentN함수를 구현하면 된다. operator 로 연산자 오버로딩을 해줘야한다.
    • class Person(
        val name: String
      ) {
        operator fun component1(): String {
          return this.name
        }
      }
  • for ( (key, value) in map.entries) 도 구조분해 문법의 일종이다.

jump와 Label

  • jump
    • return : 기본적으로 가장 가까운 enclosing function 또는 익명함수로 값이 반환
    • break : 가장 가까운 루프가 제거
    • continue : 가장 가까운 루프를 다음 step으로 보냄
    • forEach에서는 break, continue를 쓸 수 없음! (run + return or 일반 for, while문)
  • label
    • 라벨 명시하면 jump@label 가능...
    • 사용하지 않는 것을 강력추천!!!

TakeIf와 TakeUnless

  • takeIf : 주어진 조건을 만족하면 그 값이, 그렇지 않으면 null이 반환된다.
  • takeUnless : 주어진 조건을 만족하지 않으면 그 값이, 그렇지 않으면 null이 반환

 


 

 

CH20. scope function

  • scope function : 일시적인 영역을 형성하는 함수
// 기본 코드
fun printPerson(person: Person?) {
  if (person != null) {
    println(person.name)
    println(person.age)
  }
}
  • ex) let (람다를 받아 람다 결과를 반환하는 확장함수) 사용
    • // 기본 코드
      fun printPerson(person: Person?) {
        person?.let {
          println(it.name)
          println(it.age)
        }
      }
    • inline fun <T, R> T.let(block: (T) -> R): R {
        return block(this)
      }
  • 종류 , usage
    • let
      • 하나이상의 함수를 call chain 결과로 호출할 때
      • non-null 값에 대해서만 code block을 실행시킬 때
      • 일회성으로 제한된 영역에 지역변수를 만들 때 (는 메서드분리가 나을지도)
    • run
      • 객체 초기화와 반환 값의 계산을 동시에 해야할 때
    • also
      • 객체를 수정하는 로직이 call chain 중간에 필요할 때
    • apply
      • 객체 설정을 할 때 객체 수정하는 로직이 call chain 중간에 필요할 때(ex. Test Fixture 만들 때)
      •  
    • with (확장함수 X 람다 )
      • this 사용해 접근하고 this는 생략 가능
      • 특정 객체를 다른 객체로 변환해야 하는데, 모듈 간의 의존성에 의해 정적 팩토리 혹은 toClass함수를 만들기 어려울 때

 

  it 사용 it: 생략 불가, 다른 이름 붙일 수 있음 this 사용 this : 생략 가능, 다른 이름 붙일 수 없음
람다의 결과 let run
객체 그 자체 (람다 결와 무관) also apply
val value1 = person.let { //value1 = age
  it.age 
}

val value2 = person.run { //value2 = age
  this.age
}

val value3 = person.also { //value3 = person
  it.age
}

val value4 = person.apply { //value4 = person
  this.age
}
// let은 일반함수를 받음. 파라미터를 받고 파라미터에 대한 이름을 직접 넣어줄 수 있음
inline fun <T, R> T.let(block: (T) -> R): R {
  return block(this)
}

// run은 확장함수를 받음. 본인 자신을 this로 호출하고 생략할 수 있음
inline fun <T, R> T.run(block: T.() -> R):R {
  return block()
}