다른 언어 쓰느라고 클래스를 거의 쓴 적이 없다.
그래서 이해가 잘 안되서 요약을 잘 못하겠음..
4.1 클래스 계층 정의
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!")
}
class Button : Clickable, Focusable {
override fun click() = println("I was clicked")
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
}
}
fun main(args: Array<String>) {
val button = Button()
button.showOff()
button.setFocus(true)
button.click()
}
/*
I'm clickable!
I'm focusable!
I got focus.
I was clicked
*/
상속은 자바랑 비슷한데, 일반 클래스는 부모 하나, 인터페이스는 여러 개를 둘 수 있다.
class <클래스 이름> : <부모 인터페이스 | 클래스>
이런식으로 정의함. 인터페이스의 경우 반드시 override로 해서 정의해야 되는 것 까지 똑같다.
함수명이 겹칠 경우, <상속 인터페이스 이름>을 적어줘야 함.
원래 클래스를 만들었던 작성자의 의도를 지키기 위해 "상속을 위한 설계와 문서를 갖추거나, 그럴 수 없다면 상속을 금지하라" 라는 이펙티브 자바 책의 조언에 따라 기본이 final이다. 오버라이드 가능하게 하려면 class 앞과 fun 앞에 open을 붙여야 함.
표) 클래스 내에서 상속 제어 변경자의 의미
변경자 | 이 변경자가 붙은 멤버는... | 설명 |
final | 오버라이드 불가능 | 클래스 멤버의 기본 변경자다. |
open | 오버라이드 가능 | 반드시 opn을 명시해야 오버라이드할 수 있다. |
abstract | 반드시 오버라이드해야 함 | 추상 클래스의 멤버에만 이 변경자를 붙일수 있다. 추상 멤버에는 구현이 있으면 안 된다. |
override | 상위 클래스나 상위 인스턴스의 멤버를 오버라이드하는 중 | 오버라이드하는 멤버는 기본적으로 열려있다. 하위 크래스의 오버라이드를 금지하려면 final을 명시해야 한다. |
internal open class TalkativeButton : Focusable {
private fun yell() = println("Hey!")
protected fun whisper() = println("Let's talk!")
}
fun TalkativeButton.giveSpeech() { // 오류: "public"멤버가 자신의 "internal" 수신 타입인
// "TalkativeButton"을 노출함
// yell() // 오류: "yell"에 접근할 수 없음: "yell"은 "TalkativeButton"의 "private" 멤버임
// whisper() // 오류: "whisper"에 접근할 수 없음: "whisper"은 "TalkativeButton"의 "protected" 멤버임
}
표) 코틀린의 가시성 변경자
변경자 | 클래스 멤버 | 최상위 선언 |
public(기본 가시성임) | 모든 곳에서 볼 수 있다. | 모든 곳에서 볼 수 있다. |
internal | 같은 모듈 안에서만 볼 수 있다. | 같은 모듈 안에서만 볼 수 있다. |
protected | 하위 클래스 안에서만 볼 수 있다. | (최상위 선언에 적용할 수 없음) |
private | 같은 클래스 안에서만 볼 수 있다. | 같은 파일 안에서만 볼 수 있다. |
표) 자바와 코틀린의 중첩 클래스와 내부 클래스의 관계
클래스 B 안에 정의된 클래스 A | 자바에서는 | 코틀린에서는 |
중첩 클래스(바깥쪽 클래스에 대한 참조를 저장하지 않음) | static class A | class A |
내부 클래스(바깥쪽 클래스에 대한 참조를 저장함) | class A | inner class A |

class Outer {
inner class Inner {
fun getOuterReference(): Outer = this@Outer
}
}
sealed는 상위 클래스에 sealed 변경자를 붙이면 그 상위 클래스를 상속한 하위 클래스 정의를 제한할 수 있다.
상위 클래스를 바꿨을 때 이미 만들어 놓은 하위 클래스에서 버그 뜰 수 있는걸 방지하기 위함이다.
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")
}
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.left) + eval(e.right)
}

