2024. 4. 22. 14:44ㆍIT/Kotlin
자바 파일에서 코틀린 함수를 호출할 때 @JvmOverloads 어노테이션을 추가하면, 코틀린 컴파일러가 자동으로 맨 마지막 파라미터로부터 파라미터를 하나씩 생략한 자바 메서드를 추가해 준다. 이렇게 오버로딩된 함수들은 생략된 파라미터에 대해 원래 코틀린 함수의 디폴트 파라미터 값을 사용하게 된다.
자바는 클래스 외부에 선언되는 함수가 존재하지 않기 때문에 자바 파일에서 코틀린의 최상위 함수를 호출하면 컴파일러는 코틀린 최상위 함수가 포함된 소스파일과 확장자를 결합한 이름의 클래스를 생성한다. 예를 들어 코틀린 최상위 함수의 소스 파일 이름이 Hello.kt라면 HelloKt라는 클래스가 생성된다는 뜻이다. 이렇게 생성된 클래스를 이용하여 코틀린의 최상위 함수를 자바에서 정적 메서드로 호출할 수 있게 된다. ( + 코틀린 최상위 함수를 포함한 소스 파일의 패키지 선언 앞에서 @file:JvmName() 어노테이션을 사용하면 생성되는 클래스 이름을 설정할 수 있다. )
코틀린의 최상위 프로퍼티를 정의할 때 const 키워드를 붙이면 자바에서 public static final 필드로 컴파일하게 만들 수 있다. ( 단, 원시 타입과 String타입의 프로퍼티만 const로 지정할 수 있다. )
확장 함수(Extension function)는 클래스의 멤버 메서드처럼 호출하지만 실제로는 클래스 밖에 추가로 선언된 함수다. 확장 함수의 정의는 일반 함수의 정의와 크게 다르지 않는데, 단지 함수의 이름 앞에 덧붙일 클래스 이름을 추가해 주면 된다. 여기서 명시된 클래스 이름을 수신 객체 타입이라 부르고, 실제로 함수를 호출하는 해당 클래스의 객체를 수신 객체라고 부른다. 확장 함수는 인스턴스 메서드와 마찬가지로 해당 수신 객체의 멤버를 사용할 수 있지만, 패키지 혹은 클래스 내부에서 접근이 가능하게 설정된 멤버는 사용할 수 없다. 즉, 접근 제어자의 영향을 받는다.
package com.example.Practice
fun String.halfLength(): Int { //문자열 길이의 절반을 구하는 확장 함수. 수신 객체 타입 String.
return this.length / 2
}
fun main() {
val str = "Hello, world!"
// 확장 함수 호출
val halfLength = str.halfLength() //수신 객체 str.
}
확장 함수가 현재 파일의 함수와 이름이 같아 충돌이 발생한다면 as 키워드를 사용해 import 할 때 이름을 변경하여 충돌을 피할 수 있다.
package com.example.Practice.halfLength as half
fun main() {
val str = "Hello, world!"
val halfLength = str.half()
}
자바 파일에서 코틀린 확장 함수는 수신 객체를 첫 번째 인자로 받는 static 메서드로 취급된다. 위의 최상위 함수와 마찬가지로 컴파일러는 소스파일과 확장자를 결합한 클래스를 생성하고 이렇게 생성된 클래스를 사용하여 호출할 수 있다. 추가로 자바에서 확장 함수는 "수신 객체를 첫 번째 인자로 받는 static 메서드"라고 했으니 첫 번째 파라미터의 타입은 수신 객체 타입이고 따라서 해당 타입의 자손들도 확장 함수를 호출할 수 있음을 알 수 있다.
//functionText.java
val halfLength = PracticeKt.halfLength("Hello");
확장 함수는 클래스 외부에 선언되므로 상속해도 오버라이드 될 수 없다. 따라서 자식 클래스가 부모 클래스와 동일한 시그니처의 확장 함수를 선언할 수 있는데, 이렇게 양 측에 정의된 확장 함수의 실행은 참조 변수 타입에 따라 달라진다. 이는 다시 말하지만 확장 함수는 "수신 객체를 첫 번째 인자로 받는 static 메서드"이기 때문이다. 부모 타입의 참조 변수에 자식 인스턴스의 삽입은 전혀 문제 되지 않는다. 추가로 확장 함수와 멤버 함수의 시그니처가 같다면 멤버 함수가 실행된다.
open class Parent
fun Parent.foo() {
println("Extension function in Parent")
}
class Child : Parent()
fun Child.foo() {
println("Extension function in Child")
}
fun main() {
val parent: Parent = Child()
parent.foo() // 출력: "Extension function in Parent"
val child = Child()
child.foo() // 출력: "Extension function in Child"
}
확장 프로퍼티(Extension property)는 클래스의 멤버가 아니므로 상태를 저장할 수 없고, 일반적으로 해당 타입의 인스턴스에 대한 추가적인 게터/세터를 제공하는 편의 기능으로 사용된다. 상태를 저장할 수 없으니 뒷받침 필드 역시 가질 수 없고 이로 인해 기본 게터를 제공할 수도 없어 꼭 커스텀 게터를 제공해야 한다. +자바에서 확장 프로퍼티를 호출하고 싶다면 확장 함수와 마찬가지로 소스 파일에 기반한 클래스 이름으로 게터를 호출해야 한다.
var StringBuilder.lastChar: Char
get() = get(length - 1)
set(value) {
this.setCharAt(length - 1, value)
}
가변 길이 인자는 메서드를 호출할 때 원하는 개수만큼 값을 인자로 넘기면 자바 컴파일러가 배열에 그 값들을 넣어주는 기능인데, 타입 뒤에 ...를 붙이는 자바와 달리 코틀린에서는 파라미터 앞에 vararg 변경자를 붙여주면 된다. 또, 가변 인자에 배열을 허용하는 자바와 달리 코틀린에서는 스프레드 연산자(*)를 사용하여 배열의 원소들을 풀어서 전달해야 한다.
// 가변 길이 인자를 사용하는 함수 정의
fun sum(vararg numbers: Int): Int {
var total = 0
for (num in numbers) {
total += num
}
return total
}
fun main() {
// 함수 호출 시 원하는 개수만큼 인자를 전달할 수 있습니다.
val result1 = sum(1, 2, 3, 4, 5)
println("Sum 1: $result1") // 출력: Sum 1: 15
val result2 = sum(10, 20, 30)
println("Sum 2: $result2") // 출력: Sum 2: 60
}
중위 호출은 수신 객체와 유일한 인자 사이에 메서드를 넣어 편리하게 호출하는 방법으로, 인자가 하나뿐인 일반 메서드나 인자가 하나뿐인 확장 함수에 중위 호출을 사용할 수 있다. 중위 호출을 허용하는 함수를 정의하려면 메서드 선언 앞에 infix 키워드를 붙이기만 하면 된다. 추가로 Map, Pair 같은 쌍으로 이뤄진 원소를 구조 분해 선언(Destructuring declaration)을 사용하면 두 원소를 한 번에 받을 수 있다.
// 중위 호출을 사용하여 간단한 Pair 객체 생성
infix fun String.to(value: Int): Pair<String, Int> {
return Pair(this, value)
}
fun main() {
// 중위 호출을 사용하여 Pair 객체 생성
val pair = "key" to 10
// 구조 분해 선언을 사용하여 Pair 객체의 각 요소를 분리하여 변수에 할당
val (key, value) = pair
// 결과 출력
println("Key: $key, Value: $value") // 출력: Key: key, Value: 10
}
코틀린은 자바와 달리 Regex 타입이 따로 존재하기 때문에 정규식과 문자열을 서로 혼동하지 않는다. 게다가 자바에서 간단한 연산조차 정규식을 써야 했던 메서드들을 코틀린의 확장 함수를 통해 쉽게 해결할 수 있다. 참고로 이렇게 기존 라이브러리를 새 언어에서 활용하는 패턴을 라이브러리 알선(Pimp My Library) 패턴이라 부른다.
fun main() {
println("12.345-6.A".split("\\.|-".toRegex())) //정규식 객체를 통한 분해. 마침표를 와일드 카드 문자로 인식하지 않도록 이스케이프 시켰다.
println("12.345-6.A".split(".", "-")) //문자를 통한 분해.
}
fun parsePath(path: String) {
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extensionName = fullName.substringAfterLast(".")
println("Dir: $directory, Name: $fileName, Ext: $extensionName")
}
fun main() {
parsePath("/Users/yole/kotlin-book/chapter.adoc")
}
3중 따옴표 문자열 안에서는 어떤 문자도 이스케이프할 필요가 없어, 엔터키로 인한 줄 바꿈도 그대로 출력된다.
fun parsePath(path: String) {
val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
matchResult?.let {
val (directory, fileName, extension) = it.destructured
println("Dir: ${directory}, Name: ${fileName}, Ext: ${extension}")
}
}
fun main() {
parsePath("/Users/yole/kotlin-book/chapter.adoc")
}
메서드 추출을 통한 리팩토링 과정에서 추출된 함수들을 함수 내부에 중첩시켜 중복을 제거할 수 있는데, 이를 로컬 함수라고 부른다.
class Person(val name: String, val age: Int, val phoneNum: String)
fun Person.introduce() {
// 로컬 함수 정의
fun isValidInput(input: String) {
if(input.isEmpty()) {
throw IllegalArgumentException(
"It has a empty field!"
)
}
}
isValidInput(this.name)
isValidInput(this.phoneNum)
println("Name: $name, age: $age, phone: $phoneNum")
}
fun main() {
val person = Person("James", 18, "010-1234-5678")
person.introduce()
}
'IT > Kotlin' 카테고리의 다른 글
Kotlin - Smart cast, Backing field, etc (1) | 2024.04.19 |
---|---|
람다의 무명 클래스 객체 생성 (1) | 2023.10.26 |
변경 가능한 외부 로컬 변수를 포획한 람다, 클로저 (2) | 2023.10.25 |