https://thdev.tech/kotlin/2018/03/25/Kotlin-lateinit-lazy/
class 생성과 동시에 변수가 초기화되면, 재 접근시 빠르게 접근이 가능하여 이득을 볼 수 있다.
변수를 꼭 사용하는 게 아니라면 오히려 메모리 손해를 볼 수 있다.
필수 요건이 아닌 경우라면 늦은 초기화가 필요하다.
class SampleActivity {
private var sampleAdapter: SampleAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sample_main)
// 부르는 시점 초기화
sampleAdapter = SampleAdapter(ImageLoaderAdapterViewModel(this@SampleMainActivity, 3))
}
}
kotlin에서는 늦은 초기화 시 null을 명시해야 하는데, 꼭 null이 필요하지는 않다.
Java라면 무조건 null에 대한 접근이 가능하여, 언제든 null로 명시할 수 있지만 kotlin에서는 null은 필요한 경우 명시해야 한다.
java와 함께 사용하거나, null을 통해 명시가 필요한 경우
늦은 초기화를 제공하는 프로퍼티
lateinit은 꼭 변수를 부르기 전에 초기화 시켜야 하는데 아래와 같은 조건을 가지고 있다.
kotlin 1.2부터는 lateinit 초기화를 확인 할 수 있다.
실제 값을 사용할 때 lateinit을 한번 체크해줌으로써 안전하게 접근할 수 있다.
이때 아래와 같이 ::을 통해서만 접근이 가능한 .isInitialized을 사용하여 체크할 수 있다.
class SampleActivity {
private lateinit var sampleAdapter: SampleAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sample_main)
// 부르는 시점 초기화
sampleAdapter = SampleAdapter(ImageLoaderAdapterViewModel(this@SampleMainActivity, 3))
if (::sampleAdapter.isInitialized) {
sampleAdapter.addItem()
sampleAdapter.notifyDataSetChanged()
}
}
}
디컴파일한 코드를 통해 lateinit이 어떤 식으로 동작하는지 확인이 가능한데, this.sampleAdapter == null 의 코드를 확인할 수 있다.
@NotNull
public SampleAdapter sampleAdapter;
@NotNull
public final SampleAdapter getSampleAdapter() {
SampleAdapter var10000 = this.sampleAdapter;
if(this.sampleAdapter == null) {
Intrinsics.throwUninitializedPropertyAccessException("sampleAdapter");
}
return var10000;
}
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2131361820);
this.sampleAdapter = new SampleAdapter(new ImageLoaderAdapterViewModel((Context)this, 3));
SampleAdapter var10000 = this.sampleAdapter;
if(this.sampleAdapter == null) {
Intrinsics.throwUninitializedPropertyAccessException("sampleAdapter");
}
}
결국 외부에서 getSampleAdapter을 부르던 onCreate에서 this.sampleAdapter을 사용하든 null인 경우 무조건 throwUninitializedPropertyAccessException을 발생시키는 것을 확인할 수 있다.
결국 초기화를 하지 않으면 해당 메소드에 접근하는 것은 불가능하니, 꼭 초기화를 해야 한다
lateinit은 필요할 경우 언제든 초기화가 가능한 Properties였다.
이번엔 생성 후 값을 변경할 수 없는 val(immutable) 정의인 lazy properties을 살펴본다.
lazy 초기화는 기존 val 변수 선언에 by lazy을 추가함으로 lazy {}에 생성과 동시에 값을 초기화하는 방법을 사용한다.
lazy는 lateinit 과는 반대로 아래의 조건을 가지므로, 상대적으로 편하게 사용이 가능하며, 실수할 일도 줄어든다.
lazy가 늦은 초기화라고 하였으니 아래 샘플 코드의 println을 통해 확인할 수 있는데
class ExampleUnitTest {
private val subject: String by lazy {
println("subject initialized!!!!")
"Subject Initialized"
}
@Test
fun test() {
println("Not Initialized")
println("subject one : $subject")
println("subject two : $subject")
println("subject three : $subject")
}
}
위의 결과는 아래와 같음을 확인할 수 있다.
코드상 test()에서 out $sample을 출력하는 부분을 코드상 작성하였는데 그때 출력 결과는 subject initialized!!!!과 subject one : Subject Initialized이 출력되었다.
Not Initialized
subject initialized!!!!
subject one : Subject Initialized
subject two : Subject Initialized
subject three : Subject Initialized
그리고 또다시 불러올 때 subject two : Subject Initialized 만 불리는 걸 확인할 수 있다.
결국 호출 시점에 한번 초기화를 진행하고, 그 이후에는 가져다가 쓰기만 하는 것을 확인할 수 있다.
Synchronized가 아닌 경우 별도 옵션 정의를 할 수 있다.
기본 Mode는 아래와 같이 3가지이며, default로 SYNCHRONIZED을 정의하고있다.
public enum class LazyThreadSafetyMode {
/**
* 잠금은 단일 스레드만 [lazy] 인스턴스를 초기화할 수 있도록 하는 데 사용
* Locks are used to ensure that only a single thread can initialize the [Lazy] instance.
*/
SYNCHRONIZED,
/**
* Initializer function can be called several times on concurrent access to uninitialized [Lazy] instance value,
* but only the first returned value will be used as the value of [Lazy] instance.
*/
PUBLICATION,
/**
* No locks are used to synchronize an access to the [Lazy] instance value; if the instance is accessed from multiple threads, its behavior is undefined.
*
* This mode should not be used unless the [Lazy] instance is guaranteed never to be initialized from more than one thread.
*/
NONE,
}
lazy 초기화시에 옵션은 아래와 같이 설정할 수 있다.
val temp: String by lazy(옵션) {
//...
}
late-initialized-properties와 lazy delegated-properties에 대해서 정리하였다.
굳이 Nullable이 필요치 않은 곳에서는 lazy 패턴을 걸어서 주로 사용하고 있다. lateinit의 경우도 Nullable 일 필요 없고, 언제든 값을 교체하는 게 필요한 경우 사용하고 있다.
하지만 Nullable이 꼭 필요한 경우에는 사용하지 않아야 하니 주의해서 사용하길 바란다.
lateinit과 Higher-Order Function을 활용하여 interface OnClickListener 대신 아래와 같은 onClick을 만들어서 아래와 같이 사용한다.
lateinit var onClick: (position: Int) -> Unit
// 사용하는 쪽
onClick(position)
// onClick을 받는 쪽
onClick = { position ->
println("onClick position: $position")
}
lazy: 이는 지연 초기화(lazy initialization) 패턴을 구현하는 데 사용되며, 프로퍼티의 초기화를 해당 프로퍼티가 처음으로 접근될 때까지 지연시킨다. 이 방식은 프로퍼티의 초기화 작업이 많은 시간이나 리소스를 소모하는 경우, 또는 프로퍼티가 실제로 사용되지 않을 수도 있는 경우에 효율적이다.
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue) // 첫 접근 시 "computed!"를 출력 후, "Hello" 반환
println(lazyValue) // 두번째 접근부터는 이미 초기화가 되어있으므로 "Hello"만 반환
}
init: 이는 초기화 블록(initializer block)을 나타내며, 객체의 인스턴스가 생성될 때 실행, 이 블록 내에서 프로퍼티를 초기화할 수 있다.
init 블록은 객체가 생성되는 즉시 실행되므로, lazy와 달리 프로퍼티의 초기화를 지연시키지 않습니다.
class InitExample {
val initValue: String
init {
println("init block!")
initValue = "Hello"
}
}
fun main() {
val example = InitExample() // 객체 생성 시 "init block!"를 출력 후, initValue를 "Hello"로 초기화
println(example.initValue) // "Hello" 반환
}
즉, lazy
는 프로퍼티가 실제로 필요할 때까지 초기화를 지연시키며, init
는 객체가 생성될 때 바로 프로퍼티를 초기화한다.