Kotlin-In-Action | #4. 클래스, 객체, 인터페이스

보람·2022년 4월 29일
0

Kotlin-In-Action

목록 보기
5/12

클래스 계층 정의

코틀린 interface

  • 추상메서드 + 구현메서드 모두 정의 가능
  • 상태 필드는 들어갈 수 없음
  • 인터페이스는 원하는만큼 상속 가능하나 클래스는 오직 하나만 확장 가능
  • override 변경자를 꼭 사용
/**
 * 두개의 인터페이스를 상속받은 Button class
 */
class Button : Clickable, Focusable {
    override fun click() = println("I was clicked") //override 키워드 

    //이름이 같은 함수가 있는 두 interface를 상속한 경우라면 해당 함수를 override 구현해야함
    override fun showOff() {
        //두 인터페이스중 하나의 함수만 필요한 경우 하나의 함수만 호출해도 됨
        //상위 호출 : super<interface>.함수() 
        super<Clickable>.showOff()
        super<Focusable>.showOff()
    }
}

/**
 * 일반메서드와 디폴트메서드가 있는 Clickable interface
 */
interface Clickable {
    fun click() //일반 메서드 선언
    fun showOff() = println("I'm clickable!") 
    //디폴트 구현이 있는 메서드 -> 오버라이딩 해도 되고 안할 경우 여기서 구현된 함수 사용
}

/**
 * Clickable과 동일한 함수인 showOff()가 존재하는 Focusable
 */
interface Focusable {
    fun setFocus(b: Boolean) =
        println("I ${if (b) "got" else "lost"} focus.")

    fun showOff() = println("I'm focusable!")
}
  

자바와 다른점?

  • 자바는 여러개의 클래스 상속 가능 (<-> 코틀린 : 한개 상속 가능)
  • 상위 호출 타입 지정 방식 다름 : super<인터페이스>.함수

open, final, abstract : 기본은 final

  • 이것은 상속 가능 여부

    Effective Java 구절 : 상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라

  • 위 구절은 대충 잘 설계할 자신이 없으면 상속 하지마!
  • 코틀린의 클래스와 메서드는 기본적으로 final(상속X)
  • 상속을 원하면 open
  • 반드시 오버라이딩 abstract
  • 오버라이드하는중 override(하위클래스에서)
/**
 * 구현 인터페이스
 */
interface Clickable {
    fun click() //해당 함수는 반드시 구현해야함, 왜냐? 구현을 안했으니 자식이 해줘야쥬?
    fun showOff() = println("I'm clickable!") //디폴트 함수, 구현 해도 되고 안해도 됨
}

/**
 * Clickable 을 상속받는 상속이 가능한(open) RichButton 클래스 
 */
open class RichButton : Clickable { //open class -> 다른 클래스가 상속 가능

    fun disable() {} //기본은 final -> 하위 클래스가 해당 메서드를 오버라이드 할 수 없다.

    open fun animate() {} //open -> 하위 클래스에서 해당 메서드를 오버라이드 할 수 있다.

    override fun click() {}
    //override -> 오버라이드한 메서드는 기본적으로 열려있다. -> 하위 클래스에서 구현가능
    //만일 final override인 경우 -> RichButton 을 상속받는 클래스에서 override 불가능 
    //=> 나는 지금 오버라이드(재정의)하지만 나(RichButton)를 상속받는 하위클래스에선 click()을 재정의할 수 없어!! 라는 의미 
}

코틀린 가시성 | 기본은 public

  • 자바의 protected는 같은 모듈 -> 코틀린에서는 internal로 대체
  • 확장함수는 해당 클래스의 protected, private 멤버에 접근 불가
  • 외부 클래스는 내부or중첩 클래스의 private 멤버에 접근 불가
  • protected의 최상위 선언이 없는 이유는? => protected는 하위 클래스 내부에 존재해야 하는데 최상위에 선언하면 클래스 밖에 위치하기 때문

여기서 잠깐..! 프로젝트 > 모듈 > 패키지 > 클래스

  • 모듈 : 프로젝트 바로 아래 개념, 패키지+클래스
  • 패키지 : 모듈 아래 개념
    • 사용자가 지정한 패키지가 없으면 클래스는 기본 패키지에 위치
  • 클래스 : 패키지 아래 위치

