4. 클래스, 객체, 인터페이스
CS/코틀린 인 액션

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

다른 언어 쓰느라고 클래스를 거의 쓴 적이 없다.

그래서 이해가 잘 안되서 요약을 잘 못하겠음..

 

4.1 클래스 계층 정의

interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}

interface Focusable {
    fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus.")
    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()
    }
}

fun main(args: Array<String>) {
    val button = Button()
    button.showOff()
    button.setFocus(true)
    button.click()
}

/*
I'm clickable!
I'm focusable!
I got focus.
I was clicked
*/

상속은 자바랑 비슷한데, 일반 클래스는 부모 하나, 인터페이스는 여러 개를 둘 수 있다.

class <클래스 이름> : <부모 인터페이스 | 클래스>

이런식으로 정의함. 인터페이스의 경우 반드시 override로 해서 정의해야 되는 것 까지 똑같다.

함수명이 겹칠 경우, <상속 인터페이스 이름>을 적어줘야 함.

 

 

원래 클래스를 만들었던 작성자의 의도를 지키기 위해 "상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라" 라는 이펙티브 자바 책의 조언에 따라 기본이 final이다. 오버라이드 가능하게 하려면 class 앞과 fun 앞에 open을 붙여야 함.

 

표) 클래스 내에서 상속 제어 변경자의 의미

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

 

 

 

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

fun TalkativeButton.giveSpeech() { // 오류: "public"멤버가 자신의 "internal" 수신 타입인
                                   //       "TalkativeButton"을 노출함
    // yell() // 오류: "yell"에 접근할 수 없음: "yell"은 "TalkativeButton"의 "private" 멤버임
    // whisper() // 오류: "whisper"에 접근할 수 없음: "whisper"은 "TalkativeButton"의 "protected" 멤버임
}

표) 코틀린의 가시성 변경자

변경자 클래스 멤버 최상위 선언
public(기본 가시성임) 모든 곳에서 볼 수 있다. 모든 곳에서 볼 수 있다.

internal 같은 모듈 안에서만 볼 수 있다. 같은 모듈 안에서만 볼 수 있다.
protected 하위 클래스 안에서만 볼 수 있다. (최상위 선언에 적용할 수 없음)
private 같은 클래스 안에서만 볼 수 있다. 같은 파일 안에서만 볼 수 있다.

 

 

표) 자바와 코틀린의 중첩 클래스와 내부 클래스의 관계

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

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

 

 

sealed는 상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다.

상위 클래스를 바꿨을 때 이미 만들어 놓은 하위 클래스에서 버그 뜰 수 있는걸 방지하기 위함이다.

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 IllegalArgumentException("Unknown expression")
    }
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) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.left) + eval(e.right)
    }

 

 

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

코틀린은 자바에서 클래스 생성할 때 이미 직접 써서 사용하고 있는 생성자, 게터, 세터를 자동으로 해준다. 어떻게 하는건지 살펴볼거다.

 

class User(val nickname: String)

class User constructor(_nickname: String) {
    val nickname: String

    init {
        nickname = _nickname
    }
}

을 줄여쓴거다. 주 생성자부 생성자가 있는데,  주 생성자는 braket {} 밖에 쓴거고, 부 생성자는 init 같은거 써서 클래스 {} 안에 쓴거다.

또, constructor는 주 생성자 앞에 별다른 애노테이션이나 가시성 변경자가 없다면 생략해도 된다. 그럼

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

    init {
        nickname = _nickname
    }
}

이렇게 되고, 더 줄이면 처음에 보여줬던 한 줄 짜리가 되는거다.

 

 

다른 클래스가 해당 클래스를 상속하고 싶을 때는 파라미터를 받지 않더라도괄호 ()를 붙여야 함. 인터페이스는 붙지 않음. 이 규칙으로 괄호 ()가 있으면 클래스, 없으면 인터페이스를 상속한다고 파악할 수 있다.

class RadioButton: Button()

 

만약 어떠한 외부 클래스도 해당 클래스를 상속하지 못하게 하려면, constructor를 생략하지 말고 앞에 private를 붙이면 된다.

class Secretive private constructor() {}

 

사실 이정도만 되도 왠만한 클래스는 다 정의할 수 있지만, 특별한 것들을 살펴보자.

 

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

부 생성자가 2개인 놈. 주어진 파라미터 갯수에 따라 다른 행동을 취하고 싶을 때 쓴다.

 

class MyButton : View {
    constructor(ctx: Context) : super(ctx) {
        // ...
    }
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
        // ...
    }
}

 

class MyButton : View {
    constructor(ctx: Context) : this(ctx, MY_STYLE) {
        // ...
    }
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
        // ...
    }
}

 

super와 this에 따른 생성 위임 차이

 

다음은 특별한 프로퍼티

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)
}

println(PrivateUser("test@kotlinlang.org").nickname) // test@kotlinlang
println(SubscribingUser("test@kotlinlang.org").nickname) // test
interface User {
    val nickname: 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, 80687 Muenchen"
}
/*
Address was changed for Alice:
"unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".
*/

세터 잘 정의하면 로그 가능

 

만약 이 세터를 클래스 외부에서 사용 불가능하게 하고 싶다면 private를 쓸 수 있다.

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) // 3
}

 

 

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

 

모든 클래스가 정의해야 하는 특별한 메서드들이 있다. toString, equals, hashCode 등

직접 작성해보자.

class Client(val name: String, val postalColde: Int) {
    override fun equals(other: Any?): Boolean {
        if (other == null || other !is Client) {
            return false
        }
        return name == other.name && postalColde == other.postalColde
    }
    override fun toString(): String = "Client(name=$name, postalCode=$postalColde)"
    override fun hashCode(): Int = name.hashCode() * 31 + postalColde
}

 

