코틀린 스터디 4장 정리 - 클래스, 객체, 인터페이스

Posted by 김순철 on October 24, 2018

Chapter 4. 클래스, 객체, 인터페이스

스터디 날짜

2018-10-17(수)
2018-10-24(수)

  • 자바와 달리 인터페이스에 프로퍼티를 생성할 수 있다.
  • 코틀린의 선언은 기본적으로 final이며 public이다.
  • 코틀린 중첩 클래스는 외부 클래스를 참조할 수 없다.
  • 클래스를 data로 선언하면 컴파일러가 일부 표준 메소드를 생성해준다.
  • object 키워드를 사용하면 싱클턴 클래스, 동반 객체companion object, 객체 식object expression(자바의 익명 클래스)를 선언할 수 있다.

4.1 클래스 계층 정의

  • 코틀린에는 새로운 접근제한자인 sealed가 추가 되었다. sealed는 클래스 상속을 제한한다.

4.1.1 코틀린 인터페이스(Interface)

  • 코틀린 인터페이스는, Java8의 디폴트 메소드와 비슷하게, 구현되어 있는 메소드도 정의 할 수 있다.
  • 인터페이스에는 아무런 상태(field)도 들어갈 수 없다.
  • 인터페이스를 선언하는 키워드는 interface이다.
 /* 간단한 인터페이스 선언하기 */
interface Clickable {
    fun click()
}

/* 단순한 인터페이스 구현하기 */
class Button : Clickable {
    override fun click() = println("I was clicked")
}

>>> Button().click()
I was clicked
  • 자바에서는 extends, implements를 사용하지만 코틀린은 콜론(:) 뒤에 인터페이스 또는 클래스의 이름을 붙인다.
  • 자바와 동일하게 인터페이스는 제한없이 구현할 수 있지만, 클래스는 오직 1개만 상속 가능하다.
  • override 키워드는 자바의 @override와 동일한 역할을 한다. 대신 코틀린은 override 변경자modifier를 꼭 사용해야 한다.
  • 자바의 경우, 부모 클래스와 자식 클래스 간의 같은 이름의 메소드를 허용하고 있지만, 코틀린의 경우는 override 변경자를 사용하지 않으면 컴파일 오류가 발생한다.
  • 디폴트 메소드의 경우, 자바에서는 default 키워드를 붙이지만, 코틀린은 그냥 본문을 구현하면 된다.
/* 디폴트 메소드 정의 하기 */
interface  Clickable {
    fun click()
    fun showOff() = println("I'm clickable!") /* 디폴트 메소드 */
}

/* Clickable과 동일한 메소드를 구현하는 다른 메소드 */
interface Focusable {
    fun setFocus(b: Boolean) = println("I ${if(b) "got" else "lost"} focus.")
    fun showOff() = println("I'm focusable!")
}
  • 위 예제의 ClickableFocusable을 동시에 구현했을 때 showOff 메소드는 둘 중 어느 것도 선택되지 않으며, 반드시 override를 통해 구현을 해야한다. 구현하지 않는 경우 아래와 같은 컴파일 오류가 발생한다.
The class 'Button' must
override public open fun showOff() because it inherits
many implementations of it.
  • 부모의 구현된 메소드를 호출 하려면 자바와 마찬가지로 super를 사용한다.
  • super<Clickable>.showOff() 처럼 꺾쇠 괄호를 사용한다.
