데이터 보관 목적으로 만든 클래스이다.
클래스 앞에 data를 붙여준다.
data class Site(val url: String, val title: String) {
val description = ""
}
자바로 변환해서 보면, 기본적인 toString(), hashCode(), equals(), copy(), componentsN() 메소드가 구현되어 있다.
데이터 클래스의 특징은 다음과 같다.
객체의 복사본을 만들어 리턴한다. 리턴되는 객체는 깊은 복사(swallow copy)로 생성된다. copy()의 인자로 생성자에 정의된 프로퍼티를 넘길 수 있으며, 그 프로퍼티의 값만 변경되고 나머지 값은 동일한 객체가 생성된다.
생성자에 정의된 프로퍼티만 출력하고, 클래스 내에 지역변수로 선언한 프로퍼티는 출력하지 않습니다. 지역변수도 toString()에 출력하고 싶으면 직접 오버라이드해서 구현해줘야 한다.
해시코드 값은 HashSet 같은 곳의 키 값으로 사용된다. 생성자의 멤버 변수의 값으로 만들어 진다.
public int hashCode() {
String var10000 = this.url;
int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
String var10001 = this.title;
return var1 + (var10001 != null ? var10001.hashCode() : 0);
}
따라서, 프로퍼티 값이 같은 객체면 hashCode의 값이 같게 나온다.
val peopleA = People("H43RO", 23)
val peopleB = People("H43RO", 23)
println(peopleA.hashCode())
println(peopleB.hashCode())
2110922579
2110922579
하지만, 생성자의 멤버 변수만 사용되기 때문에, 지역변수가 달라져도 같은 값이 나오게 된다.
두 객체를 비교해서 Boolean 타입을 리턴한다.
println(peopleA == peopleB)
'==' 연산자 하나로, 두 객체가 동일한 값을 담고있는지 쉽게 검사할 수 있다.
또한, === 연산도 가능하다. 메모리 상 다른 객체이므로 false 를 출력한다.
하지만, 여기서도 마찬가지로 생성자의 멤버 변수만 사용해서 비교하기 때문에, 지역변수가 달라져도 같은 같은 객체로 판단한다.
🤚🏻 Java vs Kotlin 동등성 비교
갖고 있는 값이 동일한지 검사
Java : equals()
Kotlin : ==
메모리상 같은 객체인지 검사
Java : ==
Kotlin : ===
각 프로퍼티에 번호가 붙어 구조 분해가 가능한 형태가 된다.
컴파일러에 의해 아래와 같이 구조 분해가 일어나게 된다.
val (name, age) = peopleA
위 코드를 자바로 디컴파일 해봤더니, 아래와 같이 나온다.
val name = peopleA.component1()
val age = peopleA.component2()
객체가 가지고 있는 여러 값을 분해해서 여러 변수에 한꺼번에 초기화
data class 이외에도 Pair와 Triple도 구조 분해 할당이 가능하다. 변수 선언이 들어갈 수 있는 곳이라면 어디든 구조 분해를 사용할 수 있다. 특히 Map이나 List에 대해 이터레이션(반복) 할 때 굉장히 유용하다.
Map
val map = mapOf("한식" to "김치찌개")
for((key, value) in map) { //루프 변수에 구조 분해 선언을 사용
println("$key: $value")
}
>>> 한식: 김치찌개
List
val list = listOf("김치찌개", "스시", "짜장면")
for((index, value) in list.withIndex()) { //루프 변수에 구조 분해 선언을 사용
// withIndex()를 사용하여 index와 value를 모두 받음
println("${index} -> ${value}")
}
>> 0 -> 김치찌개
>> 1 -> 스시
>> 2 -> 짜장면
데이터 클래스가 일반 클래스를 상속받을 수는 있다.
Data Class를 다른 Data Class에서 상속을 받을 수 없다는 얘기다.
그럼 왜 안되는 걸까?
예를 들면, A Data Class는 자동적으로 equlas를 구현할 것이고 이를 상속받은 B Data Class 또한 자동적으로 equlas를 구현한다.
이 때 equlas 메소드를 구현하는데 충돌이 발생할 것이기 때문에 코틀린에서는 이를 금지해놓았다.
하지만 Data Class를 다른 Data Class에서 상속받아 사용하고 싶을 수 있다.
이 때는 두 가지 선택지가 있는데, 하나는 추상 클래스이고 다른 하나는 인터페이스를 사용하는 것이다.
abstract class Base(open val data1: String)
data class A(override val data1: String): Base(data1)
data class B(override val data1: String, val data2: String): Base(data1)
일반 클래스를 상속을 받을 수 있다고 위에서 언급을 했는데, 왜 가능하면 쓰지 말라는걸까?
hashCode 함수는 생성자의 멤버 변수의 값으로 만들어 지는데 이것은 객체 간의 비교를 주소값으로 하지 않고 객체의 멤버변수의 값으로 하기 위함이다.
User 데이터 클래스로 선언된 a, b 변수는 모두 다른 메모리에서 선언 됐지만 hashCode 상으로는 동일한 값을 보이므로 동등성 비교에서는 같은 값을 보인다.
data class User(val age: Int, val name: String)
fun main() {
val a = User(20, "tom")
val b = User(20, "tom")
println("a == b: ${a == b} a.hashCode(): ${a.hashCode()}, b.hashCode(): ${b.hashCode()}")
}
a == b: true a.hashCode(): 115646, b.hashCode(): 115646
하지만 일반 클래스는 내부 멤버 변수의 값이 같더라도 동등성 비교는 같지 않다고 나온다.
class User2(val age: Int, val name: String)
fun main() {
val c = User2(20, "tom")
val d = User2(20, "tom")
println("c == d: ${c == d} c.hashCode(): ${c.hashCode()}. d.hashCode(): ${d.hashCode()}")
}
c == d: false c.hashCode(): 1554547125. d.hashCode(): 617901222
그래서 왜 상속하면 안되냐!!
만약 부모 클래스의 변수를 변경했을 때는 hashCode 값이 변경되지 않는다.
open class SuperUser(var gender: Int = 0)
data class User(val age: Int, val name: String) : SuperUser(0)
fun main() {
val a = User(20, "tom")
val b = User(20, "tom").apply {
gender = 1
}
println("a == b: ${a == b} a.hashCode(): ${a.hashCode()}, b.hashCode(): ${b.hashCode()}")
}
a == b: true a.hashCode(): 115646, b.hashCode(): 115646
data class 가 자동으로 생성한 hashCode 함수가 부모 클래스의 변수를 포함하지 않아서 발생하는 문제다. 그래서 data class에서 상속을 사용하는 경우 원래 의도했던 바와 다르게 행동할 수 있는 경우가 존재한다. 그래서 가능하면 상속을 쓰지 않는 것이 좋다. 꼭 써야 한다면 data class가 자동 생성하는 hashCode가 동작할 수 있도록 추상 클래스나 인터페이스를 활용하는 것이 좋다.
abstract class SuperUser { abstract var gender: Int }
data class User(val age: Int, val name: String, override var gender: Int) : SuperUser()