定义

data是kotlin中一种很重要的数据类型,用来代替java中的bean使用,反编译后多了一些方法,方便使用

与普通class不同之处

为了举例说明以下不同,定义data类 D1,D2,D3,普通class D4

  1. data class D1<T>(
  2. val id: Int,
  3. val name: String,
  4. val inner : D3,
  5. val list: List<T>?,
  6. )
  7. data class D2(
  8. val price: Double,
  9. val desc: String,
  10. val list: List<D3>?
  11. )
  12. data class D3(
  13. val boss: String,
  14. )
  15. class D4(
  16. val boss: String,
  17. )

toString()方法

  1. private fun m2(){
  2. val d3 = D3("食尸鬼") // data类型
  3. val d4 = D4("东京食尸鬼") // 普通class
  4. println(d3.toString())
  5. println(d4.toString())
  6. }

D3(boss=食尸鬼) com.xxd.kt.data.base.D4@7a79be86

data自动生成的toString方法更加友好,可以完美打印数据类,而普通class不能自动生成这种打印。

更加高级的地方:

  1. 可以打印内部的data类型
  2. 可以打印List

    1. // 测试打印,内部包含list
    2. private fun m1() {
    3. val d3List1 = listOf(D3("深渊领主"), D3("六翼天使"), D3("八岐大蛇"))
    4. val d3List2 = listOf(D3("放射物"), D3("天地大冲撞"), D3("热力学第二定律"))
    5. val d21 = D2(123356.33, "微不足道的boss", d3List1)
    6. val d22 = D2(9999999999.99, "终极boss", d3List2)
    7. val innerD3 = D3("内部boss,哈哈")
    8. val d1 = D1(233, "毁灭的",innerD3, listOf(d21, d22))
    9. println(d1.toString())
    10. }

    D1(id=233, name=毁灭的, inner=D3(boss=内部boss,哈哈), list=[D2(price=123356.33, desc=微不足道的boss, list=[D3(boss=深渊领主), D3(boss=六翼天使), D3(boss=八岐大蛇)]), D2(price=9.99999999999E9, desc=终极boss, list=[D3(boss=放射物), D3(boss=天地大冲撞), D3(boss=热力学第二定律)])])

内部的data类型,list都会将数据打印出来,除了打印log直观,还可以用做content内容是否改变的判断条件,在DiffUtil中很有用

翻译版后的toString

  1. @NotNull
  2. public String toString() {
  3. return "D1(id=" + this.id + ", name=" + this.name + ", list=" + this.list + ")";
  4. }

都是自动生成的方法,这里有个特殊之处,this.list打印的并非地址,这是list特性,与数组打印不同

copy() 方法

先测试普通copy方法

  1. // 测试data的copy方法
  2. private fun m3() {
  3. val d3List1 = listOf(D3("深渊领主"), D3("六翼天使"), D3("八岐大蛇"))
  4. val d3List2 = listOf(D3("放射物"), D3("天地大冲撞"), D3("热力学第二定律"))
  5. val d21 = D2(123356.33, "微不足道的boss", d3List1)
  6. val d22 = D2(9999999999.99, "终极boss", d3List2)
  7. val innerD3 = D3("内部boss,哈哈")
  8. val d1 = D1(233, "毁灭的",innerD3, listOf(d21, d22))
  9. val copy = d1.copy(id = 2) // 改变id的copy
  10. println(d1)
  11. println(copy)
  12. println("${d1 === copy}") // 本身是否同一个对象,结论:不是
  13. println("${d1.inner === copy.inner}") // copy后的内部obj是否同一个对象,结论:是
  14. println("${d1.list === copy.list}") // copy后的内部list是否同一个对象,结论:是
  15. }

D1(id=233, name=毁灭的, inner=D3(boss=内部boss,哈哈), list=[D2(price=123356.33, desc=微不足道的boss, list=[D3(boss=深渊领主), D3(boss=六翼天使), D3(boss=八岐大蛇)]), D2(price=9.99999999999E9, desc=终极boss, list=[D3(boss=放射物), D3(boss=天地大冲撞), D3(boss=热力学第二定律)])]) D1(id=2, name=毁灭的, inner=D3(boss=内部boss,哈哈), list=[D2(price=123356.33, desc=微不足道的boss, list=[D3(boss=深渊领主), D3(boss=六翼天使), D3(boss=八岐大蛇)]), D2(price=9.99999999999E9, desc=终极boss, list=[D3(boss=放射物), D3(boss=天地大冲撞), D3(boss=热力学第二定律)])]) false true true

结论:data类型的copy是浅拷贝,无法拷贝内部的数据,引用的还是同一个对象