equals가 같은 Client 객체가 아니라 Any? 타입을 인자로 받는 이유는 기본 함수인 equals를 오버라이드 하려고.

 

자바에서 ==는 참조 비교, .equals는 동등성(값) 비교이고 코틀린에서 ==는 자바의 equals를 사용하기 때문에 동등성 비교, 참조는 ===을 이용한다.

그래서 hashCode까지 직접 오버라이드해서 정의하지 않으면 == 에서 에러가 날 수 있다. 왜냐하면 원소가 똑같은지 확인할 때 연산 시간을 줄이기 위해 먼저 해시값을 비교하기 때문에 값이 같아도 해시값이 달라서 값이 같은건 의미가 없다.

 

그럼 이 작업을 클래스 만들때마다 맨날 하냐? 그래서 코틀린에선 어차피 반복해서 해야하는걸 줄이기 위해 class 앞에 data를 붙이면 위의 과정을 컴파일러가 알아서 해준다.

data class Client(val name: String, val postalColde: Int)

 

copy() 같은 경우는.. 일단 왜 있냐면 데이터 클래스의 프로퍼티, 특히 HashMap 안에 들어가는 애들 같은 경우는 왠만하면 val로 정의하길 권장한다. 키 값을 프로퍼티를 사용해 만들어 놨는데 안의 내용을 변경하면 문제가 생길 수 있고, 다중 스레드에서 실행할 때 값이 안변해야 신경 쓸 게 줄어든다. 그래서 값을 변경한다기 보단 새로 만드는 방법을 쓴다.

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

val lee = Client("Lee", 123)
println(lee.copy(postalCode = 456))

 

by 키워드는 만들어진 철학이 있는데, 코틀린은 기본값으로 클래스가 final인데, 클래스가 한번 정의되면 왠만하면 변경을 안했으면 좋겠다는 뜻이다. 이유는 만약 상위 클래스를 하위 클래스가 상속하여 사용 중이었는데, 상위 클래스가 막 변하면 하위 클래스에서 같은 메서드를 써도 에러가 날 수 있기 때문. open 클래스만 확장하여 사용할 수 있고, 그래서 open 클래스만 보고도 다른 클래스가 상속하리라 짐작할 수 있다.

근데 final이라면 클래스 확장을 못하는데, 가끔 동작을 추가해야 할 때가 있다. 이때 쓰는것이 데코레이터(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(elements)
}

확장해서 쓸 수 없는 arrayListOf<T>를 사용하기 위해 일일히 자기것으로 정의하는 모습이다.

 

이게 by 키워드를 사용한 다음 코드와 같다.

class DelegatingCollection<T>(
    val innerList: Collection<T> = ArrayList<T>()
): Collection<T> by innerList

역시나 이런 귀찮은 일들은 컴파일러가 알아서 해준다. 특이한 건 by 뒤에 변수명이 온다는 것.

 

class CountingSet<T>(val innerSet: MutableCollection<T> = HashSet<T>()) : MutableCollection<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)
    }
}

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

// 3 objects were added, 2 remain

상위의 API를 그대로 가져다 쓰기 때문에 상위 API 작동 방식이 바뀌더라도 뭐 성능개선 이런것일 거라 내 함수는 문제 없이 돌아갈 것임을 짐작할 수 있다.

 

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

객체지향언어라 한 개의 인스턴스만 필요하더라도 클래스로 만들어 쓸 때(싱글톤) 쓴다고 한다. class 대신 object를 쓴다.

import java.io.File

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

fun main(args: Array<String>) {
    println(CaseInsensitiveFileComparator.compare(File("/User"), File("/user"))) // 0
    
    val files = listOf(File("/Z"), File("/a"))
    println(files.sortedWith(CaseInsensitiveFileComparator)) // [/a, /Z]
}

하지만 싱글톤은 대규모 시스템이 갖춰진 프로젝트에서는 안쓴다고 한다. 하지만 자바에는 있는 static이 없기 때문에 이걸 이용해서 static처럼 사용해서 쓴다.

 

클래스 내부에서도 정의해서 사용할 수 있다.

class User private constructor(val nickname: String) {
    companion object {
        fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
        fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))
    }
}

fun main(args: Array<String>) {
    val subscribingUser = User.newSubscribingUser("bob@gmail.com")
    val facebookUser = User.newFacebookUser(4)
    println(subscribingUser.nickname) // bob
}

클래스를 생성할 때, 생성자 말고 팩토리를 사용해서만 생성할 수 있게 만들 수 있음.

 

 

동반 객체를 일반 객체처럼 사용할 수도 있다. 동반 객체는 클래스 안에 정의된 일반 객체다.

class Person(val name: String) {
    companion object Loader {
        fun fromJSON(jsonText: String): Person = ...
    }
}

fun main(args: Array<String>) {
    val person = Person.Loader.fromJSON("{name: 'Dmitry'}")
    println(person.name) // Dmitry
    val person2 = Person.fromJSON("{name: 'Brent'}")
    println(person2.name) // Brent
}

 

무명 내부 클래스를 다른 방식으로 작성할 수도 있다.

fun countClicks(window: Window) {
    var clickCount = 0
    window.addMouseListener(object : MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }
    })
}

'CS > 코틀린 인 액션' 카테고리의 다른 글

6. 코틀린 타입 시스템  (0) 2023.04.30
5. 람다로 프로그래밍  (0) 2023.04.30
3. 함수 정의와 호출  (0) 2023.04.24
2. 코틀린 기초  (0) 2023.04.23
1. 코틀린은 무엇이며, 왜 필요한가  (0) 2023.04.23