본문 바로가기
책 정리/Kotlin IN ACTION

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

by 이석준석이 2021. 9. 10.


1. 코틀린 인터페이스디폴트 구현을 포함할 수 있으며, 프로퍼티도 포함할 수 있다.

일반 메소드 디폴트 메소드
무조건 재정의가 필요하다. 선택적으로 재정의 할 수 있다.
재정의 하는 경우, override 변경자는 필수이다.
interface Clickable {
    fun click()
    fun showOff() = println("I'm clickable!")
}

// implements 하는 경우에는 ":" 로 할 수 있다.
class ClickableImpl : Clickable {
    // override 변경자는 필수이다.
    override fun click() {
        println("I was clicked")
    }

    // 선택적으로 재정의 할 수 있다.
    override fun showOff() {
        println("override implemented method")
    }
}

fun main() {
    val impl = ClickableImpl()

    impl.click()
    impl.showOff()
}

2. 모든 코틀린 선언기본적으로 final 이며, public 이다.

  • override 한 메소드기본적으로 open 상태이다.
    • final override fun xxx() 는 이제 이 함수를 더이상 오버라이드 하지 않겠다는 의미이다.
final 오버라이드 할 수 없음
open 오버라이드 할 수 있음
abstract 반드시 오버라이드 해야 함
override 오버라이드를 한 것을 의미함

3. 가시성 제한자

final 오버라이드 할 수 없음
open 오버라이드 할 수 있음
abstract 반드시 오버라이드 해야 함
override 오버라이드를 한 것을 의미함

4. 코틀린에서 클래스내, 클래스를 생성하는 경우 (nested class)

class Outer {
    // 외부에 대한 참조가 있음
    inner class Inner {
        fun getOuterReference(): Outer = this@Outer
    }

    // 외부에 대한 참조가 없음 (자바의 static class 와 같다.)
    class staticInner {
        fun getOuterReference() {
            
        }
    }
}

5. sealed class

  • 상속을 파일내에서 제한하는 sealed 클래스를 사용할 수 있다.
  • when 에서 default 분기처리를 하지 않아도 된다.
  • sealed 클래스 자체는 private 생성자를 갖는다.
sealed class SealedCls {
    
    class X(val intValue: Int) : SealedCls()
    class Y(val stringValue : String) : SealedCls()
}

class Z(val longValue: Long) : SealedCls()

fun printType(type : SealedCls) : String = 
    when(type) {
        is SealedCls.X -> "integer"
        is SealedCls.Y -> "string"
        is Z -> "long"
    }

6. 주 생성자로 객체 초기화하기

  • 소괄호"()" 사이에 둘러싸인 코드를 주 생성자(primary constructor)라고 한다.
  • 아래 코드의 3가지 선언은 모두 같은 역할을 한다.
  • 가시성 선언자가 없다면, constructor 은 생략 가능
class User(val nickname: String)

class User2(_nickname: String) {
    val nickname = _nickname
}

class User3 constructor(_nickname: String) {
    val nickname: String
    init {
        nickname = _nickname
    }
}

// secondary
class User4 {
    val nickname: String

    constructor(_nickname: String) {
        nickname = _nickname
    }
}

7. 외부에서 생성자 사용 못하도록 하기

class PrivateUser private constructor()

8. 부생성자(secondary constructor) 를 이용하여, 상위클래스의 여러가지 생성자를 대응할 수 있다.

  • 주로 다른 클래스를 사용하기위해 상속하는경우, 호환성을 위해 사용한다.
open class User {
    val nickname: String
    val age: Int

    constructor(_nickname: String) {
        nickname = _nickname
        age = 0
    }

    constructor(_nickname: String, _age: Int) {
        nickname = _nickname
        age = _age
    }
}

class Employee : User {
    val pay: Int

    // 아래에 정의한 생성자를 this로 이용하였다.
    constructor(_nickname: String, _pay: Int) : this(_nickname, 0, _pay)

    constructor(_nickname: String, _age: Int, _pay: Int) : super(_nickname, _age) {
        pay = _pay
    }
}

9. 인터페이스 프로퍼티

  • 코틀린에서는 인터페이스에 
    • 프로퍼티 (멤버변수 + Getter + Setter) 선언이 가능하다.
  • 멤버변수만 선언했다면, 구현하는 클래스에서 이에 맞는 getter / setter 를 구현해줘야한다.
    • val : getter
    • var : setter + getter
interface User {
    val nickname: String
}

// 주생성자를 통하여 getter 를 구현
class PrivateUser(override val nickname: String) : User

// 커스텀게터를 구현
class SubscribingUser(val email: String) : User {
    override val nickname: String
        get() = email.substringBefore('@')
}

10. field(뒷바침하는 필드) 에 접근

  • 커스텀 getter / setter 에서, field 는 실제 값을 저장하는 변수를 뜻한다.
  • 커스텀 setter 에서, value 를 이용하여 field 값을 set 할 수 있다.
class User(name: String, age: Int) {
    val name: String = name
        get() {
            return field
        }

    var age: Int = age
        get() {
            return field
        }
        set(value) {
            field = value
        }
}

11. 접근자 가시성 변경

  • private set 등을 이용해서 접근자 가시성을 변경할 수 있다.
  • 아래의 예시에서는 name 을 세팅하는 경우,
    • firstName, secondName 은 해당 클래스 내에서만 변경할 수 있도록 해놓았다.