class Button : Clickable, Focusable {
    override fun click() = println("I was clicked")
    override fun showOff() {
        /* 부모 타입의 이름을 꺾쇠 괄호에 넣어서 'super'를 지정하면
           부모의 메소드를 호출 할 수 있다. */
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

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

  • 코틀린의 클래스와 메소드는 기본적으로 final이다.
  • 클래스의 상속을 허용하려면 클래스 앞에 open 변경자를 붙여야 한다.
  • 오버라이드를 허용하고 싶은 메소드나 프로퍼티 앞에도 open 변경자를 붙여야 한다.
  • 부모 클래스의 메소드를 자식 클래스에서 오버라이드 한 경우, 해당 메소드는 기본적으로 열려있다. (여기서 열려있다라는 말의 의미는 상속과 오버라이드 가능하다는 의미다)
/* 열린 메소드를 포함하는 열린 클래스 정의하기 */
open class RichButton : Clickable { /* 이 클래스는 열려있다. 상속 가능하다 */
    fun disable() { } /* open 변경자가 없기 때문에 final 이다. 자식 클래스에서 override 불가하다. */
    open fun animate() { } /* open 이므로 override 가능하다. */
    override fun click() { } /* override 한 메소드는 기본적으로 모두 열려있다. */
}
  • 오버라이드 한 메소드를 다시 오버라이드 하지 못 하게 금지하려면 메소드 앞에 final을 명시한다.
open class RichButton : Clackable {
    final override fun click() { }
}
  • 클래스의 기본적인 상속 가능 상태를 final로 정의하는 경우 스마트 캐스트를 사용할 수 있다. 스마트 캐스트는 타입 검사 뒤에 변경될 수 없는 변수에만 적용 가능하다. 이는 프로퍼티를 final로 만들어야 한다는 의미이다.

  • 코틀린에서도 클래스를 abstract로 선언할 수 있다.
  • 자바와 마찬가지로 abstract로 선언한 추상 클래스는 객체로 만들 수 없다.
  • 추상 클래스에는 구현이 없는 추상 멤버가 있으며, 이는 항상 열려있다. 따라서 추상 멤버 앞에는 open 변경자를 명시할 필요가 없다.
/* 추상 클래스 정의하기 */
abstract class Animated {
    /* 메소드 구현이 없기 때문에 이는 추상 메소드이다. */
    abstract fun animae()
    
    /* 추상 클래스에 속한 메소드라고 해도 추상 메소드가 아닌 메소드는 기본적으로 final 이다.
     * open을 사용하면 오버라이드를 허용할 수 있다. */
    open fun stopAnimating() { }
    
    /* open이 없는 비추상 메소드이므로 final이다. */
    fun animateTwice() { }
}

  • 인터페이스 멤버는 항상 열려 있으므로 final로 변경 불가능하며, open, abstract를 사용하지 않는다.
  • 인터페이스 멤버에 본문이 없으면 자동으로 추상 멤버가 되며, abstract 키워드는 생략 가능하다.
  • 코틀린의 상속 제어 변경자는 아래 표와 같다.
변경자 이 변경자가 붙은 멤버는… 설명
final 오버라이드 할 수 없음 클래스 멤버의 기본 변경자다.
open 오버라이드 할 수 없음 반드시 open을 명시해야 오버라이드 할 수 있다.
abstract 반드시 오버라이드 해야 함 추상 클래스의 멤버에만 이 변경자를 붙일 수 있다. 추상 멤버에는 구현이 있으면 안 된다.
override 상위 클래스나 상위 인스턴스의 멤버를 오버라이드하는 중 오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다.

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

  • 가시성 변경자visibility modifier코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어한다.
  • 코틀린 가시성 변경자에는 public, protected, private 변경자가 있다.
  • 코틀린에서 가시성 변경자를 생략하는 경우 모두 public 이다. 자바에서는 package-private로 설정 된다.
  • 코틀린에는 package-private가 없고, 패키지는 네임스페이스 관리를 위한 용도로만 사용 된다.
  • 코틀린에서 패키지 전용 가시성에 대안으로 internal 이라는 새로운 변경자를 도입했다. internal은 모듈 내부에서만 볼 수 있다는 뜻이다.
  • 코틀린은 최상위 선언에 대해 private를 허용한다. 최상위 선언에는 클래스, 함수, 프로퍼티 등이 포함된다.
  • 코틀린의 가시성 변경자는 아래와 같이 정리할 수 있다.
변경자 클래스 멤버 최상위 선언
public(기본 가시성) 모든 곳에서 볼 수 있다. 모든 곳에서 볼 수 있다.
internal 같은 모듈 안에서만 볼 수 있다. 같은 모듈 안에서만 볼 수 있다.
protected 하위 클래스 안에서만 볼 수 있다. (최상위 선언에 적용할 수 없음)
private 같은 클래스 안애서만 볼 수 있다. 같은 파일 안에서만 볼 수 있다.
/* 가시성 규칙 위반 예제 */

internal open class TalkativeButton : Focusable {
    private fun yell() = println("Hey!")
    protected fun whisper() = println("Let's talk!")
}

fun TalkativeButton.giveSpeech() {  /* 에러 : TalkativeButton은 `internal`이므로 `public`인 함수에서 접근 할 수 없다. */
    yell()  /* <-- 에러 : yell()은 private 이다. */
    whisper()  /* <-- 에러 : whisper()는 protected 이다. */
}
  • 자바에서는 같은 패키지 내에서 protected 멤버에 접근할 수 있지만, 코틀린은 같은 패지키 내에서라도 internal인 경우에 접근 불가능하다.
  • 코틀린에서의 protected 멤버는 오직 어떤 클래스나, 그 클래스를 상속한 클래스 안에서만 보인다.
  • 클래스를 확장한 함수는 private나 protected 멤버에 저근할 수 없다.
  • 코틀린의 public, protected, private는 자바 안에서 동일하게 유지된다. private 클래스의 경우 자바에서 package-private로 변경된다. internal의 경우는 public으로 변경 된다.

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

  • 자바와 마찬가지로 코틀린도 클래스 안에 다른 클래스를 선언할 수 있다.
  • 자바와 다르게 코틀린의 중첩 클래스nested class는 명시적 선언이 없는 한 상위 클래스 인스턴스에 접근 불가능하다.
/* 직렬화 가능한 View 선언 */

interface State: Serializable

interface View {
    fun getCurrentState(): State
    fun restoreState(state: State) {}
}
/* 자바 */
/* 자바에서 내부 클래스를 사용해 VIew 구현하기 */
public class Button implements View {
    @override
    public State getCurrentState() {
        return new ButtonState();
    }
    
    @override
    public void restoreState(State state) { /* ... */ }
    public class ButtonState implements State { /* ... */ }
}
  • 위 자바 코드에서 ButtonState 클래스를 직렬화하면 java.io.NotSerializableException: Button 이라는 오류가 발생한다.
  • ButtonState 클래스의 인터페이스인 State 가 Serializable 을 상속받고 있더라도 ButtonState 클래스는 중첩 클래스이므로 바깥쪽은 Button 클래스를 묵시적으로 포함한다. 따라서 Button은 Non-Serializable 이므로 Exception이 발생한다. 이를 해결하려면 ButtonState를 static으로 선언해야 한다. 자바에서는 중첩 클래스를 static으로 선언하면 바깥 클래스에 대한 묵시적 참조가 사라진다.
  • 코틀린에서 중첩 클래스의 기본 동작 방식은 바로 위 자바와는 정반대로 동작한다.
/* 코틀린에서 중첩 클래스를 사용해 View 구현하기 */
class Button: View {
    override fun getCurrentState(): State = ButtonState()
    override fun restoreState(state: State) { /* ... */ }
    class ButtonState: State { /* ... */ }  /* <-- 이 클래스는 자바의 static nested class와 대응한다. */
}
  • 코틀린에서 중첩 클래스에 아무런 변경자modifier가 붙지 않으면 자바 static 중첩 클래스와 같다.
  • 자바와 같이 내부 클래스에서 바깥쪽 클래스의 참조하게 하려면 inner 변경자를 붙여야 한다.
  • 자바와 코틀린의 중첩 클래스nested class와 내부 클래스inner class의 관계는 아래 표와 같다.
클래스 OuterClass 안에 정의된 클래스 A 자바 코틀린
중첩 클래스nested class (바깥쪽 클래스에 대한 참조를 저장하지 않음) static class A class A
내부 클래스inner class (바깥쪽 클래스에 대한 참조를 저장함) class A inner class A
  • 코틀린에서 바깥쪽 클래스의 인스턴스를 가리키려면 this@Outer라고 써야한다. (자바와는 다름)
class Outer {
    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }
}

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