내부 클래스와 중첩 클래스 | 기본은 중첩

  • 중첩 클래스 : 외부 클래스 참조 X
  • 내부 클래스 : 외부 클래스 참조
  • 상태 저장 클래스는 내부 클래스에 선언시 편함(why? 외부 클래스 참조 가능 -> 상태프로퍼티가 외부 클래스에 있으니까!)
  • 자바의 기본은 내부클래스 (+static -> 중첩클래스)
  • 코틀린의 기본은 중첩클래스 (+inner -> 내부클래스)

코틀린 중첩클래스

/**
  * interface View의 구현 클래스 Button
  * interface는 생성자가 없어서 ()가 X -> View 
  * 클래스 상속시 생성자 존재해서 ()가 붙음 
  */
class Button : View {
    override fun getCurrentState(): State = ButtonState()

    override fun restoreState(state: State) { /*...*/ } 

    class ButtonState : State { /*...*/ } 
    //앞에 inner가 X -> 코틀린에서는 중첩클래스가 기본
}

코틀린 inner class

/**
 * 바깥쪽 클래스 Outer
 */
class Outer {
    //코틀린에서는 class 앞에 inner를 붙이면 내부클래스가 된다
    inner class Inner {
        // 내부 클래스에서 외부클래스 참조에 접근하려면 this@외부클래스 라고 써야함
        fun getOuterReference(): Outer = this@Outer
    }
}

sealed(봉인) class : 계층 확장 제한

  • 코틀린에서 when 사용시 조건을 추가하고 싶지 않아도 else를 명시해야함
  • 상위 클래스에 sealed 를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의 제한 가능
  • sealed class > 하위클래스 추가시 : when식을 고쳐야 됨(안 고치고 싶다구? -> 그러면 컴파일 X)
/**
 * 봉인된 sealed 클래스, 아래 하위 클래스에 대해서만 when에 지정 가능(else X)
 * 앞에 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) {
        is Expr.Num -> e.value
        is Expr.Sum -> eval(e.right) + eval(e.left)
        //else X 🌝
    }

클래스 선언의 다른 방법

주 생성자와 초기화 블록

  • class name () {} -> 주 생성자
  • 아래는 모두 n 프로퍼티를 초기화해주는 User 주생성자의 세가지 표현
    • class User (val n:String) -> 제일 간결(추천!)
    • class User constructor (_n:String) { val n:String init {n=_n}
    • class User (val n:String){val n = _n}
  • 생성자 파라미터에 대한 기본값 제공 가능
    • class User (val n:String, val d:Boolean=true)
  • 상위 클래스 Button을 상속하는 하위클래스 RadioButton -> 상위 클래스 생성자 반드시 호출
    • class RadioButton:Button()
    • 다시 한번 알수 있는 사실 : interface라면 -> class RadioButton:Button -> 괄호X
  • class Secretive private constructor(){} -> 인스턴스화 X

부생성자

  • 생성자를 많이 만들일이 실제로는 없으니까 궁금하면 책 다시 읽기

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

interface User {
    val nickname: String //하위 클래스가 nickname을 얻을 수 있는 방법을 구현해야함
}

class PrivateUser(override val nickname: String) : User //()가 X -> interface 이기 때문

//아래 두 클래스의 구현이 다름을 살펴볼 것 
class SubscribingUser(val email: String) : User {
    override val nickname: String //호출시마다 새로 계산
        get() = email.substringBefore('@') //커스텀 게터
}

class FacebookUser(val accountId: Int) : User {
    //프로퍼티 초기화 식
    //객체 초기화시 계산 데이터를 뒷받침필드에 set & get 하는 형식 -> 객체 생성시 한번만 계산
    override val nickname = getFacebookName(accountId)
}
  • SubscribingUser@nickname : val에 대한 뒷받침 필드 X
  • FacebookUser@nickname : val에 대한 뒷받침 필드 존재
  • Backing Fields란? 세터에서 실제 프로퍼티 값을 읽거나(get) 저장(set)해뒀다가 게터에서 리턴(get)할 때 사용 할 수 있는 field

뒷받침 필드에 접근

class User(val name: String) {
    // address 세터에서 뒷받침하는 필드 접근
    var address: String = "unspecified"
        /**
         * field 가 뒷받침필드임 -> 필드에 접근 가능
         * getter : 읽기 가능
         * setter : 읽기 + 쓰기 모두 제공
         */
        set(value: String) {
            println("""
                Address was changed for $name:
                "$field" -> "$value".""".trimIndent())
            field = value
        }
}

