본문 바로가기
코틀린

Part 4 - 클래스, 객체, 인터페이스

by N.Damgom 2021. 9. 5.

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

  • 코틀린의 클래스와 인페이스는 자바와는 약간 다르다.
  • 인터페이스도 프로퍼티를 가질 수 있으며, 기본적으로 final과 public이다.
  • 중첩 클래스는 외부 참조를 가지지 않는다.
  • 롬복 @Data를 네이티브하게 구현할 수 있다
  • object는 클래스와 인스턴스를 선언과 동시에 생성하는 키워드이다.

4.1 클래스 계층 정의

4.1.1 인터페이스

  • 코틀린 인터페이스는 자바 인터페이스와 비슷하다.

  • 자바8처럼 구현체가 있는 메서드를 가질 수 있다.

  • interface Clickable{
        fun click()
      fun showOff() = println("I`m clickable!")
    }
    interface Focusable{
      fun showOff() = println("I`m Focusable!")
    }
    
    class Button : Clickable, Focusable{
      override fun click() = println("I was Clicked")
      //default 메서드가 여러개 있는경우 반드시 정의 필요하다. (ambiguous 문제)
      ovveride fun showOff() {
        super<Clickable>.showOff()
        super<Focusable>.showOff()
      }
    }
  • 자바에서의 extend 와 implements 모두 콜론(:) 으로 대체한다.

  • 다중 상속 불가, 다중 인터페이스 가능

  • override 변경자는 필수

  • 자바에서 코틀린 인터페이스를 쓴다면 디폴트 메서드의 구현 부분은 없는 셈 쳐야한다.(java6과 호환되므로)

4.1.2 open, final, abstract 변경자

  • 모든 하위 클래스는 상속받은 기반(base) 클래스에 의존하므로, 변경에 취약하다.

  • 그러므로 코틀린은 기본적으로 final 이다. non-final하게 만들고 싶다면 open 키워드를 사용한다.

  • open class RichButton : Clickable{ // 다른 클래스가 RichButton을 상속할 수 있다.
        fun disable(){} // final
        open fun animate() // non final
        override fun click() // override는 non final
      final override fun click() // open 상태인 override를 final 로 막을 수 있다.
    }
  • 기본으로 final 을 쓰면 스마트캐스트를 사용할 수 있다.

  • 추상클래스, 추상 멤버는 당연히 상속받아서 사용해야하기 때문에 open을 명시할 필요 없다.

  • 추상클래스에 속했더라도 비추상함수는 기본적으로 final이다.(open 사용 가능)

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

  • 아무 변경자도 없는 경우 public

  • 코틀린의 가시성 변경자(visibility modifier)는 패키지와는 무관하다.

  • internal : 같은 모듈 안에서만 볼 수 있다

  • protected: 하위 클래스에서만 볼 수 있다. (클래스 의존적이므로 최상위 선언은 불가)

  • private: 같은 클래스에서만 볼 수 있거나 같은 파일 안에서만 볼 수 있다.

  • internal open class TalkativeButton : Focusable{
      private fun yell() = ...
      protected fun whisper() = ...
    }
    
    fun TalkativeButton.giveSpeech(){// 오류: giveSpeech()는 public이나 TalkativeButton은 internal임
      yell() // private 접근 오류
      whisper() // protected 접근 오류
    }
  • 확장함수는 protected에 접근할 수 없다는 것을 유의하자.

4.1.4 중첩 클래스는 기본적으로 static

  • nested 클래스는 외부 참조가 없다.

  • 이펙티브 자바 Item24 멤버클래스는 static으로 만들어라

    메모리문제, GC 문제 생길 수 있다.

  • 바깥 참조를 포함시키고 싶다면 inner 변경자를 nested 클래스에 붙여야한다.

  • this@Outer로 바깥 참조에 접근할 수 있다.

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

  • 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)
        //default 분기가 필요없다.
      }
  • sealed 인터페이스는 자바와의 호환 문제로 만들 수 없다.

  • sealed 클래스와 같은 파일의 아무데서나 sealed 클래스를 상속한 하위 클래스를 만들 수 있다.

