[Kotlin in Action] 04장 - 클래스, 객체, 인터페이스


4장 - 클래스, 객체, 인터페이스

4.1 클래스 계층 정의

  • 아무것도 지정하지 않은 경우 기본 가시성이 자바와 다르다.
  • sealed 변경자
    • 클래스 상속을 제한

4.1.1 코틀린 인터페이스

interface Clickable {
    fun click()
}

class Button : Clickable {
    override fun click() = println("I was clicked")
}

-> Button().click()
  • 코틀린에서는 override 변경자를 꼭 사용해야 한다.
  • 인터페이스 멤버는 항상 열려 있으며 final로 변경할 수 없다.
  • 인터페이스 멤버의 경우 final, open, abstract를 사용하지 않는다.
코틀린에서 default 구현
interface Clickable {
    fun click() // 일반 메소드 선언
    fun showOff() = println("I'm clickable")    // 디폴트 구현이 있는 메소드
}

interface Focusable {
    fun showOff() = println("I'm focusable")    // 디폴트 구현이 있는 메소드
}
상속한 인터페이스의 메소드 구현 호출하기

class Button : Clickable, Focusable {
    override fun click() = println("I was clicked")
    override fun showOff() {    // 이름과 시그니처가 같은 멤버 메소드에 대해 둘 이상의 디폴트 구현이 있는 경우 인터페이스를 구현하는 하위 클래스에서 명시적으로 새로운 구현을 제공해야 한다.
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

4.1.2 open, final, abstract 변경자: 기본적으로 final

  • 자바의 클래스와 메소드는 기본적으로 상속에 대해 열려있지만 코틀린의 클래스와 메소드는 기본적으로 final 이다.
    • 어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다.
    • 오버라이드를 허용하고 싶은 메소드나 프로퍼티의 앞에도 open 변경자를 붙여야 한다.
open class RichButton : Clickable { // 이 클래스는 열려있다.
    fun disable() {}    // 이 함수는 final 이다.
    open fun animate() {}   // 이 함수는 열려있다.
    override fun click() {} // 이 함수는(상위 클래스에서 선언된) 열려있는 메소드를 오버라이드한다. 오버라이드한 메소드는 기본적으로 열려있다.
}
오버라이드 금지하기
open class RichButton : Clickable {
    final override fun click()  // final이 없는 override 메소드나 프로퍼티는 기본적으로 열려있으므로 final로 금지시킨다.
}
추상 클래스 정의하기
abstract class Animated {
    abstract fun animate()  // 추상 함수는 항상 열려 있다.
    open fun stopAnimating() {} // 추상 클래스에 속하더라도 비추상 함수는 기본적으로 파이널이지만 원한다면 open 으로 오버라이드를 허용 가능
    fun animateTwice() {}
}
  • abstract로 선언한 추상 클래스는 인스턴스화할 수 없다.
  • 추상 멤버 앞에 open 변경자를 명시할 필요가 없다.

4.1.3 가시성 변경자: 기본적으로 공개

  • 코틀린의 기본 가시성은 자바와 다르게 모두 공개(public) 이다
  • 자바의 기본 가시성인 패키지 전용(package-private)은 코틀린에 없다.
    • 코틀린은 페키지를 네임스페이스를 관리하기 위한 용도로만 사용한다.
    • 패키지 전용 가시성에 대한 대안으로 코틀린에는 internal 이라는 새로운 가시성 변경자를 도입했다.
변경자 클래스 멤버 최상위 선언
public(기본 가시성임) 모든 곳에서 볼 수 있다. 모든 곳에서 볼 수 있다.
internal 같은 모듈 안에서만 볼수 있다. 같은 모듈 안에서만 볼수 있다.
protected 하위 클래스 안에서만 볼 수 있다. (최상위 선언에 적용할 수 없음)
private 같은 클래스 안에서만 볼 수 있다. 같은 클래스 안에서만 볼 수 있다.
  • 코틀린 가시성 규칙은 단순한다.
  • protected 멤버는 오직 어떤 클래스나 그 클래스를 상속한 클래스 안에서만 보인다.
  • 클래스를 확장한 함수는 그 클래스의 privaate이나 protected멤버에 접근할 수 없다.
  • 외부 클래스가 내부 클래스나 중첩된 클래스의 private 멤버에 접근할 수 없다.

4.1.4 내부 클래스와 중첩된 클래스: 기본적으로 중첩 클래스

  • 코틀린의 중첩 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.
중첩 클래스를 사용해 코틀린에서 View 구현하기
class Button : View {
    override fun getCurrentState(): State = ButtonState()
    class ButtonState : State {}    // 자바의 정적 중첩 클래스와 대응된다.
}

nested-class

  • 내부 클래스 Inner 안에서 바깥쪽 클래스 Outer의 참조에 접근하려면 this@Outer라고 써야 한다.
class Outer {
    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }
}

4.1.5 봉인된 클래스: 클래스 계층 정의 시 계층 확장 제한

