Q. 클래스를 사용하는 클라이언트 입장에서 볼 때 실질적으로 val과 같은 역할을 하는 읽기 전용 프로퍼티를 val을 쓰지 않고 만들 수 있는가? 반대로 쓸 수만 있는 프로퍼티는 어떻게 만들 수 있을까?
잘 모르게씀....
Kotlin의 지연 초기화
📝 지연 초기화(Lazy Initialization)란
: 필드의 초기화 시점을 그 값이 처음 필요할 때까지 늦추는 기법
🤔지연 초기화를 왜 사용할까?🤔
- 소프트웨어 실행 시간 및 메모리 효율 개선
- 클래스가 초기화되는 시점에 필드의 이상적인 초기값을 모를 경우에 대한 해결책
(null 가능성을 처리하지 않아도 된다!!)
- 앱 시작 중에 많은 객체를 할당하면 시작 시간이 길어질 수 있으므로 특히 안드로이드에서 일반적으로 사용한다
일반적으로 널이 아닌 유형으로 선언된 프로퍼티는 생성자에서 초기화해야 합니다. 그러나 그렇게 하는 것이 편리하지 않은 경우가 종종 있습니다. 예를 들어, 프로퍼티는 의존성 주입을 통해 초기화하거나 단위 테스트의 설정 메서드에서 초기화할 수 있습니다. 이러한 경우 생성자에서 널이 아닌 이니셜라이저를 제공할 수는 없지만 클래스 본문 내에서 프로퍼티를 참조할 때 널 검사를 피하고 싶을 수 있습니다.
- Kotlin 공식문서 -
지연 초기화를 위해 사용하는 것으로 lateinit과 lazy가 있는데, 목적은 같지만 사용법과 주의사항이 다르다!!
상황에 맞게 사용해야 한다.
📝 lateinit
- 변수를 lateinit으로 선언하면 이후에 어떤 동작의 결과 값을 기반으로 변수를 초기화한다.
- lateinit을 사용하면 지연 초기화를 한 이후에도 값이 계속 바뀔 수 있다.
- lateinit으로 선언해놓고 이후에 지연 초기화를 하지 않는다면 컴파일 단계에서 UninitializedPropertyAccessException이 발생한다.
[프로퍼티가 lateinit이 되기 위한 조건]
1️⃣프로퍼티가 여러번 변경될 수 있기 때문에 var(가변 프로퍼티)로 정의해야 한다!
2️⃣프로퍼티의 타입은 null이 아닌 타입이어야 하고, Primitive Type (Int, Boolean, Float, Double..)이 아니어야 한다!
=> 프로퍼티가 초기화되지 않은 상태라면 null이 될 수도 있기 때문
3️⃣lateinit 프로퍼티를 정의하면서 초기화 식을 지정해 값을 바로 대입할 수 없다.
=> 초기화 시점에 값을 대입하면 lateinit을 지정하는 의미가 없다..
public class MyTest {
lateinit var subject: TestSubject
@SetUp fun setup() {
subject = TestSubject()
}
@Test fun test() {
subject.method() // dereference directly
}
}
📝 lazy
- 변수가 호출됐을 때 어떻게 초기화를 해줄 지 정의한다.
- val로 선언 => 지연 초기화가 한 번 이루어지고 나서는 이후에 값이 불변함을 보장한다.
- lazy 프로퍼티는 thread-safe하다! (다중 스레드 환경에서도 값을 하나의 스레드 안에서만 계산하기 때문에 모든 스레드에서 같은 값을 공유한다.)
val lazyValue: String by lazy {
println("computed!")
"Hello"
}
fun main() {
println(lazyValue) // computed!
// Hello
println(lazyValue) // Hello
}
📝 lateinit vs lazy
Lateinit | Lazy |
var 타입만 가능하다. | val 타입만 가능하다. |
Non-null 타입만 사용 가능하다. | Non-null, null 모두 가능하다. |
초기화 이후에도 값의 변경이 가능하다. | 초기화 이후에는 값의 변경이 불가능하다. |
클래스 생성자에서 사용이 불가능하다. | 클래스 생성자에서 사용이 불가능하다. |
커스텀 getter / setter 불가능하다. | 커스텀 getter / setter 불가능하다. |
primitive type은 사용이 불가능하다. | primitive type도 사용이 가능하다. |
객체 선언
📝 객체 선언 (Object Declaration)
- object
키워드 사용
- 코틀린에서 객체 선언은 클래스 선언과, 그 클래스에 속한 단일 인스턴스 선언을 합한 것이다.
- 객체 선언을 통해 singleton이 적용된 클래스를 만들 수 있다. (인스턴스가 단 하나만 존재함!)
=> thread-safe
- 지연 초기화 (싱글턴 클래스가 실제 로딩되는 시점까지 지연)
- 클래스와 마찬가지로 함수와 프로퍼티, 초기화 블록을 포함할 수 있다.
- 생성자를 객체 선언에 쓸 수 없다. (항상 암시적으로 만들어지기 때문에 생성자 호출이 의미가 없다!)
- 객체의 본문에 들어있는 클래으세엉 inner가 붙을 수 없다! (객체 선언은 항상 인스턴스가 하나이기 때문에 inner 변경자가 불필요하다.)
- 객체 선언에 사용한 이름 뒤에 . 을 붙이면 객체에 속한 메서드나 프로퍼티에 접근할 수 있다.
object Application {
val name = "My Application"
override fun toString() = name
fun exit() {}
}
📝 Java의 Singleton Pattern과의 비교
public class Manager{
//2. 객체 생성. 자기자신. private으로! 외부에서 부를 수 없게.
// 나라도 만들어서 일단 들고 있어
//5. static인 이유?? static으로 만들어서 미리 메모리에 올려둬야 외부에서 getManager() 쓸 수 있음
private static Manager instance = new Manager();
//1. 생성자를 private으로 막아버림. 외부에서 객체 생성할 수 없게.
// 안 만들면 자동으로 public 생성자 만들어버리니까 이거 막아야 함.
private Manager(){}
//3. public으로 유일한 객체(private) 접근은 가능하게 함.
//4. 위에서 static으로 안하면 객체가 없어서 Manager 클래스 부를 수 없음.
// Manager 클래스를 부를 때 이미 객체가 형성 돼 있어야 함.
// static으로 만들어서 미리 메모리에 올려둬야지.
public static Manager getManager(){
return instance;
}
}
// 외부에서
// Manager m1 = Manager.getManager();
// Manager m2 = Manager.getManager();
// 이렇게 불러도 둘 다 같은 배열을 가져옴.
- Kotlin에서는 object 키워드만을 이용해서 싱글톤 패턴을 간편하게 구현할 수 있지만 Java의 경우에는 위와 같이 많은 코드를 작성해야 한다.
📝 클래스와 비교할 때 객체 선언의 제약
아래의 경우처럼 객체(예시의 경우 Application)가 다른 파일에서 정의되어 있는 상황에서 객체의 멤버를 임포트해서 간단히 이름만 사용해서 참조할 수 있다.
import Application.exit // 객체의 멤버 임포트
fun main() {
println(Application.name) // 전체 이름 사용
exit() // 간단한 이름 사용
}
그러나 객체의 모든 멤버가 필요할 때는 임포트 문으로 임포트 할 수 없다!
import Application.* // 객체의 모든 멤버를 임포트 할 수는 없다. ERROR!
객체 정의 안에는 다른 클래스 정의와 같이 toString(), equals()와 같은 공통 메서드의 정의가 들어있기 때문에, 임포트를 할 때 이런 공통 메서드까지 임포트 되어서 문제가 생길 수 있다. 따라서 위와 같은 제약이 있는 것이다.