4.2 뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
코틀린은 자바에서 클래스 생성할 때 이미 직접 써서 사용하고 있는 생성자, 게터, 세터를 자동으로 해준다. 어떻게 하는건지 살펴볼거다.
class User(val nickname: String)
는
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
을 줄여쓴거다. 주 생성자랑 부 생성자가 있는데, 주 생성자는 braket {} 밖에 쓴거고, 부 생성자는 init 같은거 써서 클래스 {} 안에 쓴거다.
또, constructor는 주 생성자 앞에 별다른 애노테이션이나 가시성 변경자가 없다면 생략해도 된다. 그럼
class User (_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
이렇게 되고, 더 줄이면 처음에 보여줬던 한 줄 짜리가 되는거다.
다른 클래스가 해당 클래스를 상속하고 싶을 때는 파라미터를 받지 않더라도괄호 ()를 붙여야 함. 인터페이스는 붙지 않음. 이 규칙으로 괄호 ()가 있으면 클래스, 없으면 인터페이스를 상속한다고 파악할 수 있다.
class RadioButton: Button()
만약 어떠한 외부 클래스도 해당 클래스를 상속하지 못하게 하려면, constructor를 생략하지 말고 앞에 private를 붙이면 된다.
class Secretive private constructor() {}
사실 이정도만 되도 왠만한 클래스는 다 정의할 수 있지만, 특별한 것들을 살펴보자.
open class View {
constructor(ctx: Context){
// ...
}
constructor(ctx: Context, attr: AttributeSet) {
// ...
}
}
부 생성자가 2개인 놈. 주어진 파라미터 갯수에 따라 다른 행동을 취하고 싶을 때 쓴다.
class MyButton : View {
constructor(ctx: Context) : super(ctx) {
// ...
}
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
// ...
}
}

class MyButton : View {
constructor(ctx: Context) : this(ctx, MY_STYLE) {
// ...
}
constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) {
// ...
}
}