  • 2.3.6절에서 살펴본 코드를 다시 보자.
interface Expr
class Num(val value: Int) : Expr
class Sum(val left:Expr, val right:Expr) : Expr

fun eval(e : Expr): Int =
    when(e) {
        is Num -> e.value /* is에 의해  e가 자동으로 Num으로 캐스팅 된다 */
        is Sum -> eval(e.right) + eval(e.left)
        else -> throw IllegalArgumentException("Unknown expression")
    }
  • 위 코드는 else(디폴트 분기) 절을 사용하도록 컴파일러가 강제한다.
  • 위 코드에서 새로운 타입 검사에 대한 처리를 잊어버린다면 디폴트 분기가 선택되므로 심각한 버그가 발생할 수 있다.
  • 이를 방지하기 위해 코틀린튼 sealed 클래스를 제공한다. 상위 클래스에 sealed 변경자를 붙이면 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다. sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩시켜야 한다.
/* sealed 클래스로 식 표현하기 */
sealed class Expr {
    /* 기반 클래스의 모든 하위 클래스를 중첩 클래스로 나열한다. */
    class Num(val value: Int): Expr()
    class Sum(val left: Expr, val right: Expr): Expr()
}

fun eval(e: Expr): Int =
    when(e) {  /* <-- "when" 식이 sealed 클래스의 모든 하위 클래스를 검사하므로 else가 필요 없다 */
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
    }
  • sealed로 표시된 클래스는 자동으로 open 이다.
  • sealed 클래스에 속한 값에 대해 디폴트 분기(else 처리)를 하지 않고 when 식을 사용하면, 나중에 sealed 클래스에 새로운 하위 클래스를 추가하여도 when 식이 컴파일 되지 않는다.
  • 내부적으로 Expr 클래스는 private 생성자를 가진다. 이 생성자는 내부에서만 호출 가능하다.
  • sealed 변경자는 인터페이스에는 붙일 수 없다.

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

  • 코틀린의 생성자는 주primary생성자와 부secondary생성자가 있다.
  • 초기화 블록initialize block을 통해 초기화 로직을 추가할 수 있다.

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

class User(val nickname: String)
  • 위 코드에서 클래스 이름 뒤의 괄호로 둘러싸인 코드를 주 생성자primary constructor라고 부른다.
  • 위 코드를 명시적으로 풀어 쓰면 아래와 같다.
class User constructor(_nickname: String) {  /* 파라미터가 하나만 있는 주 생성자 */
    val nickname: String
    init {  /* <-- 초기화 블록 */
        nickname = _nickname
    }
}
  • constructor 키워드는 주 생성자나 부 생성자를 정의할 때 사용한다.
  • init 키워드는 초기화 블록을 시작한다. 초기화 블록은 객체가 생성되는 시점에 실행되는 코드가 들어간다.

