Kotlin - 함수형 프로그래밍(1): 람다, 컬렉션 연산, 멤버 참조, 최상위 함수와 프로퍼티
in DEV on Kotlin, Lambda, Mapindexed(), Indices(), Run(), Filter(), Closure,, Filter(), Filternotnull(), Any(), All(), None(), Find(), Firstornull(), Lastornull(), Count(), Filternot(), Partition(), Sumof(), Sortedby(), Minby(), Take(), Drop(),, Sortedwith(), Compareby(), Times()
코틀린 여러 함수 기능에 대해 알아본다.
소스는 github 에 있습니다.
목차
- 1. 람다
- 2. 컬렉션 연산:
hashSetOf()
,arrayListOf()
,listOf()
,hashMapOf()
- 3. 멤버 참조:
::
- 4. 최상위 함수와 프로퍼티 (정적인 유틸리티 클래스 없애기)
- 참고 사이트 & 함께 보면 좋은 사이트
개발 환경
- 언어: kotlin 1.9.23
- IDE: intelliJ
- SDK: JDK 17
- 의존성 관리툴: Gradle 8.5
1. 람다
람다는 함수 리터럴이라고도 부르며, 이름이 없고 함수 생성에 필요한 최소한의 코드만 필요하며, 다른 코드에 람다를 직접 삽입할 수 있다.
map()
을 보면 원본 List 의 모든 원소에 변환 함수를 적용해 얻은 새로운 원소로 이루어진 새로운 List 를 반환한다.
아래는 List 의 각 원소를 []
로 둘러싼 String 으로 변환하는 예시이다.
fun main() {
val list = listOf(1, 2, 3, 4)
// {} 안의 내용이 람다
val result = list.map { n: Int -> "[$n]" }
println(list) // [1, 2, 3, 4]
println(result) // [[1], [2], [3], [4]]
}
코틀린은 람다의 타입을 추론할 수 있기 때문에 람다가 필요한 위치에 바로 람다를 적을 수 있다.
바로 위의 코드를 아래처럼 더 간단히 할 수도 있다.
fun main() {
val list = listOf(1, 2, 3, 4)
// {} 안의 내용이 람다
val result = list.map { n: Int -> "[$n]" }
// 람다의 타입을 추론
// 람다를 List<Int> 타입에서 사용하고 있기 때문에 코틀린은 n 의 타입이 Int 라는 사실을 알 수 있음
var result2 = list.map { n -> "[$n]" }
println(list) // [1, 2, 3, 4]
println(result) // [[1], [2], [3], [4]]
println(result2) // [[1], [2], [3], [4]]
}
1.1. 함수 파라메터가 하나인 경우
파라메터가 하나일 경우 코틀린은 자동으로 파라메터 이름을 it
으로 만들기 때문에 더 이상 위처럼 n -> 을 사용할 필요가 없다.
fun main() {
val list = listOf(1, 2, 3, 4)
val result = list.map { "[$it]" }
println(result) // [[1], [2], [3], [4]]
val stringList = listOf("a", "b", "c")
val result2 = stringList.map({ "${it.uppercase()}" })
// 함수의 파라메터가 람다뿐이면 람다 주변의 괄호를 없앨 수 있음
val result3 = stringList.map { "${it.uppercase()}" }
println(result2) // [A, B, C]
println(result3) // [A, B, C]
}
1.2. 함수 파라메터가 여러 개인 경우
함수가 여러 파라메터를 받고 람다가 마지막 파라메터인 경우엔 람다를 인자 목록을 감싼 괄호 다음에 위치시킬 수 있다.
예를 들어 joinToString() 의 마지막 인자로 람다를 지정하면 이 람다로 컬렉션의 각 원소를 String 으로 변환 후 변환한 모든 String 은 구분자와 prefix/postfix 를 붙여서 하나로 합쳐준다.
fun main() {
val list = listOf(1, 2, 3, 4)
// 람다를 이름없는 인자로 호출
val result = list.joinToString(" ") { "[$it]" }
println(result) // [1] [2] [3] [4]
// 람다를 이름 붙은 인자로 호출할 때는 인자 목록을 감싸는 괄호 안에 람다를 위치시켜야 함
val result2 = list.joinToString(
separator = " ",
transform = { "[$it]" }
)
println(result2) // [1] [2] [3] [4]
}
1.3. 람다 파라메터가 여러 개인 경우: mapIndexed()
fun main() {
val list = listOf('a', 'b', 'c')
val result = list.mapIndexed { index, ele -> "[$index:$ele]" }
println(result) // [[0:a], [1:b], [2:c]]
}
mapIndexed()
는 List 의 원소와 원소의 인덱스를 함께 람다에 전달하면서 각 원소를 반환한다.
mapIndexed()
에 전달할 람다는 인덱스와 원소를 파라메터로 받아야 한다.
1.4. 람다가 특정 인자를 사용하지 않는 경우: List.indices()
람다가 특정 인자를 사용하지 않으면 밑줄 _
을 사용하여 람다가 어떤 인자를 사용하지 않는다는 컴파일러 경고를 무시할 수 있다.
fun main() {
val list = listOf('a', 'b', 'c')
val result = list.mapIndexed { index, _ -> "($index)" }
val result2 = List(list.size) { index -> "($index)" }
println(result) // [(0), (1), (2)]
println(result2) // [(0), (1), (2)]
}
위 함수의 경우 mapIndexed()
에 전달한 람다가 원소값은 무시하고 인덱스만 사용했기 때문에 indices
에 map()
을 적용하여 다시 나타낼 수 있다.
fun main() {
val list = listOf('a', 'b', 'c')
// List.indices() 사용
val result3 = list.indices.map {
"($it)"
}
println(result3) // [(0), (1), (2)]
}
1.5. 람다에 파라메터가 없는 경우: run()
람다에 파라메터가 없는 경우 파라메터가 없다는 것을 강조하기 위해 화살표를 남겨둘 수도 있지만 코틀린 스타일 가이드에서는 화살표를 사용하지 않는 것을 권장한다.
fun main() {
// 파라메터가 없는 람다의 경우 화살표만 넣은 경우
run({ -> println("haha") }) // haha
// 파라메터가 없는 람다의 경우 화살표를 생략한 경우
run({ println("haha2") }) // () -> kotlin.String
println({ "haha2" }) //() -> kotlin.String
println({ "haha2" }.invoke()) // haha2
}
run()
은 단순히 자신에게 인자로 전달된 람다를 호출하기만 한다.
run()
은 실제로 다른 용도에서 쓰이는데run()
에 대한 좀 더 상세한 내용은 2. 영역 함수 (Scope Function):let()
,run()
,with()
,apply()
,also()
를 참고하세요.
1.6. 람다를 통해 코드 재사용: filter()
아래는 리스트에서 짝수와 홀수를 선택하는 예시이다.
// 짝수 필터
fun filterEven(nums: List<Int>): List<Int> {
val result = mutableListOf<Int>();
for (i in nums) {
if (i % 2 == 0) {
result += i
}
}
return result
}
// 2보다 큰 수만 필터
fun filterGreaterThanTwo(nums: List<Int>): List<Int> {
val result = mutableListOf<Int>();
for (i in nums) {
if (i > 2) {
result += i
}
}
return result
}
fun main() {
val list = listOf(1, 2, 3, 4)
val evens = filterEven(list)
val greaterThanTwo = filterGreaterThanTwo(list)
println(evens) // [2, 4]
println(greaterThanTwo) // [3, 4]
}
위에서 짝수 필터와 2보다 큰 수를 필터하는 함수가 거의 동일하다.
이 경우 람다를 사용하여 하나의 함수를 사용할 수 있다.
표준 라이브러리 함수인 filter()
는 보존하고 싶은 원소를 선택하는 Predicate (Boolean 값을 돌려주는 함수) 를 인자로 받는데, 이 Predicate 를 람다로 지정하면 된다.
fun main() {
val list = listOf(1, 2, 3, 4)
val even = list.filter { it % 2 == 0 }
val greaterThenTwo = list.filter { it > 2 }
println(even) // [2, 4]
println(greaterThenTwo) // [3, 4]
}
함수가 훨씬 간결해진 것을 확인할 수 있다.
1.7. 람다를 변수에 담기
람다를 var
, val
에 담아 사용하면 여러 함수에 같은 람다를 넘기면서 로직을 재사용할 수 있다.
fun main() {
val list = listOf(1, 2, 3, 4)
// 람다를 변수에 담아서 사용
val isEven = { e: Int -> e % 2 == 0 }
val result = list.filter(isEven)
// any() 는 주어진 Predicate 를 만족하는 원소가 List 에 하나라도 있는지 검사함
val result2 = list.any(isEven)
println(result) // [2, 4]
println(result2) // true
}
1.8. 클로저 (Closure)
람다는 자신의 영역 밖에 있는 요소를 참조할 수 있다.
함수가 자신이 속한 환경의 요소를 포획(capture) 하거나 닫아버리는(close up) 것을 클로저라고 한다.
클로저가 없는 람다가 있을 수도 있고, 람다가 없는 클로저가 있을 수도 있다.
fun main() {
val list = listOf(1, 2, 3, 4)
val divider = 2
// 람다가 자신의 밖에 정의된 divider 를 포획(capture) 함
// 람다는 capture 한 요소를 읽거나 변경 가능
val result = list.filter { it % divider == 0 }
println(result) // [2, 4]
}
fun main() {
val list = listOf(1, 2, 3, 4)
var sum = 0
var divider = 2
// 람다가 capture 한 요소인 sum 을 변경함
val result = list.filter { it % divider == 0 }.forEach { sum += it }
println(result) // kotlin.Unit
println(sum) // 6
}
위 코드는 람다가 가변 함수는 sum 을 capture 하여 변경했지만, 보통은 상태를 변경하지 않는 형태로 코드를 사용한다.
fun main() {
val list = listOf(1, 2, 3, 4)
var sum = 0
var divider = 2
// 람다가 capture 한 요소인 sum 을 변경함
val result = list.filter { it % divider == 0 }.forEach { sum += it }
// 람다가 capture 한 요소인 sum 을 변경하지 않고 결과를 얻음
var result2 = list.filter { it % divider == 0 }.sum()
println(result) // kotlin.Unit
println(result2) // 6
println(sum) // 6
}
아래는 람다가 아닌 일반 함수가 함수 밖의 요소를 capture 하는 예시이다.
var x = 100
// 일반 함수가 함수 밖에 요소를 capture 함
fun useX() {
x++
}
fun main() {
useX()
println(x) // 101
}
2. 컬렉션 연산: hashSetOf()
, arrayListOf()
, listOf()
, hashMapOf()
코틀린으로 아래와 같이 컬렉션을 만들 수 있다.
package com.assu.study.kotlin2me.chap03
fun main() {
val set = hashSetOf(1, 1, 2)
val list = arrayListOf(1, 1, 2)
val list2 = listOf(1, 1, 2)
val hashmap = hashMapOf(1 to "one", 2 to "two")
println(set) // [1, 2]
println(list) // [1, 1, 2]
println(list2) // [1, 1, 2]
println(hashmap) // {1=one, 2=two}
// javaClass 는 자바에서 getClass() 와 동일
println(set.javaClass) // class java.util.HashSet
println(list.javaClass) // class java.util.ArrayList
println(list2.javaClass) // class java.util.Arrays$ArrayList
println(hashmap.javaClass) // class java.util.HashMap
println(list.last()) // 2
println(set.max()) // 2
}
.javaClass
는 자바의 getClass()
와 동일하며, 해당 객체가 어떤 클래스에 속하는지 알 수 있다.
위 코드는 코틀린이 자신만의 컬렉션 기능을 제공하지 않는다는 의미이다.
코틀린의 컬렉션은 자바 컬렉션과 똑같은 클래스이지만, 자바보다 더 많은 기능을 사용할 수 있다.
예) 리스트의 마지막 원소를 가져오거나 최대값 찾기
코틀린 타입 시스템 안에서 자바 컬렉션 클래스가 어떻게 표현되는지 추후 상세히 다룰 예정입니다. (p. 105)
함수형 언어는 map()
, filter()
, any()
처럼 컬렉션을 다룰 수 있는 여러 수단을 제공한다.
여기선 List 와 그 외 컬렉션에 사용되는 다른 연산에 대해 알아본다.
2.1. List 연산
아래는 List 를 생성하는 여러 방법이다.
fun main() {
// 람다는 인자로 추가할 원소의 인덱스를 받음
val list1 = List(10) { it }
println(list1) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// 하나의 값으로 이루어진 리스트
val list2 = List(10) { 1 }
println(list2) // [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
// 글자로 이루어진 리스트
val list3 = List(10) { 'a' + it }
println(list3) // [a, b, c, d, e, f, g, h, i, j]
// 정해진 순서를 반복
val list4 = List(10) { list3[it % 3] }
val list5 = List(10) { list3[it % 2] }
println(list4) // [a, b, c, a, b, c, a, b, c, a]
println(list5) // [a, b, a, b, a, b, a, b, a, b]
}
위의 List 생성자는 인자가 2개인데 첫 번째 인자는 생성할 List 의 크기이고, 두 번째는 생성한 List 의 각 원소를 초기화하는 람다이다.
이 람다는 원소의 인덱스를 전달받는다.
람다가 함수의 마지막 원소인 경우 람다를 인자 목록 밖으로 빼내도 된다.
MutableList 도 위의 방법으로 초기화가 가능하다.
fun main() {
val mutableList1 = MutableList(5, { 10 * (it + 1) })
// 람다가 함수의 마지막 원소이면 람다를 인자 목록 밖으로 빼내도 됨
val mutableList2 = MutableList(5) { 10 * (it + 1) }
println(mutableList1) // [10, 20, 30, 40, 50]
println(mutableList2) // [10, 20, 30, 40, 50]
}
2.2. 여러 컬렉션 함수들: filter()
, filterNotNull()
, any()
, all()
, none()
, find()
, firstOrNull()
, lastOrNull()
, count()
다양한 컬렉션 함수들이 predicate 를 받아서 컬렉션의 원소를 검사한다.
filter()
- 주어진 predicate 가 true 를 리턴하는 모든 원소가 들어있는 새로운 리스트 반환
- 모든 원소에 predicate 적용
filterNotNull()
- null 을 제외한 원소들로 이루어진 새로운 List 반환
any()
- 원소 중 어느 하나에 대해 predicate 가 true 를 반환하면 true 반환
- 결과를 찾자마자 이터레이션 중단
all()
- 모든 원소가 predicate 와 일치하는지 검사
none()
- predicate 와 일치하는 원소가 하나도 없는지 검사
find()
- predicate 와 일치하는 첫 번째 원소 검사
- 원소가 없으면 예외를 던짐
- 결과를 찾자마자 이터레이션 중단
firstOrNull()
- predicate 와 일치하는 첫 번째 원소 검사
- 원소가 없으면 null 반환
lastOrNull()
- predicate 와 일치하는 마지막 원소를 반환하며, 일치하는 원소가 없으면 null 반환
count()
- predicate 와 일치하는 원소의 개수 반환
- 모든 원소에 predicate 적용
fun main() {
val list = listOf(-3, -1, 0, 3, 5, 9)
// filter(), count()
val result1 = list.filter { it > 0 }
val result2 = list.count { it > 0 }
println(result1) // [3, 5, 9]
println(result2) // 3
// filterNotNull()
val list2 = listOf(1, 2, null)
println(list2.filterNotNull()) // [1, 2]
// find(), firstOrNull(), lastOrNull()
val result3 = list.find { it > 0 }
val result4 = list.firstOrNull { it > 0 }
val result5 = list.lastOrNull { it > 0 }
val result6 = list.firstOrNull { it < 0 }
val result7 = list.lastOrNull { it < 0 }
println(result3) // 3
println(result4) // 3
println(result5) // 9
println(result6) // -3
println(result7) // -1
// any()
val result8 = list.any { it > 0 }
val result9 = list.any { it != 0 }
println(result8) // true
println(result9) // true
// all()
val result10 = list.all { it > 0 }
val result11 = list.all { it != 0 }
println(result10) // false
println(result11) // false
// none()
val result12 = list.none { it > 0 }
val result13 = list.none { it == 0 }
println(result12) // false
println(result13) // false
}
2.3. filterNot()
, partition()
filter()
는 predicate 를 만족하는 원소들을 반환하는 반면, filterNot()
은 predicate 를 만족하지 않는 원소들을 반환한다.
partition()
은 동시에 양쪽 (predicate 를 만족하는 원소와 만족하지 않는 원소) 을 생성한다.
partition()
은 List 가 들어있는 Pair
객체를 생성한다.
fun main() {
val list = listOf(-3, -1, 0, 5, 7)
val isPositive = { i: Int -> i > 0 }
// filter(), filterNot()
val result1 = list.filter(isPositive)
val result2 = list.filterNot(isPositive)
println(result1) // [5, 7]
println(result2) // [-3, -1, 0]
// partition()
val (pos, neg) = list.partition(isPositive)
val (pos1, neg1) = list.partition { it > 0 }
println(pos) // [5, 7]
println(neg) // [-3, -1, 0]
println(pos1) // // [5, 7]
println(neg1) // [-3, -1, 0]
}
2.4. 커스텀 함수의 반환값에 구조 분해 선언 사용
7. 구조 분해 (destructuring) 선언 에서 본 것처럼 구조 분해 선언을 사용하면 동시에 Pair 원소로 초기화할 수 있다.
fun createPair() = Pair(1, "one")
fun main() {
val (i, s) = createPair()
println(i) // 1
println(s) // one
}
2.5. sumOf()
, sortedBy()
, minBy()
, take()
, drop()
3. 리스트 에서 수 타입으로 정의된 리스트에 대해 sum()
이나 비교 가능한 원소로 이루어진 리스트에 적용할 수 있는 sorted()
를 보았다.
만약 리스트의 원소가 숫자가 아니거나, 비교 가능하지 않으면 sum()
, sorted()
대신 sumBy()
, sortedBy()
를 사용할 수 있다.
이 함수들은 덧셈, 정렬 연산에 사용할 특성값을 돌려주는 함수(보통 람다)를 인자로 받는다.
sorted()
, sortedBy()
는 컬렉션을 오름차순으로 정렬하고, sortedDescending()
, sortedByDescending()
은 컬렉션을 내림차순으로 정렬한다.
minBy()
는 주어진 비교 기준에 따라 최소값을 돌려주며, 리스트가 비어있으면 null 을 리턴한다.
sumBy()
는 코틀린 1.5 부터 deprecated 예고됨
sumBy()
,sumByDouble()
은 컬렉션에 있는 값을 모두 더했을 때 overflow 가 발생할 수 있기 때문에 코틀린 1.5 부터는 deprecated 예고를 하고,sumOf()
를 권장함
sumOf()
의 경우sumBy()
와 마찬가지로 람다를 인자로 받지만 이 람다가 반환하는 값의 타입을 이용하여 합계를 계산하기 때문에 overflow 가 발생할 여지가 있는 경우 람다가 값을 BigInteger, BigDecimal 로 변환하여 돌려주면 됨예를 들어 list.sumByDouble { it.price } 가 너무 커질 경우 list.sumOf { it.price.toBigDecimal() } 이라고 하면 큰 범위의 실수로 합계를 구할 수 있음
data class Product(
val desc: String,
val price: Double,
)
fun main() {
val products =
listOf(
Product("paper", 2.0),
Product("pencil", 7.0),
)
val result1 = products.sumOf { it.price }
println(result1) // 9.0
val result2 = products.sortedByDescending { it.price }
println(result2) // [Product(desc=pencil, price=7.0), Product(desc=paper, price=2.0)]
val result3 = products.minByOrNull { it.price }
println(result3) // Product(desc=paper, price=2.0)
}
take()
, drop()
은 각각 첫 번째 원소를 취하거나 제거하고, takeLast()
, dropLast()
는 각각 마지막 원소를 위하거나 제거한다.
4개 함수 모두 취하거나 제거할 대상을 지정하는 람다를 받는 버전도 있다.
fun main() {
val list1 = listOf('a', 'b', 'c', 'X', 'z')
val list2 = listOf('a', 'b', 'c', 'X', 'Z')
var result1 = list1.takeLast(3)
println(result1) // [c, X, z]
var result2 = list1.takeLastWhile { it.isUpperCase() }
var result3 = list2.takeLastWhile { it.isUpperCase() }
println(result2) // []
println(result3) // [X, Z]
var result4 = list1.drop(1)
println(result4) // [b, c, X, z]
var result5 = list1.dropWhile { it.isUpperCase() }
var result6 = list2.dropWhile { it.isUpperCase() }
println(result5) // [a, b, c, X, z]
println(result6) // [a, b, c, X, Z]
var result7 = list1.dropWhile { it.isLowerCase() }
var result8 = list2.dropWhile { it.isLowerCase() }
println(result7) // [X, z]
println(result8) // [X, Z]
}
2.6. Set 의 연산
위에서 본 List 연산들 중 대부분은 Set 에도 사용이 가능하다.
단, findByFirst()
처럼 컬렉션에 저장된 원소 순서에 따라 결과가 달라질 수 있는 연산을 Set 에 적용하면 실행할 때마다 다른 결과를 내놓을 수 있는 점에 유의해야 한다.
fun main() {
val set = setOf("a", "ab", "ac")
// maxByOrNull()
// 컬렉션이 비어있으면 null 을 반환하기 때문에 결과 타입이 null 이 될 수 있는 타입임
val result1 = set.maxByOrNull { it.length }?.length
println(result1) // 2
// filter()
val result2 =
set.filter {
it.contains('b')
}
println(result2) // [ab]
// map()
val result3 = set.map { it.length }
println(result3) // [1, 2, 2]
}
filter()
, map()
을 Set 에 적용하면 List 로 결과를 반환받는다.
3. 멤버 참조: ::
함수 인자로 멤버 참조 ::
를 전달할 수 있다.
멤버 함수나 프로퍼티 이름 앞에 그들이 속한 클래스 이름과 ::
를 위치시켜서 멤버 참조를 만들 수 있다.
3.1. 프로퍼티 참조: sortedWith()
, compareBy()
인자로 넘길 predicate 에 대해 람다를 넘길 수도 있지만, Message::isRead 라는 프로퍼티 참조를 넘길 수도 있다.
아래는 프로퍼티에 대한 멤버 참조 예시이며, Message::isRead 가 멤버 참조이다.
data class Message(
val sender: String,
val text: String,
val isRead: Boolean,
)
fun main() {
val message =
listOf(
Message("Assu", "Hello", true),
Message("Silby", "Bow!", false),
)
val unread = message.filterNot(Message::isRead)
println(unread) // [Message(sender=Silby, text=Bow!, isRead=false)]
println(unread.size) // 1
println(unread.single().text) // Bow!
}
객체의 기본적인 대소 비교를 따르지 않도록 정렬 순서를 지정해야 하는 경우 프로퍼티 참조가 유용하다.
sorted()
를 호출하면 원본의 요소들을 정렬한 새로운 List 를 리턴하고 원래의 List 는 그대로 남아있다.
sort()
를 호출하면 원본 리스트를 변경한다.
data class Message1(
val sender: String,
val text: String,
val isRead: Boolean,
)
fun main() {
val messages =
listOf(
Message1("Assu", "AAA", true),
Message1("Assu", "CCC", false),
Message1("Silby", "BBB", false),
)
// isRead 가 false 순으로 정렬 후 text 순으로 정렬
val result1 =
messages.sortedWith(
compareBy(
Message1::isRead,
Message1::text,
),
)
// [Message1(sender=Silby, text=BBB, isRead=false), Message1(sender=Assu, text=CCC, isRead=false), Message1(sender=Assu, text=AAA, isRead=true)]
println(result1)
}
sortedWith()
는 comparator
를 사용하여 리스트를 정렬하는 라이브러리 함수이다.
3.2. 함수 참조
List 에 복잡한 기준으로 요소를 추출해야 할 경우 이 기준을 람다로 넘길수도 있지만, 그러면 람다가 복잡해진다.
이럴 때 람다를 별도의 함수로 추출하면 가독성이 좋아진다.
코틀린은 함수 타입이 필요한 곳에 바로 함수를 넘길수는 없지만 대신 그 함수에 대한 참조를 넘길 수 있다.
data class Message2(
val sender: String,
val text: String,
val isRead: Boolean,
val attachments: List<Attachment>,
)
data class Attachment(
val type: String,
val name: String,
)
// Message2.isImportant() 라는 확장 함수
fun Message2.isImportant(): Boolean =
text.contains("Money") ||
attachments.any {
it.type == "image" && it.name.contains("dog")
}
fun main() {
val messages =
listOf(
Message2(
"Assu",
"gogo",
false,
listOf(Attachment("image", "cute dog")),
),
)
// any() 에 확장 함수에 대한 참조를 전달
val result = messages.any(Message2::isImportant)
println(result) // true
}
Message2 를 유일한 파라메터로 받는 최상위 수준 함수가 있다면 이 함수를 참조로 전달할 수 있다.
최상위 수준 함수에 대한 참조를 만들 때는 클래스 이름이 없기 때문에 ::함수명
처럼 쓴다.
ata class Message3(
val sender: String,
val text: String,
val isRead: Boolean,
val attachments: List<Attachment3>,
)
data class Attachment3(
val type: String,
val name: String,
)
// Message2.isImportant() 라는 확장 함수
fun Message3.isImportant(): Boolean =
text.contains("Money") ||
attachments.any {
it.type == "image" && it.name.contains("dog")
}
fun ignore(message: Message3): Boolean = !message.isImportant() && message.sender in setOf("Assu", "Silby")
fun main() {
val message =
listOf(
Message3("Assu", "gogo!", false, listOf()),
Message3(
"Assu",
"gogo!",
false,
listOf(
Attachment3("image", "cute dog"),
),
),
)
// 최상위 수준 함수에 대한 참조 전달
val result1 = message.filter(::ignore)
val result2 = message.filter(::ignore).size
println(result1) // [Message3(sender=Assu, text=gogo!, isRead=false, attachments=[])]
println(result2) // 1
val result3 = message.filterNot(::ignore)
val result4 = message.filterNot(::ignore).size
println(result3) // [Message3(sender=Assu, text=gogo!, isRead=false, attachments=[Attachment3(type=image, name=cute dog)])]
println(result4) // 1
}
3.3. 생성자 참조: mapIndexed()
클래스 명을 이용하여 생성자에 대한 참조를 만들수도 있다.
아래에서 names.mapIndexed() 는 생성자 참조인 ::Student
를 받는다.
mapIndexed()
는 1.3. 람다 파라메터가 여러 개인 경우:mapIndexed()
를 참고하세요.
data class Student(
val id: Int,
val name: String,
)
fun main() {
val names = listOf("Assu", "Silby")
// mapIndexed() 에 인덱스와 원솔ㄹ 명시적으로 생성자에 넘김
val students =
names.mapIndexed { index, name -> Student(index, name) }
println(students) // [Student(id=0, name=Assu), Student(id=1, name=Silby)]
// mapIndexed() 에 생성자 참조를 인자로 넘김
val result = names.mapIndexed(::Student)
println(result) // [Student(id=0, name=Assu), Student(id=1, name=Silby)]
}
위처럼 함수와 생성자 참조를 사용하면 단순히 람다로 전달해야 하는 긴 파라메터 리스트를 지정하지 않아도 되기 때문에 람다를 사용할 때보다 가독성이 좋아진다.
3.4. 확장 함수 참조: times()
확장 함수에 대한 참조는 참조 앞에 확장 대상 타입 이름을 붙이면 된다.
fun Int.times12() = times(12)
class Dog
fun Dog.speak() = "Bow!"
fun goInt(
n: Int,
g: (Int) -> Int,
) = g(n)
fun goDog(
dog: Dog,
g: (Dog) -> String,
) = g(dog)
fun main() {
val result1 = goInt(11, Int::times12)
val result2 = goDog(Dog(), Dog::speak)
println(result1) // 132 (11*12 이므로)
println(result2) // Bow!
}
4. 최상위 함수와 프로퍼티 (정적인 유틸리티 클래스 없애기)
4.1. 최상위 함수: @JvmName
정적 유틸리티 함수를 사용하고 싶을 경우 최상위 함수를 사용하면 됨
객체지향 언어인 자바에서는 모든 코드를 클래스의 메서드로 작성해야 한다.
그럴 경우 특정 연산을 객체의 인스턴스 API 에 추가해서 사용해야 한다.
그 결과 다양한 정적 메서드를 모아두는 역할만 담당하며, 특별한 상태나 인스턴스 메서드는 없는 클래스들이 생겨났다.
JDK 의 Collections
클래스가 그 전형적인 예시이다.
코틀린에서는 이런 무의미한 클래스가 필요없다.
대신 함수를 소스 파일의 최상위 수준, 클래스의 밖에 위치시키면 된다.
이런 함수들은 여전히 그 파일의 맨 앞에 정의된 패키지의 멤버 함수이므로 다른 패키지에서 그 함수를 사용하고 싶을 때는 그 함수가 정의된 패키지를 임포트해야 하지만 임포트 시 유틸리티 클래스의 이름이 추가로 들어갈 필요는 없다.
test() 라는 함수를 Join.kt 라는 파일에 최상위에 정의해보자.
package com.assu.study.kotlin2me.chap03
fun test(): String = "TEST"
JVM 은 클래스 안에 있는 코드만을 실행할 수 있기 때문에 컴파일러는 이 파일을 컴파일할 때 새로운 클래스를 정의해준다.
만일 위의 test() 함수를 자바 등 다른 JVM 언어에서 호출하고 싶다면 코드가 어떻게 컴파일되는지 알아야 test() 같은 최상위 함수를 사용할 수 있으므로 코틀린이 위의 파일을 컴파일한 결과를 자바 코드로 한번 보자.
package com.assu.study.kotlin2me.chap03;
public class JoinKt {
public static String test() {
return "TEST";
}
}
코틀린 컴파일러가 생성하는 클래스 이름은 최상위 함수가 들어있던 코틀린 소스 파일의 이름과 동일하며, 코틀린 파일의 모든 최상위 함수는 이 클래스의 정적인 메서드가 된다.
따라서 자바에서 test() 를 호출할 때는 아래와 같이 호출하면 된다.
import com.assu.study.kotlin2me.chap03.JoinKt;
// ...
JoinKt.test();
만일 코틀린 최상위 함수가 포함되는 클래스의 이름을 변경하고 싶으면 파일에 @JvmName
애너테이션을 파일의 맨 앞, 패키지 이름 선언 이전에 위치시키면 된다.
코틀린
@file:JvmName("StringFunctions") // 클래스 이름을 지정하는 애너테이션
package com.assu.study.kotlin2me.chap03 // @file:JvmName 애너테이션 뒤에 패키지 문이 와야 함
fun test(): String = "TEST"
그러면 자바에서 아래와 같이 호출할 수 있다.
자바
import com.assu.study.kotlin2me.chap03.StringFunctinos;
// ...
StringFunctions.test();
@JvmName
애너테이션 문법에 대한 상세한 설명은 추후 다룰 예정입니다. (p. 113)
4.2. 최상위 프로퍼티: const
함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다. 이런 프로퍼티 값은 정적 필드에 저장된다.
어떤 데이터를 클래스 밖에 위치시키는 경우는 흔치 않지만 가끔은 유용한 경우가 있다.
예) 연산을 수행하는 횟수를 지정하는 var 프로퍼티
package com.assu.study.kotlin2me.chap03
var opCount = 0 // 최상위 프로퍼티
fun performOperation() {
opCount++ // 최상위 프로퍼티 값 변경
}
fun readOperation() {
println("opCount: $opCount") // 최상위 프로퍼티 값 읽음
}
fun main() {
performOperation()
performOperation()
readOperation() // opCount: 2
}
최상위 프로퍼티의 또 다른 사용 예시는 1.1. 소수의 특별한 타입을 위한 확장 함수 이용 의
Any.name
을 참고하세요.
최상위 프로퍼티 값으로 상수를 추가할 수도 있다.
val UNIX_LINE_SEPARATOR = "\n"
최상위 프로퍼티도 다른 프로퍼티처럼 접근자 메서드를 통해 자바 코드에 노출된다.
val 는 getter 가 생성되고, var 는 getter/setter 가 생성된다.
상수인데 getter 를 사용하면 자연스럽지 못하므로 이 상수를 public static final 필드로 컴파일하려면 const
변경자를 추가하면 된다.
단, primitive 타입과 String 타입의 프로퍼티만 const 지정이 가능하다.
// const 변경자 추가 시 컴파일하면 public static final 로 컴파일됨
const val UNIX_LINE_SEPARATOR = "\n"
위 코드가 컴파일되면 아래와 같이 된다.
public static final String UNIX_LINE_SEPARATOR = "\n";
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 브루스 에켈, 스베트라아 이사코바 저자의 아토믹 코틀린 과 드리트리 제메로프, 스베트라나 이사코바 저자의 Kotlin In Action 을 기반으로 스터디하며 정리한 내용들입니다.
- 아토믹 코틀린
- 아토믹 코틀린 예제 코드
- Kotlin In Action
- Kotlin In Action 예제 코드
- Kotlin Github
- 코틀린 doc
- 코틀린 lib doc
- 코틀린 스타일 가이드