lateinit 과 by lazy 모두 지연 초기화를 위해 사용되는 것으로만 알고 있었고, lateinit 은 var 에만 사용할 수 있고 by lazy 는 val 에만 사용할 수 있다 정도만 알고 있었습니다.. 어떻게 동작하는지는 관심이 없었는데 시간이 나서 이번 기회에 내부적으로 각각 어떻게 내부적으로 동작하는지를 알고 싶어서 분석하게 되었습니다.
1. lateinit
테스트 코드
class LateinitTest {
private lateinit var name: String
fun test() {
println(name)
}
}
위의 코드를 자바로 변환하면 아래와 같은 자바 코드가 보입니다.
lateinit 의 동작 방식을 간단히 말하면 lateinit 으로 선언된 프로퍼티에 접근하면 내부적으로 프로퍼티에 값을 읽어오기 전에 조건문을 통해 해당 프로퍼티가 null 인지 검사하여 null 이라면 초기화되어있지 않다고 판단하고 UninitializedPropertyAccessException 예외를 던지는 방식으로 간단히 구현되어습니다.
class LateinitTest {
private lateinit var name: String
fun test() {
println(name)
}
}
public final class LateinitTest {
private String name;
public final void test() {
/*
lateinit 으로 지연 초기화가 선언된 프로퍼티에 접근하려고 하면
프로퍼티가 초기화되어있는지 검사하기위한 조건문이 생성된다.
만약 초기화하지 않았다면 UninitializedPropertyAccessException
*/
String var10000 = this.name;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("name");
}
String var1 = var10000;
boolean var2 = false;
System.out.println(var1);
}
}
2. by lazy
테스트코드
class ByLazyTest {
private val name: String by lazy { "test" }
fun print() {
println(name)
}
}
위의 코드를 자바로 변환하면 아래와 같이 보입니다.
by lazy 의 동작 방식을 살펴보면 아래와 같이 동작합니다.
1. 생성자에서 초기화 코드가 있는 람다식을 lazy 메서드에 넘겨서 Lazy 객체를 가져온다
2. 지연 초기화를 사용한 프로퍼티에 접근할 때 1번에서 반환받은 Lazy 객체 내의 value 프로퍼티의 getter 를 호출한다
3. getter 에서는 한번만 람다식을 이용하여 초기화 하기 위해 내부적으로 _value 프로퍼티를 가지고 있는데 이 값이 UNINITIALIZED_VALUE 인지 아닌지 여부에따라 람다식을 실행할지 아니면 바로 _value 값을 반환할지 결정한다
4. 만약 _value 프로퍼티가 초기화되어있지 않다면 lazy 메서드에 전달한 람다식을 실행하고 그 결과를 getter 에서 반환한다
간략히 말하면 by lazy 로 선언하면 Lazy 객체가 자바 클래스 내부에 저장되고 by lazy 로 선언한 프로퍼티에 접근하는 순간 Lazy 객체의 value 프로퍼티 getter 가 동작하여 람다식을 실행하여 초기화하는 방식입니다. 물론 한번 초기화된 값은 Lazy 객체 내부에 보관하고 있다가 다시 같은 프로퍼티에 접근하면 곧바로 보관하고있던 값을 반환해줍니다.
자바로 변환된 테스트코드
public final class ByLazyTest {
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property1(new PropertyReference1Impl(Reflection.getOrCreateKotlinClass(ByLazyTest.class), "name", "getName()Ljava/lang/String;"))};
private final Lazy name$delegate;
private final String getName() {
//1. 저장해둔 Lazy 객체를 가져온다.
Lazy var1 = this.name$delegate;
KProperty var3 = $$delegatedProperties[0];
boolean var4 = false;
//2. Lazy 객체의 value 프로퍼티의 getter 를 실행하여 람다식을 실행하여 초기값을 가져온다.
return (String)var1.getValue();
}
public final void print() {
//지연 초기화가 선언된 프로퍼티에 접근하면 getName 메서드가 호출되면서 Lazy 객체에 의해 초기화된 값을 가져온다.
String var1 = this.getName();
boolean var2 = false;
System.out.println(var1);
}
public ByLazyTest() {
//1. 생성자에서 내부 프로퍼티에 람다식을 저장한 Lazy 객체를 생성한다.
this.name$delegate = LazyKt.lazy((Function0)null.INSTANCE);
}
}
초기화를 위임받은 Lazy 구현 클래스
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
//1. lazy 메서드를 통해 전달한 람다식을 저장해둔 프로퍼티
private var initializer: (() -> T)? = initializer
...
override val value: T
get() {
val _v1 = _value
//2. 이미 초기화가 되어있다면 값을 그냥 반환해준다.
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
/*
3. 초기화가 되어있지 않다면 전달한 람다식을 실행해서
이후 호출시 값을 바로 반환해주기 위해 _value 프로퍼티에 저장하고
람다식을 가진 변수를 null 로 초기화 한다 마지막으로 람다식 실행 결과값을 반환해준다.
*/
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
...
}'kotlin' 카테고리의 다른 글
| 코틀린 확장 함수 (0) | 2022.10.26 |
|---|---|
| by Delegates.observable 동작 방식 (0) | 2022.01.20 |