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

### 이번장에서는...

#### 배운점, 느낀점

- 클래스와 인터페이스
    - 싱글턴 클래스
    - 동반 객체
    - 객체 식(자바의 무명 클래스에 해당)
- 생성자와 프로퍼티
- 데이터 클래스
- 클래스 위임
- object 키워드 사용

---

- 코틀린 선언은 기본적으로 `final`이며 `public`이다.
- 중첩 클래스는 기본적으로 내부 클래스가 아니다. 코틀린 중첩 클래스에는 외부 클래스에 대한 참조가 없다.
- 필요하면 접근자를 직접 정의할 수 있다.
- 코틀린 컴파일러는 번잡스러움을 피하기 위해 유용한 메서드를 자동으로 만들어준다.
- 클래스르 data로 컴파일러가 일부 표준 메서드를 생성해준다.
- 코틀린 언어가 제공하는 위임을 사용하면 위임을 처리하기 위한 준비 메서드를 직접 작성할 필요가 없다.

### 클래스 계층 정의

- 코틀린의 가시성과 접근 변경자
    - 코틀린 가시성 및 접근 변경자는 자바와 비슷하지만 아무것도 지정하지 않은 경우 기본 가시성은 다르다.
    - 코틀린에 도입한 `sealed` 변경자는 클래스 상속을 제한한다

#### 코틀린 인터페이스

- 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메서드도 정의할 수 있다(이는 자바8의 디폴트 메서드와 비슷하다) 다만, 인터페이스에는 아무런 상태도(필드)도 들어갈 수 없다.

