6. 프로퍼티와 초기화
1. 프로퍼티의 접근
코틀린에서는 별도로 접근 메서드(Getter(), Setter())를 지정하지 않아도 자동으로 처리해주기 때문에 코드의 양을 줄일 수 있다.
/* User.kt */
class User(val id: Int, var name: String, var age: Int)
/* User.decompiled.java */
public final class User {
private final int id;
@NotNull
private String name;
private int age;
public final int getId() {
return this.id;
}
@NotNull
public final String getName() {
return this.name;
}
public final void setName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.name = var1;
}
public final int getAge() {
return this.age;
}
public final void setAge(int var1) {
this.age = var1;
}
public User(int id, @NotNull String name, int age) {
Intrinsics.checkNotNullParameter(name, "name");
super();
this.id = id;
this.name = name;
this.age = age;
}
}
커스텀 접근 메서드
- 보조 필드(Backing Field): 각 프로퍼티의 값을 읽는 특별한 식별자다. 프로퍼티 이름을 직접 사용하면 무한 재귀 호출되어 스택 오버플로가 발생하기 때문에 이를 방지하기 위해 보조 필드를 사용한다.
/* 구조 */
// 대괄호([ ])의 내용은 생략 가능
var 프로퍼티 이름[: 자료형] [= 초기화] // 읽기 쓰기 가능(var)
[get() { 게터 본문 } ]
[set(value) { 세터 본문 }]
val 프로퍼티 이름[: 자료형] [= 초기화] // 읽기 전용(val)
[get() { 게터 본문 } ]
/* 보조 필드 역할 */
var name: String = _name
get() = field
set(value) {
// ①은 자바로 변환되면 this.setName(value) 형태가 되어 무한 재귀 호출된다.
name = value // ①
field = value // 보조 필드 사용
}
...
/* 커스텀 접근 메서드 사용하기 */
class User(_id: Int, _name: String, _age: Int) {
val id: Int = _id
var name: String = _name
set(value) {
println("The name was changed")
field = value.toUpperCase() // 받은 인자를 대문자로 변경
}
var age: Int = _age
}
fun main() {
val user1 = User(1, "Kim", 30)
println("user1.name = ${user1.name}")
user1.name = "Lee" // [User] name 프로퍼티의 세터 호출
println("user1.name = ${user1.name}")
}
프로퍼티 오버라이딩
open class First {
open val x: Int = 0 // open 키워드로 오버라이딩 허용
get() {
println("First x")
return field
}
val y: Int = 0 // 기본 final 프로퍼티
}
class Second : First() {
override var x: Int = 0 // 상위 클래스의 val 프로퍼티를 var 프로퍼티로 변경 가능
get() {
println("Second x")
return field
}
// override val y: Int = 0 // 오류! 오버라이딩 불가
}
fun main() {
val second = Second()
second.x = 3
println(second.x) // 오버라이딩된 프로퍼티
println(second.y) // 상위 클래스로부터 상속받은 프로퍼티
}
2. 지연 초기화와 위임
코틀린에서 프로퍼티는 선언과 동시에 초기화를 해야한다. 하지만 의존성이 있는 초기화나 유닛 테스트를 위한 코드를 작성할 때 매번 초기화는 불편하기 때문에 지연 초기화를 사용할 수 있다.
lateinit을 사용한 지연 초기화
lateinit의 사용에는 두 가지 제한 사항이 존재한다.
- var로 선언된 프로퍼티만 가능
- 프로퍼티에 대한 게터와 세터를 사용 불가
/* lateinit을 사용한 프로퍼티 지연 초기화 */
class Person {
lateinit var name: String // 지연 초기화를 위한 선언
fun test() {
if(!::name.isInitialized) { // 초기화 여부를 검사
println("not initialized")
} else {
println("initialized")
}
}
}
fun main() {
val kim = Person()
kim.test()
kim.name = "Kim" // ① 이 시점에서 초기화됨(지연 초기화)
kim.test()
println("name = ${kim.name}") // ②
}
①에서 name 프로퍼티를 초기화하지 않고 ②에서 접근하면 kotlin.UninitializedPropertyAccessException 예외가 발생한다.
/* lateinit을 사용한 객체 지연 초기화 */
data class Person(var name: String, var age: Int)
lateinit var person1: Person // 객체 생성의 지연 초기화
fun main() {
person1 = Person("Kim", 30) // 생선자 호출 시점에서 초기화
println("${person1.name} is ${person1.age}")
}
lazy를 사용한 지연 초기화
lazy는 val로 선언한 객체나 프로퍼티의 지연 초기화를 하기위해 사용한다. lazy는 다음과 같이 세 가지 특징을 갖는다.
- 호출 시점에 by lay {...} 정의에 의해 블록 부분의 초기화를 진행한다.
- 불변의 변수 선언인 val에서만 사용 가능하다.(읽기 전용)
- val이므로 값을 다시 변경할 수 없다.
lazy는 세 가지 모드를 제공한다.
- SYNCHRONIZED: lock을 사용해 단일 스레드만이 사용하는 것을 보장(기본값)
- PUBLICATION: 여러 군데에서 호출될 수 있으나 처음 초기화된 후 반환값을 사용
- NONE: lock을 사용하지 않기 때문에 빠르지만 다중 스레드가 접근 가능(값의 일관성을 보장 못함)
/* lazy를 사용한 프로퍼티 지연 초기화 */
class LazyTest {
init {
println("init block") // ② 초기화 블록
}
val subject by lazy {
println("lazy initialized") // ⑥ 출력문 실행
"Kotlin Programming" // ⑦ lazy 반환값
}
fun flow() {
println("not initialized") // ④ 출력문 실행
println("subjec one: $subject") // ⑤ subject 프로퍼티 참조(최초 초기화 시점)
println("subjec two: $subject") // ⑧ 이미 초기화된 subject 프로퍼티 참조
}
}
fun main() {
val test = LazyTest() // ① 객체 생성
test.flow() // ③ // 메서드 호출
}
/* lazy를 사용한 객체 지연 초기화 */
class Person(val name: String, val age: Int)
fun main() {
var isPersonInstantiated: Boolean = false // 초기화 확인 용도
val person: Person by lazy { // ① lazy를 사용한 person 객체의 지연 초기화
isPersonInstantiated = true
Person("Kim", 32) // ② 이 부분이 Lazy 객체로 반환
}
val personDelegate = lazy { Person("Lee", 23) } // ③ 위임 변수를 사용한 초기화
println("person Init: $isPersonInstantiated")
println("personDelegate Init: ${personDelegate.isInitialized()}")
println("person.name = ${person.name}") // ④ 이 시점에서 초기화
println("personDelegate.value.name = ${personDelegate.value.name}") // ⑤ 이 시점에서 초기화
println("person Init: $isPersonInstantiated")
println("personDelegate Init: ${personDelegate.isInitialized()}")
}
by를 이용한 위임
- 클래스의 위임: 무분별한 상속에 따른 복잡한 문제를 방지한다. 클래스의 모든 기능 사용과 추가 확장 구현이 용이하다.
- 프로퍼티 위임과 by lazy: 다중 스레드 환경에서 조금 더 안정적으로 프로퍼티를 사용할 수 있다.
- observable() 함수와 vetoable() 함수의 위임: 코틀린의 표준 위임 구현으로 Delegates 코틀린 패키지를 임포트하여 사용한다.
/* 구조 */
<val|var|class> 프로퍼티 혹은 클래스 이름: 자료형 by 위임자
/* 클래스의 위임 사용 */
interface Car {
fun go(): String
}
class VanImpl(val power: String) : Car {
override fun go() = "은 짐을 적재하며 $power 을 가집니다."
}
class SportImpl(val power: String) : Car {
override fun go() = "은 경주용에 사용되며 $power 을 가집니다."
}
class CarModel(val model: String, impl: Car) : Car by impl {
fun carInfo() {
println("$model ${go()}") // 참조 없이 각 인터페이스 구현 클래tmdml go()에 접근
}
}
fun main() {
val myDamas = CarModel("Damas 2010", VanImpl("100마력"))
val my350z = CarModel("350Z 2008", SportImpl("300마력"))
myDamas.carInfo() // carInfo에 대한 다형성을 나타냄
my350z.carInfo()
}
/* observable() 함수 간단히 사용하기 */
import kotlin.properties.Delegates.observable
class User {
var name: String by observable("NONAME") { // 프로퍼티 위임
prop, old, new -> // 람다식 매개변수로 프로퍼티, 기존 값, 신규 값 지적
println("$old -> $new") // 이벤트 발생시 실행
}
}
fun main() {
val user = User()
user.name = "Kim" // 값이 변경될 때 첫 번째 이벤트 발생
user.name = "Lee" // 값이 변경될 때 두 번째 이벤트 발생
}
/* vetoable() 함수를 사용한 최대값 구하기 */
import kotlin.properties.Delegates.vetoable
fun main() {
var max: Int by vetoable(0) { // 초기값은 0
prop, old, new ->
old < new // 조건에 맞지 않으면 기존 값을 유지
}
println(max) // 0
max = 10 // 기존 값(0)이 신규 값(10)보다 작으므로 ture
println(max) // 10
max = 5 // 기존 값(10)이 신규 값(5)보다 크므로 false
println(max) // 10
}
3. 정적 변수와 컴패니언 객체
NOTE✏️
- 정적 변수(Static Variable): 프로그램을 실행하기 앞서 컴파일 시간에 메모리를 할당하는 것으로 프로그램 실행 전반에 걸쳐 변수의 수명이 유지된다. Link
- 컴패니언 객체(Companion Object): 코틀린은 static 키워드를 제공하지 않고 대 싱글톤(Singletion)으로 정의되는 컴패니언 객체를 제공한다. Link
- 싱글톤 패턴(Singletion pattern): 싱글톤 패턴을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 이후에 호출된 생성자는 생성한 객체를 리턴한다. Link
코틀린 컴패니언 객체 사용
class Person {
var id: Int = 0
var name: String = "Sungje"
companion object {
var language: String = "Korean"
fun work() = println("working...")
}
}
fun main() {
println(Person.language) // 인스턴스를 생성하지 않고 사용
Person.language = "English" // 기본값 변경 가능
println(Person.language)
Person.work() //매서드 실행
// Person.name // name은 컴패니언 객체가 아니므로 오류
}
자바에서 코틀린 컴패니언 객체 사용
- Companion 키워드를 생략하기 위해 @JvmStatic 애노테이션을 사용
- 프로퍼티를 자바에서 사용하기 위해 @JvmField 애노테이션을 사용
/* KCustomer.kt */
class KCustomer {
companion object {
const val LEVEL = "INTERMEDIATE"
@JvmField
val JOB = KJob()
@JvmStatic
fun login() = println("Longin...")
}
}
class KJob {
var title: String = "Programmer"
}
/* KCustomerAccess.java */
public class KCustomerAccess {
public static void main(String[] args) {
// 코틀린 클래스의 컴패니언 객체에 접근
System.out.println(KCustomer.LEVEL);
KCustomer.login(); // 애노테이션 사용시 Companion 키워드 생략 가능
KCustomer.Companion.login();
// KJob에 대한 객체 생성 후 접급
KJob kjob = KCustomer.JOB;
System.out.println(kjob.getTitle());
// KCustomer를 통한 접근
KCustomer.JOB.setTitle("Accountant");
System.out.println(KCustomer.JOB.getTitle());
}
}
최상위 함수 사용
/* PackageLevelFunction.kt */
@file:JvmName("PKLevel") // 접근할 클래스 이름을 변경
package chap06.section3
// 최상위 함수는 Java로 역컴파일되면 static final로 선언됨
fun packageLevelFunc() = println("Package-Level Function")
fun main() {
packageLevelFunc()
}
/* PackageLevelAccess.java */
package chap06.section3;
public class PackageLevelAccess {
public static void main(String[] args) {
// PackageLevelFunctionKt.packageLevelFunc();
PKLevel.packageLevelFunc(); // 변경된 이름으로 접근 가능
}
}
object와 싱글톤: 내용이 조금 변경된 클래스를 만들 때 익명 내부 클래스를 사용할 수 있다. 코틀린에서도 사용 가능하지만 object 표현식이나 object 선언으로 조금 더 쉽게 처리 가능하다.
- object 선언: 접근 시점에 객체가 생성되기 때문에 주 생성자와 부 생성자를 사용 불가능하나 초기화 블록은 사용 가능하다. object 선언에서도 클래스나 인터페이스를 상속할 수 있다.
// object 키워드를 사용한 방식
object OCustomer {
var name = "Kim"
val HOBBY = Hobby("Basketball")
fun greeting() = println("Hello Kotlin!")
init {
// 최초 접근 시점에 실행된다.
println("Init!")
}
}
// 컴패니언 객체를 사용한 방식
class CCustomer {
companion object {
const val HELLO = "hello" // 상수 표현
var name = "Lee"
@JvmField
val HOBBY = Hobby("Football")
@JvmStatic
fun greeting() = println("Hello Kotlin!")
}
}
class Hobby(val name: String)
fun main() {
OCustomer.greeting() // 객체의 접근 시점
OCustomer.name = "Choi"
println("name = ${OCustomer.name}")
println(OCustomer.HOBBY.name)
CCustomer.greeting()
println("name = ${CCustomer.name}, HELLO = ${CCustomer.HELLO}")
println(CCustomer.HOBBY.name)
}
- object 표현식: object 선언과 달리 이름이 없으며 싱글톤이 아니다. 따라서 사용될 때마다 새로운 인스턴스가 생성된다. 결과적으로 익명 내부 클래스의 형태를 object 표현식으로 구현할 수 있다.
/* object 표현식 사용하기 */
open class Superman() {
fun work() = println("Taking photos")
fun talk() = println("Talking with people.")
open fun fly() = println("Flying in the air.")
}
fun main() {
val pretendedMan = object: Superman() { // object 표현식으로 fly() 구현의 재정의
override fun fly() = println("I'm not a real superman. I can't fly!")
}
pretendedMan.work()
pretendedMan.talk()
pretendedMan.fly()
}
'Program Language > Kotlin' 카테고리의 다른 글
[Do it! 코틀린 프로그래밍] 8. 제네릭과 배열 (1) | 2021.04.25 |
---|---|
[Do it! 코틀린 프로그래밍] 7. 다양한 클래스와 인터페이스 (0) | 2021.04.19 |
[Do it! 코틀린 프로그래밍] 5. 클래스와 객체 (0) | 2021.04.10 |
[Do it! 코틀린 프로그래밍] 4. 프로그램의 흐름 제어 (0) | 2021.04.05 |
[Do it! 코틀린 프로그래밍] 3. 함수와 함수형 프로그래밍 (0) | 2021.04.03 |