4.2 생성자와 프로피티를 갖는 클래스

  • 코틀린은 주 생성자와 부 생성자를 구분한다.
  • 초기화 블록으로 초기화 로직을 추가할 수 있다.

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

  • 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드를 주 생성자라고 부른다.

  • 주 생성자는 파라미터를 지정하고 그 생성자에 의해 초기화되는 프로피터를 정의하는 두 목적으로 사용된다.

  • class User constructor(_nickname: String){ // 파라미터 하나만 있는 주 생성자, 애노테이션이나 가시성 변경자 없다면 생략가능
        val nickname = String
        init {
            nickname = _nickname // 초기화 블록
        }
    }
    
    class User(val nickname: String,
               val isSubscribed: Boolean = true)
    
    val hyun = User("현석")
    val gye = User("계영", false)
  • constructor: 주 생성자나 부 생성자 정의를 위해 사용한다.

  • init: 인스턴스 생성시 실행되는 초기화 코드, 로직이 없는 주 생성자를 보완한다.

  • 모든 생성자 파라미터에 디폴트 값을 지정하면 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어준다.

  • class TwitterUser(nickname : String) : User(nickname){...}
    open class Button // 인자가 없는 디폴트 생성자가 만들어진다.
    class RadioButton : Button(){} // 상속받으면서 디폴트 생성자 호출
    class Secretive private constructor(){} // 유일한 주 생성자는 private
  • 주 생성자에서 기반 클래스의 생성자를 호출해야 한다.

  • 상속 시 기반 클래스 뒤에는 괄호가 들어간다 (기반 클래스의 생성자) -> 인터페이스와 구별 가능

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

  • class MyButton : View {
      constructor(ctx : Context)
        : this(ctx, MY_STYLE){
          ...
        }
      }
      constructor(ctx : Context, attr: AttributeSet)
        : super(ctx, attr){
    
      }
    }
  • 클래스 바디 내부에서 constructor 키워드를 사용해서 주 생성자 없이 부생성자만 여러개 생성할 수 있다.

  • this()를 이용해서 자신의 다른 생성자를 호출할 수 있다.

  • 부생성자는 자바와의 상호운용성, 파라미터 목록이 다른 생성 방법이 필요할 때 사용한다.

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

  • interface User {
      val nickname: String
    }
    
    interface User2{
      val email: String // 반드시 구현해야 함
      val nickname: String
        get() = email.substringBefore('@') // 프로퍼티 뒷받침 필드가 없는 경우 게터와 세터가 있는 프로퍼티 선언 가능(상속가능)
    }
    
    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) // 프로퍼티 초기화 식(한번만 계산)
    }
  • User 인터페이스를 구현하는 클래스는 nickname의 값을 얻을 수 있는 방법을 제공해야한다.

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

  • class User(val name: String) {  var address: String = "unspecified"   set(value: String){     println(""":       Address was changed for $name:       "$field" -> $value".""".trimIndent())     field = value     )   }}
  • field라는 식별자를 이용해서 뒷받침 필드에 접근할 수 있다.

  • 게터에서는 field 값을 읽을 수만 있고 세터에서는 읽거나 쓸 수 있다.

  • field를 사용하지 않는 커스텀 접근자 구현을 정의하면 뒷받침하는 필드는 존재하지 않는다.(당연한 얘기)

4.2.5 접근자의 가시성 변경

  • 접근자의 가시성은 기본적으로 프로퍼티의 가시성과 같다.

  • get이나 set 앞에 변경자를 추가해서 가시성을 변경할 수 있다.

  • class LengthCounter{    var counter: Int = 0    private set // public getter는 컴파일러가 생성한다.  fun addWord(word: String){    counter += word.length  }}
  • lateinit 변경자 : 프로퍼티를 생성자가 호출된 다음 초기화한다.

  • lazy initilized : 프로퍼티 초기화를 요청이 들어올 때까지 미룬다.

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

  • Constructor나 getter/setter를 코틀린 컴파일러가 만들어준 것처럼 다른 필수 메소드도 컴파일러가 만들어준다.

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

  • toString(), equals(), hashCode() 오버라이드 필요하다.
  • 참고)JVM 언어 규칙으로 equals() true 이면 같은 hashCode()를 반환해야한다.
  • 코틀린은 내부적으로 ==를 사용할 때 equals를 호출해서 객체를 비교한다.
  • 참조 비교를 위해서는 ===를 사용한다.

