본문 바로가기

Kotlin 공부

kotlinlang.org 내가 보려고 정리 - Data classes

Data classes

데이터를 담는 것이 주 목적인 클래스.

data class User(val name: String, val age: Int)

인스턴스를 읽을 수 있게 출력하거나, 인스턴스끼리 비교하거나, 복사 등등의 기능을 수행해주는 멤버 함수들을 쓸 수 있다.

 

  • .equals / .hashCode()
  • .toString() 형식: "User(name=John, age=42)"
  • .componentN() 선언된 순서대로 property들에 대응한다(해당한다?)
  • .copy()

코드의 일관성과 의미있는 행동을 위해 데이터 클래스는 다음 요구사항을 모두 만족해야 한다.

  • primary 생성자는 적어도 하나 이상의 매개변수를 가져야 한다.
  • 모든 primary 생성자의 매개변수들은 val나 var로 마크되어 있어야 한다.
  • 데이터 클래스는 추상 클래스, 상속 가능(? open) 클래스, sealed 클래스, 또는 inner 클래스가 될 수 없다.

 

데이터 클래스 멤버(아마 멤버함수)들의 생성은 멤버들의 상속에 대해 다음을 따라야 한다.

  • equals(), hashCode(), 또는 toString()을 data class body 내에서 명시적으로 구현하거나 data class의 super class에서 final로 구현한 경우, 이러한 함수들은 생성되지 않고 구현된 것들이 사용된다.
  • 만약 (data class의) supertype(상위 타입.. data class가 상속 받은 class..?)이 오버라이딩 가능하고 호환 가능한 타입들을 반환하는 componentN() 함수를 가지고 있다면 대응되는 함수들은 data class에 생성되고 supertype의 그 함수들을 오버라이딩한다. 만약 supertype의 함수들이 호환 불가능한 특성을 가지고 있거나 final 처리가 되어있기 때문에 오버라이딩이 불가하면 에러가 보고된다.
  • componentN()과 copy() 함수들에 대한 명시적인 구현을 제공하는 것은 허용되지 않는다.

 

componentN()

더보기

Destructing declarations

val (name, age) = person
// 이러한 문법을 destructuring declaration이라고 부른다.
// 한 번에 여러개의 변수들을 생성할 수 있다.
println(name)
println(age)
// 위에서 선언된 name과 age는 독립적으로 사용할 수 있다.

// val (name, age) = person 을 컴파일하여 풀어보면 아래와 같다.
val name = person.component1() // person의 첫번째 property
val age = person.compoenent2() // person의 두번째 property

/*
person이 가진 property 수 N만큼 componentN()까지 호출 가능하다.
componentN() 함수들을 destructuring declaration에 쓰려면 operator 키워드로 마크해줘야 한다.
*/
// for 문에서도 사용된다.
// a는 component1()의 반환값을, b는 component2()의 반환값을 갖게 된다.
for ((a, b) in collection) { ... }

 

데이터 클래스는 다른 클래스들은 상속받을 수 있다.

JVM에서 생성된 클래스가 매개변수 없는 생성자를 가져야 한다면, property들에 대한 기본 값들이 정해져야 한다.

data class User(val name: String = "", val age: Int = 0)

 

Properties declared in the class body

컴퍼일러는 primary 생성자 안에 정의된 property들만을 자동으로 생성된 함수에 사용한다.

어떤 property를 생성된 함수들로부터 배제하고 싶다면, 그 property를 class body 안에 선언하면 된다.

data class Person(val name: String) {
    var age: Int = 0
}
data class Person(val name: String) {
    var age: Int = 0
}
fun main() {
    val person1 = Person("John")
    val person2 = Person("John")
    person1.age = 10 // data class body에 선언된 property
    person2.age = 20

    println("person1 == person2: ${person1 == person2}")
    // person1 == person2: true
    // == 연산자는 .equals()와 같다. 
    // age라는 property는 data class body 내부에 선언된 property이기 때문에 == 연산에서 제외할 수 있다.

    println("person1 with age ${person1.age}: ${person1}")
    // person1 with age 10: Person(name=John)
    // age는 data class body 내부에 선언되었으므로 .toString()에서 제외된다.

    println("person2 with age ${person2.age}: ${person2}")
    // person2 with age 20: Person(name=John)
    // age는 data class body 내부에 선언되었으므로 .toString()에서 제외된다.
    
    // age는 마찬가지로 .copy(), .hashCode(), .equals()에서 사용될 수 없다.
}

 

Copying

.copy() 함수를 사용하여 객체를 복사할 수 있고 몇몇 프로퍼티들의 값을 나머지 프로퍼티들의 변경 없이 대체할 수 있다.

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
// 바꾸고자 하는 프로퍼티명을 명시하여 값을 넣어준다.

 

Data classes and destructuring declaratioins

data classes에 대해 생성된 component functions들은 destructuring declarations에 사용하는 것이 가능하다.

val jane = User("Jane", 35)
val (name, age) = jane
println("$name, $age years of age")
// Jane, 35 years of age

 

Standard data classes

표준 라이브러리는 Pair와 Tirple이라는 클래스들을 제공한다. 그러나 이름이 붙여진 data classes이 더 좋은 설계라고 할 수 있는데, 왜나하면 data classes들은 프로퍼티들에 의미있는 이름을 붙임으로써 코드를 더 읽기 쉽게 만들기 때문이다.