- 확장 함수
코틀린은 자바와 100% 호환하는 것을 목표로 한다
자바 코드에서 코틀린 함수를 추가하고 싶은데
자연스럽게 코틀린 코드를 추가해서 유지보수를 효율적으로 관리하고 싶다는 니즈에서 확장 함수가 등장했다
자바로 만들어진 라이브러리를 유지보수, 확장할 때 코틀린 코드를 덧붙이는 과정에서
어떤 클래스안에 있는 메소드처럼 호출할 수 있지만 가독성을 위해 함수는 밖에 만들 수 있게 하는 방식이 확장 함수 이다
즉, 함수의 코드 자체는 클래스 밖에 작성되어 있지만 마치 클래스 안에 있는 멤버 함수 처럼 호출해서 사용할 수 있다
fun main() {
val str = "ABC"
println(str.lastChar())
}
/*
fun 확장하려는클래스.함수명(parameter): 리턴타입 {
this를 이용하여 실제 클래스 안의 값에 접근하여 로직 작성
}
*/
fun String.lastChar(): Char {
return this[this.length - 1]
}
fun 을 통해 함수임을 명시하고, 확장하려는 클래스명을 작성하여 해당 클래스에 함수를 확장할 수 있다
this를 수신객체 라고 부르며, 확장하려는 클래스를 수신객체 타입이라고 부른다
호출 시 lastChar(str)이 아닌 str.lastChar()로 마치 클래스 안에 있는 멤버 함수를 호출하는 것 처럼 사용하면 된다
- 확장 함수의 특이점 5가지
1. 확장함수가 public이고, 확장함수에서 수신객체 클래스의 private 함수를 가져오면 캡슐화가 깨지는것 처럼 보인다
그래서 확장함수는 애초에 클래스에 있는 private 또는 protected 멤버를 가져올 수 없다
2. 멤버함수와 확장함수의 시그니처가 같다면?
public class Person {
private final String firstName;
private final String lastName;
private int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public int nextYearAge() {
System.out.println("멤버 함수");
return this.age + 1;
}
...
}
import com.lannstark.lec16.Person
fun Person.nextYearAge(): Int {
println("확장 함수")
return this.age + 1
}
fun main() {
val person = Person("A", "B", 100)
person.nextYearAge()
}
다음과 같은 Person 클래스가 있을 때 nextYearAge()을 확장 함수를 통해 코틀린으로 구현하고 호출한다면
자바의 멤버함수 nextYearAge와 코틀린의 확장함수 nextYearAge중 어떤게 먼저 호출될까?
멤버함수가 우선해서 호출된다
이점을 주의하여 확장함수를 작성해야 한다!
3. 확장함수가 오버라이드 된다면?
open class Train(
val name: String = "무궁화호",
val price: Int = 5_000
)
fun Train.isExpensive(): Boolean {
println("Train 확장 함수")
return this.price >= 10000
}
class Ktx : Train("Ktx", 40_000)
fun Ktx.isExpensive(): Boolean {
println("Ktx의 확장 함수")
return this.price >= 10000
}
다음과 같이 Train을 상속 받은 Ktx 객체가 있다
양쪽 모두 확장 함수를 작성한 상태에서 함수를 호출할 경우 어떻게 될까?
fun main() {
val train: Train = Train()
train.isExpensive()
val ktx1: Train = Ktx()
ktx1.isExpensive()
val ktx2: Ktx = Ktx()
ktx2.isExpensive()
}
결과를 보면 결국 해당 변수의 현재 타입을 기준으로 호출된다
즉, 정적인 타입에 의해 어떤 확장함수가 호출될지 결정되는 것을 알 수 있다
4. 자바에서 코틀린 확장 함수를 사용할 경우
코틀린에서 클래스 외에 함수를 선언할 경우 자바 코드로 변환해보면 static 함수로 선언되기 때문에
자바에서 사용 시에는 정적 팩토리 메서드를 사용할 때 처럼 사용해야 한다
// StringUtilsKt.kt
fun String.lastChar(): Char {
return this[this.length - 1]
}
public static void main(String[] args) {
StringUtilsKt.lastChar("ABC");
}
5. 확장 프로퍼티로도 사용이 가능
fun String.lastChar(): Char {
return this[this.length - 1]
}
val String.lastChar: Char
get() = this[this.length - 1]
custom getter와 동일한 방식으로 확장 프로퍼티로 사용이 가능하다
- infix 함수 (중위 함수)
fun Int.add(other: Int): Int {
return this + other
}
infix fun Int.add2(other: Int): Int {
return this + other
}
fun main() {
3.add(4)
3.add2(4)
3 add2 4
}
infix 예약어를 사용하게 되면 '3 add2 4' 문법을 통해서도 호출이 가능하게 된다
예시는 확장 함수로 작성했지만, 멤버 변수에서도 사용이 가능하다
- inline 함수
함수가 호출되는 대신, 함수가 호출한 지점에 함수 본문을 그대로 복붙하고 싶은 경우 사용한다
inline fun Int.add3(other: Int): Int {
return this + other
}
fun main() {
3.add3(4)
}
해당 코드를 바이트 코드로 변경한 후 자바 코드로 디컴파일 시켜보면
public static final int add3(int $this$add3, int other) {
int $i$f$add3 = 0;
return $this$add3 + other;
}
public static final void main() {
byte $this$add3$iv = 3;
int other$iv = 4;
int $i$f$add3 = false;
int var10000 = $this$add3$iv + other$iv;
}
함수를 호출하는 것이 아니라 덧셈하는 로직 자체가 복사된 것을 알 수 있다
이러한 방식을 사용하는 이유는
함수를 파라미터로 전달할 때 오버헤드를 줄일 수 있다
(함수를 계속 중첩해서 쓰는 경우. 함수가 또 다른 함수를 부르고 함수를 부르고.. 함수 call chain에 대한 오버헤드가 생김)
하지만 inline 함수의 사용은 성능 측정과 함께 신중하게 사용되어야 한다
또한, 코틀린 라이브러리에서는 어느정도 최적화가 되어있기 때문에 적절하게 inline 함수가 붙어 있다
- 지역 함수
함수 안에 또 다른 함수를 선언하는 것을 지역 함수라고 한다
fun createPerson(firstName: String, lastName: String): Person {
if (firstName.isEmpty()) {
throw IllegalArgumentException("firstName 값이 비어있습니다. 현재 값: ${firstName}")
}
if (lastName.isEmpty()) {
throw IllegalArgumentException("lastName 값이 비어있습니다. 현재 값: ${lastName}")
}
return Person(firstName, lastName, 1)
}
유효성 체크 시 중복되는 코드를 제거할 때 사용할 수 있다
아래와 같이 지역 함수를 통해 리팩토링할 수 있다
fun createPerson(firstName: String, lastName: String): Person {
fun validateName(name: String, fieldName: String) {
if (name.isEmpty()) {
throw IllegalArgumentException("${fieldName} 값이 비어있습니다. 현재 값: ${name}")
}
}
validateName(firstName, "firstName")
validateName(lastName, "lastName")
return Person(firstName, lastName, 1)
}
함수를 추출하면 좋을 것 같은데
이 함수를 지금 함수 내에서만 사용하고 싶을 때 사용하면 된다
하지만 depth가 깊어지기도 하고, 코드가 깔끔하지 않아 추천하지 않음
위와 같은 코드는 차라리 Person 클래스에서 private validation 코드를 작성하여 검증해주는 편이 더 좋다
'개발 지식 > 자바 개발자를 위한 코틀린 입문' 카테고리의 다른 글
자바 개발자를 위한 코틀린 입문 - scope function (1) | 2023.08.11 |
---|---|
자바 개발자를 위한 코틀린 입문 - 코틀린 함수형 프로그래밍 (0) | 2023.08.10 |
자바 개발자를 위한 코틀린 입문 - Sealed Class (0) | 2023.08.09 |
자바 개발자를 위한 코틀린 입문 - 상속 방법과 주의점 (0) | 2023.08.09 |
자바 개발자를 위한 코틀린 입문 - 동일성/동등성 비교 방법 (0) | 2023.08.07 |