코틀린은 클래스를 상속하거나 데코레이터 패턴과 같은 디자인 패턴을 사용하지 않고도 클래스를 확장할 수 있는 기능을 제공합니다.
예를 들어 일반적으로 수정할 수 없는 코틀린의 표준 라이브러리에 기능을 추가하기 위해 확장을 사용할 수 있습니다.
MyStringExtensions.kt
fun String.first(): Char {
return this[0]
}
fun String.addFirst(char: Char): String {
return char + this.substring(0)
}
fun main() {
println("ABCD".first()) // 출력 : A
println("ABCD".addFirst('Z')) // 출력 : ZABCD
}
확장 함수 내부의 this는 확장의 대상이 되는 객채의 참조입니다. (이런한 것을 receiver 혹은 수신자 객체라고 부릅니다.)
자바로 변환을 했을 때 확장 함수는 실제론 대상 객체를 수정하지 않습니다.
자바 내부적으로 static 메서드를 만듭니다.
첫번째 인자로 확장 대상 객체를 사용합니다.
import org.jetbrains.annotations.NotNull;
public final class Java_MyStringExtensionsKt {
public static final char first(@NotNull String $this) {
return $this.charAt(0);
}
}
확장하려는 클래스에 동일한 명칭의 함수가 존재할 경우 클래스 내부의 함수가 우선이 됩니다.
class MyExample {
fun printMessage() = println("메인 클래스 출력")
}
fun MyExample.printMessage() = println("메서드 출력")
fun String.lowercase(b : String): String {
return b.uppercase()
}
fun main() {
MyExample().printMessage() // 메인 클래스 출력
println(a.lowercase()); // abcd 출력
}
null인 경우 내부에서 this == null 과 같은 형태로 null 검사를 수행할 수 있습니다.
이렇게 처리하면 안전연산자 '?'로 체크 할 필요없이 호출할 수 있습니다.
// 메서드에 안전연산자를 붙힘
fun MyExample?.printNullOrNotNull() {
if (this == null) println("널인 경우에만 출력")
else println("널이 아닌 경우에만 출력")
}
fun main() {
var myExample: MyExample? = null
// null이 확정적으로 들어있어도 실행
myExample?.printNullOrNotNull()
// 원래는 안전 연산자가 없고 null이 확정적으로 들어있으면 오류
myExample.printNullOrNotNull() // 하지만 메서드 내에서 null check를 했기에 정상 실행
// 출력 : 널인 경우에만 출력
myExample = MyExample()
myExample.printNullOrNotNull()
// 출력 : 널이 아닌 경우에만 출력
}
코틀린의 클래스는 자바와 마찬가지로 타입 파라미터 를 가질 수 있습니다.
class MyGenerics<T>(val t: T)
제네릭을 사용한 클래스의 인스턴스를 만들려면 타입 아규먼트를 제공하면 됩니다.
fun main() {
val generics = MyGenerics<String>("테스트")
}
또한 인자를 통해 타입 추론이 가능한 경우 타입 아규먼트를 생략할 수 있습니다.
fun main() {
val generics = MyGenerics("테스트")
}
타입 추론이 가능하기 때문에 변수의 타입에 타입 아규먼트를 추가해도되고 그렇지 않은 경우 타입 아규먼트를 생성자에서 추가해도 됩니다.
val list1: MutableList<String> = mutableListOf()
val list2 = mutableListOf<String>()
어떤 타입이 들어올지 알수 없지만 안전하게 사용하고 싶은 경우 스타 프로젝션 구문을 제공합니다
val list: List<*> = listOf<String>("테스트")
제네렉에서 파라미터화된 타입이 서로 어떤 관계에 있는 지 설명하는 개념입니다.
변성은 크게 공변성, 반공변성 그리고 무공변성으로 나뉩니다.
이펙티브 자바에선 공변성과 반공변성을 설명할때 PECS 규칙을 언급합니다.
PECS는 Producer-Extends, Consumer-Super의 약자입니다.
공변성을 쓰지 않아 문제가 되는 케이스
※ CharSequence는 String의 부모 클래스입니다.
class MyGenerics<T>(val t: T)
fun main() {
val generics = MyGenerics<String>("테스트")
val charGenerics : MyGenerics<CharSequence> = generics // 컴파일 오류
}
String이 아닌 타입을 넣지 못하게 막으므로 의미있는 컴파일 오류지만 특정 상황에선 공변성이 필요할 수 있습니다.
공변성이 필요한 상황에서는 공변성 키워드인 out을 사용해서 해결할 수 있습니다.
class MyGenerics<out T>(val t: T)
fun main() {
val generics = MyGenerics<String>("테스트")
val anygenerics : MyGenerics<CharSequence> = generics
}
out을 사용하면 MyGenerics<CharSequence>가 MyGenerics<String>의 상위 타입이므로 공변성이 적용됩니다.
반공변성을 쓰지 않아 문제가 되는 케이스
class Bag<T> {
fun saveAll(
to: MutableList<T>,
from: MutableList<T>,
) {
to.addAll(from)
}
}
fun main() {
val bag = Bag<String>()
// in이 없다면 컴파일 오류
bag.saveAll(mutableListOf<CharSequence>(""), mutableListOf<String>(""))
}
이때 반공변성 키워드인 in을 사용해 상위 타입도 전달 받을 수 있도록 합니다.
class Bag<T> {
fun saveAll(
to: MutableList<in T>,
from: MutableList<T>,
) {
to.addAll(from)
}
}
fun main() {
val bag = Bag<String>()
bag.saveAll(mutableListOf<CharSequence>(""), mutableListOf<String>(""))
}
공변성과는 반대로 Bag<String>가 Bag<CharSequence>의 상위 타입이 되므로 반공변성입니다.
in, out 어떤 것도 지정하지 않은 경우에는 무공변성이며 String은 CharSequence의 하위 타입이고 MyGenerics<String>와 MyGenerics<CharSequence>는 아무 관계도 아니고 아무런 영향을 주지 않는 상황을 말합니다.
출처 : fastcampus