본문 바로가기
프로그래밍/코틀린

코틀린 스터디 - 4장.클래스, 객체, 인터페이스

by 직장인 투자자 2021. 12. 8.

내용

  • 클래스, 인터페이스
    • 인터페이스에 추상 프로퍼티 선언 가능
    • 기본 접근자 final + public
    • 중첩 클래스는 기본적으로 내부 클래스 아님
  • 뻔하지 않은 생성자, 프로퍼티
    • 짧은 주 생성자 구문으로도 거의 모든 경우 커버 가능
  • 데이터 클래스
    • 클래스를 data로 선언, 컴파일러가 일부 표준 메서드 자동생성
  • 클래스 위임
    • 위임을 처리하기 위한 준비 메소드 자동생성
  • object 키워드 사용
    • 싱글톤 클래스, 동반 객체, 객체 식(자바의 무명 클래스) 표현 시 사용

4.1 클래스 계층 정의

4.1.1 코틀린 인터페이스

  • 추상메서드 , 구현이 있는 메소드 정의 가능
  • 상태(필드) 정의 불가 
- 자바에서는 extends와 implements 키워드를 사용, 코틀린에서는 클래스 이름 뒤에 `콜론(:)` 사용
  - 자바와 마찬가지로 클래스는 인터페이스는 원하는 만큼 개수 제한없이 구현 가능, 클래스는 하나만 확장 가능
- 자바 `@Override`와 비슷한 `override` 변경자 사용
  - 상위 클래스, 인터페이스 메소드 재정의 표시
  - 자바와 달리 override 변경자를 꼭 사용해야함
  - 실수로 재정의하는 경우를 방지, override 없이 시그니처가 같은 메소드 재정의 시 컴파일 오류발생
    - > 'click' hides member of supertype 'Clickable' and needs 'override' modifier

interface Focusable {
    fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus.")
    fun showOff() = println("I'm focusable!") 
}
  • 한 클래스에서 Clickable와 Focusable 이 두 인터페이스를 구현하면 모두 showOff 메서드 있기 때문에 오버라이드 메서드를 직접 제공하지 않으면 컴파일 오류 발생
    • Class 'Button' must override public open fun showOff(): Unit defined in Clickable because it inherits multiple interface methods of it
    • 코틀린은 자바 6과 호환되게 설계되어 일반 인터페이스와 디폴트 메서드 구현이 정적 메서드로 들어있는 클래스를 조합해 구현
      자바에서 디폴트 메서드가 포함된 코틀린 인터페이스를 구현할 수 없고, 혹 구현하고 싶다면 모든 메서드에 대해 본문을 작성해야 한다.

4.1.2 open, final, abstract 변경자 : 기본적으로 final

  • 자바는 final로 명시적으로 상속 금지
  • 취약한 기반 클래스(fragile base class)
    • 하위 클래스가 기반 클래스가 변경되어 영향받는 문제
    • 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메서드를 오버라이드 할 위험이 존재
  • Effective Java - 상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라
    • 특별히 하위 클래스에서 오버라이드 하게 의도된 클래스, 메서드가 아니면 final로 만들어라
  • 코틀린은 클래스와 메서드는 기본적으로 final
  • 어떤 클래스의 상속을 허용하려면 클래스 앞에 open 변경자 추가
  • 메서드, 프로퍼티 오버라이드 허용 시 open 변경자 추가
  • 클래스의 기본적인 상속 가능상태가 final로 얻을 수 있는 큰 이익은 다양한 경우에 스마트 캐스트가 가능하다는 점
    클래스 프로퍼티의 경우 이는 val 이면서 커스텀 접근자가 없는 경우에만 스마트 캐스트를 쓸 수 있다.
    프로퍼티는 기본적으로 final 이기 때문에 대부분의 프로퍼티를 스마트 캐스트에 활용할 수 있다.
  • abstract 클래스 선언 가능
    • 추상 클래스는 인스턴스화 불가
    • 추상 클래스는 기본이 open
      abstract class Animated {
        abstract fun animate()
        open fun stopAnimating() {} // 비추상함수는 기본적으로 final, open 키워드로 명시해야 오버라이드 가능
        fun animateTwice() // final
      }
  • 인터페이스 멤버는 final, open, abstract 키워드 사용 X, 항상 open

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

  • 자바의 접근 제한자와 비슷
  • 기본 가시성은 public
  • 자바의 기본 가시성인 package-private 은 코틀린에 없고, 패키지를 네임스페이스를 관리하기 위한 용도로만 사용
    • 대신 새로운 가시성 internal 사용, 같은 모듈 내부에만 볼 수 있음이라는
    • 모듈은 한 번에 한꺼번에 컴파일되는 코틀린 파일 집합 의미
    • 인텔리 j, 이클립스, 메이븐, 그 레들 등의 프로젝트가 모듈이 될 수 있음
    • 모듈 내부 가시성은 진정한 캡슐화 제공
  • protected
    • 자바 : 패키지 안에서 접근 가능
    • 코틀린 : 상속한 클래스 안에서만 접근 가능
    • 클래스를 확장한 함수는 그 클래스의 private 나 protected 멤버에 접근 불가
  • 자바에서 클래스는 private 접근제한자를 만들 수 없으므로 코틀린 private 클래스를 패키지-전용 클래스 컴파일
  • internal 는 바이트코드 상 public , 다만 internal 멤버의 이름을 보기 나쁘게 바꾼다는(mangle) 사실을 기억
    • 모듈밖에서 오버라이드하는 경우 와 사용하는 경우를 방지하기 위해

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

  • 자바와 차이 : inner 키워드 없이는 바깥쪽 클래스 인스턴스에 접근불가
  • interface State: Serializable

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

