클래스 계층 정의

코틀린 인터페이스

  • 코틀린 인터페이스는 자바 8 인터페이스와 비슷하다.
  • 상위 클래스나 상위 인터페이스에 있는 프로퍼티나 메소드를 오버라이드할 때는 override 변경자를 붙여줘야 된다.
    • 실수로 상위 클래스의 메소드를 오버라이드하는 경우를 방지해준다.
  • 한 클래스에서 두 인터페이스를 함께 구현했을 때, 같은 디폴트 메소드가 있다면 컴파일 오류가 발생한다. 이 경우는 하위 클래스에 직접 구현하게 강제한다.
  • 상위 타입의 메소드 호출하는 방식은 아래를 참고한다.
class Button : Clickable, Focusable {  
   override fun click() {  
      println("I was Clicked")  
   }  
  
   override fun showOff() {  
      super<Clickable>.showOff()  
      super<Focusable>.showOff()  
   }  
}

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

  • 취약한 기반 클래스(fragile base class): 하위 클래스가 기반 클래스에 대해 가졌던 가정이 기반 클래스를 변경함으로써 꺠져버린 경우
  • 어떤 클래스가 자신을 상속하는 방법에 대해 정확한 규칙을 제공하지 않는다면 그 클래스의 클라이언트는 기반 클래스를 작성한 사람의 의도와 다른 방식으로 메소드를 오버라이드할 위험이 있다.
  • 인터페이스는 항상 열려 있으며 final로 변경할 수 없다.
상속 제어 변경자이 변경자가 붙은 멤버는설명
final오버로이드할 수 없음클래스 멤버의 기본 변경자다.
open오버라이드할 수 있음반드시 open을 명시해야 오버라이드할 수 있다.
abstract반드시 오버라이드해야 함추상 클래스의 멤버에만 이 변경자를 붙일 수 있다. 추상 멤버에는 구현이 있으면 안 된다.
override상위 클래스나 상위 인스턴스의 멤버를 오버라이드하는 중오버라이드하는 멤버는 기본적으로 열려있다. 하위 클래스의 오버라이드를 금지하려면 final을 명시해야 한다.

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

  • 어떤 클래스의 기반 타입 목록에 들어있는 타입이나제네릭 클래스의 타입 파라미터에 있는 타입의 가시성은 그 클래스의 가시성과 같거나 더 높아야된다.
  • 메소드의 시그니처에 사용된 모든 타입의 가시성은 그 메소드의 가시성과 같거나 더 높아야 한다.
  • 코틀린의 선언과 그에 해당하는 자바 선언에 차이가 존재한다.
    • 코틀린에서 private 클래스를 자바에서 패키지 전용 클래스로 컴파일한다.
    • 코틀린에서 internal 변경자는 바이트코드상에서는 public이 된다. 따라서, 코틀린에서 접근할 수 없는 대상을 자바에서 접근할 수 있는 경우가 생긴다.
      • 하지만 코틀린 컴파일러가 internal 멤버의 이름을 보기 나쁘게 바꾼다.
    • 코틀린에서 protected는 하위 클래스만 볼 수 있지만, 자바에서는 같은 패키지에 속한 코드는 볼 수 있다.
변경자클래스 멤버최상위 선언
public(기본 가시성임)모든 곳에서 볼 수 있다.모든 곳에서 볼 수 있다.
internal같은 모듈 안에서만 볼 수 있다.같은 모듈 안에서만 볼 수 있다.
protected하위 클래스 안에서만 볼 수 있다.(최상위 선언에 적용할 수 없음)
private같은 클래스 안에서만 볼 수 있다.같은 파일 안에서만 볼 수 있다.

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

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

  • 코틀린에서 바깥쪽 클래스의 인스턴스를 가리키는 참조에 접근하려면 아래와 같이 써야된다.
class Outer {  
   inner class Inner {  
      fun getOuterReference():  Outer = this@Outer  
   }  
}

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

  • 코틀린 컴파일러는 when을 사용해 타입의 값을 겁사할 때 꼭 디폴트 분기인 else 분기르 ㄹ 덧붙이게 강제한다.
    • 디폴트 분기가 있으면 이런 클래스 계층에 새로운 하위 클래스를 추가하더라도 컴파일러가 when이 모든 경우를 처리하는지 제대로 검사할 수 없다.
  • sealed 클래스는 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다.
  • sealed 클래스의 하위 클래스를 정의할 때는 반드시 같은 파일에서 정의를 해야된다.
  • when식에서 sealed클래스의 모든 하위 클래스를 처리한다면 디폴트 분기가 필요없다.
  • sealed 클래스에 속한 값에 대해 디폴트 분기를 사용하지 않고 when 식을 사용하면 나중에 sealed 클래스의 상속 계층에 새로운 하위 클래스를 추가해도 when 식이 컴파일되지 않는다.
  • sealed 클래스는 2가지 가시성 변경자의 생성자를 가질 수 있다.
    • protected(기본값), private