手动来一层深拷贝

  1. private fun m4() {
  2. val d3List1 = listOf(D3("深渊领主"), D3("六翼天使"), D3("八岐大蛇"))
  3. val d3List2 = listOf(D3("放射物"), D3("天地大冲撞"), D3("热力学第二定律"))
  4. val d21 = D2(123356.33, "微不足道的boss", d3List1)
  5. val d22 = D2(9999999999.99, "终极boss", d3List2)
  6. val innerD3 = D3("内部boss,哈哈")
  7. val d1 = D1(233, "毁灭的", innerD3, listOf(d21, d22))
  8. val copyList = mutableListOf<D2>()
  9. d1.list?.forEach {
  10. copyList.add(it.copy())
  11. }
  12. val copy = d1.copy(inner = d1.inner.copy(), list = copyList) // 改变id的copy
  13. println("${d1 === copy}") // 本身是否同一个对象,结论:不是
  14. println("${d1.inner === copy.inner}")
  15. println("${d1.list === copy.list}")
  16. println("${d1.list!![0].list === copy.list!![0].list}")
  17. }

false false false true

结论:手动实现的深拷贝已经不是同一个对象,但是因为只手动拷贝了一层,第二层中还是同一个对象,所以最后一个判断结果为true

深拷贝

在使用data数据类型时,很多时候都需要进行深拷贝。
如:Android中的RecyclerView使用DiffUtil进行比较,如果不深拷贝data数据,永远不会产生payload局部刷新。

深拷贝方法很多,这里最终选择的序列化+反序列化的方式进行深拷贝,因为这种方法简单,不需要考虑反射等问题,而且兼容list,map,array等的深拷贝。

github地址:https://github.com/sergey-volkov-lm/kotlin-deep-copy-helper
需要引入jackson的包

  1. // jackson序列化工具,做kotlin的deepCopy使用
  2. api 'com.fasterxml.jackson.core:jackson-core:2.9.0'
  3. api 'com.fasterxml.jackson.core:jackson-databind:2.9.0'