- sealed class
  - 상위 클래스를 상속한 하위 클래스 정의를 제한
  - sealed 클래스의 하위 클래스를 정의할 때는 반드시 상위 클래스 안에 중첩
  - 봉인된 클래스는 클래스 외부에 자신을 상속한 클래스를 둘 수 없다.
  - 기본 가시성변경자는 open

sealed class Expr { // 기반 클래스를 sealed 로 봉인
    class Num(val value: Int) : Expr() 
    class Sum(val left: Expr, val right: Expr) : Expr()
}
  • When 타입 체크 사용 시 else (자바 default) 분기 필요 없어진다.
  • sealed 인터페이스는 자바 쪽에서 구현하지 못해 없음

코틀린 1.1부터 봉인된 클래스와 같은 파일에 하위 클래스 생성 가능, 데이터 클래스로 하위 클래스 정의 가능

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

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

  • init : 초기화 블록, 인스턴스화 될 때 실행
  • 주 생성자는 별도 코드를 포함할 수 없으므로, 초기화 블록 필요
    // 클래스 이름 뒤에 오는 괄호로 둘러싸인 코드가 주 생성자(primary constructor)
    class User(val nickname: String)
    
- 위 3개 선언은 동일 맨 첫번째가 가장 간결한 선언

class User(val nickname: String, val isSubscribed: Boolean = true)

>>> val hyun = User("현석")
>>> println(hyun.isSubscribed)
 true
>>> val gye = User("계영", false) // 순서지정
>>> println(gye.isSubscribed)
 false
>>> val hey = User("혜원", isSubscribed=false) // 이름지정
>>> println(hey.isSubscribed)
 false
  • 기반 클래스의 생성자 호출 지정
    open class User(val nickname: String) { /* ... */ }
    class TwitterUser(nickname: String) : User(nickname) { /* ... */ }
  • 기반 클래스를 상속한 하위 클래스는 반드시 기반 클래스의 생성자를 호출해야 한다.
    open class Button // 인자가 없는 디폴트 생성자가 만들어짐
    class RadioButton: Button()
  • 인터페이스 구현인지 클래스 상속인지 구분은 괄호로 구분하면 된다.

클래스 외부에서 인스턴스화 막기

class Secretive private constructor() {} // 이 클래스의 유일한 주 생성자는 비공개다.

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

  • 인자에 대한 디폴트 값을 제공하기 위해 부 생성자를 여럿 만들지 말자
    open class View {
      constructor(ctx: Context) {  // 부생성자
          // ...
      }
      constructor(ctx: Context, attr: AttributeSet) { // 부생성자
          // ...
      }
    }
    
- `this()` 통해 클래스 자신의 생성자 호출 가능

class MyButton : View {
    constructor(ctx: Context): this(ctx, MY_STYLE) { // 다른 생성자에게 위임
        // ...
    }
    constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
        // ...
    }
}
  • 모든 부 생성자는 최종적으로 상위 클래스의 생성자를 호출해야 한다.

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

  • 인터페이스에 추상 프로퍼티 선언 가능
    //  User 인터페이스를 구현하는 클래스가 nickname의 값을 얻을 수 있는 방법을 제공해야한다는 뜻
    interface User {
      val nickname: String
    }
    
- 인터페이스에는 추상 프로퍼티뿐 아니라 게터/세터가 있는 프로퍼티도 선언 가능

interface User {
    val email: String
    val nickname: String
        get() = email.substringBefore('@') // 프로퍼티에 뒷받침하는 필드 없음, 대신 매번 결과를 계산
}

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 // 뒷받침하는 필드 값 변경
        }
}
// val user = User("Alice")
// user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
// >>> Address was changed for Alice:
// "unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".

