이번 포스팅에서는 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)]
*/
}