관리 메뉴

너와 나의 스토리

[Kotlin] 클래스와 인터페이스1 - 초기화(주 생성자와 부 생성자)/가시성 변경자/프로퍼티 본문

Programming Language/Kotlin

[Kotlin] 클래스와 인터페이스1 - 초기화(주 생성자와 부 생성자)/가시성 변경자/프로퍼티

노는게제일좋아! 2021. 6. 14. 10:19
반응형

코틀린의 클래스와 인터페이스는 자바 클래스, 인터페이스와는 약간 다르다. 

  • 인터페이스에 프로퍼티 선언이 들어갈 수 있다.
  • 코틀린 선언은 기본적으로 final이며 public이다. 

 

클래스 계층 정의

코틀린 인터페이스

  • 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메서드도 정의할 수 있다. -> Java 8의 default 메서드와 비슷
    • 코틀린에서는 default 같은 키워드 따로 추가 안 하고 평범한 메서드처럼 선언하면 됨.
  • 다만, 인터페이스에 필드는 들어갈 수 없다.
  • 단순한 인터페이스를 구현해보자.
    • 자바의 extends와 implements를 코틀린에서는 구분하지 않고 ':'로 모두 처리한다.
    • 코틀린에서는 override 변경자는 선택이 아닌 필수로 사용해야 한다.
interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}

class Button : Clickable {
    override fun click() = println("I was clicked")
    override fun showOff() = super<Clickable>.showOff()
}

fun main(args: Array<String>) {
    Button().click() // output: I was clicked 
    Button().showOff() // output: I'm clickable!
}
  • clickable 인터페이스에서 showOff() 메서드를 정의했지만 Button에서도 오버라이딩 메서드를 제공해야 한다. 
    • 위 예제처럼 super 키워드를 통해 인터페이스에서 정의한 메서드를 그대로 사용할 수 있다. 

 

 

open, final, abstract 변경자

final

  • 코틀린은 기본적으로 상속에 대해 열려있지만 클래스와 메서드는 기본적으로 final이다.

open

  • 어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다. 
  • 그와 더불어 오버라이드를 허용하고 싶은 메서드나 프로퍼티의 앞에도 open 변경자를 붙여야 한다.

abstract

  • abstract로 선언한 추상 클래스는 인스턴스화할 수 없다. 
  • 추상 클래스에는 구현이 없는 추상 멤버가 있기 때문에 하위 클래스에서 그 추상 멤버를 오버라이드해야만 하는 게 보통이다. 
  • 이 때문에, 추상 멤버는 항상 open임으로 open 키워드를 붙일 필요가 없다.

 

Visibility modifier 

  • 기본적으로 코틀린 가시성 변경자는 자바와 비슷하다. 
    • public, protected, private 변경자가 있다.
  • 코틀린은 자바와 달리 아무 변경자도 없는 경우 선언은 모두 public이다. 
  • 자바의 기본 가시성인 package-private은 코틀린에 없다. 
    • 코틀린은 패키지를 namespace를 관리하기 위한 용도로만 사용하기 때문에 package를 가시성 제어에 사용하지 않는다.
  • 대신 코틀린에는 internal이라는 가시성 변경자가 있다. 
    • inernal은 모듈 내부에서만 볼 수 있다는 뜻이다.
    • 모듈(module): 한 번에 컴파일되는 코틀린 파일들
  • 코틀린에서는 외부 클래스가 내부 클래스나 중첩된 클래스의 private 멤버에 접근할 수 없다.

 

 

내부 클래스와 중첩 클래스

  • 코틀린의 중첩 클래스(nested class)는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다. 
  • 설명 추후 추가

 

 

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

class User(val nickname: String)
  • 위 클래스 선언을 보면 클래스 이름 뒤에 괄호가 있다. 
    • 이 괄호로 둘러싸인 코드를 주 생성자(primary constructor)라고 부른다.
  • 주 생성자는 아래의 두 가지 목적에 쓰인다.
    • 생성자 파라미터를 지정
    • 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의
  • 이를 풀어쓰면 다음과 같다.
    • constructor: 주 생성자나 부 생성자 정의를 시작할 때 사용
    • init: 초기화 블록을 시작. 초기화 블록은 주 생성자와 함께 사용된다. 주 생성자는 제한적이기 때문에 별도의 코드를 포함할 수 없으므로 초기화 블록이 필요하다. 
      • 초기화  블록을 클래스 안에 여러 개 선언할 수 있다.
class User constructor(_nickname: String) {
    val nickname: String

    init {
        nickname = _nickname
    }
}
  • 주 생성자 앞에 별다를 어노테이션이나 가시성 변경자가 없다면 constructor를 생략해도 된다.
