오늘도 더 나은 코드를 작성하였습니까?

클래스 객체 인터페이스(생성자와 프로퍼티를 갖는 클래스 선언) 본문

Kotlin in Action/코틀린 기초

클래스 객체 인터페이스(생성자와 프로퍼티를 갖는 클래스 선언)

hik14 2020. 8. 13. 19:43

코틀린은 주(primary) 생성자와 부(secondary) 생성자를 구분한다.

초기화 블록을 통해 초기화 로직을 추가할 수 있다.

 

클래스 초기화: 주 생성자와 초기화 블록 

class User( 주 생성자  ){ ... }

 

주 생성자는 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 2가지 목적을 가지고 있다.

 

class User constructor(_nickname: String){
    val nickname: String
    
    init {
        nickname = _nickname
    }
}

* _nickname은 자바처럼 this.nickname = nickname ; 과같은 파라미터와 멤버 변수의 이름이 동일할 때 모호성을 없애기 위해 사용된다.(this를 사용해도 된다.)

 

contstructor - 주 생성자 또는 부 생성자를 정의를 시작할 때 사용된다.

init -  초기화 블록을 설정할 때 시작 사용된다.

 

초기화 블록

- 객체 생성될 때 호출된다.

- 주 생성자는 제한적이라 필요한 코드가 있으면 초기화 블록으로 처리한다.

- 여러 개 선언할 수 있다.

 

class User (_nickname: String){
    val nickname: String = _nickname
}

별다른 로직이 필요 없어 초기화 블록을 없애버렸다.

contstructor 주 생성자 앞에 별다른 애노테이션이나 가시성 변경자를 넣을 필요가 없다면 생략 가능하다.

 

*프로퍼티를 초기화하는 식이나 초기화 블록 안에서만 주 생성자의 파라미터를 참조할 수 있다. 

 

class User (val nickname: String)

별다른 로직이 필요 없고 주 생성자 파라미터로 바로 프로퍼티를 초기화한다면 val 키워드를 붙여 정의와 초기화를 간략하게 할 수 있다.

 

class User (val nickname: String, val isSubscribed: Boolean = true)

함수 파라미터와 동일하게 주 생성자 파라미터 역시 default 값을 지정할 수 있다.

 

*만약 모든 주 생성자 파라미터에 default 값을 지정한다면 코틀린 컴파일러는 자동으로 파라미터가 존재하지 않는(디폴트 값으로만 구성되도록) 생성자를 만들어준다. DI(의존성 주입) 라이브러리를 이용할 때 유용하다.

 

*자바와 같이 어떤 생성자도 생성하지 않는다면 빈 생성자를 생성해준다. 

--> 상속 및 인터페이스 구현 시 :  className() 괄호가 존재하면 클래스다. 

open class User (val nickname: String){}

class TwitterUser(nickname: String): User(nickname){  }

오픈된 클래스인 User를 상속하고 상속받을 User의 nickname 프로퍼티에 주생 성자의 파라미터인 nickname의 값을 넘겨준다.

class TwitterUser private constructor(nickname: String): User(nickname){  }

주 생성자를 통해 객체 생성을 막으려면 private 변경자를 붙이면 된다. 

 

부 생성자: 상위 클래스를 다른 방식으로 초기화

* 인자에 디폴트 값을 제공하기 위해 부 생성자를 여러 개 만들면 안된다. 대신 파라미터의 디폴트 값을 제공해라 

 

이 클래스는 주 생성자를 선언하지 않고 2개의 부 생성자로 이루어져 있다.

부 생성자는 constructor 키워드로 시작하고 파라미터를 받을 수 있고 블록 안에 로직을 넣을 수 있다.

open class View{
    
    constructor(ctx: Context){
        
    }
    
    constructor(ctx: Context, attr: AttributeSet){
        
    }
}

: super(parm) { ... } 자바와 같이 super키워드로 생성을 상위 클래스에 위임할 수 있다.

:this(parm)으로 동일 클래스 다른 생성자에게 위임한다.

 

class MyButton: View{

    constructor(ctx: Context): this(ctx, My_STYLE){}

    constructor(ctx: Context, attr: AttributeSet): super(ctx, attr){}
}

* 클래스에 주 생성자가 존재하지 않는다면