```kotlin

package part4

interface Clickable {
    fun click()
}

package part4;

import kotlin.Metadata;

@Metadata(
        mv = {1, 7, 1},
        k = 1,
        d1 = {"\\u0000\\u0010\\n\\u0002\\u0018\\u0002\\n\\u0002\\u0010\\u0000\\n\\u0000\\n\\u0002\\u0010\\u0002\\n\\u0000\\bf\\u0018\\u00002\\u00020\\u0001J\\b\\u0010\\u0002\\u001a\\u00020\\u0003H&¨\\u0006\\u0004"},
        d2 = {"Lpart4/Clickable;", "", "click", "", "org.example.playground.main"}
)
public interface Clickable {
    void click();
}
package part4

class Button : Clickable {

    override fun click() = println("I was clicked")

}

fun main() {
    val buttonJava = Button()
    buttonJava.click()
}
인터페이스 안에 본문이 있는 메서드 정의하기
package part4

interface Clickable {
    fun click()  // 일반 메서드 선언
    fun showOff() = println(" I'm clickable!") // 디폴트 구현이 있는 메서드
}

동일한 메서드를 구현하는 다른 인터페이스 정의
package part4

class Button : Clickable, Focusable {

    override fun click() = println("I was clicked")

    override fun showOff() { // 이름과 시그니처가 같은 멤버 메서드에 대해 둘 이상의 디폴트 구현이 있는 경우 인터페이스를 구현하는 하위 클래스에서 명시적으로 새로운 구현을 제공해야한다.
        super<Clickable>.showOff()
        super<Focusable>.showOff() // 상위 타입의 이름을 꺽쇠 괄호(<>)사이에 넣어서 "super"를 지정하면 어떤 상위 타입의 멤버 메서드를 호출할지 지정할 수 있다
    }

}

fun main() {
    val buttonJava = Button()
    buttonJava.click()
    buttonJava.showOff()
}

open, final, abstract 변경자: 기본적으로 final
취약한 기반 클래스
package part4

open class RichButton : Clickable { // 이 클래스는 열려있다. 다른 클래스가 이 클래스를 상속할 수 있다.

    fun disable() {} // 이 함수는 파이널이다. 하위 클래스가 이 메서드를 오버라이드할 수 없다.

    open fun animate() {} // 이 함수는 열려있다. 하위 클래스에서 이 메서드를 오버라이드해도 된다.

    override fun click() {} // 이 함수는 (상위 클래스에서 선언된) 열려있는 메서드를 오버라이드 한다. 오버라이드한 메서드는 기본적으로 열려있다.

}
package part4;

import kotlin.Metadata;
import part4.Clickable.DefaultImpls;

@Metadata(
        mv = {1, 7, 0},
        k = 1,
        xi = 2,
        d1 = {"\\u0000\\u0014\\n\\u0002\\u0018\\u0002\\n\\u0002\\u0018\\u0002\\n\\u0002\\b\\u0002\\n\\u0002\\u0010\\u0002\\n\\u0002\\b\\u0003\\b\\u0016\\u0018\\u00002\\u00020\\u0001B\\u0005¢\\u0006\\u0002\\u0010\\u0002J\\b\\u0010\\u0003\\u001a\\u00020\\u0004H\\u0016J\\b\\u0010\\u0005\\u001a\\u00020\\u0004H\\u0016J\\u0006\\u0010\\u0006\\u001a\\u00020\\u0004¨\\u0006\\u0007"},
        d2 = {"Lpart4/RichButton;", "Lpart4/Clickable;", "()V", "animate", "", "click", "disable", "playground"}
)
public class RichButton implements Clickable {
    public final void disable() {
    }

    public void animate() {
    }

    public void click() {
    }

    public void showOff() {
        DefaultImpls.showOff(this);
    }
}

오버라이드 금지하기
package part4

open class RichButton : Clickable { // 이 클래스는 열려있다. 다른 클래스가 이 클래스를 상속할 수 있다.

    fun disable() {} // 이 함수는 파이널이다. 하위 클래스가 이 메서드를 오버라이드할 수 없다.

    open fun animate() {} // 이 함수는 열려있다. 하위 클래스에서 이 메서드를 오버라이드해도 된다.

    final override fun click() {} // 이 함수는 (상위 클래스에서 선언된) 열려있는 메서드를 오버라이드 한다. 오버라이드한 메서드는 기본적으로 열려있다.
    // 금지하려면 final을 붙여준다
}
추상 클래스 정의하기
package part4

abstract class Animate { // 이 클래스는 추상 클래스다. 이 클래스의 인스턴스를 만들 수 없다.
    abstract fun animate() // 이 함수는 추상 함수다. 이 함수에는 구현이 없다. 하위 클래스에서는 이 함수를 반드시 오버라이드 해야만 한다

    open fun stopAnimating() { // 추상 클래스에 속했더라도 비 추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드 할 수 있다.
    }

    fun animateTwice() {  // 추상 클래스에 속했더라도 비 추상 함수는 기본적으로 파이널이지만 원한다면 open으로 오버라이드 할 수 있다.
    }

}
변경자 이 변경자가 붙은 멤버는 설명
final 오버라이드할 수 없음 클래스의 멤버의 기본 변경자
open 오버라이드할 수 있음 반드시 open을 명시해야 오버라이드할 수 있다.
abstract 반드시 오버라디으 해야 함 추상 클래스의 멤버에만 이 변경자를 붙일 수 있다. 추상 멤버에는 구현이 있으면 안 된다.
override 상위 클래스나 상위 인스턴스의 멤버를 오버라이드 하는 중 오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다.

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

변경자 클래스 멤버 최상위 선언
public(기본 가시성임) 모든 곳에서 볼 수 있다 모든 곳에서 볼 수 있다
internal 같은 모듈 안에서만 볼 수 있다 같은 모둘 안에서만 볼 수 있다.
protected 하위 클래스 안에서만 볼 수 있다 (최상위 선언에 적용할 수 없음)
private 같은 클래스 안에서만 볼 수 있다 같은 파일 안에서만 볼 수 있다
package part4

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

fun TalkativeButton.giveSpeech() { // 오류: "public" 멤버가 자신의 "internal" 수신 타입인 "TalkactiveButton"을 노출함
    yell()  // 오류 yell에 접근할 수 없음: yell은 TalkactiveButton의 private 멤버임
    whisper() // 오유 whisper에 접근할 수 없음: whisper는 TalkactiveButton의 protected 멤버임
}
public class TalkativeButton implements Focusable {
    private final void yell() {
        String var1 = "Hey!";
        System.out.println(var1);
    }

    protected final void whisper() {
        String var1 = "Let's talk!";
        System.out.println(var1);
    }

    public void setFocus(boolean b) {
        DefaultImpls.setFocus(this, b);
    }

    public void showOff() {
        DefaultImpls.showOff(this);
    }
}

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

직렬화할 수 있는 상태가 있는 뷰 선언
package part4

class Button2 : View {
    override fun getCurrentState(): State = ButtonState()

    override fun restoreState(state: State) {
    }

    class ButtonState : State {}
}
클래스 B 안에 정의된 클래스 A 자바에서는 코틀린에서는
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) static class A class A
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) class A inner class A
package part4

class Outer {

    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }
}

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

인터페이스 구현을 통해 식 표현하기
package part4

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 Sum -> eval(e.left) + eval(e.right)
        else ->
            throw IllegalAccessException("Unknown expression")
    }

package part4

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 식이 모든 하위 클래스를 검사하므로 별도의 else 분기가 없어도 된다.
    when (e) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.left) + eval(e.right)
    }

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

클래스 초기화: 주 생성자와 초기화 블록
class User constructor(_nickname: String) { // 파라미터가 하나만 있는 주 생성자

    val nickname: String

    init { // 초기화 블록
        nickname = _nickname
    }
}
public final class User {
    @NotNull
    private final String nickname;

    @NotNull
    public final String getNickname() {
        return this.nickname;
    }

    public User(@NotNull String _nickname) {
        Intrinsics.checkNotNullParameter(_nickname, "_nickname");
        super();
        this.nickname = _nickname;
    }
}
package part4

class User(_nickname: String) { // 파라미터가 하나만 있는 주 생성자

    val nickname = _nickname
}
class User(val nickname: String) { // val은 파라미터에 상응하는 프로퍼티가 생성된다는 뜻이다.

}
package part4

class User(
    val nickname: String,
    val isSubscribed: Boolean = true // 생성자 파라미터에 대한 디폴트 값을 제공한다.
) {

}
/*
* 클래스에 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출해야할 필요가 있다.
* 기반 클래스를 초기화 하려면 기반 클래스 이름 뒤에 괄호를 치고 생성자 인자를 넘긴다.
* */
open class User(
    val nickname: String,
) {}

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

open class ButtonKt // 인자가 없는 디폴트 생성자가 만들어진다
class RadioButton : ButtonKt() // ButtonKt의 생성자는 아무인자도 받지 않지만, Button 클래스를 상속한 하위 클래스는 반드시 Button 클래스의 생성자를 호출해야한다.
class Secretive private constructor() // 이 클래스의 유일한 주생성자는 비공개다.
부 생성자: 상위 클래스를 다른 방식으로 초기

open class View2 {

    constructor(ctx: Context) {  // 부생성자

    }

    constructor(ctx: Context, attr: Attribute) { // 부생성자

    }
}
class Mybutton : View2 {
    constructor(ctx: Context) : super(ctx) {} // 상위 클래스의 생성자를 호출한다.
    constructor(ctx: Context, attr: Attribute) : super(ctx, attr) {}
}
class Mybutton : View2 {
    constructor(ctx: Context) : this(ctx, MY_STYLE) {}
    constructor(ctx: Context, attr: Attribute) : super(ctx, attr) {}
}
인터페이스에 선언된 프로포티 구현
interface User {
    val nickname: String //User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야한다는 뜻
}
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) // 프로퍼티 초기화식

    fun getFacebookName(accountId: Int): String {
        return accountId.toString()
    }
}
interface User {
    val email: String
    val nickname: String
        get() = email.substringBefore('@') //프로퍼티에 뒷받침하는 필드가 없다. 대신 매번 결과를 계산해 돌려준다
}

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

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 = "Elsenheimerstrasse 47, 80689 Muenchen"
}
접근자의 가시성 변경
class LengthCounter {
    var counter: Int = 0
        private set  // 이 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다

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

fun main(args: Array<String>) {
    val lengthCounter = LengthCounter()
    lengthCounter.addWord("Hi!")
    println(lengthCounter.counter)
}

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

모든 클래스가 정의해야 하는 메서드
toString, equals, hashCode
class Client(val name: String, val postalCode: Int) {
    override fun toString(): String {
        return "Client(name='$name',postalCode = $postalCode)"
    }

    override fun equals(other: Any?): Boolean { // Any는 java.lang.Object에 대응하는 클래스로. 코틀린의 모든 클래스의 최상위 클래스다. Any?는 널이 될 수 있는 타입이므로 other은 null 일 수 있다.
        if (this === other) return true // other가 Client 인지 검사한다.
        if (javaClass != other?.javaClass) return false
        // if(other == null || other !is Client) // is 검사는 자바의 instanceof와 같다. in 연산자의 결과를 부정해주는 연산자가 !in연산자 이다
        // return false

        other as Client

        if (name != other.name) return false // 두 객체의 프로퍼티 값이 서로 같은지 검사한다.
        if (postalCode != other.postalCode) return false
        // return name == other.name && postalCode == other.postalCode

        return true
    }

    override fun hashCode(): Int {
        var result = name.hashCode()
        result = 31 * result + postalCode
        return result
    }

}

fun main(args: Array<String>) {
    val client1: Client = Client("mala", 2)
    println(client1.toString())

    /* 코틀린에서 == 연산자는 참조 동일성을 검사하지 않고 객체의 동등성을 검사한다. 따라서 == 연산은 equals 호출하는 식으로 컴파일된다.
    * 코틀린에서는 == 연산자가 두 객체를 비교하는 기보적인 방법이다. == 는 내부적으로 equals 호출해서 객체를 비교한다.
    * 따라서 클래스가 equals 오버라이드하면 == 를 통해 안전하게 그 클래스의 인스턴스를 비교할 수 있다.
    * 참조 비교를 위해서는 === 연산자를 사용할 수 있다. === 연산자는 자바에서 객체의 잠조를 비교할 때 사용하는 === 연산자와 같다.*/
    val client2: Client = Client("heaven", 3)
    println(client1 == client2)

    val client3: Client = Client("mala", 2)
    println(client1 == client3)

    val processed = hashSetOf(Client("mala", 2))
    println(processed.contains(Client("mala", 2)))

}
데이터 클래스: 모든 클래스가 정의해야 하는 메서드 자동 생성
data class Client2(val name: String, val postalCode: Int)
public final class Client2 {
    @NotNull
    private final String name;
    private final int postalCode;

    @NotNull
    public final String getName() {
        return this.name;
    }

    public final int getPostalCode() {
        return this.postalCode;
    }

    public Client2(@NotNull String name, int postalCode) {
        Intrinsics.checkNotNullParameter(name, "name");
        super();
        this.name = name;
        this.postalCode = postalCode;
    }

    @NotNull
    public final String component1() {
        return this.name;
    }

    public final int component2() {
        return this.postalCode;
    }

    @NotNull
    public final Client2 copy(@NotNull String name, int postalCode) {
        Intrinsics.checkNotNullParameter(name, "name");
        return new Client2(name, postalCode);
    }

    // $FF: synthetic method
    public static Client2 copy$default(Client2 var0, String var1, int var2, int var3, Object var4) {
        if ((var3 & 1) != 0) {
            var1 = var0.name;
        }

        if ((var3 & 2) != 0) {
            var2 = var0.postalCode;
        }

        return var0.copy(var1, var2);
    }

    @NotNull
    public String toString() {
        return "Client2(name=" + this.name + ", postalCode=" + this.postalCode + ")";
    }

    public int hashCode() {
        String var10000 = this.name;
        return (var10000 != null ? var10000.hashCode() : 0) * 31 + Integer.hashCode(this.postalCode);
    }

    public boolean equals(@Nullable Object var1) {
        if (this != var1) {
            if (var1 instanceof Client2) {
                Client2 var2 = (Client2) var1;
                if (Intrinsics.areEqual(this.name, var2.name) && this.postalCode == var2.postalCode) {
                    return true;
                }
            }

            return false;
        } else {
            return true;
        }
    }
}
데이터 클래스와 불변성: copy() aptjem
package part4

class Client2(val name: String, val postalCode: Int) {
    fun copy(
        name: String = this.name,
        postalCode: Int = this.postalCode
    ) = Client2(name, postalCode)

    override fun toString(): String {
        return "Client2(name='$name', postalCode=$postalCode)"
    }

}

fun main(args: Array<String>) {
    val yun = Client2("mala", 1)
    println(yun.copy(postalCode = 2))
}
클래스 위임: by 키워드 사용

class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()

    override val size: Int
        get() = innerList.size

    override fun contains(element: T): Boolean {
        return innerList.contains(element)
    }

    override fun containsAll(elements: Collection<T>): Boolean {
        return innerList.containsAll(elements)
    }

    override fun isEmpty(): Boolean {
        return innerList.isEmpty()
    }

    override fun iterator(): Iterator<T> {
        return innerList.iterator()
    }
}
class DelegatingCollection<T>(
    innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList {}
클래스 위임 사용하기

class CountingSet<T>(
    val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet { // MutableCollection의 구현을 innerSet에게 위임한다.
    var objectsAdded = 0

    override fun add(element: T): Boolean {
        objectsAdded++
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectsAdded += elements.size
        return innerSet.addAll(elements)
    }
}

fun main(args: Array<String>) {
    val cset = CountingSet<Int>()
    cset.addAll(listOf(1, 1, 3))
    println("${cset.objectsAdded} objects were added, ${cset.size} remain")
}

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

객체 선언: 싱글턴을 쉽게 만들기
object Payroll {
    val allEmployees = arrayListOf<Person>()
    fun calculateSalary() {
        for (person in allEmployees) {
            ....
        }
    }
}

fun main(args: Array<String>) {
    Payroll.allEmployees.add(Person(...))
    Payroll.calculateSalary()
}
import java.io.File

object CaseInsensitiveFileComparator : Comparator<File> {
    override fun compare(o1: File, o2: File): Int {
        return o1.path.compareTo(o2.path, ignoreCase = true)
    }
}

fun main(args: Array<String>) {
    val files = listOf(File("/Z"), File("/a"))
    println(files.sortedWith(CaseInsensitiveFileComparator))
}
data class Person(val name: String) {
    object NameComparator : Comparator<Person> {
        override fun compare(o1: Person, o2: Person): Int =
            o1.name.compareTo(o2.name)
    }
}

fun main(args: Array<String>) {
    val persons = listOf(Person("Bob"), Person("Alice"))
    println(persons.sortedWith(Person.NameComparator))
}
public static final class NameComparator implements Comparator {
    @NotNull
    public static final Person.NameComparator INSTANCE;

    public int compare(@NotNull Person o1, @NotNull Person o2) {
        Intrinsics.checkNotNullParameter(o1, "o1");
        Intrinsics.checkNotNullParameter(o2, "o2");
        return o1.getName().compareTo(o2.getName());
    }

    // $FF: synthetic method
    // $FF: bridge method
    public int compare(Object var1, Object var2) {
        return this.compare((Person) var1, (Person) var2);
    }

    private NameComparator() {
    }

    static {
        Person.NameComparator var0 = new Person.NameComparator();
        INSTANCE = var0;
    }
}
동반 객체: 팩토리 메서드와 정적 멤버가 들어갈 장소
class A {
    companion object {
        fun bar() {
            println("Companion object called")
        }
    }
}

fun main(args: Array<String>) {
    A.bar()
}
package part4

class UserCompanionEx {

    val nickname: String

    constructor(email: String) {
        nickname = email.substringBefore('@')
    }

    constructor(facebookAccountId: Int) {
        nickname = getFacebookName(facebookAccountId)
    }

    private fun getFacebookName(facebookAccountId: Int): String {
        return facebookAccountId.toString()
    }
}

class UserCompanionEx private constructor(val nickname: String) { // 주 생성자를 비공개로 만든다.

    companion object { // 동반 객체로 만든다
        fun newSubscribingUser(email: String) = User(email.substringBefore('@'))

        fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId)) // 페이스북 사용자 ID로 사용자를 만드는 팩토리 메서드

        private fun getFacebookName(accountId: Int): String {
            return accountId.toString()
        }
    }

}


class PersonCompanion(val name: String) {
    companion object Loader { // 동반 객체에 이름을 붙인다.
        fun fromJSON(jsonText: String): PersonCompanion = PersonCompanion(jsonText)
    }
}

fun main(args: Array<String>) {
    val person = PersonCompanion.Loader.fromJSON("{ name: 'Dirty' }")
    println(person.name)

    val person2 = PersonCompanion.fromJSON("{ name: 'Brent' }")
    println(person2.name)
}
동반 객체에서 인터페이스 구현
interface JSONFactory<T> {
    fun fromJSON(jsonText: String): T
}

class PersonJson(val name: String) {
    companion object : JSONFactory<PersonJson> {
        override fun fromJSON(jsonText: String): PersonJson = PersonJson(jsonText) // 동반 객체가 인터페이스를 구현한다.
    }
}
동반 객체 확장

class PersonExtend(val firstName: String, val lastName: String) {
    companion object {

        fun print() {
            println("여기 들어오니???")
        }
        // 비어있는 동반 객체를 선언한다.
    }
}

fun PersonExtend.Companion.fromJSON(json: String): PersonExtend {
    // 확장 함수를 선언한다
    PersonExtend.print()
    return PersonExtend("first", "last")
}

fun main(args: Array<String>) {
    val p = PersonExtend.fromJSON("json")
}
객체 식: 무명 내부 클래스를 다른 방식으로 작성

abstract class PersonB(val name: String, val age: Int) {
    abstract fun info()
}

fun main(args: Array<String>) {
    val mala: PersonB = object : PersonB("mala", 0) {
        override fun info() {
            TODO("Not yet implemented")
        }
    }
}

[출처] Kotlin IN ACTION