📖 4.1 클래스 계층 정의
🔖 4.1.1 코틀린 인터페이스
- 코틀린 인터페이스 안에는 추상 메서드뿐 아니라 구현이 있는 메서드도 정의할 수 있다.
- 다만, 인터페이스에는 아무런 상태도 들어갈 수 없다.
interface Clickable {
fun click()
}
class Button : Clickable {
override fun click() = println("I was clicked")
}
fun main() {
Button().click()
}
- 상속이나 composition에서 모두 클래스 이름 뒤에 콜론을 붙이고 인터페이스나 클래스 이름을 적는 방식을 사용한다.
- 클래스는 인터페이스를 원하는 개수 제한 없이 마음대로 구현할 수 있지만 클래스는 오직 하나만 확장할 수 있다.
- 오버라이드를 할 때 override 변경자를 꼭 사용해야 한다.
- 상위 클래스에 있는 메서드 오버라이딩 방지
interface Clickable {
fun click()
fun showOff() = println("I'm clickable!")
}
- 디폴트 구현 가능
interface Focusable {
fun setFocus(b: Boolean) = println("I ${if (b) "got" else "lost"} focus.")
fun showOff() = println("I'm focusable!")
}
- 한 클래스에서 두 인터페이스를 함께 구현한 후, 동일한 showOff 디폴트 구현을 선택하려면 컴파일러 에러가 발생함.
class Button : Clickable, Focusable {
override fun click() = println("I was clicked")
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
🔖 4.1.2 open, final, abstract 변경자: 기본적으로 final
open class RichButton : Clickable {
fun disable() {}
open fun animate() {}
override fun click() {}
}
- 기본적으로 모든 클래스와 메서드는 final이다.
- 클래스에 대해 하위 클래스를 만들 수 없고, 기반 클래스의 메서드를 하위 클래스가 오버라이드할 수도 없다.
- 어떤 클래스의 상속을 허용하려면 open 변경자를 붙여야 한다.
- 메서드나 프로퍼티 또한 마찬가지
open class RichButton : Clickable {
final override fun click() {}
}
- 명시적으로 오버라이드 금지하려면 final 표시
abstract class Animated { // 추상클래스의 인스턴스 만들 수 없음.
abstract val animationSpeed: Double // 추상 프로퍼티: 하위 클래스는 반드시 값이나 접근자 제공
val keyframes: Int = 20
open val frames: Int = 60
abstract fun animate() // 추상함수: 하위 클래스는 반드시 오버라이드해야함.
open fun stopAnimating() {}
fun animateTwice() {}
}
- 추상클래스는 인스턴스화할 수 없다.
🔖 4.1.3 가시성 변경자: 기본적으로 공개
- 가시성 변경자는 코드 기반에 있는 선언에 대한 클래스 외부 접근을 제어
- public, protected, private
- 기본적으로 public
- module 안으로만 한정된 가시성을 위해 internal 제공
- module: 함께 컴파일되는 코틀린 파일의 집합
- 패키지 전용 가시성 개념이 없음
internal open class TalkativeButton {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}
fun TalkativeButton.giveSpeech() { // 오류
yell() // 오류
whisper() // 오류
}
- 기반 타입 목록에 들어있는 타입이나 제네릭 클래스의 타입 파라미터에 들어있는 타입의 가시성은 그 클래스 자신의 가시성과 같거나 더 높아야 하고, 메서드의 시그니처에 사용된 모든 타입의 가시성은 그 메서드의 가시성과 같거나 더 높아야 함.
- 클래스를 확장한 함수는 그 클래스의 private이나 protected 멤버에 접근할 수 없다.
🔖 4.1.4 내부 클래스와 내포된 클래스: 기본적으로 내포 클래스
- 자바와 달리 내포 클래스는 명시적으로 요청하지 않는 한 바깥쪽 클래스 인스턴스에 대한 접근 권한이 없다.
interface State : Serializable
interface View {
fun getCurrentState(): State
fun restoreState(state: State) {}
}
class Button : View {
override fun getCurrentState(): State = ButtonState()
override fun restoreState(state: State) {}
class ButtonState : State {}
}
- 내포된 클래스에 아무런 변경자도 없으면 자바 static 내포 클래스와 같다.
class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
- 내부 클래스에서 바깥쪽 클래스의 참조에 접근하려면 this@Outer라고 써야 한다.
🔖 4.1.5 봉인된 클래스: 확장이 제한된 클래스 계층 정의
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
fun eval(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval(e.left) + eval(e.right)
else -> throw IllegalArgumentException("Unknown expression")
}
- when을 사용할 때 else는 강제이다.
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 Num -> e.value
is Sum -> eval(e.left) + eval(e.right)
}
- sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스의 가능성을 제한할 수 있다.
- 하위 클래스들은 반드시 컴파일 시점에 알려져야 하며, sealed 클래스가 정의된 패키지와 같은 패키지에 속해야 하며, 모든 하위클래스가 같은 모듈 안에 위치해야 한다.
- 디폴트 분기(else)가 필요 없다.
- sealed 변경자는 클래스가 추상 클래스임을 명시한다.
- abstract를 붙일 필요가 없음.
- 추상 멤버를 선언한 수 있음.
sealed interface Toggleable {
fun toggle()
}
class LightSwitch : Toggleable {
override fun toggle() = println("Lights!")
}
class Camera: Toggleable {
override fun toggle() = println("Camera!")
}
- 봉인된 인터페이스도 똑같은 규칙을 따름.
- 봉인된 인터페이스가 속한 모듈이 컴파일되고 나면 이 인터페이스에 대한 새로운 구현을 밖에서 추가할 수 없다.
📖 4.2 뻔하지 않은 생성자나 프로퍼티를 갖는 클래스 선언
🔖 4.2.1 클래스 초기화: 주 생성자와 초기화 블록
class User(val nickname: String)
- 클래스 이름 뒤에 오는 괄호로 둘러쌓인 코드를 주 생성자라고 부른다.
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
- constructor 키워드는 주 생성자나 부 생성자 정의를 시작할 때 사용한다.
- init 키워드는 초기화 블록을 시작한다.
- 주 생성자 앞에 별다른 어노테이션이나 가시성 변경자가 없다면 constructor를 생략해도 된다.
class User(val nickname: String, val isSubscribed: Boolean = true)
- 생성자 파라미터에도 기본값을 정의할 수 있다.
open class User(val nickname: String)
class SocialUser(nickname: String) : User(nickname)
- 기반 클래스의 생성자가 인자를 받아야 한다면 클래스의 주 생성자에서 기반 생성자를 호출해야 할 필요가 있다.
- 클래스를 정의할 때 별도로 생성자를 정의하지 않으면 컴파일러가 자동으로 인자가 없는 디폴트 생성자를 만들어준다.
- 기반 클래스의 이름 뒤에 빈 괄호 필요
- 인터페이스의 경우 생성자가 없으므로 괄호가 없음.
class Secretive private constructor(private val agentName: String) {}
- 클래스 외부에서 인스턴스화하지 못하게 막고 싶다면 private 선언
🔖 4.2.2 부 생성자: 상위 클래스를 다른 방식으로 초기화
open class Downloader {
constructor(url: String?) {}
constructor(uri: URI?) {}
}
- 코틀린은 코틀린의 디폴트 파라미터 값과 이름 붙은 인자 문법을 사용하므로 부생성자가 필요할 일이 적다.
- 상위 클래스에서 super() 키워드를 통해 자신에 대응하는 상위 클래스 생성자를 호출한다.
- this()를 통해 클래스 자신의 다른 생성자를 호출할 수 있다.
class MyDownloader : Downloader() {
constructor(url: String?) : this(URI(url))
constructor(uri: URI?) : super(uri)
}
- 클래스에 주 생성자가 없다면 모든 부 생성자는 반드시 상위 클래스를 초기화하거나 다른 생성자에게 생성을 위임해야 한다.
- 부 생성자가 필요한 주된 이유는 자바 상호운영성이다.
🔖 4.2.3 인터페이스에 선언된 프로퍼티 구현
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 SocialUser(val accountId: Int) : User {
override val nickname: getFacebookName(accountId)
}
fun getNameFromSocialNetwork(accountId: Int) = "bottleh$accountId"
- 인터페이스의 프로퍼티 구현하기
- 인터페이스에 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수도 있다.
- 위와 같은 게터와 세터는 뒷받침하는 필드를 참조할 수 없다.
interface EmailUser {
val email: String
val nickname: String
get() = email.substringBefore('@')
}
- 하위 클래스는 email을 반드시 오버라이딩
- 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라는 특별한 식별자를 통해 뒷받침하는 필드에 접근할 수 있다.
- var인 경우에는 게터나 세터 모두에 field가 없어야 한다.
🔖 4.2.5 접근자의 가시성 변경
class LengthCounter {
var counter: Int = 0
private set // 클래스 밖에서 이 프로퍼티의 값을 바꿀 수 없다.
fun addWord(word: String) {
counter += word.length
}
}
- 클래스 외부에서 이 프로퍼티에 값을 쓰려하면 컴파일 시점 오류가 발생
📖 4.3 컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임
🔖 4.3.1 모든 클래스가 정의해야 하는 메서드
class Customer(val name: String, val postalCode: Int)
🛠️ 문자열 표현: toString()
- 기본 제공되는 객체의 문자열 표현은 클래스 이름과 객체의 주소를 표현함.
class Customer(val name: String, val postalCode: Int) {
override fun toString() = "Customer(name=$name, postalCode=$postalCode)"
}
🛠️ 객체의 동등성: equals()
class Customer(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if (other == null || other !is Customer)
return false
return name == other.name && postalCode == other.postalCode
}
}
- 코틀린에서 == 연산자는 참조 동일성을 검사하지 않고 객체의 동등성을 검사한다.
- Any는 Java의 Object에 대응되는 모든 클래스의 최상위 클래스
🛠️ 해시 컨테이너: hashCode()
class Customer(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if (other == null || other !is Customer)
return false
return name == other.name && postalCode == other.postalCode
}
override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
- JVM 언어에서는 hashCode가 지켜야 하는 equals()가 true를 반환하는 두 객체는 반드시 같은 hasCode()를 반환해야 한다.
🔖 4.3.2 데이터 클래스: 모든 클래스가 정의해야 하는 메서드를 자동으로 생성
data class Customer(val name: String, val postalCode: Int)
- equals, hasCode, toString을 모두 포함한다.
🛠️ 데이터 클래스와 불변성: copy() 메서드
class Customer(val name: String, val postalCode: Int) {
fun copy(name: String = this.name, postalCode: Int = this.postalCode): Customer = Customer(name, postalCode)
}
- 불변 객체를 사용하면 프로그램에 대해 훨씬 쉽게 추론할 수 있다.
- 데이터 클래스는 copy 메서드를 제공한다.
- 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 copy 메서드
🛠️ 클래스 위임: by 키워드 사용
Decorator 패턴
class DelegatingCollection<T>: Collection<T> {
private val innerList = arrayListOf<T>()
override val size: Int get() = innerList.size
override fun isEmpty(): Boolean = innerList.isEmpty()
override fun contains(element: T): Boolean = innerList.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
override fun iterator(): Iterator<T> = innerList.iterator()
}
class DelegatingCollection<T>(innerList: Collection<T> = mutableListOf<T>()) : Collection<T> by innerList
- 상속을 허용하지 않는 클래스에게 새로운 동작을 추가해야 할 때 쓰는 방법
- 기존 클래스 대신 사용할 수 있는 데코레이터(새로운 클래스)를 만들되, 기존 클래스와 같은 인터페이스를 데코레이터가 제공하고 기존 클래스를 데코레이터 내부 필드로 유지하는 것
- 새로 정의해야 하는 기능은 데코레이터의 메서드로 새로 정의
- 기존 기능이 그대로 필요한 부분은 데코레이터의 메서드가 기존 클래스의 메서드에게 요청을 전달
- 단점: 준비 코드가 상당히 많이 필요함.
- 코틀린은 by 키워드를 통해 제공함.
📖 4.4 object 키워드: 클래스 선언과 인스턴스 생성을 한꺼번에 하기
object 키워드를 쓰는 상황
- 객체 선언
- 동반 객체
- 객체 식
- 자바의 익명 내부 클래스
🔖 4.4.1 객체 선언: 싱글턴을 쉽게 만들기
object Payroll {
val allEmployees = arrayListOf<Person>()
fun calculateSalary() {
for (person in allEmployees) {
}
}
}
- 객체 선언은 object 키워드로 시작
- 객체 선언은 클래스를 정의하고 그 클래스의 인스턴스를 만들어 변수에 저장하는 모든 작업을 한 문장으로 처리
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
fun main() {
println(
CaseInsensitiveFileComparator.compare(
File("/User"), File("/user")
)
)
}
- 일반 객체(클래스 인스턴스)를 사용할 수 있는 곳에서는 항상 싱글턴 객체를 사용할 수 있다.
🔖 4.4.2 동반 객체: 팩토리 메서드와 정적 멤버가 들어갈 장소
- 코틀린 클래스 안에는 정적인 멤버가 없다.
- 패키지 수준의 최상위 함수와 객체 선언을 활용
- 대부분 최상위 함수를 활용하는 편을 더 권장한다.
class MyClass {
companion object {
fun callMe() {
println("Companion object called")
}
}
}
fun main() {
MyClass.callMe()
}
- 클래스 안에 정의된 객체 중 하나에 companion이라는 특별한 표시를 붙일 수 있다.
- 객체 멤버에 접근할 때 자신을 감싸는 클래스의 이름을 통해 직접 사용할 수 있게 된다.
fun main() {
val myObject = MyClass()
myObject.callMe() // error
}
- 해당 클래스의 인스턴스는 동반 객체의 멤버에 접근할 수 없다.
class User {
val nickname: String
constructor(email: String) {
nickname = email.substringBefore('@')
}
constructor(socialAccountId: Int) {
nickname = getSocialNetworkName(socialAccountId)
}
}
class User private constructor(val nickname: String) {
companion object {
fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
fun newSocialUser(accountId: Int) = User(getNameFromSocialNetwork(accountId))
}
}
- 동반 객체가 private 생성자를 호출하기 좋은 위치다.
- 바깥쪽 클래스의 private 생성자도 호출할 수 있다.
- 즉, 팩토리 패턴을 구현하기 가장 적합한 위치가 될 수 있다.
- 팩토리 메서드는 그 팩토리 메서드가 선언된 클래스의 하위 클래스 객체를 반환할 수도 있다.
🔖 4.4.3 동반 객체를 일반 객체처럼 사용
class Person(val name: String) {
companion object Loader {
fun fromJSON(jsonText: String): Person = /* */
}
}
- 필요하다면 동반객체에 이름을 붙일 수 있다.
🛠️ 동반 객체에서 인터페이스 구현
interface JSONFactory<T> {
fun fromJSON(jsonText: String): T
}
class Person(val name: String) {
companion object : JSONFactory<Person> {
override fun fromJSON(jsonText: String): Person = /* */
}
}
- 동반 객체도 인터페이스를 구현할 수 있다.
🛠️ 동반 객체 확장
class Person(val firstName: String, val lastName: String) {
companion object {
}
}
fun Person.Companion.fromJSON(json: String): Person {
}
- 마치 동반 객체 안에서
fromJSON함수를 정의한 것처럼 호출할 수 있다. - 동반 객체에 대한 확장 함수를 작성할 수 있으려면 원래 클래스에 동반 객체를 꼭 선언해야 한다.
🔖 4.4.4 객체 식: 익명 내부 클래스를 다른 방식으로 작성
interface MouseListener {
fun onEnter()
fun onClick()
}
class Button(private val listener: MouseListener) {
}
fun main() {
Button(object : MouseListener {
override fun onEnter() {}
override fun onClick() {}
})
}
- 객체 식을 사용해 익명 객체를 만든다.
- 객체 선언과 같지만 이름이 빠졌다.
- 객체 식은 익명 객체 안에서 여러 메서드를 오버라이드해야 하는 경우에 훨씬 더 유용하다.
📖 4.5 부가 비용 없이 타입 안전성 추가: 인라인 클래스
fun addExpense(expense: Int) {
// 비용을 미국 달러의 센트 단위로 저장
}
- 200엔 지출을 추가하려면 Int 타입으로만 받기 때문에 다른 의미의 값을 전달하는 것을 막을 수 없다.
- 전형적인 해법은 Int 대신 Class를 사용하는 것
class UsdCent(val amount: Int)
fun addExpense(expense: UsdCent) {
// 비용을 미국 달러의 센트 단위로 저장
}
- 위와 같이 구현하면 GC 비용이 증가하게 된다.
@JvmInline
value class UsdCent(val amount: Int)
- 인라인 클래스를 사용하면 성능을 희생하지 않고 타입 안정성을 얻을 수 있다.
- 실행 시점에 UsdCent의 인스턴스는 감싸진 프로퍼티로 대체된다.
- 인라인으로 표시하려면 클래스가 프로퍼티를 하나만 가져야 하며, 그 프로퍼티는 주 생성자에서 초기화돼야 한다.
- 인라인 클래스는 클래스 계층에 참여하지 않는다.
- 다른 클래스를 상속할 수도, 다른 클래스가 상속할 수도 없음.
- 인터페이스 상속, 메서드 정의, 계산된 프로퍼티를 제공