모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 된다.

 

* 부생성자의 주된 목적은 자바와의 상호운용에 있다.

 

인터페이스에 선언된 프로퍼티의 구현

interface User{
    val nickname: String
}

class PrivateUser(override val nickname: String): User

class SubscribingUser(val email: String): User{
    override val nickname: String
        get() = email.substringBefore("@")
}

class FacebookUser(val accountId: Int): User{
    override val nickname = getFacebookName(accountId)

    private fun getFacebookName(accountId: Int): String = "hello"
}


fun main() {

    val user1 = PrivateUser(nickname = "hoho")
    val user2 = SubscribingUser(email = "hahaha@gmail.com")
    val user3 = FacebookUser(accountId = 1)

    println(user1.nickname)
    println(user2.nickname)
    println(user3.nickname)
}

PrivateUser

- 주 생성자 안에 프로퍼티에 직접 선언 후 override를 통해 추상 프로퍼티를 구현한다.

 

SubscribingUser

- 커스텀 겟터를 통해 추상 프로퍼티를 설정한다  백킹 필드에 값을 저장하는 것이 아님. 

 

FacebookUser

- 초기화 함수로 추상 프로퍼티를 초기화한다. 

 

*SubscribingUser nickname은 매번 호출될 때마다 계산되는 겟터를 사용하고

FacebookUser 한번 식을 이용한 저장으로 필드에 저장 후 불러온다.

 

interface User{
    val email: String
    
    val nickname: String
        get() = email.substringBefore("@")
}

인터페이스는 추상 프로퍼티뿐 아니라 겟터 셋터를 가있는 프로퍼티를 선언할 수 있는데 이때 백킹 필드는 없다

그 이유는 인터페이스는 상태를 저장할 수 없다.

 

하위 구현 클래스는 email 은 반드시 override로 구현을 해야 되지만 nickname 같은 경우는 오버라이드 하지 않고 상속할 수 있다.

게터와 세터에서 백킹 필드에 대한 접근

값을 저장하는 동시에 로직을 실행할 수 있게 하기 위해서는 접근자(get set) 안에서 프로퍼티의 백킹 필드에 접근할 수 있어야 한다.

 

class User{
    var address: String = "unspecificed"

        set(value: String){
            println(""" 
           Address was Changed :
            "$field" -> $value.
        """.trimIndent())
            field = value
        }
}

fun main() {
    val user = User()

    println(user.address)
    user.address= "우리집이야!"
    println(user.address)
}

field라는 식별자를 이용해서 백킹 필드에 접근할 수 있으며 getter에서는 읽을 수만 있고 setter에서는 쓸 수만 있다.

 

컴파일러는 디폴트 겟터 셋터를 사용하건 커스텀 접근자 정의 하던 field를 사용하는 프로퍼티에 백킹 필드를 생성한다.

하지만 field를 사용하지 않는 커스텀 접근자를 구현을 정의했다면  백킹 필드는 존재하지 않는다.

(val 은 get var 은 get set에서 모두 사용하지 않는다면)

 

접근자(getter, setter)의 가시성 변경

 

접근자의 가시성은 기본적으로 프로퍼티 가시성과 동일하지만 추가로 가시성 변경자를 통해 변경할 수 있다.

 

class LengthCounter{
    
    var counter: Int = 0
     private set
    
    fun addWord(word: String) {
        counter += word.length
    }
}

counter 프로퍼티는 추가되는 단어의 길이만큼 증가해야 돼서 변경 가능한 멤버 변수이다.

하지만 외부에서 추가적인 변경을 하면 안 되기 때문에 set을 private로 지정하였다.

 

 

프로퍼티에 추가적으로 다룰내용

 

lateinit는

null 이 될 수 없는 프로퍼티에 지정하면 프로퍼티를 생성자가 호출된 다음에 초기화하겠단 것이다.

 

lazy initalized는

요청이 들어오면 비로소 초기화된다, 더 일반적인 위임 프로퍼티이다.

 

@JvmField

프로퍼티에 붙이면 접근자가 없는 public 필드를 만든다.

 

const

애노테이션을 더 편리하게  다룰 수 있고 원시 타입 및 String 타입인 값을 애노테이션의 인자로 활용할 수 있다.