  • 위 예제에서는 nickname 프로퍼티 초기화 코드를 nickname 프로퍼티 선언에 포함시킬 수 있으므로 초기화 코드를 생략할 수 있다.
  • 주 생성자 앞에 별다른 Annotation이나 가시성 변경자가 없다면 constructor 키워드를 생략할 수 있다.
  • 위 2가지를 적용하면 아래와 같이 코드를 변경할 수 있다.
class User(_nickname: String) {  /* <-- 파라미터가 하나뿐인 주 생성자 */
    val nickname = _nickname  /* <-- 프로퍼티를 주 생성자의 파라미터로 초기화 한다. */
}
  • 아래와 같이 초기화도 가능하다.
class User(val nickname: String)
  • 위 3개 방식을로 선언한 User 클래스는 모두 동일하다.
  • 아래와 같이 생성자 파라미터에도 디폴트 값을 정의할 수 있다.
class User(val nickname: String, val isSubscribed: Boolean = true)

>>> val hyun = User("현석")
>>> println(hyun.isSubscribed)
true

>>> val gye = User("계영", false)
>>> println(gye.isSubscribed)
false

>>> val hey = User("혜원", isSubscribed = false)
>>> println(hey.isSubscribed)
false
  • 슈퍼 클래스를 초기화 하려면 슈퍼 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘긴다.
open class User(val nickname: String) { ... }
class Twitteruser(nickname: String) : User(nickname) { ... }
  • 클래스 생성시 별도로 생성자를 정의하지 않으면 아무 것도 하지 않는 인자가 없는 디폴트 생성자를 만든다.
  • Button의 생성자는 아무 인자도 받지 않지만, Button 클래스를 상속한 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야 한다.
open class Button  /* <-- 인자가 없는 디폴트 생성자가 만들어진다. */
class RadioButton: Button()
  • 위 규식으로 인해 슈퍼 클래스의 이름 뒤에는 꼭 빈 괄호가 들어간다. 생성자 인자가 있는 경우 괄호 안에 인자가 들어간다.
  • 인터페이스는 생성자가 없기 때문에 인터페이스를 구현하는 경우 인터페이스 이름 뒤에는 아무 괄호도 없다.
  • 이 괄호 여부를 보면 부모가 클래스인지 인터페이스인지 구분 할 수 있다.
  • 어떤 클래스를 클래스 외부에서 인스턴스화하지 못 하게 막고 싶다면 모든 생성자를 private로 만들면 된다.
/* 인스턴스화 불가능하게 만들기 위해 주 생성자에 private를 붙인다. */
class Secretive private constructor() {}  /* <-- 이 클래스의 주 생성자는 비공개이다. */

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

open class View {
    constructor(ctx: Context) {  /* 부 생성자 */
        // 코드
    }
    
    constructor(ctx: Context, attr: AttributeSet) {  /* 부 생성자 */
        // 코드
    }
}
  • 부 생성자는 constructor 키워드로 시작한다.
  • 이 클래스를 확장하면서 똑같이 부 생성자를 정의할 수 있다.
class MyButton: View {
    constructor(ctx: Context): super(ctx) {
        // ...
    }
    
    constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
        // ...
    }
}
  • 자바와 마찬가지로 this()를 통해 자신의 다른 생성자를 호출할 수 있다.
class MyButton: View {
    constructor(ctx: Context): this(ctx, MY_STYLE) {  /* <-- 다른 생성자에게 위임 */
        // ...
    }
    
    constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
        // ...
    }
}
  • 코틀린에서 부 생성자가 필요한 주 된 이유는 자바 상호운용성 때문이다.
  • 또다른 부 생성자의 존재 이유는 4.4.2절에서 설명한다.

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

  • 코틀린 인터페이스에는 추상 프로퍼티를 생성할 수 있다.
interface User {
    val nickname: String
}
  • 위 코드는 User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 구현해야 한다는 의미다. 이 인터페이스를 구현하는 방법은 아래 예제와 같다.
/* 인터페이스의 프로퍼티 구현하기 */
/* 방법 1 */
class PrivateUser(override val nickname: String) : User  /* <-- 주 생성자에 있는 프로퍼티 */

/* 방법 2 */
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')  /* <-- 커스텀 Getter */
}

/* 방법 3 */
class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId)  /* <-- 프로퍼티 초기화 식 */
}

>>> println(PrivateUser("test@kotlinlang.org").nickname)
test@kotlinlang.org

