5 minute read

코틀린에서 컬렉션 만들기

  • 자바에서 자주 사용되는 컬렉션들을 코틀린에서는 어떻게 사용하는지 알아보자.
val set = hashSetOf(1, 7, 53) // 1, 7, 53이 들어있는 hashSet
val list = arrayListOf(1, 7 , 53) // 1, 7, 53이 들어있는 arrayList
val map = hashMapOf(1 to "one", 7 to "Seven", 53 to "fifty-three") // {1: "one", 7:"Seve", 53: "fifty-tree"} 형태의 hashMap
hashMapOf를 선언할 때 사용한 to는 특별한 키워드가 아니라 일반 함수이다.
위에서 생성한 객체가 어떤 클래스에 속하는지 확인해보자.

위의 결과에서 확인할 수 있는 점은 코틀린이 자체 컬렉션을 제공해서 그것을 사용하는 것이 아니라,
표준 자바 컬렉션 클래스를 그대로 사용한다는 점이다.
하지만 코틀린에서는 리스트의 마지막 원소를 가져오거나, 컬렉션의 최대 값등을 찾을 수 있는 더 많은 기능을 제공한다.
아래의 예시를 확인해보자.

함수를 호출하기 쉽게 만들기

자바에서 컬렉션의 모든 원소를 출력하기 위해서는 toString() 메소드를 오버라이딩해서 구현해야 할 것이다.
그렇지 않으면 컬렉션의 해쉬코드가 출력될 것이다.
하지만 코틀린에서는 파이썬처럼 컬렉션을 print문으로 출력하면 사용자가 원하는대로 모든 원소를 출력해준다.

만약 "," 로 구분하는 것이 아닌 ";"로 구분한다거나, "["가 아닌 "("로 컬렉션을 둘러싸고 싶다면 어떻게 해야할까?
그런 기능을 할 수 있는 함수를 직접 만들어보자.

잘 작동한다. 하지만 이 함수의 호출 부분을 좀 더 다듬을 수 있을 것 같다.

이름 붙인 인자

위의 함수에 인자를 전달할 때 각 인자가 무엇을 의미하는지 알기가 어렵다.
만약 지금은 그렇지 않더라도, 함수에서 받아야하는 인자가 10개라면 시그니처를 살펴보지 않고는 구분하기 힘들 것이다.
아래와 같이 코틀린에서는 파이썬처럼 인자에 이름을 명시하여 전달할 수 있다.
println(joinToString(list, separator = "; ", prefix = "(", postfix = ")")) 
println(joinToString(list, prefix = "(", postfix = ")", separator = "; ")) // 이름을 명시하면 순서를 변경해도 된다.
어느 하나라도 이름을 명시하게 되면 그 뒤에 오는 모든 인자는 이름을 꼭 명시해야 한다.
또한 이름을 명시하면 순서를 변경해도 된다.

코틀린은 JDK 6와 호환되기 때문에 불행히도 자바로 작성한 코드를 호출할 때는 이름 붙인 인자를 사용할 수 없다.

디폴트 파라미터 값

코틀린에서는 파이썬처럼 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있다.
디폴트 값을 사용하면 자바에서 많이 사용되는 오버로드 중 상당수를 피할 수 있다.
디폴트 값을 사용해 joinToString 함수를 개선해보자.

이렇게 디폴트 파라미터를 사용하면 디폴트 파라미터가 없는 인자만 전달하거나, 디폴트 파라미터가 없는 인자와
디폴트 값을 변경하고 싶은 인자만 전달등이 순서와 관계없이 가능하게 된다.

자바에는 디폴트 파라미터 값이라는 개념이 없어서 디폴트 파라미터가 달린 코틀린 함수를 자바에서 호출하길 원하면
@JvmOverLoads 어노테이션을 사용하여 함수의 파라미터를 하나씩 생략한 오버로딩 메소드를 추가할 수 있다.

정적인 유틸리티 클래스 없애기: 최상위 함수와 프로퍼티

자바 프로그램을 작성할 때 어느 객체의 메소드로써 작동하는 것이 아닌, 유틸리티로써 사용하는 함수를 만드는 경우가 많다. 
그런 경우 또 그러한 유틸리티 함수들만 모아놓은 특별한 상태나 객체를 만들지 않는 유틸리티 클래스들이 생겨나게 된다.

코틀린에서는 이런 무의미한 클래스가 필요없고 파이썬처럼 함수를 파일의 최상위 수준, 클래스 밖에 위치시킬 수 있다.
당장의 지금까지 작성한 main 함수만 보더라도 클래스에 속하지 않고 파일의 최상위에 선언되어 있다.

최상위 함수는 어떻게 실행될 수 있을지 아래의 예시를 보자

Test라는 코틀린 파일에 com.chohongjae.kotlinTraining 라는 package를 선언하고, 최상위에 joinToString 함수를 선언하였다.
JVM은 클래스 안에 들어있는 코드만을 실행할 수 있기 때문에 컴파일러는 이 파일을 컴파일할 때 "코틀린 파일이름의 클래스"를 정의해준다.

따라서 Test.kt 파일의 클래스에 속하지 않은 모든 최상위 함수들은 컴파일하면 TestKt 클래스에 정적 메소드가 된다.
즉 Test.kt 파일을 컴파일하면 아래와 같은 클래스가 생기게 된다.
package com.chohongjae.kotlinTraining;

public class TestKt {
    public static String joinToString(...) {...};
}
따라서 코틀린에서는 아래와 같이 import 해서 사용하면 되고

자바에서도 아래와 같이 class를 import 해서 사용하면 된다.

