본문 바로가기
Kotlin

Kotlin #8 : 코틀린의 프로퍼티, 자바의 필드, 지연 초기화, lazy, 위임 230220

by haheehee 2023. 2. 22.

++ 코틀린의 프로퍼티(Properties) ++

- 변수 선언과 기본적인 접근 메서드를 모두 가지고 있음

- 내부적으로 생성 (따로 접근 메서드를 만들지 않아도 됨)

 

** 자바의 필드 : 단순한 변수 선언만 가지기 때문에 접근을 위한 메서드를 따로 만들어야 한다.

 

++ 게터(Getter)와 세터(Setter) ++

- 게터와 세터를 합쳐 접근 메서드(Access methods)라고 함

- 자바에서는 모든 필드에 대한 접근 메서드를 만들어야 함

 

value: 세터의 매개변수로 외부로부터 값을 가져옴

- 외부의 값을 받을 변수가 되므로 value 대신에 어떤 이름이든지 상관 없음

 

field: 프로퍼티를 참조하는 변수로 보조 필드(backing field)로 불림

- 프로퍼티를 대신할 임시 필드로 만일 프로퍼티를 직접 사용하면 게터나 세터가 무한 호출되는 재귀에 빠짐

 

var 프로퍼티이름[: 프로퍼티자료형] [= 프로퍼티 초기화]
	[get() { 게터 본문 } ]
	[set(value) {세터 본문}]

val 프로퍼티이름[: 프로퍼티자료형] [= 프로퍼티 초기화]
	[get() { 게터 본문 } ]

** 불변형인 val은 Getter만 설정 가능하다.


package com.example.pp167

class Person {
    // 멤버 필드
    private String name
    private int age
    
    // 생성자
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // 게터와 세터
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    public int getAge() {
        return age;
    }
    ...
}

자바의 Getter, Setter


package com.example.pp171

class User(_id: Int, _name: String, _age: Int) {
    // 프로퍼티
    val id: Int = _id
        get() = field
    
    var name: String = _name   
        get() = field
        set(value) {
            field = value
        }
    
    var age: Int = _age
        get() = field
        set(value) {
            field = value
        }
}

fun main() {
    val user1 = User(1, "Kildong", 30)
    // user1.id = 2 // 에러. val 프로퍼티는 값 변경 불가
    user1.age = 35  // 세터 동작
    println("user1.age = ${user1.age}") // 게터 동작
}

게터와 세터 예제

Getter와 Setter

- field는 꼭 이 단어로 사용해야 하는 의무필드이다.

 

package com.example.pp173

// 커스텀 게터와 세터의 사용
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, "kildong", 35)
    user1.name = "coco"  // 여기서 사용자 고유의 출력 코드가 실행된다.
    println("user3.name = ${user1.name}")
}

커스텀 Getter와 Setter 사용하기

 

package com.example.pp174

import java.lang.AssertionError

class User(_id: Int, _name: String, _age: Int) {
    val id: Int = _id
    private var tempName: String? = null
    var name: String = _name
        get() {
            if(tempName == null) tempName = "NONAME"
            return tempName ?: throw AssertionError("Asserted by others")
        }
    var age: Int = _age
}

fun main() {
    val user1 = User(1, "kildong", 35)
    user1.name = ""
    println("user3.name = ${user1.name}")
}

임시적인 보조 프로퍼티

 

package com.example.pp175

open class First {
    open val x: Int = 0 // 오버라이딩 가능
        get() {
            println("First x")
            return field
        }
    val y: Int = 0  // open 키워드가 없으면 final 프로퍼티
}

class Second: First() {
    override val x: Int = 0 // 부모와 구현이 다름
        get() {
            println("Second x")
            return field + 3
        }
    // override val y: Int = 0  // 에러. 오버라이딩 불가능
}

fun main() {
    val second = Second() 
    println(second.x)   // 오버라이딩된 두 번쨰 클래스 객체의 x
    println(second.y)   // 부모로부터 상속받은 값
}

프로퍼티의 오버라이딩 사용하기

 


++ 지연 초기화 ++

<변수나 객체의 값은 생성시 초기화 필요>

- 클래스에서는 기본적으로 선언하는 프로퍼티 자료형들은 null을 가질 수 없음

- 하지만, 객체의 정보가 나중에 나타나는 경우 나중에 초기화 할 수 있는 방법 필요

- 지연 초기화를 위해 lateinit과 lazy 키워드 사용

** 선언만 먼저 하고, 값대입을 나중에 한다고 생각하면 된다.

 

++ lateinit을 사용한 지연 초기화 ++

의존성이 있는 초기화나 unit 테스트를 위한 코드를 작성 시

    예) Car클래스의 초기화 부분에 Engine 클래스와 의존성을 가지는 경우 Engine 객체가 생성되지 않으면 완전하게 초기화 할 수 없는 경우

    예) 단위(Unit) 테스트를 위해 임시적으로 객체를 생성 시켜야 하는 경우

프로퍼티 지연 초기화

     클래스를 선언할 때 프로퍼티 선언은 null을 허용하지 않는다.

     하지만, 지연 초기화를 위한 lateinit 키워드를 사용하면 프로퍼티에 값이 바로 할당되지 않아도 됨

** var로 선언된 프로퍼티만 가능

** 프로퍼티에 대한 게터와 세터를 사용할 수 없음

 