class User {
    var name: String = ""
        set(value) {
            field = value
            firstName = value.substringBefore(" ")
            secondName = value.substringAfter(" ")
        }

    var firstName : String = ""
        private set
    var secondName : String = ""
        private set

    fun getFullName(): String = "firstname is $firstName and secondname is $secondName"
}

12. toString / equals / hashCode

  • equals
    • 코틀린에서는 '==' 을 이용하여 equals 비교를 한다.
    • '===' 을 이용하여 reference 를 비교할 수 있다.
  • hashCode
    • hash 컨테이너를 사용할 때, 키로 사용할 수 있다.

13. 데이터클래스 (toString + equals + hashCode + copy + ...)

  • 데이터클래스를 사용하므로써, 위에 명시한 편의메소드를 코틀린 컴파일러가 생성해준다.
  • copy 메소드는 Shallow copy 를 지원한다.
data class DataCls(val name: String, val age: Int, val innerCls : Inner)

class Inner

fun main() {
    val inner = Inner()
    val a = DataCls("lee", 20, inner)
    val b = DataCls("lee", 20, inner)

    assertTrue(a==b)
    assertFalse(a===b)

    val of = setOf<DataCls>(a)
    assertTrue(of.contains(a))

    val copy = a.copy()

    assertTrue(a==b)
    assertFalse(a===copy)

    assertTrue(a.innerCls===copy.innerCls)
}

14. 클래스 위임을 통한 불필요 코드 implements 하지 않기

  • 자바로 변환하여 보면, 
    • 아래의 추가로 오버라이드 한, add / addAll 을 제외한 함수들은 by 키워드를 통해 innerSet에 위임하였다.
class CountingSet<T>(
    val innerSet: MutableCollection<T> = HashSet<T>()
) : MutableCollection<T> by innerSet {
    var addedCnt = 0
        private set
    override fun add(element: T): Boolean {
        addedCnt++
        return innerSet.add(element)
    }

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

15. object 키워드를 통한 싱글턴 생성

  • object 키워드 : 클래스를 정의하면서, 인스턴스를 생성한다.
  • 생성자를 가질 수 없다.
  • 상속이나 구현은 가능하다.
  • nested object 또한, 인스턴스는 한개만 생성된다.
object Calculator {
    fun plus(a: Int, b: Int): Int = a + b
    fun minus(a: Int, b: Int): Int = a - b
}

fun main() {
    assertSame(3, Calculator.plus(1, 2))
    assertSame(-1, Calculator.minus(1, 2))
}

자바로 디컴파일한 결과이다.

  • private 생성자를 가진 INSTANCE 를 제공한다.
public final class Calculator {
   @NotNull
   public static final Calculator INSTANCE = new Calculator();

   private Calculator() {
   }

   public final int plus(int a, int b) {
      return a + b;
   }

   public final int minus(int a, int b) {
      return a - b;
   }
}

16. 동반 객체 (companion object)

  • 클래스 내부에 정의하여, 클래스와 하나로 사용할 수 있다.
  • 코틀린은 static 키워드를 지원하지 않는다.
  • 최상위함수 / 객체 선언 을 통해 이를 대신할 수 있다.
    • 객체선언은 클래스의 private 멤버에 접근할 수 있다. (아래의 팩토리 패턴을 사용할 때 유용하다.)
class A {
    companion object Inside {
        fun bar() {
            println("Inside Companion Object")
        }
    }
}

fun main() {
    A.bar()
    A.Inside.bar()
}

17. 동반 객체를 이용한 팩토리 패턴

  • 동반 객체를 사용하는 경우, 클래스 내부의 private 멤버에 접근할 수 있다.
  • 이를 이용하여 팩토리 패턴을 사용할 수 있다.
class Account private constructor(val nickname: String) {
    companion object {
        fun createNaverAccount(nickname : String) = Account("$nickname@naver.com")
        fun createKakaoAccount(nickname : String) = Account("$nickname@kakao.com")
    }
}

fun main() {
    val naver = Account.createNaverAccount("lsj")
    val kakao = Account.createKakaoAccount("lsj")

    assertEquals("lsj@naver.com", naver.nickname)
    assertEquals("lsj@kakao.com", kakao.nickname)
}

18. 동반 객체 확장

  • 동반 객체를 확장하기 위해서는, 비어있는 companion 오브젝트가 필수이다.
  • 동반 객체의 기본이름인 Companion 으로 접근할 수 있다.
data class Person(val name: String) {
    companion object {
    }
}

fun Person.Companion.fromJson(json: String) : Person {
    return Person(json)
}

fun main() {
    val fromJSON = Person.fromJson("test")
    assertEquals("test", fromJSON.name)
}

19. object 키워드를 이용한 Anonymous 클래스 작성

  • object 키워드를 통해 아래의 예시처럼 구현할 수 있다.
    • 인터페이스 구현
    • 클래스 확장
  • object 키워드를 사용했더라도, 싱글턴이 아니다.
interface X {
    fun a()
    fun b()
}

class Y(val x: X)

fun main() {
    Y(object : X {
        override fun a() {
            TODO("Not yet implemented")
        }

        override fun b() {
            TODO("Not yet implemented")
        }
    })
}

'책 정리 > Kotlin IN ACTION' 카테고리의 다른 글

3. 함수 정의와 호출  (0) 2021.08.29
2. 코틀린 기초  (0) 2021.08.28
1. 코틀린이란 무엇이며, 왜 필요한가?  (0) 2021.08.28