super와 this에 따른 생성 위임 차이
다음은 특별한 프로퍼티
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 = getFacebookName(accountId)
}
println(PrivateUser("test@kotlinlang.org").nickname) // test@kotlinlang
println(SubscribingUser("test@kotlinlang.org").nickname) // test
interface User {
val nickname: String
val nickname: String
get() = email.substringBefore('@')
}
만약 인터페이스 내에서 게터를 정해줬으면, 얘는 반드시 오버라이드 할 필요 없음.
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value".""".trimIndent())
field = value
}
}
fun main(args: Array<String>) {
val user = User("Alice")
user.address = "Elsenheimerstrasse 47, 80687 Muenchen"
}
/*
Address was changed for Alice:
"unspecified" -> "Elsenheimerstrasse 47, 80687 Muenchen".
*/
세터 잘 정의하면 로그 가능
만약 이 세터를 클래스 외부에서 사용 불가능하게 하고 싶다면 private를 쓸 수 있다.
class LengthCounter {
var counter: Int = 0
private set
fun addWord(word: String) {
counter += word.length
}
}
fun main(args: Array<String>) {
val lengthCounter = LengthCounter()
lengthCounter.addWord("Hi!")
println(lengthCounter.counter) // 3
}
4.3 컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임
모든 클래스가 정의해야 하는 특별한 메서드들이 있다. toString, equals, hashCode 등
직접 작성해보자.
class Client(val name: String, val postalColde: Int) {
override fun equals(other: Any?): Boolean {
if (other == null || other !is Client) {
return false
}
return name == other.name && postalColde == other.postalColde
}
override fun toString(): String = "Client(name=$name, postalCode=$postalColde)"
override fun hashCode(): Int = name.hashCode() * 31 + postalColde
}
equals가 같은 Client 객체가 아니라 Any? 타입을 인자로 받는 이유는 기본 함수인 equals를 오버라이드 하려고.
자바에서 ==는 참조 비교, .equals는 동등성(값) 비교이고 코틀린에서 ==는 자바의 equals를 사용하기 때문에 동등성 비교, 참조는 ===을 이용한다.
그래서 hashCode까지 직접 오버라이드해서 정의하지 않으면 == 에서 에러가 날 수 있다. 왜냐하면 원소가 똑같은지 확인할 때 연산 시간을 줄이기 위해 먼저 해시값을 비교하기 때문에 값이 같아도 해시값이 달라서 값이 같은건 의미가 없다.
그럼 이 작업을 클래스 만들때마다 맨날 하냐? 그래서 코틀린에선 어차피 반복해서 해야하는걸 줄이기 위해 class 앞에 data를 붙이면 위의 과정을 컴파일러가 알아서 해준다.
data class Client(val name: String, val postalColde: Int)
copy() 같은 경우는.. 일단 왜 있냐면 데이터 클래스의 프로퍼티, 특히 HashMap 안에 들어가는 애들 같은 경우는 왠만하면 val로 정의하길 권장한다. 키 값을 프로퍼티를 사용해 만들어 놨는데 안의 내용을 변경하면 문제가 생길 수 있고, 다중 스레드에서 실행할 때 값이 안변해야 신경 쓸 게 줄어든다. 그래서 값을 변경한다기 보단 새로 만드는 방법을 쓴다.
class Client(val name: String, val postalCode: Int) {
// ...
fun copy(name: String = this.name, postalCode: Int = this.postalCode) = Client(name, postalCode)
}
val lee = Client("Lee", 123)
println(lee.copy(postalCode = 456))
by 키워드는 만들어진 철학이 있는데, 코틀린은 기본값으로 클래스가 final인데, 클래스가 한번 정의되면 왠만하면 변경을 안했으면 좋겠다는 뜻이다. 이유는 만약 상위 클래스를 하위 클래스가 상속하여 사용 중이었는데, 상위 클래스가 막 변하면 하위 클래스에서 같은 메서드를 써도 에러가 날 수 있기 때문. open 클래스만 확장하여 사용할 수 있고, 그래서 open 클래스만 보고도 다른 클래스가 상속하리라 짐작할 수 있다.
근데 final이라면 클래스 확장을 못하는데, 가끔 동작을 추가해야 할 때가 있다. 이때 쓰는것이 데코레이터(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 iterator(): Iterator<T> = innerList.iterator()
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
}
확장해서 쓸 수 없는 arrayListOf<T>를 사용하기 위해 일일히 자기것으로 정의하는 모습이다.
이게 by 키워드를 사용한 다음 코드와 같다.
class DelegatingCollection<T>(
val innerList: Collection<T> = ArrayList<T>()
): Collection<T> by innerList
역시나 이런 귀찮은 일들은 컴파일러가 알아서 해준다. 특이한 건 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(c: Collection<T>): Boolean {
objectsAdded += c.size
return innerSet.addAll(c)
}
}
fun main(args: Array<String>) {
val cast = CountingSet<Int>()
cast.addAll(listOf(1, 1, 2))
println("${cast.objectsAdded} objects were added, ${cast.size} remain")
}
// 3 objects were added, 2 remain
상위의 API를 그대로 가져다 쓰기 때문에 상위 API 작동 방식이 바뀌더라도 뭐 성능개선 이런것일 거라 내 함수는 문제 없이 돌아갈 것임을 짐작할 수 있다.
4.4 object 키워드: 클래스 선언과 인스턴스 생성
객체지향언어라 한 개의 인스턴스만 필요하더라도 클래스로 만들어 쓸 때(싱글톤) 쓴다고 한다. class 대신 object를 쓴다.
import java.io.File
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(file1: File, file2: File): Int {
return file1.path.compareTo(file2.path, ignoreCase = true)
}
}
fun main(args: Array<String>) {
println(CaseInsensitiveFileComparator.compare(File("/User"), File("/user"))) // 0
val files = listOf(File("/Z"), File("/a"))
println(files.sortedWith(CaseInsensitiveFileComparator)) // [/a, /Z]
}
하지만 싱글톤은 대규모 시스템이 갖춰진 프로젝트에서는 안쓴다고 한다. 하지만 자바에는 있는 static이 없기 때문에 이걸 이용해서 static처럼 사용해서 쓴다.
클래스 내부에서도 정의해서 사용할 수 있다.
class User private constructor(val nickname: String) {
companion object {
fun newSubscribingUser(email: String) = User(email.substringBefore('@'))
fun newFacebookUser(accountId: Int) = User(getFacebookName(accountId))
}
}
fun main(args: Array<String>) {
val subscribingUser = User.newSubscribingUser("bob@gmail.com")
val facebookUser = User.newFacebookUser(4)
println(subscribingUser.nickname) // bob
}
클래스를 생성할 때, 생성자 말고 팩토리를 사용해서만 생성할 수 있게 만들 수 있음.
동반 객체를 일반 객체처럼 사용할 수도 있다. 동반 객체는 클래스 안에 정의된 일반 객체다.
class Person(val name: String) {
companion object Loader {
fun fromJSON(jsonText: String): Person = ...
}
}
fun main(args: Array<String>) {
val person = Person.Loader.fromJSON("{name: 'Dmitry'}")
println(person.name) // Dmitry
val person2 = Person.fromJSON("{name: 'Brent'}")
println(person2.name) // Brent
}
무명 내부 클래스를 다른 방식으로 작성할 수도 있다.
fun countClicks(window: Window) {
var clickCount = 0
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}
})
}
'CS > 코틀린 인 액션' 카테고리의 다른 글
6. 코틀린 타입 시스템 (0) | 2023.04.30 |
---|---|
5. 람다로 프로그래밍 (0) | 2023.04.30 |
3. 함수 정의와 호출 (0) | 2023.04.24 |
2. 코틀린 기초 (0) | 2023.04.23 |
1. 코틀린은 무엇이며, 왜 필요한가 (0) | 2023.04.23 |