Kotlin 공식 Example로 공부하기 - Special Classes(data class, enum class, sealed class, object, companion object)
Special Classes
- 저는 전문 번역가도 아니고, 의역을 넘어 오역, 심지어 그냥 제가 읽고 싶은대로 읽은 내용이 있을 수 있습니다.
- 개인 공부를 한 것을 포스트로 남기고 있으며 틀린 부분이 있으면 지적해주시면 수정하도록 하겠습니다.
- 원문 : https://play.kotlinlang.org/byExample/03_special_classes/01_Data classes
Data Classes (데이터 클래스)
Data Class를 사용하면 값을 저장하는 클래스(ex. DTO, VO)를 쉽게 만들 수 있습니다.
Data Class는 자동으로 메소드들을 제공합니다.
- 주 생성자에 선언된 모든 프로퍼티를 기준으로 컴파일러가 자동으로 생성해줍니다.
equals()
/hashCode()
pairtoString()
copy()
componentN() function
프로퍼티 선언순서로 값을 가져오는 함수 (Destructuring Declarations에서 다룸)
data class User(val name: String, val id: Int) // 1
fun main() {
val user = User("Alex", 1)
println(user) // 2 User(name=Alex, id=1)
val secondUser = User("Alex", 1)
val thirdUser = User("Max", 2)
println("user == secondUser: ${user == secondUser}") // 3
println("user == thirdUser: ${user == thirdUser}")
println(user.hashCode()) // 4
println(thirdUser.hashCode())
// copy() function
println(user.copy()) // 5
println(user.copy("Max")) // 6
println(user.copy(id = 2)) // 7
println("name = ${user.component1()}") // 8
println("id = ${user.component2()}")
}
data
키워드를 이용해서 데이터 클래스를 정의합니다.toString
메소드가 자동으로 생성되어println
으로 멋지게 출력하도록 합니다.- 자동 생성된
equals
메소드는 두 인스턴스의 모든 프로퍼티가 같으면 두 인스턴스는 같다고 판단합니다. - 같은 데이터 클래스 인스턴스는 같은
hashCode()
값을 갖습니다. (user.hashCode()=secondUser.hashCode()) - 자동 생성된
copy()
메소드로 새로운 인스턴스를 쉽게 만들 수 있습니다. - 복사할 때, 특정 속성을 변경할 수 있습니다.
copy
는 클래스의 생성자와 같은 순서로 매개변수를 적용합니다. (name:String, id:Int 이므로 user.copy("Max") 했을 때, name:String이 생성자에서 제일 앞에 있으므로 name이 "Max"로 변경된 것을 확인할 수 있습니다.) - 지정 인자(named arguments)와 함께
copy
를 사용하여 생성자의 매개 변수 순서와 상관없이 속성 값을 변경할 수 있습니다. - 자동 생성된
componentN
함수로 프로퍼티 값을 불러올 수 있습니다.(N은 클래스에 선언된 프로퍼티 순서)
Enum Classes (열거형 클래스)
Enum 클래스는 고유한 값의 집합을 나타내는 타입을 모델링하는데 사용됩니다. (ex. States, Modes, Directions, ...)
enum class State {
IDLE, RUNNING, FINISHED // 1
}
fun main() {
val state = State.RUNNING // 2
val message = when (state) { // 3
State.IDLE -> "It's idle"
State.RUNNING -> "It's running"
State.FINISHED -> "It's finished"
}
println(message)
}
- 3가지 열거형 인스턴스를 갖는 단순한 enum 클래스를 정의합니다. 열거형 인스턴스의 수는 항상 유한해야하고 모두가 구분되어야 합니다.
- 클래스 이름으로 열거형 인스턴스에 접근합니다.
- Enum을 사용할 때, 만약
when
expression이 출중하다면(enum의 모든 경우의 수를 다 사용했다면)else
케이스가 필요하지 않다고 추론할 수 있습니다.
열거형은 다른 클래스처럼 프로퍼티와 메소드를 가질 수 있습니다. 세미콜론으로 열거형 인스턴스와 구분
enum class Color(val rgb: Int) { // 1
RED(0xFF0000), // 2
GREEN(0x00FF00),
BLUE(0x0000FF),
YELLOW(0xFFFF00);
fun containsRed() = (this.rgb and 0xFF0000 != 0) // 3
}
fun main() {
val red = Color.RED
println(red) // 4
println(red.containsRed()) // 5
println(Color.BLUE.containsRed()) // 6
}
- 프로퍼티와 메소드를 갖는 열거형 클래스를 정의합니다.
- 각 인스턴스는 생성자의 매개 변수에 대한 인수 전달을 해야합니다.
- 열거형 클래스의 멤버 변수는 인스턴스 정의로부터 세미콜론에 의해 구분됩니다.
- 디폴트(default)
toString
메소드는 인스턴스의 이름을 리턴합니다. (여기서는 "RED") - 열거형 인스턴스의 메소드를 호출합니다.
- 열거형 클래스 이름을 통해 메소드를 호출합니다.
Sealed Classes
Sealed class를 사용하면 상속의 사용을 제한할 수 있습니다.
sealed class를 선언하면, sealed class가 선언된 동일한 파일 내에서만 서브 클래스를 만들 수 있습니다.
sealed class가 선언된 파일 외부에서 서브클래스를 생성할 수 없습니다.
✔ 내용이 좀 빈약해서 공식 문서의 Sealed Class 부분 첨부합니다.
sealed class Mammal(val name: String) // 1
class Cat(val catName: String) : Mammal(catName) // 2
class Human(val humanName: String, val job: String) : Mammal(humanName)
fun greetMammal(mammal: Mammal): String {
when (mammal) { // 3
is Human -> return "Hello ${mammal.name}; You're working as a ${mammal.job}" // 4
is Cat -> return "Hello ${mammal.name}" // 5
} // 6
}
fun main() {
println(greetMammal(Cat("Snowy")))
}
- sealed 클래스를 정의합니다. (
sealed
키워드를 붙입니다.) - 서브클래스를 정의합니다. 모든 서브클래스는 반드시 sealed 클래스가 정의된 파일에 있어야 합니다.
when
expression의 인자(argument)로 sealed class의 인스턴스를 사용합니다.- 스마트 캐스트가 이뤄져
Mammal
이Human
으로 캐스팅됩니다. - 스마트 캐스트가 이뤄져
Mammal
이Cat
으로 캐스팅됩니다. else
케이스는 필요하지 않습니다. sealed class의 가능한 모든 서브 클래스가when
케이스에 포함되어 있으므로else
케이스는 필요하지 않습니다. non-sealed 슈퍼클래스를 쓴다면else
케이스는 필요합니다.
Object Keyword
코틀린에서 클래스와 객체는 대부분의 객체 지향 언어와 동일한 방식으로 동작합니다.
(클래스는 청사진(붕어빵틀), 객체는 클래스의 인스턴스(붕어빵))
보통 클래스를 정의하고 그 클래스의 인스턴스를 여러 개 생성합니다.
import java.util.Random
class LuckDispatcher { //클래스(붕어빵틀) 정의
fun getNumber() { //메소드 정의
var objRandom = Random()
println(objRandom.nextInt(90))
}
}
fun main() {
val d1 = LuckDispatcher() //인스턴스 생성
val d2 = LuckDispatcher()
d1.getNumber() //인스턴스의 메소드 호출
d2.getNumber()
}
코틀린에는 object
키워드도 있습니다.
이 키워드는 단일 구현으로 데이터 타입을 얻는데 사용됩니다.
자바개발자라면 단일 구현의 의미를 싱글톤(Singleton)이라고 생각하면 됩니다.
싱글톤은 두 개의 스레드가 동시에 클래스의 인스턴스를 생성하려고 하더라도 오직 한 개의 인스턴스만 생성되는 것을 보장하는 것을 말합니다.
코틀린에서 이를 달성하기 위해서는 간단하게 object
키워드로 선언하면 됩니다.
object
키워드로 생성한 인스턴스에는 클래스도 없고 생성자도 없습니다 오직 게으른 인스턴스(lazy instance)입니다.
왜 게으르다(lazy) 할까요? 왜냐하면 객체에 접근할 때
한 번 생성되기 때문입니다. 반대로, 접근하기 전까지는 생성되지 않습니다.
Object Expression
다음은 **object
expression**의 기본 사용법입니다.
클래스 선언에서 사용할 필요가 없습니다. 단일 객체를 생성하고 멤버를 선언한 후 하나의 함수 내에서 접근합니다.
이와 같은 객체는 자바에서 종종 익명 클래스의 인스턴스 생성됩니다.
fun rentPrice(standardDays: Int, festivityDays: Int, specialDays: Int): Unit { //1
val dayRates = object { //2
var standard: Int = 30 * standardDays
var festivity: Int = 50 * festivityDays
var special: Int = 100 * specialDays
}
val total = dayRates.standard + dayRates.festivity + dayRates.special //3
print("Total price: $$total") //4
}
fun main() {
rentPrice(10, 2, 1) //5
}
- 파라미터를 갖는 함수를 생성합니다.
- 결과 값을 계산할 때 사용할 객체를 생성합니다.
- 객체의 프로퍼티에 접근합니다.
- 결과를 출력합니다.
- 함수를 호출합니다. 이 때가 실제로 객체가 만들어질 때 입니다.
Object Declaration
object
선언으로도 사용할 수 있습니다. 이것은 expression
이 아니고 변수 할당에도 사용할 수 없습니다.
object
선언된 객체에 직접 접근하여 사용해야합니다.
object DoAuth { //object 선언
fun takeParams(username: String, password: String){ //object 메소드 정의
println("input Auth parameters = $username:$password")
}
}
fun main(){
DoAuth.takeParams("foo", "qwerty") //메소드 호출, 이 때 실질적으로 객체가 생성됩니다.
}
Companion Objects
클래스 안에 있는 object
선언은 다른 유용한 사례를 정의합니다 : 동반자 객체(companion object)
문법적으로 자바의 static 메소드와 유사하다. 클래스 이름을 한정자(Qualifier)로 사용하여 객체 멤버를 호출합니다.
만약 코틀린에서 companion object를 사용할 계획이라면, package-level 함수 대신에 사용할 것을 고려해봐야합니다.
class BigBen { //클래스 정의
companion object Bonger { //companion object 정의, 이름은 생략 가능
fun getBongs(nTimes: Int) { //companion object 메소드 정의
for (i in 1 .. nTimes) {
print("BONG ")
}
}
}
}
fun main() {
BigBen.getBongs(12) //클래스 이름을 통한 companion object 메소드 호출
}