위의 호출 예시처럼 컬렉션만 인자 값으로 넣어도 나머지 파라미터는 디폴트 값을 사용할 수 있다.
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수 선언 쪽에서 지정된다는 사실을 기억하라.
자바에는 디폴트 파라미터 값이라는 개념이 없어서 코틀린 함수를 자바에서 호출하는 경우에는 그 코틀린 함수가 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다. 그럴 때 @JvmOverloads 애노테이션을 함수에 추가할 수 있다. @JvmOverloads를 함수에 추가하면 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 하나씩 생략한 오버로딩한 자바 메서드를 추가해준다.
- kotlin IN ACTION 109 ~ 110p -
정적인 유틸리티 클래스 없애기: 최상위 함수
자바를 아는 사람은 객체지향 언어인 자바에서는 모든 코드를 클래스의 메서드로 작성해야 한다는 사살을 알고 있다. 보통 그런 구조는 잘 작동한다. 하지만 실전에서는 어느 한 클래스에 포함시키기 어려운 코드가 많이 생긴다. 일부 연산에는 비슷하게 중요한 역할을 하는 클래스가 둘 이상 있을 수도 있다. 중요한 객체는 하나뿐이지만 그 연산을 객체의 인스턴스 API에 추가해서 API를 너무 크게 만들고 싶지는 않은 경우도 있다.
코틀린에서는 이런 무의미한 클래스가 필요 없다. 대신 함수를 직접 소스 파일 최상위 수준, 모든 다른 클래스의 밖에 위치시키면 된다.
아래 예시는 join.kt에 최상위 메서드를 만든 예시이다.
package strings
fun joinToString(...) : String { ... }
자바로 변환하면 아래와 같다.
package strings;
public clas JoinKt { public static String joinToString(...) { ... } }
코틀린 join.kt파일을 컴파일한 결과와 같은 클래스를 자바 코드로 보여준다. 파일 명으로 클래스가 생성되는 것을 볼 수 있다.
최상위 함수가 포함된 클래스의 이름을 바꾸고 싶다면 @JvmName 애노테이션을 사용하면 파일 명을 변경할 수 있다.
@file:JvmName("StringFunctions")
- kotlin IN ACTION 111 ~ 113p -
최상위 프로퍼티
최상위 함수와 마찬가지로 클래스 밖에 프로퍼티를 추가할 수 있다.
상수를 추가하고 싶다면 const val str = ""과 같이 const를 추가하면 상수로 사용할 수 있다.
자바로 변환하면 public static final과 동일하다
- kotlin IN ACTION 113 ~ 114p -
메서드를 다른 클래스에 추가: 확장 함수와 확장 프로퍼티
확장 함수는 소유하고 있는 API가 아니더라도 함수를 추가할 수 있는 기능이다. 그러면 기존 API를 재작성하지 않고도 편리한 기능을 사용할 수 있다.
fun String.lastChar() : Char = this.get(this.length - 1)
위와 같이 String 클래스에 lastChar라는 함수를 추가하여 마지막 자리의 문자를 얻는 기능을 추가하였다. 여기서 String을 수신 객체 타입이라고 하고 this를 수신 객체라고 한다.
이런 게 추가된 함수를 사용하면 아래와 같다.
>>> println("Kotlin".lastChar()) // n
String 클래스에 추가된 lastChar 함수를 바로 사용할 수 있다. 뿐만 아니라 수신 객체 멤버에 this 없이 접근할 수도 있다.
fun String.lastChar() : Char = get(length - 1)
단, 함장 함수는 클래스 내부에서만 사용할 수 있는 private, protected와 같은 멤버를 사용할 수 없다.
확장 함수를 사용하기 위해서는 다른 클래스나 함수와 마찬가지로 임포트 해야만 한다. 확장 함수를 정의하자마자 어디서든 그 함수를 쓸 수 있다면 한 클래스에 같은 이름의 확장 함수가 둘 이상 있어서 이름이 충돌하는 경우가 자주 생길 수 있다.
import strings.lastChar // import strings.* // * 를 사용해도 잘 된다. val c = "Kotlin".lastChar()
한 파일 안에서 다른 여러 패키지에 속해있는 이름이 같은 함수를 가져와 사용해야 하는 경우 이름을 바꿔서 임포트 하면 이름 충돌을 막을 수 있다.
import strings.lastChar as last val c = "Kotlin".last()
따라서 임포트 할 때 이름을 바꾸는 것이 확장 함수 이름 충돌을 해결할 수 있는 유일한 방법이다.
- kotlin IN ACTION 115 ~ 117p -
확장 함수로 유틸리티 함수 정의
이전에 만들었던 joinToString을 최종 버전으로 만들어보자.
fun <T> Collection<T>.joinToString( // Collection<T>에 대한 확장 함수 separator: String, // 디폴트 값 지정 prefix: String, // 디폴트 값 지정 postfix: String): String { // 디폴트 값 지정
val result = StringBuilder(prefix)
for((index, element) in this.withIndex()) { // this는 수신 객체를 가리킨다. 여기서 T의 원소로 이뤄진 컬렉션이다. if (index > 0) result.append(separator) result.append(element) }
result.append(postfix) return result.toString() }
확장 함수를 이용해서 Collection에서 바로 사용할 수 있게 하였다. 또한 구체적인 원소 타입을 지정할 수 있다.
listOf(1, 2, 3).joinToString(separator = "; ", prefix = "(", postfix = ")") // (1; 2; 3) listOf("one", "two", "three").joint(" ") // one two three
- kotlin IN ACTION 118 ~ 119p -
동적 디스패치와 정적 디스패치
실행 시점에 객체 타입에 따라 동적으로 호출될 대상 메서드를 결정하는 방식을 동적 디스패치(dynamic dispatch)라고 한다. 반면 컴파일 시점에 알려진 변수 타입에 따라 정해진 메소드를 호출하는 방식은 정적 디스패치(static dispatch)라고 부른다. 참고로 프로그래밍 언어 용어에서 '정적'이라는 말은 컴파일 시점을 의미하고, '동적'이라는 말은 실행 시점을 의미한다.
- kotlin IN ACTION 120p -
확장 함수는 오버라이드 할 수 없다.
일반적인 오버라이드부터 확인해보자
open class View { open fun click() = println("view clicked" }
class Button : View { override fun click() = println("button clicked") }
val view: View = Button() view.click()
button clicked
위의 예제를 보면 알 수 있듯이 View 타입 변수에 대해 click과 같은 일반 메서드를 호출했는데 click을 Button 클래스가 오버라이드 했다면 실제로 Button이 오버라이드 한 click이 호출된다.
하지만 확장 함수는 클래스 외부에 있는 것이기 때문에 오버라이드가 되지 않는다.
fun View.showOff() = println("I'm a view!") fun Button.showOff() = println("I'm a button!")
val view: View = Button() view.showOff()
I'm a view!
view가 가리키는 객체의 실제 타입은 Button이지만, 이 경우 view의 타입이 View이기 때문에 무조건 View의 확장 함수가 호출된다.
일반 함수는 동적으로 결정되지만, 확장 함수는 정적으로 결정된다.
/* 노트 */ 어떤 클래스를 확장한 함수와 그 클래스의 멤버 ㅂ함수의 이름과 시그니처가 같다면 확장 함수가 아니라 멤버 함수가 호출된다(멤버 함수의 우선순위가 더 높다). 클래스의 API를 변경할 경우 항상 이를 염두에 둬야 한다. 여러분이 코드 소유권을 가진 클래스에 대한 확장 함수를 정의해서 사용하는 외부 클라이언트 프로젝트가 있다고 하자. 그 확장 함수와 이름과 시그니처가 같은 멤버 함수를 여러분의 클래스 내부에 추가하면 클라이언트 프로젝트를 재 컴파일하는 순간부터 그 클라이언트는 확장 함수가 아닌 새로 추가된 멤버 함수를 허용하게 된다.
- kotlin IN ACTION 120 ~ 122p -
메서드를 다른 클래스에 추가: 확장 프로퍼티
확장 함수와 동일하게 수신 객체를 추가해서 프로퍼티를 만들 수 있다.
val String.lastChar get() = get(length - 1)
확장 프로퍼티는 뒷받침하는 필드가 없어서 기본 게터 구현을 제공할 수 없으므로 최소한 게터는 꼭 정의해야 한다. 마찬가지로 초기화 코드에서 계산한 값을 담을 장소가 전혀 없으므로 초기화 코드도 쓸 수 없다.