class Cage {
private val animals: MutableList<Animal> = mutableListOf()
fun getFirst(): Animal {
return animals.first()
}
fun put(animal: Animal) {
this.animals.add(animal)
}
fun moveForm(cage: Cage) {
this.animals.addAll(cage.animals)
}
}
abstract class Animal(val name: String)
abstract class Fish(name: String) : Animal(name)
class GoldFish(name: String) : Fish(name)
class Crap(name: String) : Fish(name)
fun main() {
val cage = Cage()
cage2.put(Crap("잉어"))
// type miss match 오류
val carp: Carp = cage2.getFirst()
}
- cage에 잉어만 있지만 getFirst() 메소드를 호출하면 Animal이 나온다.
- 이 문제를 해결하기 위해서 제네릭을 사용한다.
class Cage2<T> {
private val animals: MutableList<T> = mutableListOf()
fun getFirst(): T {
return animals.first()
}
fun put(animal: T) {
this.animals.add(animal)
}
fun moveForm(cage: Cage2<T>) {
this.animals.addAll(cage.animals)
}
}
fun main() {
val cage2 = Cage2<Crap>()
cage2.put(Crap("잉어"))
val crap: Crap = cage2.getFirst()
}
- 제네릭 를 사용하면 Cage2 클래스를 생성할 때 타입을 지정할 수 있다.
fun main() {
val goldFishCage = Cage2<GoldFish>()
goldFishCage.put(GoldFish("금붕어"))
val fishCage = Cage2<Fish>()
// type miss match 오류
fishCage.moveForm(goldFishCage)
}
- Cage 간의 함수 호출인데 왜 GlideFish를 Fish로 옮길 수 없을까?
- 이는 제네릭과 무공변을 알아야 한다.
- 상위 타입이 들어가는 자리에 하위 타입이 대신 위치할 수 있다
- Cage2에 Cage2를 넣을 수 없다.
- Cage2에 Cage2는 아무관계가 없다
- Cage2는 무공변 (in-variant, 불공변)하다 라고 말한다.
- 왜 Fish와 GoldFish 간의 상속관계까 제네릭 클래스에서 유지되지 않을까?
- 왜 제네릭 클래스는 타입 파라미터 간의 상속관계까 있더라도 무공변할까?
- Java의 배열은 제네릭과 다르다.
- A 객체가 B 객체의 하위 타입이라면, A 배열이 B 배열의 하위 타입으로 간주된다.
- Java의 배열은 공변 하다.
String[] strs = new String[]{"A", "B", "C"}
Object[] objs = strs // String[]은 Object[]의 하위 타입이니 objs에 strs를 넣을 수 있다.
objs[0]=1; // 컴파일상 가능함
- objs는 Object[] 타입이니 1을 넣을 수 있을 것처럼 컴파일상의 문제는 없어 보이지만 objs는 사실 String[] 이기 때문에 int를 넣을 수 없다.
- 때문에 런터임 때 에러가 발생한다.
- 타입 세이프하지 않는 코드로 위험하다.
import java.util.List;
List<String> strs = List.of("A", "B", "C")
List<Object> objs = strs // Type Missmatch!
- List는 제네릭을 사용하기 때문에 공변인 Array와 다르게 무공변하다.
- 위 코드는 컴파일 때에 원천적으로 불가능하다.
- 그러기 때문에 제네릭은
class Cage2<T> {
fun moveForm(cage: Cage2<out T>) {
this.animals.addAll(cage.animals)
}
}
- out을 붙이면 moveForm 함수를 호출할 때 Cage2는 공변하게 된다.
- 변성을 준다는 것은 무공변에서 무공변으로 된다.
- out을 통해서 변셩(variance)를 주었기 때문에 out을 variance anootation 이라고도 부른다.
fun getFirst(): T {
return animals.first()
}
fun put(animal: T) {
this.animals.add(animal)
}
fun moveForm(otherCage: Cage2<out T>) {
otherCage.getFirst() // 사용 가능
otherCage.put(this.getFirst()) // 오류 발생
this.animals.addAll(otherCage.animals)
}
- otherCage는 데이터를 꺼내는 getFirst() 함수만 사용할 수 있다.
- otherCage는 생상자 (데이터를 꺼내는)역할만 할 수 있다.
- 허용해주는 경우 타입 안전성이 깨져 런타임 오류가 발생한다.
- this는 잉어를 말하지만 otherCage는 금붕어를 말하는 것이기 때문에 잉어를 옮기는 것은 불가능하다
fun moveForm(otherCage: Cage2<in T>) {
otherCage.animals.addAll(this.animals)
}
- in이 붙은 otherCage는 데이터를 받을 수만 있다. otherCage는 소비자이다.
- out: (함수 파라미터 입장에서) 생상자, 공변
- in: (함수 파라미터 입장에서) 소비자, 반공변
- 코틀린의 List는 불변 컬렉션이라 데이터를 꺼낼 수만 있다.
- 그러나 contains(), containsAll()은 타입파라미터 E를 받아야 한다.
- 이런 경우 @UnsafeVariance를 사용한다.
- 원래는 out 선언지점변성을 활용해 E를 함수 파라미터에 쓸 수 없지만, @UnsafeVariance를 이용해 함수 파라미터에 사용할 수 있다.
- 우리는 Cage 클래스에 Animal만 사용하고 싶다.
- 타입 파라미터에는 Int나 String도 들어올 수 있다.
- 타입 파라미터 T에 Animal과 Animal의 하위 타입만 들어오게 하고 싶은 경우 타입 파라미터에 제약 조건을 줄 수 있다. 이를 제네릭 제약 이라고 한다.
class Cage5<T : Animal> {}
위 처럼 구현할 수 있다.
class Cage5<T>(
private val animals: MutableList<T> = mutableListOf()
) where T : Animal, T : Comparable<T> {
fun getFirst(): T {
return animals.first()
}
fun put(animal: T) {
this.animals.add(animal)
}
}
- 를 통해서 non-null 타입 한정에 사용 가능
- 제네릭은 JDK 초기 버전 부터 있던 개념이 아니다. 그러기 때문에 이전 버전과 호환성을 지켜야 한다.
- List도 런타임 때는 타입 정보 String을 제거 하는 방향으로 호환성을 지켰다.
- 이로 인해 Java에서는 raw type을 만들 수 있다. List list = List.of(1, 2, 3); (권장되는 방식은 아님)
- 코틀린은 언어 초기부터 제네릭이 고려되었기 때문에 raw type을 만들 수 없다.
- 하지만 코틀린 JVM 위에서 동작하기 때문에 런타임 때는 타입 정보가 사라진다. 이를 타입 소거라 부른다.
- 런타임 환경에서는 List인지는 확인이 가능해도 String인지 확인할 수 없다.
- star projection, 해당 타입 파라미터에 어떤 타입이 들어 있을지 모른다는 의미
- 타입 정보만 모를뿐, List의 기능을 사용할 수는 있다.
- data가 List 타입 이니 데이터를 가져올 수 는 있지만 정확히 어떤 타입인지는 모르기 때문에 Any로 받아야 한다.
- MutableList 안에 어떤 타입이 들어 있을지 모르니 데이터를 넣을 수는 없다.
fun main() {
val num = 3
num.toSuperString()
// 정상 동작
println("${num::class.java}: $num")
val str = "ABC"
str.toSuperString()
// 정상 동작
println("${str::class.java}: $str")
}
private fun <T> T.toSuperString() {
// 오류 발생 Cannot use 'T' as reified type parameter. Use a class inst
println("${T::class.java}: $this")
}
- 클래스 이름: Value를 출력하는 toSuperString 메소드가 있다.
- 하지만 위 코드는 컴파일을 할 수 없다. 제네릭 함수에서 타입 파라미터 T에 대해서 클래스 정보를 가져오려고 하는 순간 이 타입 파라미터 정보는 런타임 때 소거 되기 때문에 정보를 가져올 수 없어서 에러가 발생하게 됩니다.
- 반면 클래스 제네릭 타입이 아닌 경우는 타입을 가져올 수 있기 때문에 정상 동작한다.
- 하지만 우리는 T의 정보를 가져오고 싶은 경우가 있다. 이럴 때 inline를 + reified 사용한다.
inline fun <reified T> T.toSuperString() {
println("${T::class.java}: $this")
}
- inline + reified를 시용하면 런타임 시점에도 타입 파라미터 T의 정보가 소실하지 않게 된다.
- inline 함수는 코드의 본문을 호출 지점으로 이동시켜 컴파일 되는 함수를 의미한다.
- T 자체를 본문에 옮겨 쓰게 되기 때문에 이 자체가 제네릭의 타입 파라미터로 간주되지 않는다.
- reified 키워드도 함계가 있다. reified 키워드가 붙은 T를 이용해 T의 인스턴스를 만들거나 T의 companion object를 가져올 수 없다.
- 변성이란 어떤 클래스에 계층관계가 있다고 하고 이런 계층 관계가 있는 클래스가 제네릭 클래스의 타입 파라미터로 들어왔을 때 제니릭 클래스의 타입 파라미터에 따라 제네릭 클래스의 상속관계가 어떻게 되는지를 나타내는 용어
- 타입 파라미터끼리 상속관계가 있더라도 제네릭 클래스로 넘어오면 상속관계가 없다는 의미
- 제네릭 클래스가 무공변한 이유는 자바의 배열은 기본적으로 본경하기 때문에 타입이 안전하지 않는 문제가 많이 생길 수 있다. 그래서 이 문제를 해결 하기 위해서 제네릭 클래스는 아예 원천적으로 차단하기 위해서 무공변하게 설계 되었다.
- co에서도 알 수 있듯이 협력적인 의미가 있다.
- 타입 파라미터의 상속관계가 제네릭 클래스에도 동일하게 유지된다는 의미이다.
- 예를 들어 물고기는 금붕어의 상위 타입이니까 물고기 케이지는 그붕어 케이지의 상위 타입으로 간주된다.
- 기본적으로 제네릭은 무공변하지만 코틀린에서는 out 변셩 키워드를 사용하면 제네릭 클래스의 특정 지점 혹은 전체 지점에서 공변하게 만들 수 있다.
- 반공변은 공변과 반대로 생각하면 된다.
- 타입 파라미터의 상속 관계가 제네릭 클래스에서 반대로 유지된다는 의미이다.
- 코틀린에서는 in 변셩 키워드를 사용해서 제네릭 클래스의 특정 지점 혹은 전체 지점에서 반공변하게 만들 수 있다.
- 선언 지점 변성은 클래스 자체를 선언할 때 이 클래스 전체를 공변하게 하거나 반공변하게 하는 것을 의미한다.
- 클래스 자차게 공변하거나 반공변하려면 그에 따라 데이터를 생상만 하거나 소비만 해야 한다.
- 사용지점 변셩이란 특정 함수 또는 특정 변수에 대해 공변, 반공변을 만드는 방법이다.
- out, in 키워드를 사용해서 특정 지점에서만 공변하거나 반공변하게 만들 수 있다.
- 제네릭 제약은 타입 파라미터에 대한 제약을 주는 것을 의미한다.
- lateinit 키워드는 변수의 인스턴스화 시점과 변수 초기화 시점을 분리하고자 할 때 사용됩니다.
- lateinit 변수는 컴파일 단계에서 nullable 변수로 바꾸고, 변수에 접근하려 할 때 null이면 예외가 발생한다.
- lateinit은 주로 초기화 시점이 명확하지 않지만, 사용하기 전에 반드시 초기화될 변수에 사용됩니다. 이는 클래스의 프로퍼티가 생성자에서 초기화되지 않고, 나중에 초기화될 수 있음을 나타냅니다. 그러나 이 변수는 기본 타입(primitive types)에는 사용할 수 없고, non-null 타입의 객체 참조에만 사용할 수 있습니다.
- lateinit은 primitive 타입에 사용할 수 없다. 코틀린으 Int / Long은 자바의 int, long으로 변환된다. 그런데 lateinit은 nullable 변수로 변혼환되어야 한다. 그러기 때문에 primitive 타입에 사용할 수 없다.
lateinit
키워드가 non-null 타입에만 사용될 수 있는 이유는 lateinit
변수가 나중에 초기화될 것이라는 약속을 통해 null safety를 보장하기 위함입니다. 코틀린은 null safety를 중요한 언어 기능 중 하나로 취급하며, 개발자가 null 관련 오류를 컴파일 시점에 잡을 수 있도록 설계되었습니다.
lateinit
을 사용하는 주된 목적 중 하나는 non-null 타입의 프로퍼티를 선언하면서 동시에 즉시 초기화하지 않아도 되게 하는 것입니다. 이는 특히 객체의 초기화 과정이 복잡하거나, 초기화에 필요한 정보가 인스턴스 생성 시점에는 불완전하거나 불확실할 때 유용합니다. 예를 들어, Android 개발에서 액티비티의 뷰가 생성되는 시점이나 의존성 주입이 완료되는 시점은 인스턴스 생성 시점과 다를 수 있습니다.
만약 lateinit
변수가 null을 허용하는 타입이라면, 이 변수가 초기화되었는지 여부를 체크하는 추가적인 null 체크 로직이 필요하게 됩니다. 이는 코틀린의 null 안전성 원칙과 상충되며, lateinit
의 주된 목적인 "나중에 반드시 초기화될 것"이라는 약속을 약화시키게 됩니다. 따라서, lateinit
은 non-null 타입에만 사용되어, 변수가 사용되기 전에 반드시 초기화될 것임을 보장하고, 이를 통해 코드 내에서 안전하게 non-null 타입을 사용할 수 있도록 합니다. 이러한 설계는 코틀린이 추구하는 타입 안전성과 일관성을 유지하는 데 기여합니다.
class Person {
lateinit var name: String
val isKim: Boolean
get() = name.startsWith("김")
val maskingName: String
get() = name.substring(0, 1) + "*".repeat(name.length - 1)
}
- lateinit 사용 방법
public final class Person {
public String name;
@NotNull
public final String getName() {
String var10000 = this.name;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("name");
}
return var10000;
}
- 해당 코드를 디컴파일 해보면 lateinit 변수가 사용되는 부분에서 null 체크를 하고 있다.
- nullable 변수인 name에 초기화 여부를 검증하여 호출 시점에는 초기화가 완료되야지만 정상적인 호출이 가능하다.
- 이러한 구조이기 때문에 lateinit 변수는 non-null 타입에만 사용할 수 있다.
- 객체 인스턴스화 시점과 변수의 초기화 시점을 분리하고 싶을때 사용한다.
- 변수를 초기화 할 때 지정된 로직을 1회만 실행 시키고 싶다.
- 값을 가져오는 비용이 크고, 해당 변수가 사용되지 않ㄹ을 수도 있다면 초기화 로직을 1회만 실행 시키고 싶을 수 있다.
class Person {
val name: String
get() {
Thread.sleep(2_000)
return "홍길동"
}
}
- name을 사용하지 않으면
Thread.sleep(2_000)
이 실행되지 않지만, nmae을 쓸때마다 sleep이 실행된다.
class Person {
val name: String
init {
Thread.sleep(2_000)
name = "홍길동"
}
}
Thread.sleep(2_000)
은 1회만 호출되지만, name을 지정하지 않는 경우에도 sleep이 호출된다.
class Person {
private val _name?: String? = null
val name: String
get() {
if (_name == null) {
Thread.sleep(2_000)
_name = "홍길동"
}
return _name!!
}
}
- name이 필요한 시점에 실행되며, 한 번 실행하면 그 이후에는 실행되지 않는다.
- 꼭 필요한 경우에만 초기화 로직을 실행하고 싶을 때 사용한다.
class Person {
private val name by lazy {
Thread.sleep(2_000)
"홍길동"
}
}
- lazy는 코틀린에서 제공하는 함수이고, 함수를 파라미터로 받는다.
- 이 함수는 name의 getter가 최초 호춣 될때 실행되고, 기본적으로 Thread-Safe 하다.
- lateinit 초기화를 지연시킨 변수, 초기화 로직이 여러 곳에 위치할 수 이싿. 초기화 없이 호출하면 예외가 발생한다.
- lazy 초기화를 get 호출 전으로 지연시킨 변수, 초기화 로직은 변수 선언과 동시에 한 곳에만 위치할 수 있다.
class Person {
private val _name?: String? = null
val name: String
get() {
if (_name == null) {
Thread.sleep(2_000)
_name = "홍길동"
}
return _name!!
}
}
객체 인스턴스와 시점과 변수의 초기화 시점을 분리하고 동시에 최초로 변수를 한번만 초기화 싶은 경우 위와 같은 코드로 작성할 수 있다. 해당 코드를 반복 템플릿화 하면 다음같이 작성할 수 있다.
class LazyInitProperty<T>(val init: () -> T) {
private var _value: T? = null
val value: T
get() {
if (_value == null) {
this._value = init()
}
return _value!!
}
}
class Person {
private val delegateProperty = LazyInitProperty {
Thread.sleep(2_000)
"홍길동"
}
val name: String
get() = delegateProperty.value
}
- Person의 getter가 호출되면, 곧바로 LazyInitProperty의 getter가 호출된다.
- 이런 패턴을 위임패턴 이라고 부른다.
- getValue, SetValue가 있어야 by를 사용할 수 있다.
class LazyInitProperty<T>(val init: () -> T) {
private var _value: T? = null
val value: T
get() {
if (_value == null) {
this._value = init()
}
return _value!!
}
operator fun getValue(thisRef: Any, property: KProperty<*>): T {
return value
}
}
class Person {
val name: String by LazyInitProperty {
Thread.sleep(2_000)
"홍길동"
}
}
- getValue 코드를 작성하면 Person에서 by LazyInitProperty 바로 사용할 수 있다.
class Person {
val age: Init by notNull()
}
- primitive type에는 lateinit을 사용할 수 없지만 notNull()을 사용할 수 있다.
public inline fun <T> obserable(
initialValue: T,
crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit
)
- observable은 onChange함수는 setter가 호출될 때마다 호출 된다.
class Person4 {
var age: Int by Delegates.observable(20) { _, oldValue, newValue ->
if (oldValue != newValue) {
println("oldValue: $oldValue, newValue: $newValue")
}
}
}
setter가 호출될 때 onChange 함수가 true를 반환하면 변경 적용, false를 반환하면 인전 값이 그대로 남는다.
class Person6 {
@Deprecated("이름을 사용하지 않는다", ReplaceWith("age"))
var num: Int = 0
val age: Int by this::num
}
- 프로퍼티 앞에 ::를 붙이면, 위임 객체로 사용할 수 있다.
- 코드 사용자들이 age로 코드를 바꾸면 그때 num을 제거하면 된다.
class Person7(map: Map<String, Any>) {
val name: String by map
val age: Int by map
}
- geeter가 호출되면 map["name"], map["age"]를 호출한다.
예를 들어 2,000,000개의 랜덤 과일 중 사과를 골라 10,000개의 평균을 계산해보자
data class Fruit(val name: String, val price: Long)
val avg = fruits
.filter { it.name > "사과" }
.map { it.price }
.take(10_000)
.average()
- 주어진 200만건의 과일 중 사과를 골라 임시 List을 만든다.
- 앞에서 만들어진 임시 List에서 가격만 골라 List을 만든다.
- 마지막으로 List에서 10,000개만 골라 평균을 계산한다.
- 최초에 있던 200만건의 과일들에서 사과만을 한번 피렅링 하며, 이때 리스트가 임시적으로 만들어진다.
- 그리고 다시 한 번 가격만을 필터링하는 가정에 중간 임시 컬렉션이 생긴다
- 얀산의 각 단계마다 중간 컬렉션이 임시로 생성된다.
- 예를 들어 전체 과일의 20% 정도가 사과라고 하면 이제 처음 200만 개의 20%인 40만 개 짜리 임시List가 만들어지고 다시 40만개 짜리 List가 만들어진다.
- 중간 컬렉션을 만들지 않는다.
- 각 단계(filter, map)가 모든 원소에 적용에 적용되지 않을 수 있다.
- 한 원소에 대해 모든 연산을 수행하고, 다음 원소로 넘어간다.
- 최종연산이 나오기 전까지 계산 자체를 미리 하지 않는다. 이를 지연연산 이라고 한다.
fruits
// 중간 연산 시작
.asSequence()
.filter { it.name > "사과" }
.map { it.price }
.take(10_000)
// 중간 연산 종료
.average() // 최종 연산
- 첫 번째 원소에 대해서 필터링 시도, 사과가 아니라 skip
- 두 번째 원소는 사과로 필터링 통과 map과 take 연산을 수행
- 10,000개의 원소가 나올 때까지 반복
- 총 200만개 중 40만개가 사과라면 39만개의 사과는 아예 검사조차 하지 않는다.
- 10,000개가 다 모이면 더 이상 filter, map 연산을 수행하지 않고 평균을 계산한다.
- 해당 행위는 최종 연산의 평균을 구하는 작업으로 average 작업을 하지 않으면 실제 로직은 수행되지 않고 average를 구하는 순간 연산들이 적용이 된다. 이러기 때문에 지연연산이라고 한다.
- 대략 Iterable은 85초, Sequence는 1초가 걸린다.
- 항상 Sequence가 빠르지 않는다 모수가 적은 경우 경우 Iterable이 더 빠를 수 있다.
- 고차 함수: 파라미터로 함수를 받거나 함수를 반환하는 함수
fun add(a: Int, b: Int): Int {
return a + b
}
fun compute(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int {
return operation(num1, num2)
}
- 파라미터에 함수를 받고 있음으로 고차함수이다.
// 람다식
compute(5, 2) { a, b -> a + b }
compute(
num1 = 5,
num2 = 2,
operation = { a, b -> a + b }
)
// 익명 함수
compute(5, 2, fun(a: Int, b: Int): Int {
return a + b
})
- 람다식, 익명 함수를 함숙밧 또는 함수 리터널 이라고 합니다.
- 함수 리터럴이란 소스 코드의 고정된 값을 나타내는 표기법
- 함수값/함수 리터럴: 일반 함수와 달리 변수로 간주하거나 파라미터에 넣을 수 있는 함수
- 람다: 이름이 없는 함수
- 람다식: 함수값 / 힘수 리터럴을 표현 하는 방법 1
- 익명함수: 함수값 / 힘수 리터럴을 표현 하는 방법 2
// 람다식
compute(5, 2) { a, b -> a + b }
// 익명 함수
compute(5, 2, fun(a: Int, b: Int): Int {
return a + b
})
- 람다식은 반환 타임을 적을 수없다.
- 람다식 안에는 return을 쓸 수 없다.
- 익명 함수는 반환 타입을 적을 수 있다.
- 익명 함수에서는 return을 쓸 수 있다.
fun main() {
iterate(listOf(1, 2, 3, 4, 5)) { num ->
if (num == 3) {
return // 'return' is not allowed here
}
println(num)
}
}
- return 가장 가까운 fun 키워드를 종료하는 기능
- 위 코드는 main 함수가 가장 가까운 fun 키워드이기 때문에 에러가 발생한다. 이를 non-local return이라고 한다.
val add = fun Int.(other: Long): Int = this + other.toInt()
- 컴파일 해보면 function2 라는 객체로 만들어진다.
- 고차함수에서 삼수를 넘기면, FunctionN 클래스로 변환된다.
- 함수를 변수처럼 사용할 때 마다 코틀린은 함수를 1급 시민으로 다루기 때문에 FunctionN 객체가 만들어진다.
- 고차 함수를 사용하게 되면 FunctionN 클래스가 만들어지고 인스턴스화 되어야 하므로 오버헤드가 발생할 수 있다.
- 함수에서 변수를 포획할 경우, 해당 변수를 Ref라는 객체로 감싸야한다. 때문에 오버헤드가 발생할 수 있다.
- 고차함수를 쓰지만, 성능 부담을 없앨 수 있는 방식이 inline 함수이다.
- 함수를 호출하는 쪽에 함수 본문을 붙여 넣게 된다.
// 코틀린 코드
fun main() {
val num1 = 1
val num2 = 2
val result = add(num1, num2)
}
inline fun add(num1: Int, num2: Int): Int {
return num1 + num2
}
public final class _15강Kt {
public static final void main() {
int num1 = 1;
int num2 = 2;
int $i$f$add = false;
int var10000 = num1 + num2;
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final int add(int num1, int num2) {
int $i$f$add = 0;
return num1 + num2;
}
}
- add 함수 호출 출력 대신에 더샘 로직 자체가 메인 함수안들오 들어가 있다. 이것이 inline 함수이다.
- 이러한 특성으로 고차 함수 즉 다른 함수를 받는 함수를 inline 하게 되면 그때마다 함수 콜이 반복해서 발생하지 않으니 조금 더 성능을 끌어올릴 수 있다.
fun main() {
repeat(2) { println("Hello World") }
}
inline fun repeat(times: Int, exec: () -> Unit) {
for (i in 1..times) {
exec()
}
}
// 디컴파일
public static final void main() {
int i$iv = 1;
while(true) {
String var5 = "Hello World";
System.out.println(var5);
if (i$iv == var3) {
return;
}
++i$iv;
}
}
- repeat 함수 전체라 main 함수 안으로 들어가 있다.
- exec 함수에 넣었던 println 까지 인라이닝 되었다. 즉 인라이닝 함수느 기본적으로 나 자신 함수 외에도 내가 받고 있는 파라미터까지 인라인을 진행한다.
fun main(exec: () -> Unit) {
repeat(2) { exec }
}
- 만약 exec를 외부에서 넘겨 받아서 사용한다면, exec 함수는 인라인 되지 않는다.
// 디컴파일
public static final void main(@NotNull Function0 exec) {
Intrinsics.checkNotNullParameter(exec, "exec");
int times$iv = 2;
while(true) {
exec.invoke(); // exec을 알 수 없기 때문에 인라인 되지 않는다.
if (i$iv == var4) {
return;
}
++i$iv;
}
}
- mian 함수에 Function0를 넘겨 받고 있다.
- exec을 알 수 없기 때문에 인라인 되지 않는다.
inline fun repeat(times: Int, noinline exec: () -> Unit) {
for (i in 1..times) {
exec()
}
}
- noinline 키워드를 사용하면 exec 함수는 인라인 되지 않는다.
- noinline 함수는 인라인에만 관여하지 않고 non-local return에도 사용할 수 있게 해준다.
inline fun iterate(numbers: List<Int>, exec: (Int) -> Unit) {
for (number in numbers) {
exec(number)
}
}
iterate(listOf(1, 2, 3, 4, 5)) { num ->
if (num == 3) {
return // 'return' is not allowed here, 으로 에러가 발생 했지만 iterate 함수에 inline 키워드를 추가 하면 non-local return이 가능하다.
}
println(num)
}
- iterate와 exec이 함께 main 안으로 들어가기 때문에 non-local return이 가능하다.
- 단, 이 return은 main 함수를 return 하게된다. (가장 가까운 fun 키워드를 종료하기 때문에)
- 이것을 방지 하기 위해서는 crossinline 키워드를 사용한다.
@FunctionalInterface
public interface Runnable {
public abstract void run();
}
- Sing Abstract Method의 약자로 인터페이스에 단 하나의 추상 메소드만 있는 경우를 의미
- 대표적으로 자바의 Runnable, Callable, Comparator 등이 있다. 이것을 SAM 인터페이스라고 말한다.
fun main(exec: () -> Unit) {
val filter: StringFilter = { s -> s.startsWith("A") } // 컴파일 오류
val filter: StringFilter = StringFilter { s -> s.startsWith("A") } // StringFilter 명시하면 가능
}
- 자바에서는 SAM 인터페이스를 람다(자바의 람다)로 인스턴스화 할 수 있다.
- 코틀린에서는 SAM 인터페이스를 람다(코틀린의 람다)로 인스턴스화 할 수 없다.
- 코틀린에서
StringFilter { s -> s.startsWith("A") }
코드 전체를 SAM 생성자라고 한다. - 코틀린에서는 자바와 다르게 인스턴스화하고 싶은 인터페이스 이름과 람다식을 함께 작성 해야한다.
fun main() {
consumeFilter({ s -> s.startsWith("A") })
}
fun consumeFilter(filter: StringFilter) {}
- 만약 변수에 넣을게 아니라, 파라미터에 넣을거라면 바로 람다식을 사용할 수 있다.
- 암시적인 SAM 인스턴스화가 이루어질 경우에는 의도하지 않은 SAM 인스턴스화가 발생할 수 있다.
- 함수 이름까지 똑같은 두개의 타겟 함수가 있으면 consumeFilter라고만 암시적으로 람다식만 작성하면 이 둘 중 조금더 구체적인 StrinfFilter가 호출된다.
- 이런 현상을 막으려면 SAM 생성자를 직접적으로 작성하면된다.
- 코틀린에서 SAM 인터페이스를 만들려면 추상 메소드가 1개인 인터페이스 앞에 fun을 붙이면된다.
- 물론 코틀린에서는 함수를 1급 시민으로 간주해서 옮길 수 있기 때문에 굳이 SAM 인터페이스를 만들 필요가 없다.
- 어노테이션은 붙여 개발자에게 의견을 알리거나, 특별한 일이 일어나도록 만들 수 있다.
- 특별한 일은 리플렉션과 어노테이션이 합쳐졌을 때 일어날 수 있다.
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Shape
- @Retention: 어노테이션이 저장되고 유지되는 방식을 제어한다.
- SOURCE: 컴파일러가 코드를 컴파일할 때만 어노테이션 정보를 유지한다.
- BINARY: 컴파일러가 클래스 파일을 생성할 때까지 어노테이션 정보를 유지한다.
- RUNTIME(기본 값): 클래스 파일이 JVM에 로딩될 때까지 어노테이션 정보를 유지한다.
- @Target: 어노테이션을 어디에 붙일지 선택할 수 있다.
- CLASS: 클래스, 인터페이스, 객체, 어노테이션 클래스
- ANNOTATION_CLASS: 어노테이션 클래스
- TYPE_PARAMETER: 제네릭 타입 파라미터
- PROPERTY: 프로퍼티
- FIELD: 필드
- LOCAL_VARIABLE: 지역 변수
- VALUE_PARAMETER: 파라미터
- CONSTRUCTOR: 생성자
- FUNCTION: 함수
- PROPERTY_GETTER: 프로퍼티 getter
- PROPERTY_SETTER: 프로퍼티 setter
- TYPE: 타입 사용
- EXPRESSION: 표현식
- FILE: 파일
- TYPEALIAS: 타입 별칭
- ALL: 모든 대상
annotation class Shape(
val text: String,
val number: Int,
val clazz: KClass<*>
)
- KClass: 코드로 작성한 클래스를 표현한 클래스
- 어노테이션의 정확한 위치가 중요하다.
- val name은 geeter 이기도 하기 때문에 getter 함수에 붙인것으로도 해석할 수 있다.
- 코틀린의 간결한 문법은 한 위치에 다양한 언어적 요소가 위치할 수 있게한다. 그러기 떄문에 정획히 어떤 요소에 어노테이션을 붙였는지 알려주어야 한다.
@get:Shape val name
으로 정확하게 getter에 붙였다는 것을 명시할 수 있다. 이런 문법을 use-site target 이라고 한다.- 여러 언오 요소에 어노테이션을 붙일 수 있다면 param > property > field 순서로 동작한다.
@Target(AnnotationTarget.FILE)
annotation class Shape
- 만약 어노테이션이 Target을 지정해주고 있다면, 해당 언어 요소에 어노테이션이 붙게 된다.
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FILE, AnnotationTarget.CLASS)
@Repeatable
annotation class Shape(
val text: String,
val number: Int,
val clazz: KClass<*>
)
@Shape("a", 1, Sample::class)
@Shape("b", 1, Sample::class)
class Sample {
}
- 코틀린에서는 자바와 달리 Repeatable 어노테이션을 추가하여 반복적으로 어노테이션을 사용할 수 있다.
@Repeatable
이 없으면 Sample 클래스에@Shape
어노테이션을 복수개 설정할 수 없다.
- 함수 executeAll(obj: Any)를 만든다.
- obj가 @Executable 어노테이션을 갖고 있으면, obj에서 파라미터가 없고 반환 타입이 unit인 함수를 모두 실행한다.
- 결국 reflection api는 우리가 작성한 코드를 표현하는 코드이다. 이러한 정보를 가지고 있는 KClass가 Reflection 객체다
- 예를 들어 왼쪽에 있는 GoldFish 클래스는 name 이라는 프로퍼티를 가지며, swim 이라는 함수를 가지고 있다.
- K class, K Property, K Function 그외에도 KParameter, KType, KAnnotatedElement 등이 있다.
fun main() {
val kClass: KClass<Sample1> = Sample1::class
val ref = Sample1()
val kClass1: KClass<out Sample1> = ref::class
val kotlin: KClass<out Any> = Class.forName("kotlin.reflect.KClass").kotlin
}
Class<T>
는 자바의 리플렉션 객체이고KClass<T>
는 코틀린의 리플렉션 객체이다.- 코틀린의 리플렉션 객체가 별도로 있는 이유는 inner class, inline class, sealed class 등 코틀린만의 특징을 반영하기 위해서이다.
- kClassifier: 클래스인지 타입 파라미터인지 구분
- KAnnotatedElement: 어노테이션이 붙을 수 있는 언어 요소
- KClass: 코틀린 클래스 표현
- KType: 코틀린에 있는 타입 표현
- KParameter: 코틀린에 있는 파라미터를 표현
- KTypeParameter: 코틀린에 있는 타입 파라미터를 표현
- KCallable: 호출될 수 있는 언어 요소를 표현
- KFunction: 코틀린에 있는 함수를 표현
- KProperty: 코틀린에 있는 프로퍼티를 표현