>>> println(SubscribingUser("test@kotlinglang.org").nickname)
test
  • PrivateUser는 주 생성자 안에 프로퍼티를 직접 선언하였다. 이 프로퍼티는 User의 추상 프로퍼티를 구현하고 있으므로 override를 표시해야 한다.
  • SubscribingUser는 커스텀 게터로 nickname 프로퍼티를 설정한다. 이 프로퍼티는 매번 이메일 주소에서 별명을 계산해 반환한다.
  • FackbookUser는 초기화 식으로 nickname 값을 초기화한다. getFackbookName은 다른 곳에서 정의 되어있다고 가정한다.
  • SubscribingUser와 FacebookUser의 nickname 구현의 차이에 주의한다. SubscribingUser는 매번 호출될 때마다 substringBefore()를 호출하여 계산하고 있는 반면, FacebookUser의 nickname은 객체 초기화 시점에 getFacebookName()의 반환 값을 nickname 필드에 저장하고 있다.
  • 인터페이스에는 추상 프로퍼티 뿐만 아니라, Getter와 Setter가 있는 프로퍼티를 선언할 수 있다.
interface User {
    val email: String
    val nickname: String
        get() = email.substringBefore('@')  /* 프로퍼티에 뒷받침하는 필드가 없다. 대신 매번 결과를 계산해 돌려준다. */
}
  • 위 인터페이스에는 추상 프로퍼티인 email과 커스텀 게터가 있는 nickname 프로퍼티가 있다.
  • 위 인터페이스를 구현할 때는 반드시 email를 오버라이드해야 한다. nickname은 오버라이드하지 않고 상속 받을 수 있다.
  • 인터페이스에 선언된 프로퍼티와 달리 클래스에 구현된 프로퍼티는 뒷받침하는 필드backing field를 원하는대로 사용할 수 있다.

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

  • 프로퍼티의 2가지 유형에는 (1)값을 저장하는 프로퍼티와 (2)커스텀 접근자에서 매번 값을 계산하는 프로퍼티가 있다.
  • 코틀린에서는 이 2가지 유형을 조합하는 프로퍼티를 만들 수 있다. 이를 위해서는 접근자 안에서 뒷받침하는 필드backing field에 접근할 수 있어야 한다.
/* 세터에서 뒷받침하는 필드 접근하기 */
class User(val name: String) {
    var address: String = "unspecified"
        set(value: String) {
            println(""""
                Address was changed for ${name}:
                "${field}" -> "${value}".""".trimIndent())  /* 뒷받침하는 필드 값 읽기 */
            field = value  /* 뒷받침하는 필드 값 변경하기 */
        }
}

>>> val user = User("Alice")
>>> user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
Address was changed for Alice:
"unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".
  • 프로퍼티의 값을 바꿀 때는 user.address = “new value” 처럼 필드 설정 구문을 사용한다. 이는 내부적으로 address의 세터를 호출한다.
  • field라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다. 게터에서는 field 값을 읽기만 가능하고, 세터에서는 값을 읽거나 쓸 수 있다.
  • 변경 가능 프로퍼티의 게터와 세터 중 한쪽만 직접 정의해도 된다.

4.2.5 접근자의 가시성visibility 변경

  • 접근자의 가시정은 기본적으로 프로퍼티와 동일하다.
  • get 이나 set 앞에 가시정 변경자를 추가해서 변경할 수 있다.
class LengthCounter {
    var counter: Int = 0
        private set  /* <-- 이 클래스 밖에서 이 프로퍼티의 값을 변경할 수 없다. */
    fun addWord(word: String) {
        counter += word.length
    }
}

>>> val lengthCounter = LengthCounter()
>>> lengthCounter.addWord("Hi!")
>>> println(lengthCounter.counter)
3

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

  • 자바 플랫폼에서는 클래스가 equals, hashCode, toString 등의 메소드를 구현해야 한다.
  • 코틀린에서는 이런 메소드 구현 작업을 보이지 않는 곳에서 자동으로 해준다.

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

  • 코틀린 클래스도 자바와 마찬가지로 toString, equals, hashCode 등을 오버라이드 할 수 있다.
class Client(val name: String, val postalCode: Int)

문자열 표현: toString()

/* Client에 toString() 구현하기 */

class Client(val name: String, val postalCode: Int) {
    override fun toString() = "Client(name=${name}, postalCode=${postalCode}"
}

>>> val client1 = Client("오현석", 4122)
>>> println(client1)
Client(name=오현석, postalCode=4122)

객체의 동등성: equals()

>>> val client1 = Client("오현석", 4122)
>>> val client2 = Client("오현석", 4122)
>>> println(client1 == client2)
false
  • 위 에제에서는 두 객체가 동일하지 않다. 객체의 주소를 비교하기 때문이다. 두 객체의 값 비교를 위해서는 equals()를 오버라이드 해야 한다.