함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓을 수 있다.
어떤 연산의 수행한 횟수를 저장하는 파일의 최상위에 위치한 프로퍼티를 만들어보자.
var opCount = 0

fun performOperation() {
    opCount ++
}

fun main(args: Array<String>) {
    println(opCount) // 0
    performOperation()
    println(opCount) // 1
}
이런 최상위 프로퍼티도 사실 접근자 메소드(게터, 세터)를 통해 자바 코드에 노출되기 때문에,    
보통 상수로 활용하려는 목적에서는 자연스럽지 모하다.
따라서 public static final 필드로 컴파일 되어야 하는데, 이렇게 상수로 활용할 경우에는
const 변경자를 추가하면 public static final 필드로 컴파일하게 만들 수 있다.
const var SEPARATOR  = "\n"
public class TestKt {
    public static final String SEPARATOR = "\n";    
}

메소드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티

코틀린에서는 내가 작성한 클래스도 아니고, 내가 소유한 클래스도 아니지만 어떠한 클래스에 원하는
메소드를 추가할 수 있는 확장함수라는 기능을 제공한다.
심지어 해당 클래스가 자바나 코틀린이아니고, JVM 위에서 돌아가기만하면 확장함수를 구현할 수 있다.

즉 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만, 그 클래스의 외부에서 추가적으로 선언된 함수다.

간단하게 코틀린에서 기본적으로 제공되는 String 클래스에 문자열의 마지막 문자를 돌려주는 메소드를 작성해보자. 
package com.chohongjae.kotlinTraining

fun String.lastChar() = this.get(this.length - 1)
확장할 클래스의 이름 뒤에 "." 과 확장하려는 메소드의 이름을 덧붙이기만 하면 된다.
클래스 이름을 수신 객체 타입이라 부르며, 확장 함수가 호출되는 대상이 되는 값(객체)를 수신 객체라고 부른다.
package com.chohongjae.kotlinTraining

fun String.lastChar() = this.get(this.length - 1)
// String -> 수신 객체 타입
// this -> 수신 객체
확장함수도 다른 일반 클래스 멤버를 호출하는 구문과 똑같다.
package com.chohongjae.kotlinTraining

fun String.lastChar() = this.get(this.length - 1)

fun main(args: Array<String>) {
    println("Kotlin".lastChar()) // n
    // String 클래스 -> 수신 객체 타입
    // "Kotlin" -> 수신 객체
}
일반 메소드의 본문에서 this 키워드를 사용할 떄와 마찬가지로 확장 함수 본문에서도 this를 사용할 수 있고,
일반 메소드와 마찬가지로 this를 생략할 수 있다.
또한 확장 함수 내부에서 수신 객체의 메소드나 프로퍼티도 사용할 수 있다.
package com.chohongjae.kotlinTraining

fun String.lastChar() = get(length - 1)

임포트와 확장 함수

확장 함수를 사용하기 위해서는 그 함수를 다른 클래스나 함수와 마찬가지로 임포트해서 사용하면 된다.
import com.chohongjae.kotlinTraining.lastChar

fun main(args: Array<String>) {
    println("JAVA".lastChar())
}

자바에서 확장 함수 호출

내부적으로 확장 함수는 수신 객체를 첫 번째 인자로 받는 정적 메소드다.
따라서 자바에서 단지 정적 메소드를 호출하면서 첫 번째 인자로 수신 객체를 넘기기만 하면 된다.

확장 함수로 유틸리티 함수 정의

이제 확장 함수를 통해서 아까 만든 joinToString 함수를 개선해보자.
fun <T> Collection<T>.joinToString(separator: String = ", ", prefix: String = "(", postfix: String = ")"): String {
    val result = StringBuilder(prefix)
    for ((index, element) in this.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}

fun main(args: Array<String>) {
    println(arrayListOf("1", "2").joinToString(separator = "; ")) // (1; 2)
}
이와같이 이제 joinToString을 마치 클래스의 멤버 메소드인 것처럼 호출할 수 있다.

확장 함수는 오버라이드할 수 없다.

코틀린의 메소드 오버라이드도 다른 언어의 메소드 오버라이드와 같이 작동한다.
하지만 확장 함수는 오버라이드할 수 없다.
코틀린의 일반적인 메소드 오버라이드부터 확인해보자.
open class View {
    open fun click() = println("View Clicked")
}

class Button : View() {
    override fun click() = println("Button Clicked")
}

fun main(args: Array<String>) {
    val view: View = Button()
    view.click() // Button Clicked
}
하지만 확장 함수는 이런 식으로 작동하지 않는다.
확장 함수는 클래스 밖에 선언되므로, 부모 클래스의 확장 함수를 자식 클래스의 확장 함수에서 오버라이드하더라도
실제로는 확장 함수를 호출할 때, 수신 객체로 지정한 변수의 정적 타입에 의해 어떤 확장 함수가 호출될지 결정된다.
open class View {
    open fun click() = println("View Clicked")
}

class Button : View() {
    override fun click() = println("Button Clicked")
}

fun View.showOff() = println("I'm a view")
fun Button.showOff() = println("I'm a Button")

fun main(args: Array<String>) {
    val view: View = Button()
    view.showOff() // "I'm a view"
}
위와 같이 코틀린은 호출될 확장 함수를 정적으로 결정하기 떄문에 확장 함수를 오버라이드할 수 없다.

어떤 클래스의 확장 함수와 멤버 함수의 이름과 시그니처가 같다면 해당 함수를 실행할 때 멤버 함수가 호출된다.
멤버 함수의 우선수위가 더 높기 떄문이다.

확장 프로퍼티

p.122

Categories:

Updated:

Comments