
이번 포스팅에서는 Kotlin data class의 특징에 대해 알아볼 예정입니다.
자주 사용하고 있었지만, 특징을 제대로 분석해 본 적은 없었네요😅
class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)
fun main() {
val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
println(gameData)
/*
Output
GameData@4e0e2f2a
*/
}
class를 사용한다면 toString을 직접 구현해야 합니다.
toString을 구현하지 않으면 위와 같이 className@hashCode 형태로 출력됩니다.
data class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)
fun main() {
val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
println(gameData)
/*
Output
GameData(gameId=1, year=2024, month=3, day=1)
*/
}
data class는 모든 프로퍼티 값을 보여주는 형태로 toString을 지원해줍니다.
equals는 동일한 클래스로부터 생성된 객체에 대하여, 프로퍼티 값을 바탕으로 서로 다른 객체를 같은 객체라고 판단할지 여부를 결정하는 함수입니다.
class의 경우, equals를 따로 정의하지 않은 경우에 두 객체의 메모리 주소가 같은지를 비교합니다. (Java Object 클래스의 equals와 동일)
class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)
fun main() {
val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
val gameData2 = GameData(gameId = 1, year = 2024, month = 3, day = 1)
println(gameData)
println(gameData2)
println(gameData.equals(gameData2)) // gameData == gameData2 와 동일하게 작동함
/*
Output
GameData@4e0e2f2a
GameData@73d16e93
false
*/
}
따라서 직접 equals 함수를 구현해야 프로퍼티를 기준으로 서로 다른 객체를 동일한 객체로 판단할 수 있습니다.
class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int) {
override fun equals(other: Any?): Boolean {
return if (other == null || other !is GameData) {
false
} else {
gameId == other.gameId && year == other.year && month == other.month && day == other.day
}
}
}
fun main() {
val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
val gameData2 = GameData(gameId = 1, year = 2024, month = 3, day = 1)
println(gameData)
println(gameData2)
println(gameData.equals(gameData2)) // gameData == gameData2 와 동일하게 작동함
/*
Output
GameData@4e0e2f2a
GameData@73d16e93
true
*/
}
하지만 data class는 equals를 지원하기 때문에, 별도의 구현 없이도 프로퍼티가 같은 객체를 서로 같은 객체로 판단할 수 있습니다.
data class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)
fun main() {
val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
val gameData2 = GameData(gameId = 1, year = 2024, month = 3, day = 1)
println(gameData)
println(gameData2)
println(gameData == gameData2)
/*
Output
GameData(gameId=1, year=2024, month=3, day=1)
GameData(gameId=1, year=2024, month=3, day=1)
true
*/
}
hashCode는 객체의 메모리 주소를 기반으로 생성되는 값을 의미합니다.
따라서 동일한 객체에 대해서는 항상 동일한 해시 코드가 생성되지만, 서로 다른 객체는 서로 다른 해시 코드가 생성됩니다.
이러한 특징 때문에 hashCode는 아래와 같은 상황에서 주로 사용됩니다.
HashMap, HashSet 과 같은 자료 구조에서 객체를 저장 및 검색할 때 사용따라서 hashCode 함수를 적절하게 오버라이딩해서 사용해야 하며,
data class는 이 hashCode 함수를 지원합니다.
data class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)
fun main() {
val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
val gameData2 = GameData(gameId = 1, year = 2024, month = 3, day = 1)
println(gameData.hashCode())
println(gameData2.hashCode())
/*
Output
1974949
1974949
*/
}
data class는 임의의 객체에 대하여, 생성자에 정의된 프로퍼티 값을 변경하여 복사하는 기능을 지원합니다.
data class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)
fun main() {
val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
val gameData2 = gameData.copy(gameId=2)
println(gameData)
println(gameData2)
/*
Output
GameData(gameId=1, year=2024, month=3, day=1)
GameData(gameId=2, year=2024, month=3, day=1)
*/
}
하지만 이는 얕은 복사이기 때문에, 프로퍼티로 객체를 가지고 있는 경우에는 깊은 복사를 따로 구현해야 합니다.
실제로 아래에서 gameData를 copy한 gameData2의 playerList를 변경했음에도 불구하고, gameData의 playerList도 동일하게 변경 됨을 확인할 수 있습니다.
data class PlayerData(val name: String, val number: Int)
data class GameData(
val gameId: Long,
val year: Int,
val month: Int,
val day: Int,
val playerList: ArrayList<PlayerData>
)
fun main() {
val gameData =
GameData(
gameId = 1,
year = 2024,
month = 3,
day = 1,
playerList = arrayListOf(PlayerData("기상호", 6), PlayerData("성준수", 31))
)
val gameData2 = gameData.copy(gameId = 2)
println(gameData)
println(gameData2)
gameData2.playerList.clear()
println(gameData)
println(gameData2)
/*
Output
GameData(gameId=1, year=2024, month=3, day=1, playerList=[PlayerData(name=기상호, number=6), PlayerData(name=성준수, number=31)])
GameData(gameId=2, year=2024, month=3, day=1, playerList=[PlayerData(name=기상호, number=6), PlayerData(name=성준수, number=31)])
GameData(gameId=1, year=2024, month=3, day=1, playerList=[])
GameData(gameId=2, year=2024, month=3, day=1, playerList=[])
*/
}
Kotlin 공식 문서에 따르면, data class는 Destructuring declarations를 지원한다고 합니다.
내부적으로는 componentN 함수가 생성되어, 생성자에 포함된 프로퍼티 순서대로 component1, component2 ... 와 같이 사용할 수 있습니다.
data class PlayerData(val name: String, val number: Int)
data class GameData(
val gameId: Long,
val year: Int,
val month: Int,
val day: Int,
val playerList: ArrayList<PlayerData>
)
fun main() {
val gameData =
GameData(
gameId = 1,
year = 2024,
month = 3,
day = 1,
playerList = arrayListOf(PlayerData("기상호", 6), PlayerData("성준수", 31))
)
val (gameId, year, month, day) = gameData
val (_, _, _, _, playerList) = gameData
println("gameId=$gameId, year=$year, month=$month, day=$day, playerList=$playerList")
/*
Output
gameId=1, year=2024, month=3, day=1, playerList=[PlayerData(name=기상호, number=6), PlayerData(name=성준수, number=31)]
*/
}