객체 지연 초기화

- 객체 생성 시 lateinit을 통한 지연 초기화 가능

data class Person(var name:String, var age:Int)

lateinit var person1: Person // 객체 생성의 지연 초기화

fun main() {
	person1 = Person1("Kildong",30) // 생성자 호출 시점에서 초기화됨
	print(person1.name + " is " + person1.age.toString())
}

 

lazy를 사용한 지연 초기화

lazy를 통한 지연 초기화 특징 : _val만 가능

- 호출 시점에 by lazy {...} 정의에 의해 블록 부분의 초기화를 진행

- 불변의 변수 선언인 val에서만 사용 가능(읽기 전용)

- val이므로 값을 다시 변경할 수 없다. 


package com.example.pp179

class Person {
    lateinit var name: String // 늦은 초기화를 위한 선언

    fun test() {
        if(!::name.isInitialized) { //프로퍼티의 초기화 여부 판단
            println("not initialized")
        } else {
            println("initialized")
        }
    }
}

fun main() {
    val kildong = Person()
    kildong.name = "Kildong"    // 이 시점에서 초기화된다.(지연 초기화)
    kildong.test()
    println("name = ${kildong.name}")
}

lateinit을 이용해 늦은 초기화하기

:: 참고하다, 참조하다 라는 뜻.

 

package com.example.pp182

class LazyTest {
    init {
        println("init block")
    }
    val subject by lazy {
        println("lazy initialized")
        "Kotlin Programming"    // lazy 반환값
    }
    fun flow() {
        println("not initialized")
        println("subject one: $subject")    // 최초 초기화
        println("subject two: $subject")    // 이미 초기화된 값 사용
    }
}

fun main() {
    val test = LazyTest()
    test.flow()
}

by lazy로 선언된 프로퍼티 지연 초기화 하기

 


++ 위임(delegation) ++

- 하나의 클래스가 다른 클래스에 위임하도록 선언

- 위임된 클래스가 가지는 멤버를 참조없이 호출

 

< val|var|class> 프로퍼티 혹은 클래스 이름: 자료형 by 위임자

 

코틀린의 기본 라이브러리는 open되지 않은 최종 클래스

- 표준 라이브러리의 무분별한 상속의 복잡한 문제들을 방지

- 단, 상속이나 직접 클래스의 기능 확장을 하기 어려움

 

- 위임을 통해 상속과 비슷하게 최종 클래스의 모든 기능을 사용하면서 동시에 기능을 추가 확장 구현 가능

 

 

 


package com.example.pp187

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()}")   // 참조 없이 각 인터페이스 구현 클래스의 go를 접근
    }
}
fun main() {
    val myDamas = CarModel("Damas 2010", VanImpl("100마력"))
    val my350z = CarModel("350Z 2008", SportImpl("350마력"))

    myDamas.carInfo()   // carInfo에 대한 다형성을 나타냄
    my350z.carInfo()
}

클래스 위임 사용하기

VanImpl은 구현 클래스

SportImpl은 역시 Car에서 구현한 구현 클래스

go()메소드를 재정의(오버라이드)

 


by lazy {...} 도 위임

- 사용된 프로퍼티는 람다식 함수에 위임되어 함수에 의해 사용

  1.  lazy 람다식 함수는 람다를 전달받아 저장한 Lazy 인스턴스를 반환한다.
  2. 최초 프로퍼티의 게터 실행은 lazy에 넘겨진 람다식 함수를 실행하고 결과를 기록한다.
  3. 이후 프로퍼티의 게터 실행은 이미 초기화되어 기록된 값을 반환한다.

정적 변수와 Companion 객체

사용 범위에 따른 분류

- 지역(local)

- 전역(global)

 

보통 클래스는 동적으로 객체를 생성하는데 정적으로 고정하는 방법 :

  • 동적인 초기화 없이 사용할 수 있는 개념으로 자바에서는 static 변수 또는 객체
  • 코틀린에서는 이것을 컴페니언 객체(Companion object)로 사용
  • 프로그램 실행 시 고정적으로 가지는 메모리로 객체 생성 없이 사용
  • 단, 자주 사용되지 않는 변수나 객체를 만들면 메모리 낭비

package com.example.pp190

class Person {
    var id: Int = 0
    var name: String = "Youngdeok"
    companion object {
        var language: String = "Korean"
        fun work() {
            println("working...")
        }
    }
}

fun main() {
    println(Person.language)    // 인스턴스를 생성하지 않고 기본값 사용
    Person.language = "English" // 기본 값 변경 가능
    println(Person.language)    // 변경된 내용 출력
    Person.work()   // 메서드 실행
    // println(Person.name) // name은 companion object가 아니므로 에러
}

컴패니언 객체 사용해보기 예제

컴패니언 객체는 실제 객체의 싱글톤(singleton)으로 정의됨

 

 

1.

package com.example.pp191;

// 자바의 Customer 클래스
public class Customer {
    public static final String LEVEL = "BASIC"; // static 필드
    public static void login() {    // static 메서드
        System.out.println("Login...");
    }
}

2.

package com.example.pp191

// 코틀린에서의 자바의 static 접근
fun main() {
    println(Customer.LEVEL)
    Customer.login()
}

코틀린에서의 자바의 static 멤버 사용 예제

 

댓글