이번글에서는 모든 프로그램에서 핵심이라 할 수 있는 함수 정의와 호출 기능을 코틀린이 어떻게 개선했는지 살펴본다.
먼저, 코틀린에서 컬렉션을 어떻게 지원하는지와 여러가지 사용방법에 대해 살펴보자.
아래와 같이 컬렉션을 사용할 수 있으며, 표준 자바 컬렉션을 그대로
지원하기 때문에 자바 코드와 상호작용하기에 유리하다.
또한, 코틀린 컬렉션은 자바 컬렉션과 똑같은 클래스지만, 코틀린에서는
자바보다 더 많은 기능을 쓸 수 있다. 예를 들어 리스트의
마지막 원소를 가져오거나 수로 이루어진 콜렉션의 최대값을 찾을 수 있다.
val set: HashSet<Int> = hashSetOf(1, 7, 2) // hashSet
val list: ArrayList<Int> = arrayListOf(1, 7, 2) // arrayList
val map: HashMap<Int, String> = hashMapOf(1 to "one", 2 to "two") // hashMap
val strings: List<String> = listOf("first", "second", "fourteenth") // List
코틀린으로 작성한 함수를 호출할 때는 함수에 전달하는 인자 중 일부(또는 전부)의
이름을 명시할 수 있다.
joinToString(strings, separator = " ", prefix = " ", postfix = ".")
fun joinToString(collections: List<String>, separator: String, prefix: String, postfix: String) {
// ...
}
이름을 붙인 인자는 다음에 살펴볼 디폴트 파라미터 값과 함께 사용할 때 쓸모가 많다.
자바에서 일부 클래스에서 오버로딩한 메소드가 너무 많아진다는 문제가 있다.
java.lang.Thread 구현을 살펴보면 많은 오버로딩을 볼 수 있다.
코틀린에서는 함수 선언에서 파라미터의 디폴트 값을 지정할 수 있으므로
이런 오버로드 중 상당수를 피할 수 있다.
fun <T>joinToString (
collections: Collection<T>,
separator: String = ", ",
prefix: String = "",
postfix: String = "" ) {
}
함수의 디폴트 파라미터 값은 함수를 호출하는 쪽이 아니라 함수 선언하는 쪽에서 지정해야 한다.
자바에는 디폴트 파라미터 값이라는 개념이 없어서 코틀린 함수를 자바에서 호출해야 하는 경우에는 그 코틀린 함수가 디폴트 파라미터 값을 제공하더라도 모든 인자를 명시해야 한다.
이럴때, @JvmOverloads 애노테이션을 함수에 추가할 수 있다. 함수에
추가하면 코틀린 컴파일러가 자동으로 맨 마지막 파라미터부터 파라미터를
하나씩 생략한 오버로딩한 자바 메소드를 추가해준다.
다시말하면, 모든 경우의 메소드를 모두 생성해준다.
@JvmOverloads fun<T> joinToString ( collection: Collection<T>,
// ...
) {}
// 자바에서 아래와 같이 사용
String joinToString(Collection<T> collection, String seperator, String prefix, String postfix);
String joinToString(Collection<T> collection, String seperator);
자바에서는 모든 코드를 클래스 기반에서 작성해야 한다. 하지만 특정 도메인 영역에는 포함되기 어려운 유틸에 관련된 변수나 메소드를 생성해야 할 경우가 있는데, 클래스를 하나 생성하고 그 클래스에 static 멤버로 선언하여 사용하는 것이 보통이다.
코틀린에서는 이런 무의미한 클래스가 필요 없다.
코틀린에서는 코틀린 파일(.kt)의 최상위 수준, 모든 다른 클래스의 밖에 선언된 메서드와 프로퍼티는 static 멤버로 처리한다.
이런 함수나 프로퍼티는 패키지의 멤버이므로 다른 패키지에서 사용하고자 한다면 해당 패키지를 import 해야 한다.
아래와 같이 strings라는 이름의 패키지에 prefix.kt라는 파일을 생성하여 함수 하나를 선언해보자.
package strings
fun prefixAddFirst(s: String, prefix: String): String {
return prefix+s
}
>>> print(prefixAddFirst("hello", "Juhee! "))
Juhee! hello
코틀린도 JVM에서 실행되기 때문에, 코틀린 컴파일러는 이 파일을 컴파일 할 때
코틀린 파일명으로 새로운 클래스를 정의해준다.
예제와 같은 경우 파일명이 prefix.kt이므로 PreFixKt라는 이름으로 클래스가
만들어진다.
컴파일 후에는 자바에서는 다음과 같은 코드로 인식될 것이다.
package strings
public class PrefixKt {
public static String prefixAddFirst(String s, String prefix) {
return prefix+s
}
}
코틀린 최상위 함수가 포함되는 클래스의 이름을 바꾸고 싶다면 파일에 @JvmName 어노테이션을 추가하면 된다.
@file:JvmName("StringFunctions") // 클래스 이름을 지정하는 애노테이션
package strings // 어노테이션 다음에 패키지 문이 와야 한다.
fun jointToString(...): String {...}
함수와 마찬가지로 프로퍼티도 파일의 최상위 수준에 놓일 수 있다.
var opCount = 0 // 최상위 프로퍼티를 선언한다.
fun performOperation() {
opCount++ // 최상위 프로퍼티의 값을 변경한다.
}
fun reportOperationCount() {
print("Operation performed $opCount times") // 최상위 프로퍼티 값을 읽는다.
}
함수와 마찬가지로 최상위 프로퍼티도 정적 필드에 저장 된다.
최상위 프로퍼티를 활용해 val로 선언하면 상수로 사용할 수 있다.
val UNIX_LINE_SEPARATOR = "\n"
기본적으로 최상위 프로퍼티도 일반적인 프로퍼티처럼 접근자 메소드(getter, setter)를 통해 자바코드에 노출된다.
val의 경우 게터, var의 경우 게터와 세터가 생긴다.
이를 자바에서 사용하려면 getter를 이용해야 하므로, 자연스럽지 못하다. 더
자연스럽게 사용하려면 이 상수를 public static final 필드로 컴파일 해야 한다.
const 변경자를 추가하면 프로퍼티를 public static final 필드로
컴파일하게 만들 수 있다.
const val UNIX_LINE_SEPARATOR = "\n"
위 코드는 다음 자바와 동일한 바이트 코드를 만들어 낸다.
public static final String UNIX_LINE_SEPARATOR = "\n"
코틀린에서 확장함수는 어떤 클래스의 멤버 메소드인 것처럼 호출할 수 있지만
그 클래스의 밖에 선언된 함수다.
따라서 확장함수는 이미 정의된 클래스에 새로운 기능을 추가하여
사용하고자 할 때 기존 클래스를 수정하지 않고도 필요한 기능을
추가할 수 있다.
아래는 String 객체를 확장한 함수이다.
fun String.lastChar():Char = this.get(this.length - 1)
// String.lastChar() 중 String을 수신 객체 타입이라고 한다.
// this.get(this.length - 1) 중 this를 수신 객체라고 한다.
확장 함수를 만들려면 추가하려는 함수 이름 앞에 그 함수가 확장할
클래스의 이름을 덧붙이기만 하면 된다.
클래스 이름을 수신 객체 타입(receiver type)이라 부르며, 확장 함수가
호출되는 대상이 되는 값을 수신 객체(receiver object)라고 부른다.
println("kotlin".lastChr())
// 여기서 수신객체는 kotlin이며, 수신객체 타입은 String 이다.
위의 사용은 마치 String 클래스에 새로운 메소드를 추가하는 것과 같다.
위 확장함수 식에서 this는 생략 가능하다.
확장 함수 내부에서 일반적인 인스턴스 메소드의 내부에서와 마찬가지로 수신 객체의 메소드나 프로퍼티를 바로 사용할 수 있지만 캡슐화를 해치치 않는다.
클래스 안에서 정의한 메소드와 달리 확장 함수 안에서 클래스 내부에서만 사용할 수 있는 private 멤버나 protected 멤버를 사용할 수 없다.
확장함수는 프로젝트 안의 모든 소스코드에서 사용할수 있는 것은 아니고, 다른 파일(.kt)에서 사용하기 위해서는 그 함수를 import 해야 한다.
한 파일 안에서 다른 여러 패키지에 속해 있는 이름이 같은 함수를 가져와야 한다면, 충돌을 막기 위해 as 키워드를 이용해서 이름을 변경할 수 있다.
import strings.lastChar as last
val c = "Kotlin".last()
확장 함수는 오버라이드 할 수 없다. 확장 함수는 클래스의 일부가 아니라 클래스 밖에 선언 되기 때문이다.
자바에서는 확장함수를 StringUtil.kt파일에 정의했다면, StringUtilKt.lastChar(“java”)로 호출 가능하다.
확장 함수를 이용하여 확장 프로퍼티를 만들 수 있다. 바로 멤버변수의 accessor를 확장함수로 커스텀 하는 방법인데, 위에서 만난 확장 함수를 확장 프로퍼티로 변경하는 코드를 살펴보자.
fun String.lastChr(): Char = this.get(this.length - 1)
//getter함수를 확장함수로 커스텀 하여 확장프로퍼티로 만듦
val String.lastChr : String
get() = get(length-1)
이런식으로 setter를 커스텀하여서도 가능하다.
var StringBuilder.lastChar: Char
get() = get(length-1)
set(value: Char) {
this.setCharAt(length - 1, value)
}
>>> println("Kotlin".lastChr)
n
>>> val sb = StringBuilder("Kotlin?")
>>> sb.lastChr = '!'
>>> println(sb)
Kotlin!
자바에서는 가변 인자로 타입뒤에 “…” 을 사용한다.
코틀린에서는 vararg 변경자를 붙인다.
//자바
public static void main(String... args) { ... }
//코틀린
fun main(vararg values: String) {
val list = listOf("list=", *values) //*연산자가 배열 내용을 펼쳐줍니다
println(list)
}
main("a","b","c") //"[list=, a, b]" 출력
1 to "one" 처럼 to 를 중간에 사용하는 것을 중위 호출이라고 한다.
이때 to는 일반 메소드이다.
val mapData = mapOf(
1 to "one", //1(객체) to(메소드이름), "one"(유일한 인자)
2 to "two", //to 메소드를 공백으로 구분하여 사용 가능
3.to("three") //to는 메소드이기때문에 .to(...) 로도 사용 가능
)
일반 메소드를 중위 호출이 가능하도록 만드려면 infix 변경자를 메소드
앞에 추가해야 된다.
infix 변경자의 인수는 한개만 가능하다.
//infix 를 사용한 중위 호출 생성
infix fun Any.to(other:Any) = Pair(this, other)
//잘못된 사용. 컴파일 에러. 인수는 한개여야 합니다
infix fun Any.to(other:Any, other2:Any) = Pair(this, other)
코틀린 표준 라이브러리 클래스 중에서 Pair 클래스가 있다.
Pair 클래스는 두 쌍의 값을 가지고 있다.
1 to “one” 중위 호출은 Pair 클래스를 리턴하고,
이 Pair는 (number, name)에 값을 설정한다.
val (number, name) = 1 to "one" //number=1, name="one"
구조 분해 선언은 for 문에서 인덱스와 값을 변수에 저장할 수 있다.
for((index, value) in list.withIndex()) { //withIndex() 를 이용해서 구조 분해 선언
println("$index, $value")
}
Reference
https://lovia98.github.io/blog/kotlin-static.html
https://hongku.tistory.com/360
https://pompitzz.github.io/blog/Kotlin/kotlinInAction.html#%E1%84%8E%E1%85%AC%E1%84%89%E1%85%A1%E1%86%BC%E1%84%8B%E1%85%B1-%E1%84%92%E1%85%A1%E1%86%B7%E1%84%89%E1%85%AE%E1%84%8B%E1%85%AA-%E1%84%8E%E1%85%AC%E1%84%89%E1%85%A1%E1%86%BC%E1%84%8B%E1%85%B1-%E1%84%91%E1%85%B3%E1%84%85%E1%85%A9%E1%84%91%E1%85%A5%E1%84%90%E1%85%B5