  • sealed 변경자
    • 상위 클래스를 상속한 하위 클래스 정의를 제한
sealed 클래스로 식 표현하기
sealed class Expr { // 기반 클래스를 sealed로 봉인한다.
    class Num(val value: Int) : Expr()  // 기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열한다.
    class Sum(val left: Expr, val right: Expr) : Expr()
}

fun eval(e: Expr): Int =
    when (e) {  // "when" 식이 모든 하위 클래스를 검사하므로 별도의 "else" 분기가 없어도 된다.
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }

4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언

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

다양한 초기화 생성 방법
class User(val nickname: String)    //파라미터가 하나뿐인 주 생성자

class User(val nickname: String, val isSubscribed: Boolean = true)  // 디폴트 값을 제공하는 생성자

class TwitterUser(nickname: String) : User(nickname) {} // 기반 클래스를 초기화하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자를 넘긴다.

open class Button   // 인자가 없는 디폴트 생성자 생성

class RadioButton: Button() // Button 클래스를 상속한 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 한다.

class Secretive private constructor() {} // 비공개 생성자 - 클래스 외부에서 인스터늣화하지 못하게 막을려면 private 선언

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

부 생성자를 생성하는 방법
open class View {
    constructor(ctx: Context) {}
}

open class MyButton : View {
    constructor(ctx: Context) : super(ctx) {}   // 상위 클래스의 생성자 호출
}

open class MyButton : View {
    constructor(ctx: Context) : this(ctx, MY_STYLE) {}   // 이 클래스의 다른 생성자에게 위임

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

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

추상 프로퍼티 선언이 들어있는 인터페이스 선언의 예
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)  // 프로퍼티 초기화 식 - 필드에 저장했다가 불러오는 방식
}

4.2.4 게터와 세터에서 뒷받침하는 필드에 접근

세터에서 뒷받침하는 필드 접근하기
class User(val name: String) {
    val address: String = "unspecified"
        set(value: String) {
            println("""
                Adress was changed for $name:
                "$field" -> "$value".""".trimIndent())  // 뒷받침하는 필드 값 읽기
            field = value   // field라는 특별한 식별자를 통해 뒷받침하는 필드 값 변경
        }
}

4.2.5 접근자의 가시성 변경

비공개 세터가 있는 프로퍼티 선언하기
class LengthCounter {
    var counter: Int = 0
        private set // 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다.
    fun addWord(word: String) {
        counter += word.length
    }
}

4.3 컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임

  • 코틀린 컴파일러가 데이터 클래스의 유용한 메소드를 자동으로 만들어 준다.
    • equals, hashCode, toString

4.3.1 모든 클래스가 정의해야 하는 메소드

  • 문자열 표현 : toString()
Client에 toString() 구현하기
class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}
  • 객체의 동등성: equals()
Client에 equals() 구현하기
class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?) = Boolean {
        if(other == null || other !is Client)   // 코틀린의 is 는 자바의 instanceof 와 같다.
            return false
        return name == other.name && postalCode == other.postalCode
    }
}
  • 해쉬 컨테이너: hashCode()
