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
- 가변컬렉션 : 컬렉션에 element 추가, 삭제 가능
- 불변이라고 하더라도 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})
-
- 과일이름 (Key) -> List<과일> (value) Map
- 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})
-
- id (key) -> 과일(Value)
중첩된 컬렉션 처리
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함수를 만들기 어려울 때
- let
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()
}