4.3.2 데이터 클래스: 필요한 메서드 자동 완성 (^▽^)

  • data class Client(val name: String, val postalCode: Int)
  • data 키워드를 클래스 앞에 붙여줌으로써 필요한 메서드를 기본적으로 만들어준다.

  • 데이터 클래스는 불변 클래스가 권장된다.(다중 스레드, 해쉬맵 키로 사용 등)

  • copy() 메서드: 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있다.

  • //copy를 직접 정의하는 경우fun copy(name: String = this.name,        postalCode: Int = this.postalCode) = Client(name, postalCode)

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

  • 상속을 우회하는 방법으로 데코레이터 패턴을 사용한다.

  • //상속을 허용하지 않는 경우class Car{  fun drive() = ...}class SuperCar(val car: Car){  fun drive(){    car.drive()  }  fun boost(){    ...  }}//상속할 수 있는 경우class DelegatingCollection<T>: Collection<T>{  private val innerList = arrayListOf<T>()    override fun size: Int get() = innerList.size()  ...다른 모든 컬렉션 메서드들}
  • 데코레이터를 만들 때 상위 클래스의 메서드를 구현해야하므로 코드가 번잡하다.

  • 코틀린은 인터페이스를 구현할 때 by 키워드로 인터페이스에 대한 구현을 다른 객체에 위임 중이라는 것을 명시할 수 있다.

  • class CountingSet<T>(  innerSet: MutableCollection<T> = Hashset<T>()): MutableCollection<T> by innerSet { // by 키워드로 위임을 명시한다.    var objectsAdded = 0    //특정 메서드를 오버라이드 할 경우  //컴파일러가 생성한 메서드 대신 오버라이드 메서드가 쓰인다.  override fun add(element: T): Boolean{    objectsAdded++    return innerSet.add(element)  }}
  • CountingSet 과 MutableCollection 구현 간 의존관계가 생기지 않는다.

  • CountingSet은 오버라이드하더라도 Collection api 구현은 하지 않는다.

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

  • object 키워드는 클래스를 정의하는 동시에 인스턴스를 생성한다.
  • 싱글턴, 동반 객체(companion object), 무명 내부 클래스 대신 쓰인다.

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

  • 싱글턴을 객체 선언으로 구현할 수 있다.

  • 객체 선언: 클래스 선언 + 그 클래스에 속한 단일 인스턴스 선언

  • object Payroll{ // 객체 선언은 object 키워드로 시작한다.  val allEmployees = arrayListOf<Person>()  fun calculateSalary(){    for(person in allEmployees){      ...    }  }}
  • 프로퍼티, 메소드, 초기화 블록을 사용할 수 있으나 생성자는 사용할 수 없다.

  • 객체 선언도 클래스나 인터페이스를 상속할 수 있다.

  • Stateless 객체 구현에 편리하다.

  • object CaseInsensitiveFileComparator: Comparator<File> {  override fun compare(file1: File, file2: File): Int{    return file1.path.compareTo(file2.path, ignoreCase = true)  }}>>println(CaseInsensitiveFileComparator.compare(file1, file2))
  • 클래스 안에서 object를 선언할 수도 있다. (static한 특성이지, composite 관계가 아니다.)

  • data class Person(val name: String){  object NameComparator : Comparator<Person>{    override fun compare(p1: Person, p2: Person): Int =      p1.name.compareTo(p2.name)  }}

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

  • 코틀린은 static 대신 최상위 함수와 객체 선언을 사용한다.

  • 클래스 안에 선언된 object 중 하나에 companion 이라는 키워드를 붙이면 그 객체는 동반 객체가 된다.

  • 객체의 이름을 따로 지정할 필요 없이 클래스 이름을 사용한다 -> static이랑 같다.

  • 따라서 static한 팩토리 대신 companion object 안에 팩토리 메서드를 선언하면 된다.

  • class User private constructor(val nickname: String){  companion object{    fun newSubscribingUser(email: String) = User(email.substringBefore('@'))    fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))  }}
  • 동반 객체 또한 일반 객체이다. 이름을 붙이거나, 상속 받거나, 동반 객체 안에 확장함수와 프로퍼티를 정의할 수 있다.

  • 이름이 없을 경우 Companion이라는 이름으로 참조할 수 있다.

  • 확장 static 메서드를 만들 때도 companion object를 쓰면 된다

  • class Person(...){  ...    companion object{    //비어있는 companion object  }}fun Person.Companion.fromJSON(json: String): Person{  ...}val p = Person.fromJSON(json)

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

  • 무명객체 : 특정 코드에서만 사용하는 상속받은 객체(ex. Runnable)

  • window.addMouseListener{  object: MouseAdapter(){    override fun mouseClicked(e: MouseEvent){      ...    }    override fun mouseEntered(e: MouseEvent){      ...    }  }}val listener = object: MouseAdapter(){    override fun mouseClicked(e: MouseEvent){      ...    }    override fun mouseEntered(e: MouseEvent){      ...    }  }
  • 무명 객체는 싱글턴이 아니다.

  • 로컬 변수가 effective final 일 필요는 없다.

'코틀린' 카테고리의 다른 글

Part 5 - 람다로 프로그래밍  (0) 2021.09.09
Part 3 - 함수 정의와 호출  (0) 2021.08.29
Part 2 - 코틀린 기초  (0) 2021.08.20
Part 1 - 코틀린 소개  (0) 2021.08.01