val user = User("Bona")
user.address = "Elsenheimerstrasse 47, 80687 Muenchen" 
//세터 호출

>> Address was changed for Bona:
   "unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".
  • 초기 선언을 "unspecified" 으로 해줬기 때문에 field 사용시 "unspecified" 로 출력

data class, by(위임)

//기본클래스 Client
class Client(val name: String, val postalCode: Int)
  • 기본 클래스에서 아래와 같은 함수를 오버라이드 해야 원하는 결과를 얻을 수 있음
    • toString() : 객체 문자열 표현 오버라이드 해야 Client@5e9f2dsf같은 알아보기 힘든 문자열이 안나옴
    • == + toHashCode()
      • equals(==) 오버라이드시 반드시 toHashCode()를 오버라이드 해야 함 : equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 하기때문 -> 해시 코드가 같아야만 equals에서 실제 값을 비교

위 세가지 함수를 오버라이드 하지 않으려면?

data class 사용

//data class Client -> 위 세 함수 오버라이드 필요 X
data class Client(val name: String, val postalCode: Int)

by : 클래스 위임

  • 상위클래스 변경시 하위클래스의 가정이 깨질 수 있음 -> 때문에 코틀린에서는 final을 사용하여 상속 불가능이 default
    • 상속을 허용하지 않는 클래스 대신 새로운 클래스를 만드는 데코레이터 패턴이 있으나 준비코드가 상담함😩
      => by 키워드를 통해 위임중임을 명시 가능
/**
 * 필요한 함수만 새로 구현(override)하는 위임된 클래스 TestSet
 */
class TestSet<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)
    }
    //오버라이드 하지 않은 그밖에 MutableCollection 함수는 그냥 사용 가능!
}

object : 클래스 선언, 인스턴스 생성

object 키워드를 사용하는 여러 상황

=> 객체 생성시 사용한다는 공통점 존재

  • 객체선언 : 싱글턴 정의(클래스, 인스턴스 선언을 동시에)
    • 하나의 인스턴스 필요시
    • Payroll.allEmployees.add(Person()) : object.프로퍼티(arrayList).프로퍼티함수
    • object도 상속 받기 가능
  • companion object(동반객체) : 팩토리메서드, 정적멤버가 들어가는 장소
    • 코틀린 : 자바의 static 키워드 X
    • 동반객체는 외부클래스(자신을 둘러싼 클래스)의 모든 private 멤버(생성자)에 접근 가능 -> 이 말 뜻은.. private 생성자를 호출하기 좋은 위치란 소리🌝
fun getFacebookName(accountId: Int) = "fb:$accountId"

//User 클래스의 주생성자가 private or protected 가 아니라면 아래와 같이 확장함수로 새로운 팩토리 메서드 선언 가능
//why? 확장함수는 해당 클래스의 protected, private 멤버에 접근 불가
//fun User.Companion.newSecretUser(userId:Int) :User = User(userId.hashCode().toString())

/**
 * 주 생성자를 private 으로 만든다
 */
class User private constructor(var nickname: String) {
    /**
     * 동반 객체로 선언 -> private 생성자 접근 가능
     * 동반객체에 이름 붙이기 가능 : companion Object newUser => User.newUser.newSubscribingUser("bob@gmail.com")
     * 아래는 이름이 없는 동반객체 => User.newSubscribingUser("bob@gmail.com")
     */
    companion object { //:인터페이스 or :클래스()로 상속 가능
        fun newSubscribingUser(email: String) =
            User(email.substringBefore('@')) //@이전것만 set

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

val subscribingUser = User.newSubscribingUser("bob@gmail.com")
println(subscribingUser.nickname) //bob
profile
백엔드 개발자

0개의 댓글