/* Client에 equals() 구현하기 */
class Client(val name: String, val postalCode: Int) {
    override fun equals(other: Any?) : Bollean {  /* Any는 자바의 Object에 대응하는 클래스다. Any?는 널이 될 수 있는 타입이므로 "other"는 null일 수 있다. */
        if (other == null || other !is Cleint)
            return false
            
        return name == other.name && postalCode == other.postalCode
    }
    
    override fun toString() = "Client(name=${name}, postalCode=${postalCode}"
}
  • 위 코드는 이제 client1 == client2가 true를 반환하게 해준다. 하지만 Client 클래스로 더 복잡한 작업을 수행하면 제대로 작동하지 않는 경우가 있다. hashCode 정의를 빠뜨린 것이 원인이다.

해시 컨테이너: hashCode()

  • 자바에서는 equals를 오버라이드할 때 반드시 hashCode도 함께 오버라이드해야 한다.
>>> val processed = hashSetOf(Client("오현석", 41222))
>>> println( processed.contains(Client("오현석", 412222)))
false
  • 위 예제에서 false가 나오는 이유는 hashCode를 정의하지 않았기 때문이다. HashSet은 원소를 비교할 때 빠른 비교를 위해 두 값의 해시 코드가 같은 경우에만 실제 값을 비교한다. 위 예제에서는 해시 코드가 다르므로 false가 반환된다.
/* Client에 hashCode 구현하기 */
class Client(val name: String, val postalCode: Int) {
    ...
    override fun hashCode() : Int = name.hashCode() * 31 + postalCode
}
  • 코틀린에서는 위에서 재정의 했던 toString, equals, hashCode를 모두 자동으로 생성할 수 있다.

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

  • 코틀린은 data 키워드를 클래스 앞에 붙이면 필요한 메소드를 컴파일러가 자동으로 만들어 준다. data 변경자가 붙은 클래스를 데이터 클래스라고 부른다.
/* Client를 데이터 클래스로 선언하기 */

data class Client(val name: String, val postalCode: Int)
  • 위 예제의 클래스는, 앞서 우리가 재정의한, equals, hashCode, toString 이 자동으로 생성되었다.

데이터 클래스와 불변성: copy() 메소드

  • 데이터 클래스의 프로퍼티에 var를 사용할 수 있으나, 가능한 모든 프로퍼티를 읽기 전용(val)으로 만들어 데이터 클래스를 불변immutable클래스로 만들라고 권장한다.
  • 코틀린은 객체를 복사할 때 일부 프로퍼티를 바꿀 수 있게 해주는 copy 메소드를 제공한다.
  • Client의 copy를 직접 구현하면 아래와 같다.
class Client(val name: String, val postalCode: Int) {
    ...
    fun copy(name: String = this.name, postalCode = this.postalCode) = Client(name, postalCode)
}

>>> val lee = Client("이계영", 4122)
>>> println(lee.copy(postalCode = 4000))
Client(name=이계영, postalCode=4000)

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

  • 코틀린에서는 기본적으로 클래스를 final로 취급하여 open 변경자로 열어둔 클래스만 확장할 수 있게 해준다.
  • 상속을 허용하지 않는 클래스에 새로운 동작을 추가 해야하는 경우가 발생하며, 이럴 때 사용하는 일반적인 방법은 데코레이터Decorator패턴이다. 데코레이터 패턴의 단점은 준비 코드가 상당히 많다는 것이다. 아래 예를 보자.
class DelegatingCollection<T>: Collection<T> {
    private val innerList = arrayListOf<T>()
    override val size: Int get() = innerList.size
    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(element)
}
  • 이러한 준비 코드를 코틀린에서는 일급 시민first-class 기능으로 지원한다. 인터페이스를 구현할 때 by 키워드를 통해 그 인터페이스에 대한 구현을 다른 캑체에 위임 중이라는 사실을 명시할 수 있다.