class User(_nickname: String) {
    val nickname = _nickname
}
  • 주 생성자의 파라미터로 프로퍼티를 초기화한다면 그 주 생성자 파라미터 이름 앞에 val을 추가하는 방식으로 프로퍼티 정의와 초기화를 간략히 쓸 수 있다.
class User(val nickname: String)
  • 디폴트 값을 정의할 수도 있다.
class User(val nickname: String = "hororolol")
  • 클래스의 인스턴스를 만들려면 new 키워드 없이 생성자를 직접 호출하면 된다.
val kotlin = User("Kotlin")
println(kotlin.nickname) // output: Kotlin

 

기반 클래스 초기화

open class Button // 인자가 없는 디폴트 생성자가 자동으로 만들어짐

class RadioButton: Button()
  • Button의 생성자는 아무 인자도 받지 않지만, Button 클래스를 상속한 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 한다. 
    • 즉, 기반 클래스의 이름 뒤에 꼭 빈 괄호를 넣어줘야 한다. 생성자 인자가 있다면 괄호 안에 인자가 들어간다.

 

 

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

  • 코틀린에서는 디폴트 파라미터 값을 줄 수 있으므로 생성자를 여러 개 두는 상황이 적다.
  • 그래도 생성자가 여럿 필요한 경우가 가끔 있다.
open class View {
    constructor(cts: Context) {
        //...
    }
    constructor(ctx: Context, attr: AttributeSet) {
        //...
    }
}

class MyButton : View {
    constructor(ctx: Context)
            : super(ctx) {
        //...
    }
    constructor(ctx: Context, attr: AttributeSet)
            : super(ctx, attr) {
        //...
    }
}
  • MyButton 클래스는 주 생성자를 선언하지 않고, 부 생성자만 2가지 선언한다.
  • 부 생서자는 constructor 키워드로 시작한다. 
  • 부 생성자는 여러개 생성할 수 있다. 
  • 클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 한다.

 

 

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

  • 코틀린에서는 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다. 
interface User {
    val nickname: String
}
  • 이는 User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야 한다는 뜻이다. 
  • User 인터페이스를 구현하는 방법을 몇 가지 살펴보자.
class PrivateUser(override val nickname: String) : User
  • PrivateUser는 주 생성자 안에 프로퍼티를 직접 선언하는 간결한 구문을 사용한다. 
    • 이 프로퍼티(nickname)는 User의 추상 프로퍼티를 구현하고 있으므로 override를 꼭 표시해야 한다. 
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')
}
  • SubscribingUser는 커스텀 getter로 nickname 프로퍼티를 구현한다. 
    • 이 프로퍼티는 매번 이메일 주소에서 nickname을 계산해서 반환한다. 
class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId)
}
  • FacebookUser에서는 초기화 식으로 nickname을 초기화한다. 
    • 이는 객체 초기화 식을 이용하였기 때문에, 한 번만 getFacebookName() 메서드를 호출하게 된다.

 

프로퍼팅 어떤 값을 저장하되 그 값을 변경하거나 읽을 때마다 정해진 로직을 실행하는 유형의 프로퍼티를 만드는 방법

  • 값을 저장하는 동시에 로직을 실행하기 위해서는 접근자 안에서 프로퍼티를 뒷받침하는 필드에 접근할 수 있어야 한다. 
  • 프로퍼티에 저장된 값의 변경 이력을 로그에 남기려는 경우를 생각해보자.
class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println("""
            Address was changed for $name: "$field" -> "$value" // 뒷받침하는 필드 값 읽기
            """.trimIndent())
            field = value  // 뒷받침하는 필드 값 변경
        }
}

fun main(args: Array<String>) {
    val user = User("Alice")
    user.address = "Elsenheimerstrasses 47"
    // output: Address was changed for Alice: "unspecified" -> "Elsenheimerstrasses 47"
}
  • $field: 현재 address 주소
  • $value: 새로운 address 주소
  • field라는 식별자를 통해 뒷받침하는 필드에 접근할 수 있다.

 

 

Private setter가 있는 프로퍼티

class LengthCounter {
    var counter: Int = 0
        private set  // counter 프로퍼티 값을 클래스 밖에서 바꿀 수 없음

    fun addWord(word: String) {
        counter += word.length
    }
}

fun main(args: Array<String>) {
    val lengthCounter = LengthCounter()
    lengthCounter.addWord("Hi!")
    println(lengthCounter.counter) // output: 3
//    lengthCounter.counter = 4 -> error
}

 

 

 

 

출처:

- [Kotlin IN ACTION]

 

 

 

 

 

 

 

반응형
Comments