4.2.5 접근자의 가시성 변경

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

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

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

  • 자바와 마찬가지로 동등성 비교를 위해 사용
  • == 연산자는 자바와 달리, 객체의 동등성(equals을 호출)을 검사
  • 동일성(주소 값)을 확인하고 싶으면 === 사용
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean { // Any는 java.lang.Object에 대응하는 모든 클래스의 최상 클래스
    if (other == null || other !is Client)
        return false
    return name == other.name &&
        postalCode == other.postalCode
}
}
- `hashCode()`
  - Hash를 사용하는 Collections 등에서는 equals()가 true를 반환하는 두 객체는 반드시 같은 `hashCode()`를 반환해야 한다.

class Client(val name: String, val postalCode: Int) {
    // ...
    override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}

4.3.2 데이터 클래스: 모든 클래스가 정의해야 하는 메서드 자동 생성

  • data 변경자, lombok @Data 느낌
  • toString, equals, hashcode 자동생성
    data class Client(val name: String, val postalCode: Int)
  • 데이터 클래스와 불변성 - copy()
    • 데이터 클래스의 프로퍼티는 val 필요는 없음, 하지만 val로 선언해서 불변 클래스 생성을 권장
    • 프로퍼티 값 변경은 copy로 객체를 복사하자

data 선언 시 아래와 같이 copy 가 구현된다고 보면 된다.

fun copy(name: String = this.name, postalCode: Int = this.postalCode) = Client(name, postalCode)

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

  • 상속을 허용하지 않는 클래스에 새로운 동작을 추가
  • 일반적인 방법인 데코레이터 패턴을 by 키워드를 이용해서 자동생성
    class DelegatingCollection<T> (
      innerList: Collection<T> = ArrayList<T>()
    ) : Collection<T> by innerList<> {}

필요하다면 오버라이드 메서드 생성 가능

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

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

  • 싱글톤 패턴을 object 선언 기능을 통해 기본 지원
    //... val allEmployees = arrayListOf<Person>()
      fun calculateSalary() {
          for (person in allEmployees) {
              // ...
          }
      }
    }
    

Payroll.allEmployees.add(Person(/...))
Payroll.calculateSalary()

- 클래스, 인터페이스 상속가능

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

클래스 안에 object 선언 가능

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

4.4.2 동반 객체: 팩토리 메서드와 정적 멤버가 들어갈 장소

  • 정적 멤버 없고, 자바 static 키워드 지원 X
  • 패키지 수준의 최상위 함수와 object 선언을 활용
  • 대신 companion object 키워드를 사용해서 동반 객체를 사용할 수 있고, static method 호출처럼 사용할 수 있다.
    class A {
      companion object {
          fun bar() {
              println("Companion called")
          }
      }
    }
    >>> A.bar()
    Companion called

동반 객체는 자신을 둘러싼 클래스의 모든 private 멤버에 접근 가능, 이에 팩토리 패턴을 구현하기 적합

class User private constructor(val nickname: String) { // 주 생성자 비공개
  companion object {
      fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
      fun newFacebookUser(accountId: Int) = User(getFacebookName(facebookAccountId))
  }
}

4.4.3 동반 객체를 일반 객체처럼 사용

  • 동반 객체는 클래스 안에 정의된 일반 객체다.
    class Person(val name: String) {
       companion object Loader { // 동반 객체에 이름지정
            // ...
       }
    }
  • 동반 객체에서 인터페이스 구현
    interface JSONFactory(T) {
      fun fromJSON(jsonText: string): T
    }
    
- 동반 객체는 자바에서 정적필드로 컴파일
- `@JvmStatic` 옵션을 사용하면, 자바 정적멤버로 생성
- 동반 객체 확장 함수 생성 방법은 아래와 같다.

class Person(val name: String) {
     companion object { // 필히 비어있는 동반객체 선언
     }
}

fun Person.Companion.fromJSON(json: String): Person { // 확장 함수 선언
    // ...
}

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

  • 무명 객체를 정의할 때도 object 키워드 사용
    //... object: MouseAdapter() { // MouseAdapter 확장하는 무명객체 선언
          override fun mouseClicked(e: MouseEvent) {
              // ...
          }
          override fun mouseEntered(e: MouseEvent) {
              // ...
          }
      }
    )
    
- 자바 무명 클래스와 달리 여러 인터페이스 구현, 클래스 확장 가능
- 무명 객체는 객첵 식 쓰일때 마다 새로운 인스턴스 생성
- 자바와 달리 final 이 아닌 변수를 객체 식 안에서 사용가능

fun countClicks(window: Window) {
    var clickCount = 0 // 로컬변수
    window.addMouseListener(object: MouseAdapter() {
        override fun mouseClicked(e: MouseEvent) {
            clickCount++
        }
    }
}
반응형

댓글