코틀린

Do it! 코틀린 프로그래밍 - 06 프로퍼티와 초기화

조요피 2021. 7. 13. 21:11

자바에서는 getter와 setter의 역할을 하는 메서드를 직접 만들어야 한다.

하지만, 코틀린에서는 프로퍼티의 getter와 setter를 자동으로 만든다.

코틀린에서 프로퍼티는 반드시 초기화돼야 하는데 초기화를 나중에 할 수 있도록

lateinit, lazy를 사용할 수 있다.

06-1 프로퍼티의 접근

클래스 내에 선안한 변수를 '프로퍼티'라고 한다.

자바에서는 이런 변수를 '필드'라고 한다.

코틀린에서는 변수의 선언 부분과 기본적인 접근 메서드를 모두 가지고 있기 때문에

프로퍼티라는 새로운 이름으로 부른다.

 

자바에서는 private 필드에 직접 접근할 수 없고

setter나 getter를 통한 접근만 허용한다.

필드가 점점 늘어나면 접근 메서드도 함께 많아지게 된다. (최대 단점)

 

코틀린에서는 객체를 생성하고 점(.) 표기법으로 프로퍼티에 접근한다.

프로퍼티에 직접 접근하는 것처럼 보이나 실제로는 getter 또는 setter로 접근한다.

 

다음과 같이 getter와 setter를 직접 지정할 수도 있다.

package com.momokddi.demo

class Demo(_name: String, _age: Int) {
    val name: String = _name
        get() = field

    var age: Int = _age
        get() = field
        set(value) {
            field = value
        }
}

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

*field : 프로퍼티를 참조하는 변수

 

다음과 같이 getter와 setter를 변경하여 작성할 수 있다.

class Demo() {
    var age: Int = 0
        get() {
            return when {
                field <= 20 -> 30
                field >= 50 -> 40
                else -> field
            }
        }
}

fun main(){
    val d = Demo()

    d.age = 20
    println(d.age)
}

// 30

06-2 지연 초기화와 위임

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

하지만, lateinit 키워드를 사용하면 프로퍼티에 값이 바로 할당되지 않아도 컴파일러가 허용한다.

lateinit

  • var로 선언된 프로퍼티만 가능
  • 프로퍼티에 대한 getter와 setter를 사용할 수 없다.
class Demo() {
    lateinit var name: String
}

fun main() {
    val d = Demo()
    d.name = "hi"
    println(d.name)

lazy

  • 호출 시점에 by lazy {...} 정의에 의해 블록 부분의 초기화를 진행한다.
  • 불변의 변수 선언인 val에서만 사용 가능하다(읽기 전용).
class Demo() {
    val name by lazy {
        println("lazy init")
        "yopi" // lazy 반환 값
    }
}

fun main() {
    val d = Demo()
    println("not init")
    println(d.name) // 최초 초기화
    println(d.name) // 초기화된 값을 사용
}

//not init
//lazy init
//yopi
//yopi

객체 지연 초기화

class Demo(val name: String, val age: Int)

fun main() {
    val d: Demo by lazy {
        Demo("yopi", 10)
    }
    val d2 = lazy { Demo("kkk", 99) }

    println("d2 init, ${d2.isInitialized()}")

    println("d, ${d.name}") // 초기화
    println("d2, ${d2.value.name}") // 초기화

    println("d2 init, ${d2.isInitialized()}")
}

//d2 init, false
//d, yopi
//d2, kkk
//d2 init, true

객체의 프로퍼티나 메서드가 접근되는 시점에서 초기화가 된다.

by lazy & lazy

by lazy는 객체의 위임을 나타내며 lazy는 변수에 위임된 Lazy 객체 자체를 나타내므로 value를 한 단계 거쳐 멤버로 접근해야 한다.

*lazy는 사실 람다식

by를 이용한 위임(Delegation)

특정 클래스를 확장하거나 이용할 수 있도록 by를 통한 위임이 가능.

하나의 클래스가 다른 클래스에 위임하도록 선언하여 위임된 클래스가 가지는 멤버를 참조 없이 호출할 수 있게 된다.

프로퍼티 위임이란 프로퍼티의 getter와 setter를 특정 객체에게 위임하고 그 객체가 값을 읽거나 쓸 때 수행하도록 만드는 것.

프로퍼티 위임을 하려면 위임을 받을 객체에 by 키워드를 사용하면 된다.

클래스의 위임

왜 위임을 사용하나?

기본적으로 코틀린이 가지고 있는 표준 라이브러리는 final 형태의 클래스이므로 상속이나 직접 클래스의 기능 확장이 어렵게 된다. 이렇게 어렵게 만들어 둠으로써 표준 라이브러리의 무분별한 상속에 따른 복잡한 문제를 방지할 수 있다.

필요한 경우에만 위임을 통해 상속과 비슷하게 해당 클래스의 모든 기능을 사용하면서 동시에 기능을 추가 확장 구현할 수 있는 것.

interface Memo {
    fun hi() : String
}

class Demo() : Memo {
    override fun hi(): String {
        return "hi, Demo"
    }
}

class Cemo() : Memo {
    override fun hi(): String {
        return "hi, Cemo"
    }
}

class By(impl : Memo) : Memo by impl {
    fun f() {
        println(hi())
    }
}

fun main() {
    val a = By(Demo())
    val b = By(Cemo())

    a.f()
    b.f()
}

//hi, Demo
//hi, Cemo

observable() 사용 방법

import kotlin.properties.Delegates

class Demo() {
    var name : String by Delegates.observable("noname") {
        property, oldValue, newValue ->  println("$oldValue -> $newValue")
    }
}


fun main() {
    val d = Demo()
    d.name = "a"
    d.name = "b"
}

//noname -> a
//a -> b

vetoable() 사용 방법

import kotlin.properties.Delegates

class Demo() {
    var age: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
        newValue > oldValue
    }
}

fun main() {
    val d = Demo()

    d.age = -1
    println(d.age)

    d.age = 10
    println(d.age)

    d.age = 5
    println(d.age)

    d.age = 20
    println(d.age)
}

//0
//10
//10
//20

06-3 정적 변수와 컴패니언 객체

모든 변수나 클래스의 객체는 동적으로 생성해서 사용해야 할까?

동적인 초기화 없이 사용할 수 있는 개념이 있다.

  • 정적 변수 (Static Variable)
  • 컴패니언 객체(Companion Object)

정적 변수와 컴패니언 객체

정적 변수나 메서드를 사용하면 프로그램 실행 시 메모리를 고정적으로 가지게 되어 따로 인스턴스화할 필요 없이 사용할 수 있다.

 

코틀린에서는 정적 변수를 사용할 때 static 키워드가 없는 대신 컴패니언 객체를 제공.

class Demo() {
    val name = "yopi"
    val age = 9

    companion object {
        val coName = "yyj"
        val coAge = 99
        fun f(){
            println("$coName $coAge")
            // println("$name $age") // error
        }
    }
}

fun main() {
    println("${Demo.coName} ${Demo.coAge}")
    Demo.f()
}

//yyj 99
//yyj 99

Object 표현식

open class Demo() {
    fun f1() {
        println("f1")
    }

    open fun f2() {
        println("f2")
    }
}

fun main() {
    val d = object : Demo() {
        override fun f2() {
            println("override f2")
        }
    }

    d.f2()
}

// override f2

딱 한 번만 구현되는 인터페이스 구현 클래스를 정의하기가 부담스러운 경우에 위와 같이 사용할 수 있다.