修改了一部分内容

  1. package com.xxd.common.extend
  2. import com.fasterxml.jackson.core.JsonGenerator
  3. import com.fasterxml.jackson.core.type.TypeReference
  4. import com.fasterxml.jackson.databind.DeserializationFeature
  5. import com.fasterxml.jackson.databind.JsonNode
  6. import com.fasterxml.jackson.databind.ObjectMapper
  7. import com.fasterxml.jackson.databind.SerializationFeature
  8. import com.fasterxml.jackson.databind.exc.InvalidFormatException
  9. import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
  10. import com.fasterxml.jackson.databind.node.ArrayNode
  11. import com.fasterxml.jackson.databind.node.JsonNodeType
  12. import com.fasterxml.jackson.databind.node.ObjectNode
  13. import com.xxd.common.extend.ArrayModificationMode.*
  14. /** Exposed so you can configure it */
  15. val mapper: ObjectMapper = ObjectMapper().findAndRegisterModules()
  16. .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
  17. .configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false)
  18. .configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true)
  19. .configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true)
  20. /**
  21. * Enables easy copying object tree with deeply nested properties.
  22. * You can use this method on data classes as well as collections.
  23. * If you mess up with [propertyPath]/[newValue], there are a couple of exceptions to show what's wrong.
  24. *
  25. * @param propertyPath property pointers separated by '/'. Use number as pointer to array element. Examples:
  26. * order/creator/username; lines/0/lineId; 5/products
  27. * @param newValue object of the same type as what is currently sitting at propertyPath
  28. * @param arrayModificationMode if your last property pointer is array index, you can replace, add/insert or remove element at this index
  29. *
  30. * @throws IllegalArgumentException
  31. * @throws IllegalStateException
  32. * @throws InvalidFormatException
  33. * @throws UnrecognizedPropertyException
  34. * @throws IndexOutOfBoundsException
  35. *
  36. * @author Sergey Volkov
  37. *
  38. * 这里使用的jackson的解析,需要引入2个jackson的包
  39. * 内部使用是序列化+反序列化的操作来实现深拷贝,而且不需要实现Serializable,Parcelable接口,非常方便
  40. * 解析加入了缓存配置,初次反序列化时间比较长,需要300-500mm,之后再次反序列化都是100mm以内,10000个数据的集合反序列化页只花了44mm,100个数据的集合反序列化1mm
  41. * 原来内实现了拷贝时替换某些字段,而且必须替换,这里增加了原始拷贝,不做替换操作
  42. * github地址:https://github.com/sergey-volkov-lm/kotlin-deep-copy-helper
  43. * 需要引入的2个jackson包:
  44. * implementation 'com.fasterxml.jackson.core:jackson-core:2.9.0'
  45. * implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.0'
  46. */
  47. inline fun <reified T : Any> T.deepCopy(
  48. propertyPath: String? = null,
  49. newValue: Any? = null,
  50. arrayModificationMode: ArrayModificationMode = REPLACE
  51. ): T = deepCopy(propertyPath, newValue, arrayModificationMode, object : TypeReference<T>() {})
  52. /**
  53. * You are not supposed to use this directly, but it reduces amount of inlined code
  54. */
  55. fun <T : Any> T.deepCopy(
  56. propertyPath: String?,
  57. newValue: Any?,
  58. arrayModificationMode: ArrayModificationMode,
  59. type: TypeReference<T>
  60. ): T {
  61. // 这里为修改原来的内容,不需要替换下,不需要做匹配替换操作,直接序列化,反序列化即可
  62. if (propertyPath == null || propertyPath.isEmpty()) {
  63. val sourceJsonNode = mapper.valueToTree<JsonNode>(this)
  64. val resultJson = mapper.writeValueAsString(sourceJsonNode)
  65. return mapper.readValue(resultJson, type)
  66. }
  67. val pathTokensRaw: List<String> = propertyPath.split('/')
  68. require(!pathTokensRaw.contains("")) {
  69. "propertyPath must not contain empty parts"
  70. }
  71. val onlyDigitsRegex = """\d+""".toRegex()
  72. val wordRegex = """\w+""".toRegex()
  73. val pathTokens: List<PathToken> = pathTokensRaw.map {
  74. PathToken(
  75. stringValue = it,
  76. type = when {
  77. it.matches(onlyDigitsRegex) -> {
  78. PathTokenType.ARRAY_INDEX
  79. }
  80. it.matches(wordRegex) -> {
  81. PathTokenType.PROPERTY
  82. }
  83. else -> {
  84. throw IllegalArgumentException("propertyPath must contain only [A-Za-z0-9] chars")
  85. }
  86. },
  87. )
  88. }
  89. val sourceJsonNode = mapper.valueToTree<JsonNode>(this)
  90. val newValueJsonNode = mapper.valueToTree<JsonNode>(newValue)
  91. var parentNode: JsonNode = sourceJsonNode
  92. pathTokens.dropLast(1).forEach {
  93. parentNode = if (it.type == PathTokenType.ARRAY_INDEX) {
  94. parentNode.get(it.intValue) ?: error("Bad index in propertyPath")
  95. } else { // property
  96. parentNode.get(it.stringValue) ?: error("Bad property in propertyPath")
  97. }
  98. }
  99. val lastPathToken = pathTokens.last()
  100. when (parentNode.nodeType) {
  101. JsonNodeType.ARRAY -> {
  102. check(lastPathToken.type == PathTokenType.ARRAY_INDEX) {
  103. "Bad propertyPath. Expected array index at the end."
  104. }
  105. val parentArrayNode = parentNode as ArrayNode
  106. val index = lastPathToken.intValue
  107. if (index > parentArrayNode.size()) {
  108. throw IndexOutOfBoundsException("Can't set/add/insert element at index $index. Check propertyPath.")
  109. }
  110. when (arrayModificationMode) {
  111. REPLACE -> {
  112. parentArrayNode.set(index, newValueJsonNode)
  113. }
  114. INSERT_APPEND -> {
  115. parentArrayNode.insert(index, newValueJsonNode)
  116. }
  117. REMOVE -> {
  118. parentArrayNode.remove(index)
  119. }
  120. }
  121. }
  122. JsonNodeType.OBJECT -> {
  123. check(lastPathToken.type == PathTokenType.PROPERTY) {
  124. "Bad propertyPath. Expected property name at the end."
  125. }
  126. (parentNode as ObjectNode).set(lastPathToken.stringValue, newValueJsonNode)
  127. }
  128. else -> error("Unexpected parent JsonNode type: ${parentNode.nodeType}, raw value: $parentNode")
  129. }
  130. val resultJson = mapper.writeValueAsString(sourceJsonNode)
  131. return mapper.readValue(resultJson, type)
  132. }
  133. data class PathToken(
  134. val type: PathTokenType,
  135. val stringValue: String
  136. ) {
  137. val intValue: Int
  138. get() = if (type == PathTokenType.ARRAY_INDEX)
  139. stringValue.toInt()
  140. else
  141. error("PathToken $stringValue is property, not array! Check propertyPath.")
  142. }
  143. enum class PathTokenType {
  144. /** Property name, to be specific */
  145. PROPERTY,
  146. /** Integer, starting from 0 */
  147. ARRAY_INDEX
  148. }
  149. enum class ArrayModificationMode {
  150. REPLACE,
  151. INSERT_APPEND,
  152. REMOVE
  153. }