한창 지연 초기화 방법에 대해 공부하고 있었다.
그 중에서 lateinit var를 공부하던 중 생겨난 의문점들을 탐구한 과정을 적어보려한다.
검색해 볼 수도 있겠지만 디컴파일을 통해 Java에서 어떻게 표현될지를 보는게 좋을듯하다.
일단 lateinit var로 프로퍼티를 만들어보자.
class Test {
lateinit var age: Int
// 에러: 'lateinit' modifier is not allowed on properties of primitive types
}
lateinit은 원시타입의 프로퍼티가 허락되지 않는다는 에러가 발생하게 된다.
이걸 다시 디컴파일 해보면 내부에서 어느 부분이 에러인지 나온다.
public final class Test {
public int age = 20;
public final int getAge() {
int var10000 = this.age;
if (var10000 == null) { // Operator '==' cannot be applied to 'int', 'null'
Intrinsics.throwUninitializedPropertyAccessException("age");
}
return var10000;
}
public final void setAge(int var1) {
this.age = var1;
}
}
int와 null 사이에서 == 연산자를 쓸 수 없다고 나온다.
이유는 String과 같이 참조 타입이라면 null이 가능해서 비교식이 성립하지만, int는 primitive 타입이다 보니 그럴 수 없는 것이다.
이처럼 lateinit은 내부적으로 getter에서 임의의 변수인 var10000에 lateinit을 할 필드를 넣어두고 이것이 null값인지 확인해보고 null이라면 exception을 날린다.
그럼 어떻게 하면 이게 성립되게끔 할 수 있을까?
class Test {
lateinit var age: Integer
// This class shouldn't be used in Kotlin. Use kotlin.Int instead.
}
조금 웃긴 방법으로 자바의 Integer 클래스를 사용하는 방법이 있다.
Integer 클래스는 참조형 타입이기 때문에 null을 가질 수 있어서이다.
물론 IDE에서 코틀린의 Int를 사용하라고 권장한다.😅
이어서 String 타입을 보겠다.
class Test {
lateinit var age: String
}
디컴파일 하면?
public final class Test {
public String age;
@NotNull
public final String getAge() {
String var10000 = this.age;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("age");
}
return var10000;
}
public final void setAge(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.age = var1;
}
}
코틀린에서 Int가 자바에선 int로 디컴파일 되던 것과 달리 String 타입은 똑같이 String 타입으로 디컴파일된다.
이유는 자바에서 String은 int, double 등의 값 타입과는 달리 참조 타입이기 때문이다.
따라서 클래스이자 참조타입인 String은 null을 가질 수 있기에 getter의 해당 부분에서 에러가 나질 않는다.
if (var10000 == null) { // String인 var10000은 null이 들어갈 수 있기 때문에 에러가 나질 않는다.
Intrinsics.throwUninitializedPropertyAccessException("age");
}
기계적으로 var변수를 나중에 초기화고 싶으면 lateinit을 사용한다라는 고정관념으로 써왔지만, 왜 그래야하는지는 생각하지 않고 써왔기에 이번 기회에 정리해보려고 한다.
일단 먼저 var 타입의 프로퍼티를 선언해보자.
class Test {
var age: Int = 20
}
이걸 Java로 디컴파일 해보면?
public final class Test {
private int age = 20;
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
}
var 타입이다 보니 읽기, 쓰기 기능을 할 getter와 setter가 구현되어있다.
이어서 val 타입으로 프로퍼티를 선언하면 어떻게 될까?
class Test {
val name: String = "bandal"
}
디컴파일하면?
public final class Test {
@NotNull
private final String name = "bandal";
@NotNull
public final String getName() {
return this.name;
}
}
재할당이 불가하여 setter가 따로 없는 모습을 보인다.
억지로 lateinit val로 프로퍼티를 선언하면 어떻게 될까?
class Test {
lateinit val age: String
// 에러: 'lateinit' modifier is allowed only on mutable properties
}
lateinit은 오직 가변 프로퍼티만 허용된다는 에러를 발생시킨다.
이유는 무엇일까? 눈치가 빠른 사람이라면 알아차릴 수도 있다.
한번 디컴파일 해보자.
public final class Test {
public String name;
@NotNull
public final String getName() {
String var10000 = this.name;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("name");
}
return var10000;
}
}
위의 var 타입을 선언한 것과 비교했을 때 무엇이 다른가?
바로 setter가 없다는 것이다.
val로 프로퍼티를 선언시엔 동시에 초기화를 해야 사용 가능하다.(by lazy로 지연초기화를 하는 것은 잠시 논외로하자)
위의 디컴파일 된 코드로는 name이 초기화가 되어있지 않다는 에러를 낸다.
var의 경우 초기화를 안하고 setter를 통해 나중에 값을 다시 넣어주게끔 구현이 가능하지만, val의 경우 setter가 구현되어 있지 않아 초기화 말곤 name에 값을 넣을 수 없다.
그렇게되면 초기화를 해줘야하는데 그러면 lateinit을 쓰는 이유가 없다.
그래서 lateinit은 후에 계속 값을 할당할 수 있는 var만 가능하다.
이거는 혹시나 궁금해하는 사람이 있을까봐 같이 정리해둔다.
일단 한번 써보자.
class Test() {
lateinit var name: String?
// 에러: 'lateinit' modifier is not allowed on properties of nullable types
}
디컴파일을 해보면
public final class Test {
public String name;
@Nullable
public final String getName() {
String var10000 = this.name;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("name");
}
return var10000;
}
public final void setName(@Nullable String var1) {
this.name = var1;
}
}
사실 내부적으로 오류가 나진 않는다.
맞다 사실 코드상의 오류가 있는것이 아니라 논리적인 오류 때문에 허용하지 않는 것이다.
lateinit var
는 지연 초기화를 사용하기 위함이었다.
근데 lateinit var name: String?
은 애초에 var name: String? = null
로 치환이 가능하다.
null로 초기화를 해두었다가 나중에 다시 값을 넣어주면 되는 것이다.
- 왜 primitive 타입만 안될까?
lateinit의 내부구현은 null과 비교하는 구문이 있다.
값 타입인 primitive 타입은 null을 가질 수 없으며, null과 비교자체가 불가능 하기 때문에 primitive 사용할 수 없다.
- 왜 var만 가능하고 val은 lateinit을 사용하지 못할까?
var은 내부적으로 getter와 setter가 구현되어 있지만, val은 getter만 구현이 되어있다.
lateinit의 경우 초기화를 지연하기 때문에 선언과 동시에 초기화를 하는 구문이 없다.
즉 setter를 사용하여 값을 다시 넣어줘야하는 이 경우에 setter를 가지고 있지 않은 val은 부적합하다.
따라서 lateinit은 오직 var만 사용이 가능한 것이다.
- 왜 nullable한 타입은 가질 수 없을까?
lateinit var
는 지연 초기화를 사용하기 위함이었다.
근데 lateinit var name: String?
은 애초에 var name: String? = null
로 치환이 가능하다.
null로 초기화 해두고 나중에 원하는 값을 다시 넣어주면 그만이다.
새로운 의견이나 이의가 있다면 언제든 댓글로 알려주시기 바랍니다!😆