Kotlin In Action #4
클래스, 객체, 인터페이스
뻔하지 않은 생성자와 프로퍼티를 갖는 클래스 선언
자바에서 생성자를 하나 이상 선언할 수 있고, 코틀린도 비슷하지만 주 생성자와 부 생성자를 구분함.
코틀린에서는 초기화 블록을 통해 초기화 로직을 추가할 수 있음.
클래스 초기화: 주 생성자와 초기화 블록
class User(val nickname: String)
- 클래스에서 중괄호가 아닌 괄호로 둘러싸인 코드를 주 생성자라 함.
- 주 생성자는 생성자 파라메터를 지정하고, 생성자 파라메터에 의해 초기화되는 프로퍼티를 정의하는 두 가지 목적에 사용 됨.
위 코드를 명시적인 선언으로 풀어보자.
class User constructor(_nickname: String) {
val nickname: String
init {
nickname = _nickname
}
}
- constructor 키워드는 주 생성자나 부 생성자 정의를 시작할 때 사용.
- init 키워드는 초기화 블록을 시작.
- 초기화 블록에는 클래스의 객체가 만들어질 때 실행될 초기화 코드가 들어감.
- 주로 초기화 블록은 주 생성자와 함께 사용.
- 생성자 파라메터 _nickname에서 밑줄(_)은 프로퍼티와 생성자 파라메터를 구분할때 사용
class User(val nickname: String,
val isSubscribed: Boolean = true) // 디폴트 값 제공
fun main() {
val user = User("0n1dev")
println(user.isSubscribed)
}
기반 클래스가 있다면 주 생성자에서 기반 클래스의 생성자를 호출해야 할 필요가 있음.
open class User(val nickname: String,
val isSubscribed: Boolean = true)
class TwitterUser(nickname: String): User(nickname) {}
fun main() {
val user = TwitterUser("0n1dev")
println(user.isSubscribed)
}
유일한 주 생성자의 변경자로 private를 붙이면, 외부에서 인스턴스화할 수 없음.
class Secretive private constructor()
부 생성자: 상위 클래스를 다른 방식으로 초기화
open class View {
constructor(ctx: Context) {
}
constructor(ctx: Context, attr: AttributeSet) {
}
}
super나 this를 사용하여 다른 생성자에게 위임 할 수 있음.
open class View {
constructor(ctx: Context) {
}
constructor(ctx: Context, attr: AttributeSet) {
}
}
class MyButton: View {
constructor(ctx: Context): this(ctx, AttributeSet.NameAttribute as AttributeSet) {
}
constructor(ctx: Context, attr: AttributeSet): super(ctx, attr) {
}
}
인터페이스에 선언된 프로퍼티 구현
인터페이스에 추상 프로퍼티 선언을 넣을 수 있음.
interface User {
val nickname: String
}
해당 User 인터페이스를 구현하는 클래스는 nickname의 값을 얻을 수 있는 방법을 제공해야 한다는 뜻.
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
get() = getFacebookName(accountId)
private fun getFacebookName(accountId: Int): String {
TODO("Not yet implemented")
}
}
인터페이스에는 추상 프로퍼티뿐 아니라 게터와 세터가 있는 프로퍼티를 선언할 수 있음.
interface User {
val email: String
val nickname: String
get() = email.substringBefore('@')
}
추상 프로퍼티 email은 반드시 오버라이드해야 하며, nickname은 오버라이드하지 않고 상속 가능.
게터와 세터에서 뒷받침하는 필드에 접근
세터에서 뒷받침하는 필드 접근하기
class User(val name: String) {
var address: String = "unspecified"
set(value: String) {
println("""
Address was changed for $name:
"$field" -> "$value"
""".trimIndent()) // 뒷받침하는 필드 값 읽기
field = value // 뒷받침하는 필드 값 변경
}
}
접근자의 가시성 변경
class LengthCounter {
var counter: Int = 0
private set
fun addWord(word: String) {
counter += word.length
}
}
컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임
코틀린은 필수 메서드(equals, hashCode, toString 등)로 인한 잡음 없이 코드를 깔끔하게 유지할 수 있음.
모든 클래스가 정의해야 하는 메서드
Client 클래스의 초기 정의
class Client(val name: String, val postalCode: Int)
문자열 표현: toString()
class Client(val name: String, val postalCode: Int) {
override fun toString(): String = "Client(name='$name', postalCode=$postalCode)"
}
객체의 동등성: equals()
내부에 동일한 데이터를 포함하는 경우 그 둘을 동등한 객체로 간주.
자바에서 ==는 원시 타입과 참조 타입을 비교할 때 사용.
원시 타입의 경우 두 피연산자의 값이 같은지 비교. (동등성)
참조 타입의 경우 두 피연산자의 주소가 같은지 비교. (동일성)
자바에서 동등성을 알려면 equals를 호출해야 함.
코틀린에서는 == 연산자가 두 객체를 비교하는 기본적인 방법.
==는 내부적으로 equals를 호출해서 객체를 비교.
참조 비교를 위해서는 === 연산자를 사용.
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if (other == null || other !is Client) {
return false
}
return name == other.name &&
postalCode == other.postalCode
}
}
해당 코드는 복잡한 작업을 수행해보면 제대로 작동하지 않는 경우가 있음.
hashCode 정의를 빠뜨려서 그렇다.
해시 컨테이너: hashCode()
equals를 오버라이드할 때 반드시 hashCode도 함께 오버라이드해야 함.
JVM 언어에서는 equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 함.
원소를 비교할 때 비용을 줄이기 위해 먼저 객체의 해시 코드를 비교하기 때문.
class Client(val name: String, val postalCode: Int) {
override fun equals(other: Any?): Boolean {
if (other == null || other !is Client) {
return false
}
return name == other.name &&
postalCode == other.postalCode
}
override fun hashCode(): Int = name.hashCode() * 31 + postalCode
}
데이터 클래스: 모든 클래스가 정의해야 하는 메서드 자동 생성
어떤 클래스가 데이터를 저장하는 역할만을 수행한다면 toString, equals, hashCode를 반드시 오버라이드해야 함.
코틀린에서는 data라는 변경자를 클래스 앞에 붙이면 필요한 메서드를 컴파일러가 자동으로 만들어줌.
data class Client(val name: String, val postalCode: Int)
해당 클래스는 아래 요구사항의 메서드를 포함.
- 인스턴스 간 비교를 위한 equals
- HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode
- 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현을 만들어주는 toString
위 세개 말고 몇 가지 유용한 메서드를 추가로 생성해줌.
데이터 클래스와 불변성: copy() 메서드
데이터 클래스의 프로퍼티가 꼭 val일 필요는 없음.
하지만 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들라고 권장.
HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수적.
코틀린은 copy 메서드를 제공하면서 객체를 메모리상에서 직접 바꾸는 대신 복사본을 만들며 일부 프로퍼티를 바꿀수 있게 해줌.
클래스 위임: by 키워드 사용
상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때가 있음.
이럴 때 사용하는 일반적인 방법이 데코레이터 패턴.
class DelegatingCollection<T>: Collection<T> {
private val innerList = arrayListOf<T>();
override val size: Int
get() = innerList.size
override fun contains(element: T): Boolean = innerList.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = containsAll(elements)
override fun isEmpty(): Boolean = isEmpty()
override fun iterator(): Iterator<T> = iterator()
}
- 데코레이터 패턴을 구현하는데 있어서 위와 같이 단순한 인터페이스를 구현하는것 조차 복잡한 코드를 작성해야 함.
- 이러한 위임을 언어가 제공하는 일급 시민 기능으로 지원하는것이 코틀린의 장점.
class DelegatingCollection<T>(
innerList: Collection<T> = ArrayList<T>()
): Collection<T> by innerList
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)
}
}
'Programming > Kotlin' 카테고리의 다른 글
Kotlin In Action #5 (0) | 2022.01.10 |
---|---|
[당근마켓 밋업 정리] - Kotlin Coroutines 톺아보기 (0) | 2022.01.09 |
Kotlin In Action #3 (0) | 2021.12.22 |
Kotlin In Action #2 (0) | 2021.12.11 |
Kotlin In Action #1 (0) | 2021.12.06 |