sealed class Expr {  
   class Num(val value: Int) : Expr()  
   class Sum(val left: Expr, val right: Expr) : Expr()  
}

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

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

  • 주 생성자: 생성자 파라미터를 지정하고 그 생성자 파라미터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적에 쓰인다.
  • constructor: 주 생성자나 부생성자 정의를 시작할 때 사용하는 키워드
    • 제한적이기 떄문에 별도의 코드를 포함할 수 없으므로 초기화 블록이 필요하다.
    • 주 생성자 앞에 별다른 애노테이션이나 가시성 변경자가 없다면 constructor를 생략해도 된다.
  • init: 초기화 블록을 시작하는 키워드
    • 주 생성자와 함께 사용된다.
class User constructor(_nickname: String) {  
   val nickname: String  
   init {  
      nickname = _nickname  
   }  
}
  • 주 생성자의 파라미터로 프로퍼티를 초기화 한다면 그 주 생성자 파라미터 이름 앞에 val를 추가하는 방식으로 프로퍼티 정의와 초기화를 간략히 쓸 수 있다.
  • 생성자 파라미터에도 디폴트 값을 정의할 수 있다.
    • 모든 생성자 파라미터에 디폴트 값을 지정하면 컴파일러가 자동으로 파라미터가 없는 생성자를 만들어준다.
class User2 (val nickname: String, val isSubscribed: Boolean = true)
  • 클래스에 기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출해야 할 필요가 있다.
    • 인터페이스는 생성자가 없기 때문에 어떤 클래스가 인터페이스를 구현하는 경우 그 클래스의 상위 클래스 목록에 있는 인터페이스 이름 뒤에는 아무 괄호도 없다.
class TwitterUser(nickname:String) : User(nickname)
  • 클래스를 정의할 때 별도로 생성자를 정의하지 않으면 컴파일러가 자동으로 인자가 없는 디폴트 생성자를 만들어준다.
open class Button 

class RadioButton: Button()
  • 클래스 외부에서 인스턴스화하지 못하게 막고 싶다면 모든 생성자를 private으로 만들면 된다.
class Secretive private constructor()

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

  • 인자에 대한 디폴트 값을 제공하기 위해 부 생성자를 여럿 만들지 말라. 대신 파라미터의 디폴트 값을 생성자 시그니처에 직접 명시하라.
  • 부 생성자는 super() 키워드를 통해 자신에게 대응하는 상위 클래스 생성자를 호출한다.
  • 부 생성자는 this() 키워드를 통해 자신의 다른 생성자를 호출할 수 있다.
open class View {  
   constructor(ctx: String) {}  
   constructor(ctx: String, attr: String) {}  
}  
  
class MyButton: View {  
   constructor(ctx: String) : this(ctx, "abc") {}  
   constructor(ctx: String, attr: String): super(ctx, attr)  
}

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

  • 인터페이스에 추상 프로퍼티 선언을 넣을 수 있다.
  • 인터페이스에 있는 프로퍼티 선언에는 뒷받침하는 필드나 게더 등의 정보가 들어있지 않다.
    • 인터페이스를 구현한 하위 클래스에서 상태 저장을 위한 프로퍼티 등을 만들어야 한다.
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: String = getFacebookName(accountId)  
        
   private fun getFacebookName(accountId: Int) : String {  
      return accountId.toString()  
   }  
}
  • 인터페이스에는 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수도 있다.
    • 게터와 세터에는 뒷받침하는 필드를 참조할 수 없다.
interface User2 {  
   val email: String  
   val nickname: String  
      get() = email.substringBefore('@')  
}

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

  • 접근자 본문에서는 field라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다.
    • 게터에서는 field 값을 읽을 수만 있고, 세터에서는 field 값을 읽거나 쓸 수 있다.
  • 뒷받침하는 필드가 있는 프로퍼티와 그런 필드가 없는 프로퍼티의 차이
    • 게터나 세터에 field를 사용하는 프로퍼티에 대해 뒷받침하는 필드를 생성해준다.
    • field를 사용하지 않는 커스텀 접근자 구현을 정의하면 뒷받침하는 필드는 존재하지 않는다.

접근자의 가시성 변경

  • 접근자의 가시성은 기본적으로는 프로퍼티의 가시성과 같다.
  • 원한다면 get이나 set 앞에 가시성 변경자를 추가해서 접근자의 가시성을 변경할 수 있다.
