lateinit과 Delegates.notNull는 초기화를 늦추고 객체에 null이 들어가지 않게 합니다. 비슷해보이지만 여러가지 차이점이 있습니다. 또한 내부적인 구현이 다르기 때문에 성능차이가 발생할 수도 있습니다. 이 글에서 이 둘의 특징과 구현의 차이점에 대해서 알아보겠습니다.
lateinit 소개
Kotlin - 프로퍼티 초기화를 지연하는 방법, Late init과 Lazy init 소개에서 설명한 것처럼 프로퍼티의 초기화를 늦게 하는데 도와주는 기능입니다. 여기서 간단히 특징에 대해서 나열해보겠습니다. 자세한 것은 위의 글을 참고해주세요.
- var 프로퍼티만 사용할 수 있음
- primitive type(Int, Boolean)은 사용할 수 없음
- 프로퍼티에 null을 넣을 수 없음
- Custom getter/setter를 만들 수 없음
- 초기화 전에 사용하면 UninitializedPropertyAccessException이 발생
lateinit 내부 구현
아래는 Kotlin - 프로퍼티 초기화를 지연하는 방법, Late init과 Lazy init 소개에서 소개한 예제 코드입니다.
class Rectangle {
lateinit var area: Area
fun initArea(param: Area): Unit {
this.area = param
}
}
class Area(val value: Int)
fun main() {
val rectangle = Rectangle()
rectangle.initArea(Area(10))
println(rectangle.area.value)
}
위 코드를 자바로 디컴파일해보면 다음과 같습니다. (일부 생략하였고 전체코드는 위 글을 참고해주세요.) 객체가 처음 null로 설정되기 때문에, 초기화를 하지 않고 사용하면 예외가 발생합니다.
public final class Rectangle {
@NotNull
public Area area;
@NotNull
public final Area getArea() {
Area var10000 = this.area;
if (var10000 == null) {
Intrinsics.throwUninitializedPropertyAccessException("area");
}
return var10000;
}
...
}
public static final void main() {
Rectangle rectangle = new Rectangle();
rectangle.initArea(new Area(10));
int var1 = rectangle.getArea().getValue();
System.out.println(var1);
}
Delegates.notNull() 소개
Delegates.notNull()는 객체를 non-null 타입으로 만들어버립니다. 그래서 var로 선언하였지만 객체에 null을 할당할 수는 없습니다. 대신 null이 아닌 객체를 할당할 수 있습니다.
우리는 Delegates.notNull()을 아래 코드처럼 사용할 수 있습니다. 객체를 선언할 때 뒤에 by Delegates.notNull<타입>()
를 붙여주면 됩니다.
만약 nonNullString = null
처럼 null을 입력하려고 하면 컴파일이 에러가 발생합니다.
var nonNullString: String by Delegates.notNull<String>()
nonNullString = "Hello World"
println("Non null value is: ${nonNullString}")
nonNullString = null // 컴파일 에러, non-null 타입에 null을 넣을 수 없음
만약 초기화를 하지 않고 객체를 사용하면, IllegalStateException이 발생합니다.
var nonNullString: String by Delegates.notNull<String>()
println("Non null value is: ${nonNullString}")
이런 식으로요. 초기화되지 않은 객체를 사용할 때 예외가 발생하는 것은 lateinit과 동일합니다. 또한 null을 넣을 수 없다는 것이 동일합니다.
Exception in thread "main" java.lang.IllegalStateException: Property nonNullString should be initialized before get.
at kotlin.properties.NotNullVar.getValue(Delegates.kt:62)
at foo.main.kotlin.Kotlin23Kt.main(kotlin23.kt:29)
간단히 Delegates.notNull()의 특징을 정리해보겠습니다.
- primitive type 도 사용할 수 있다
- 초기화 전에 사용하면 예외가 발생한다
- null을 설정하려고 하면 컴파일 에러가 발생한다
- late init보다 성능이 안좋다(다음에 설명)
Delegates.notNull() 내부 구현
아래 코드는 위에서 사용한 예제입니다.
var nonNullString: String by Delegates.notNull<String>()
nonNullString = "Hello World"
println("Non null value is: ${nonNullString}")
위 코드를 자바로 변환해보면 다음과 같습니다.
public final class Test {
static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)
Reflection.mutableProperty0(new MutablePropertyReference0Impl(
Reflection.getOrCreateKotlinPackage(
Test.class, "kotlin"), "nonNullString", "<v#0>"))};
public static final void main(@NotNull String[] args) {
ReadWriteProperty nonNullString = Delegates.INSTANCE.notNull();;
KProperty var3 = $$delegatedProperties[0];
nonNullString.setValue((Object)null, var3, "Hello World");
String var4 = "Non null value is: " + (String)nonNullString.getValue((Object)null, var3);
System.out.println(var4);
}
}
디컴파일된 코드를 보시면 코틀린에서 String 타입으로 Delegates.notNull
을 적용하였는데요.
디컴파일해보니 타입이 ReadWriteProperty로 변해있습니다. 따라서, lateinit처럼 코드적으로 예외처리를 하는 것이 아니라
ReadWriteProperty 내부에 자신이 선언한 타입을 감싸고(wrapping) 있는 구조입니다.
그렇기 때문에 primitive type도 사용할 수 있습니다. 반면에 lateinit은 사용할 수 없습니다.
lateinit과 Delegates.notNull()의 차이점
이 둘의 내부 구현 원리를 안다면 차이점에 대해서 쉽게 이해할 수 있습니다.
Delegates.notNull
은 특정 객체가 선언한 객체를 감싸기 때문에 primite type도 사용할 수 있습니다. 하지만 lateinit
은 선언한 타입을 사용하고 초기값을 null로 설정하기 때문에 primitive type은 사용할 수 없습니다. 이런 구조적인 차이 때문에 Delegates.notNull
가 오버헤드가 클 수 밖에 없습니다.
만약 많은 데이터를 처리해야 한다면 이런 구조에서 오는 성능상의 차이가 발생할 수 있습니다.
- Delegates.notNull은 primitive type도 사용할 수 있지만 lateinit은 사용할 수 없다
- primitive type을 사용하려면 Delegates.notNull을 사용해야 한다
- Delegates.notNull가 lateinit보다 더 무겁게 동작한다
- Delegates.notNull는 Dagger와 같은 injection 라이브러리와 함께 쓰기 어려울 수 있다
- lateinit은 custom getter/setter를 만들 수 없다.
참고
Related Posts
- Kotlin - 배열에서 최소 값, 최대 값 찾기
- Kotlin - 2차원 배열 선언, 초기화 방법
- Kotlin - 배열 선언, 초기화 방법
- Kotlin - 리스트, 배열 길이 가져오기
- Kotlin - 리스트에서 최대, 최소 값 찾기
- Kotlin - for 반복문, 배열/리스트 순회
- Kotlin - Timer, 주기적으로 함수 실행
- Kotlin - sleep, 쓰레드 몇 초 지연
- Kotlin - Thread 생성 및 실행
- Kotlin에서 정규표현식 사용하기
- Kotlin - 문자열 길이 계산
- Kotlin - 문자열 비교 방법(equals, ==, compareTo)
- Kotlin - 2개의 배열 하나로 합치기
- Kotlin - 2개의 List 하나로 합치기
- Kotlin - 디렉토리의 모든 파일 리스트 출력
- Kotlin - 리스트 정렬 방법 (sort, sortBy, sortWith)
- Kotlin - 문자열 뒤집기 (Reverse String)
- Kotlin - 랜덤 숫자 생성 (Random, SecureRandom)
- Kotlin - Range, 숫자 범위 표현
- Kotlin - 음수를 양수로 변환, math.abs()
- Kotlin - List를 Set로 변환
- Kotlin - Set를 List로 변환
- Kotlin - 문자열에서 숫자(int)만 추출하는 방법
- Kotlin - Map을 List로 변환하는 방법
- Kotlin - File, Directory가 존재하는지 확인
- Kotlin - List를 Map으로 변환
- Kotlin - List의 중복 요소 제거
- Kotlin - List를 Array로 변환
- Kotlin - 엘비스 연산자 (Elvis Operation)
- Kotlin - Array를 List로 변환
- Kotlin - String을 Float으로 변환
- Kotlin - String을 Double으로 변환
- Kotlin - String을 Int로 변환
- Kotlin - String을 Long으로 변환
- Kotlin - String Null 또는 Empty 체크