class DelegatingCollection<T>(
    innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
  • 클래스 안에 있던 모든 오버라이드 메소드가 없어졌다. 컴파일러가 해당 메소드를 자동으로 생성한다.
  • 이 기법을 이용해 원소를 추가한 횟수를 기록하는 컬렉션을 구현해보자.
class CountingSet<T>(
    val innerSet: MutableCollection<T> = HashSet<T>()
) : MutalbeCollection<T> by innerSet {
    var objectsAdded = 0
    override fun add(element: T) : Boolean {
        objectsAdded++
        return innerSet.add(element)
    }
    
    override fun addAll(c: Collection<T>) : Boolean {
        objectsAdded += c.size
        return innerSet.addAll(c)
    }
}

>>> val cest = CountingSet<Int>()
>>> cset.addAll(listOf(1, 1, 2))
>>> println("${cset.objectsAdded} objects were added, ${cset.size} remain")
3 objects were added, 2 remain

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

  • 코틀린에서 object 키워드는, 여러 상황에서 사용하지만, 클래스를 정의하면서 동시에 객체instance를 생성한다는 의미이다.

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

  • 자바에서는 보통 클래스의 생성자를 private으로 제한하고 정적 필드에 유일한 객체를 저장하는 싱글턴 패턴singleton pattern을 통해 이를 구현한다.
  • 코틀린은 객체 선언 기능을 통해 싱글턴을 언어에서 기본 지원한다. 객체 선언은 클래스 선언과 그 클래스에 속한 단일 인스턴스의 선언을 합친 선언이다.
object Payroll {
    val allEmployees = arrayListOf<Person>()
    fun calculateSalary() {
        for (person in allEmployees) {
            ...
        }
    }
}
  • 객체 선언은 object 키워드로 시작한다.
  • 객체 선언 안에는 프로퍼티, 메소드, 초기화 블록 등이 들어갈 수 있다. 하지만 생성자는 객체 선언에 쓸 수 없다.
  • 싱글턴 객체는 객체 선언문이 있는 위치에서 생성자 호출 없이 즉시 만들어진다.
  • 변수와 마찬가지로 객체 선언에 사용한 이름 뒤에 마침표(.)를 붙여 메소드나 프로퍼티에 접근할 수 있다.
Payroll.allEmployees.add(Person(...))
Payroll.calculateSalary()
  • 객체 선언도 클래스나 인스턴스를 상속할 수 있다.
  • Comparator 인스턴스를 만드는 것은 객체 선언의 가장 좋은 예다.
/* 객체 선얼을 사용해 Comparator 구현하기 */
object CaseInsensitiveFileComparator : Comparator<File> {
    override fun compare(file1: File, file2: File) : Int {
        return file1.path.compareTo(file2.path, ignoreCase = true)
    }
}

>>> println(CaseInsensitiveFileComparator.compare(File("/User", File("/user"))))
0
  • 일반 객체(클래스 인스턴스)를 사용할 수 있는 곳에는 항상 싱글턴 객체를 사용할 수 있다.
>>> val files = listOf(File("/Z"), File("/a"))
>>> println(files.sortedWith(CaseInsensitiveFileComparator))
[/a, /Z]
  • 클래스 안에서도 객체를 선언할 수 있고, 여전히 인스턴스는 단 하나뿐이다.
/* 중첩 객체를 사용해 Comprator 구현하기 */
data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(p1: Person, p2: Person): Int = p1.name.compareTo(p2.name)
    }
}

>>> val persons = listOf(Person("Bob"), Persion("Alice"))
>>> println(persons.sortedWith(Person.NameComparator))
[Person(name=Alice), Person(name=Bob)]
  • 코틀린의 객체 선언은 자바 클래스로 컴파일시 유일한 인스턴스에 대한 정적 필드가 있는 자바 클래스로 변경 된다. 이 때 인스턴스 필드의 이름은 항상 INSTANCE 이다.
/* 자바 */
CaseInsensitiveFileComparator.INSTANCE.compare(file1, file2);

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

  • 코틀린 클래스 안에는 정적인 멤버는 없다. 자바의 static 키워드를 지원하지 않는다.
  • static을 지원하지 않는 대신 패키지 수준의 최상위 함수와 객체 선언을 활용하며, 대부분의 경우 최상위 함수를 활용하는 것을 권장한다.
  • 최상위 함수는 클래스 내부의 private 멤버에 접근 불가능하기 때문에 클래스에 중첩된 객체 선언의 멤버 함수를 정의하여 사용한다.
  • 클래스 안에 정의된 객체에 companion이라는 표시를 붙이면 클래스의 동반 객체companion object를 만들 수 있다.
  • 동반 객체의 프로퍼티나 메소드에 접근하려면 그 동반 객체가 정의된 클래스 이름을 사용한다. 이 때 객체의 이름은 따로 지정할 필요가 없다. 이 형태는 자바의 정적 메소드 호출이나 정적 필드 사용과 동일하다.
class A {
    companion object {
        fun bar() {
            println("Companion object called")
        }
    }
}

>>> A.bar()
Companion obejct called
  • 동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근할 수 있다. 또한 동반 객체는 바깥쪽 클래스의 private 생성자도 호출 할 수 있으며, 패토리 패턴을 구현하기 가장 적합하다.
  • 아래는 두 개의 클래스를 하나의 클래스로 합치면서 사용자 객체를 생성하는 예제이다.
/* User를 상속받는 2개의 클래스 구현하기 */
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')  /* <-- 커스텀 Getter */
}

class FacebookUser(val accountId: Int) : User {
    override val nickname = getFacebookName(accountId)  /* <-- 프로퍼티 초기화 식 */
}
/* 2개의 클래스를 User 클래스 안으로 합치기 */
class User {
    val nickname: String
    constructor(email: String) {  /* 부 생성자 */
        nickname = email.substringBefore('@')
    }
    