class LengthCounter {  
   var counter: Int = 0  
      private set  
   fun addWord(word: String) {  
      counter += word.length  
   }  
}

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

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

  • toString(): 인스턴스의 문자열 표현을 얻을 방법을 제공한다. 주로 디버깅과 로깅 시 이 메소드를 사용한다.
  • equals(): 객체의 동등성을 만족하는 조건을 제공한다.
    • 코틀린에서는 == 연산자는 내부적으로 equals를 호출해서 객체를 비교한다. 따라서 클래스가 equals를 오버라이드하면 ==를 통해 안전하게 동등성을 비교할 수 있다.
    • 코틀린에서 객체의 참조를 비교할 때는 === 연산자를 사용할 수 있다.
    • hashCode 정의를 빠뜨리면 제대로 작동하지 않는다.
  • hashCode(): JVM 언어에서는 equals()true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 한다는 제약이 있다.
    • HashSet은 원소를 비교할 때 비용을 줄이기 위해 객체의 해시 코드를 비교하고 같은 경우에만 실제 값을 비교한다.

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

  • 코틀린에서는 data라는 변경자를 클래스 앞에 붙이면 아래 메소드를 컴파일러가 자동으로 만들어준다.
    • equals
    • hashCode
    • toString
  • 불변 클래스
    • HashMap 등의 컨테이너에 키의 프로ㅓ퍼티를 변경하면 컨테이너 상태가 잘못될 수 있다.
    • 불변 객체를 주로 사용하는 프로그램에서는 스레드가 사용 중인 데이터를 다른 스레드가 변경할 수 없으므로 불변 객체를 사용하면 스레드를 동기화해야 할 필요가 줄어든다.
  • copy(): 불변 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 메소드
    • 데이터 클래스에서 편의 메소드로 제공한다.

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

  • 하위 클래스가 상위 클래스의 일부를 오버라이드 하면, 상위 클래스에 대해 갖고 있던 가정이 깨져서 코드가 정상적으로 작동하지 못하는 경우가 생길 수 있다.
    • 그래서 코틀린은 기본적으로 클래스를 final로 취급하기로 결정했다.
  • 하지만 종종 상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때가 있는데, 사용하는 하는 일반적인 방법이 데코레이터 패턴이다.
    • 데코레이터 패턴의 단점은 준비 코드가 상당히 많이 필요하다는 점이다.
  • 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(elements: Collection<T>): Boolean {  
      objectsAdded += elements.size  
      return innerSet.addAll(elements)  
   }  
}

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

  • 객체 선언(object declaration): 싱글턴을 정의하는 방법
  • 동반 객체(companion object): 인스턴스 메소드는 아니지만 어떤 클래스와 관련이 있는 메소드와 팩토리 메소드를 담을 때 쓰인다.
  • 객체 식: 자바의 무명 내부 클래스(anonymouse inner class)

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

  • object 키워드를 붙이면 된다.
object Payroll {
	val allEmployees = arrayListOf<Person> ()
	fun calculateSalary () {
		for (person in allEmployees) {
			...
		}
	}
}

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

  • 코틀린 클래스 안에는 정적인 멤버가 없다.
  • 패키지 수준의 최상위 함수의 경우 private으로 표시된 비공개 멤버에 접근할 수 없다.
  • companion 키워드를 사용하면 해당 클래스의 동반 객체를 만들 수 있다. 동반 객체는 이름을 따로 지정할 필요가 없다.
  • 동반 객체는 private 생성자를 호출하기 좋은 위치다.
    • 팩토리 패턴을 구현하기 가장 적합한 위치다.

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

  • 동반 객체는 클래스 안에 정의된 일반 객체다.
  • 동반 객체도 이름을 지정할 수 있다.
class Member private constructor(val id: Long, val name: String) {  
  
   companion object Factory {  
      private var nextId: Long = 1L;  
      fun of(name: String): Member {  
         return Member(nextId++, name)  
      }  
   }  
}
  • 동반 객체도 인터페이스를 구현할 수 있다.
class Member private constructor(val id: Long, val name: String) {  
  
   companion object : Factory<Member> {  
        
      private var nextId: Long = 1L;  
        
      override fun of(name: String): Member {  
         return Member(nextId++, name)  
      }  
   }  
}
  • 동반 객체는 클래스에 정의된 인스턴스를 가리키는 정적 필드로 컴파일 된다.
    • 동반 객체에 이름을 붙이지 않았다면 자바 쪽에서 Companion이라는 이름으로 그 참조에 접근할 수 있다.
    • 자바에서 사용하기 위해 정적인 멤버로 만들어야 되면 @JvmStatic 애노테이션을 코틀린 멤버에 붙이면 된다.
    • @JvmField 애노테이션을 최상위 프로퍼티나 객체에 선언된 프로퍼티 앞에 붙인다.
  • 동반 객체에 대해 새로운 메소드를 다른 곳에서 정의할 수 있다.
fun Member.Companion.from(name: String): Member {  
   return Member.of(name)  
}

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

  • 무명 객체를 정의할 때도 object 키워드를 쓴다.

  • 자바의 무명 클래스와 같이 객체 식 안의 코드는 그 식이 포함된 함수의 변수에 접근할 수 있다.