Client에 hashCode() 구현하기
class Client(val name: String, val postalCode: Int) {
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

4.3.2 데이터 클래스: 모든 클래스가 정의해야 하는 메소드 자동 생성

  • data 변경자를 클래스 앞에 붙이면 필요한 메소드를 컴파일러가 자동으로 만들어 준다.
data class Client(val name: String, val postalCode: Int)

4.3.3 클래스 위임: by 키워드 사용

  • 데코레이터 패턴 - 상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스를 만들되 기존 클래스와 같은 인터페이스를 데코레이더가 제공하게 만들고, 기존 클래스를 데이레이터 내부에 필드로 유지하는 것
    • by 키워드로 간단하게 구현 가능
class DelegatingCollection<T>(
    innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList()

4.4 object 키워드: 클래스 선언과 인스턴스 생성

4.4.1 객체 선언: 싱글턴을 쉽게 만들기

  • 싱글턴을 언어에서 긴본 지원
  • 객체 선언 - 클래스 선언과 클래스에 속한 단일 인스턴스의 선언을 합친 선언
object Payroll {
    val allEmployees = arrayListOf<Person>()
    fun calculateSalary() {
        for (person in allEmployees) {

        }
    }
}
중첩 객체를 사용해 Comparator 구현하기
data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int = p1.name.compareTo(p2.name)
    }
}
  • 클래스 안에서 싱글턴 객체를 생성해도 인스턴스는 단 하나뿐이다.

4.4.2 동반 객체: 팩토리 메소드와 정적 멤버가 들어갈 장소

  • 코틀린 언어는 자바 static 키워드를 지원하지 않는다.
  • companion 표시를 붙이면 그 클래스의 동반 객체로 만들 수 있다.
    • 동반 객체의 멤버를 사용하는 구문은 자바의 정적 메소드 호출이나 정적 필드 사용 구문과 같아진다.
class A {
    companion object {
        fun bar() {
            println("Companion object called")
        }
    }
}

-> A.bar()
  • 동반 객체는 팩토리 패턴을 구현하기 가장 적합한 위치
    • 바깥쪽 클래스의 private 생성자도 호출 가능
부 생성자를 팩토리 메소드로 대신하기
class User private constructor(val nickname: String) {
    companion object {
        fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
    }
}

-> User.newSubscribingUser("bob@gmail.com")

4.4.3 동반 객체를 일반 객체처럼 사용

동반 객체에 이름 붙이기
class Person(val name: String) {
    companion object Loader {
        fun fromJSON(jsonText: String): Person = ...
    }
}

// 아래 두가지 방법 모두 가능
-> Person.Loader.fromJSON("{name: 'Dmitry')"})
-> Person.fromJSON("{name: 'Dmitry')"})
동반 객체에서 인터페이스 구현
interface JONSFactory<T> {
    fun fromJSON(jsonText: String) : T
}

class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonText: String) : Person = ...  // 동반 개체가 인터페이스를 구현한다.
    }
}
동반 객체 확장
// 비즈니스 로직 모듈
class Person(val firstName: String, val lastName: String) {
    companion object {  // 비어있는 동반 객체 선언
    }
}

// 클라이언트/서버 통신 모듈
fun Person.Companion.fromJSON(json: String) : Person { // 확장 함수 선언, 동반 객체 기본 이름인 Companion 사용
}

-> Person.fromJSON(json)
  • 동반 객체 안에서 fromJSON 함수를 정의한 것처럼 fromJSON을 호출 가능
    • 실제로는 클래스 밖에서 정의한 확장 함수

4.4.4 객체 식: 무명 내부 클래스를 다른 방식으로 작성

  • 무명 객체 정의할 때도 object 키워드 사용
  • 자바와 달리 여러 인터페이스를 구현하거나 클래스를 확장하면서 인터페이스 구현 가능
val listener = object : MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {...}
    override fun mouseEntered(e: MouseEvent) {...}
}
무명 객체 안에서 로컬 변수 사용하기
fun countClicks(window: Window) {
    var clickCount = 0  // 로켤 변수 정의
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++    // 로컬 변수의 값을 변경
        }
    })
}