    constructor(facebookAccountId: Int) {  /* 부 생성자 */
        nickname = getFacebookName(facebookAccountId)
    }
}
  • 위 예제를 표현하는 더 유용한 방법은 클래스의 인스턴스를 생성하는 팩토리 메소드를 만드는 것이다. 아래 예제를 보자.
/* 부 생성자를 팩토리 메소드로 대신하기 */
class User private constructor(val nickname: String) {  /* 주 생성자를 비공개로 만든다. */
    companion object {
        fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
        fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))
    }
}

>>> val subscribingUser = User.newSubscribinUser("bob@gmail.com")
>>> val facebookUser = User.newFacebookUser(4)
>>> subscribingUSer.nickname
bob

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

  • 동반 객체는 클래스 안에 정의된 일반 객체다. 따라서 이름을 붙이거나, 인터페스를 상속하거나, 동반 객체 안에 확장 함수와 프로퍼티를 정의할 수 있다.
/* 동반 객체에 이름 붙이기 */
class Person(val name: String) {
    companion object Loader {  /* <-- 동반 객체에 이름을 붙인다. */
        fun fromJSON(jsonText: String) : Person = ...
    }
}

>>> person = Person.Loader.fromJSON("{name: 'Dmitry'}")\
>>> person.name
Dmitry
>>> person2 = Person.fromJSON("{name: 'Brent'}")
>>> person2.name
Brent
  • 동반 객체에 이름을 생략하는 경우 동반 객체의 이름은 자동을 Companion이 된다.

동반 객체에서 인터페이스 구현

/* 동반 객체에서 인터페이스 구현하기 */
interface JSONFactory<T> {
    fun fromJSON(jsonText: String) : T
}

class Person(val name: String) {
    companion object : JSONFactory<Person> {
        override fun fromJSON(jsonText: String) : Person = ... /* <-- 동반 객체가 인터페이스를 구현헌다. */
    }
}
  • JSON으로부터 각 원소를 다시 만들어내는 추상 팩토리가 있다고 가정한다면, Person 객체를 그 팩토리에게 넘길 수 있다.
fun loadFromJSON<T>(factory: JSONFactory<T>): T {
    ...
}

loadFromJSON(Person)  /* <-- 동반 객체의 인스턴스를 함수에 넘긴다. */

동반 객체 확장

/* 동반 객체에 대한 확장 함수 정의하기 */

// 비지니스 로직 모듈
class Person(val firstName: String, val lastName: String) {
    companion object {  /* <-- 비어있는 동반 객체 선언 */
    
    }
}

// 클라이언트/서버 통신 모듈
fun Person.Companion.fromJSON(json: String) : Person {
    ...
}

val p = Person.fromJSON(json)
  • 위 예제는 마치 동반 객체 안에서 fromJSON 함수를 정의한 것처럼 fromJSON을 호출할 수 있으나, 실제로 fromJSON은 클래스 밖에서 정의한 함수이다.
  • 동반 객체에 대한 확장 함수를 작성하려면 원래 클래스에 동반 객체를 꼭 선언해야 한다.

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

  • object 키워드는 무명 객체anonymous object를 정의할 때도 사용한다.
/* 무명 개체로 이벤트 리스너 구현하기 */

window.addMouseListener (
    object : MouseAdapter() {  /* <-- MouseAdapter를 확장하는 무명 객체를 선언 */
        override fun mouseClicked(e: MouseEvent) {  /* <-- MouseAdapter의 메소드 오버라이드 */
            // ...
        }
        
        override fun mouseEntered(e: MouseEvent) {  /* <-- MouseAdapter의 메소드 오버라이드 */
            // ...
        }
    }
)
  • 위 예제는 객체 선언에서와 동일하지만, 객체 이름이 빠져있다.
  • 객체에 이름을 붙여야 한다면 변수에 무명 객체를 대입하면 된다.
val listener = object : MouseAdapter {
    override fun mouseClicked(e: MouserEvent) { ... }
    override fun mouseEntered(e: MouserEvent) { ... }
}
  • 하나의 인터페이스만 구현하거나 하나의 클래스만 확장할 수 있는 자바의 무명 내부 클래스와 다르게, 코틀린은 여러 인터페이스를 구현하거나, 클래스를 확장하면서 인터페이스를 구현할 수 있다.
  • 객체 선언과 달리 무명 객체는 싱글턴이 아니다. 객체 식이 쓰일 때마다 새로운 인스턴스가 생성된다.
  • 무명 클래스는 자신이 포함된 함수의 변수에 접근할 수 있으며, 자바와 다르게 final이 아닌 변수도 객체 식 안에서 사용할 수 있다. 또한 그 값을 변경할 수도 있다.
fun countClicks(window: Window) {
    var clickCount = 0  /* <-- 로컬 변수 정의 */
    window.addMouseListener(object: MouseAdaptoer() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++  /* <-- 로컬 변수의 값을 변경한다. */
        }
    })
    // ...
}