定义
data是kotlin中一种很重要的数据类型,用来代替java中的bean使用,反编译后多了一些方法,方便使用
与普通class不同之处
为了举例说明以下不同,定义data类 D1,D2,D3,普通class D4
data class D1<T>(
val id: Int,
val name: String,
val inner : D3,
val list: List<T>?,
)
data class D2(
val price: Double,
val desc: String,
val list: List<D3>?
)
data class D3(
val boss: String,
)
class D4(
val boss: String,
)
toString()方法
private fun m2(){
val d3 = D3("食尸鬼") // data类型
val d4 = D4("东京食尸鬼") // 普通class
println(d3.toString())
println(d4.toString())
}
D3(boss=食尸鬼) com.xxd.kt.data.base.D4@7a79be86
data自动生成的toString方法更加友好,可以完美打印数据类,而普通class不能自动生成这种打印。
更加高级的地方:
- 可以打印内部的data类型
可以打印List
// 测试打印,内部包含list
private fun m1() {
val d3List1 = listOf(D3("深渊领主"), D3("六翼天使"), D3("八岐大蛇"))
val d3List2 = listOf(D3("放射物"), D3("天地大冲撞"), D3("热力学第二定律"))
val d21 = D2(123356.33, "微不足道的boss", d3List1)
val d22 = D2(9999999999.99, "终极boss", d3List2)
val innerD3 = D3("内部boss,哈哈")
val d1 = D1(233, "毁灭的",innerD3, listOf(d21, d22))
println(d1.toString())
}
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
@NotNull
public String toString() {
return "D1(id=" + this.id + ", name=" + this.name + ", list=" + this.list + ")";
}
都是自动生成的方法,这里有个特殊之处,this.list
打印的并非地址,这是list特性,与数组打印不同
copy() 方法
先测试普通copy方法
// 测试data的copy方法
private fun m3() {
val d3List1 = listOf(D3("深渊领主"), D3("六翼天使"), D3("八岐大蛇"))
val d3List2 = listOf(D3("放射物"), D3("天地大冲撞"), D3("热力学第二定律"))
val d21 = D2(123356.33, "微不足道的boss", d3List1)
val d22 = D2(9999999999.99, "终极boss", d3List2)
val innerD3 = D3("内部boss,哈哈")
val d1 = D1(233, "毁灭的",innerD3, listOf(d21, d22))
val copy = d1.copy(id = 2) // 改变id的copy
println(d1)
println(copy)
println("${d1 === copy}") // 本身是否同一个对象,结论:不是
println("${d1.inner === copy.inner}") // copy后的内部obj是否同一个对象,结论:是
println("${d1.list === copy.list}") // copy后的内部list是否同一个对象,结论:是
}
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是浅拷贝,无法拷贝内部的数据,引用的还是同一个对象
手动来一层深拷贝
private fun m4() {
val d3List1 = listOf(D3("深渊领主"), D3("六翼天使"), D3("八岐大蛇"))
val d3List2 = listOf(D3("放射物"), D3("天地大冲撞"), D3("热力学第二定律"))
val d21 = D2(123356.33, "微不足道的boss", d3List1)
val d22 = D2(9999999999.99, "终极boss", d3List2)
val innerD3 = D3("内部boss,哈哈")
val d1 = D1(233, "毁灭的", innerD3, listOf(d21, d22))
val copyList = mutableListOf<D2>()
d1.list?.forEach {
copyList.add(it.copy())
}
val copy = d1.copy(inner = d1.inner.copy(), list = copyList) // 改变id的copy
println("${d1 === copy}") // 本身是否同一个对象,结论:不是
println("${d1.inner === copy.inner}")
println("${d1.list === copy.list}")
println("${d1.list!![0].list === copy.list!![0].list}")
}
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的包
// jackson序列化工具,做kotlin的deepCopy使用
api 'com.fasterxml.jackson.core:jackson-core:2.9.0'
api 'com.fasterxml.jackson.core:jackson-databind:2.9.0'
修改了一部分内容
package com.xxd.common.extend
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.databind.exc.InvalidFormatException
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
import com.fasterxml.jackson.databind.node.ArrayNode
import com.fasterxml.jackson.databind.node.JsonNodeType
import com.fasterxml.jackson.databind.node.ObjectNode
import com.xxd.common.extend.ArrayModificationMode.*
/** Exposed so you can configure it */
val mapper: ObjectMapper = ObjectMapper().findAndRegisterModules()
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.configure(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE, false)
.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true)
.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true)
/**
* Enables easy copying object tree with deeply nested properties.
* You can use this method on data classes as well as collections.
* If you mess up with [propertyPath]/[newValue], there are a couple of exceptions to show what's wrong.
*
* @param propertyPath property pointers separated by '/'. Use number as pointer to array element. Examples:
* order/creator/username; lines/0/lineId; 5/products
* @param newValue object of the same type as what is currently sitting at propertyPath
* @param arrayModificationMode if your last property pointer is array index, you can replace, add/insert or remove element at this index
*
* @throws IllegalArgumentException
* @throws IllegalStateException
* @throws InvalidFormatException
* @throws UnrecognizedPropertyException
* @throws IndexOutOfBoundsException
*
* @author Sergey Volkov
*
* 这里使用的jackson的解析,需要引入2个jackson的包
* 内部使用是序列化+反序列化的操作来实现深拷贝,而且不需要实现Serializable,Parcelable接口,非常方便
* 解析加入了缓存配置,初次反序列化时间比较长,需要300-500mm,之后再次反序列化都是100mm以内,10000个数据的集合反序列化页只花了44mm,100个数据的集合反序列化1mm
* 原来内实现了拷贝时替换某些字段,而且必须替换,这里增加了原始拷贝,不做替换操作
* github地址:https://github.com/sergey-volkov-lm/kotlin-deep-copy-helper
* 需要引入的2个jackson包:
* implementation 'com.fasterxml.jackson.core:jackson-core:2.9.0'
* implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.0'
*/
inline fun <reified T : Any> T.deepCopy(
propertyPath: String? = null,
newValue: Any? = null,
arrayModificationMode: ArrayModificationMode = REPLACE
): T = deepCopy(propertyPath, newValue, arrayModificationMode, object : TypeReference<T>() {})
/**
* You are not supposed to use this directly, but it reduces amount of inlined code
*/
fun <T : Any> T.deepCopy(
propertyPath: String?,
newValue: Any?,
arrayModificationMode: ArrayModificationMode,
type: TypeReference<T>
): T {
// 这里为修改原来的内容,不需要替换下,不需要做匹配替换操作,直接序列化,反序列化即可
if (propertyPath == null || propertyPath.isEmpty()) {
val sourceJsonNode = mapper.valueToTree<JsonNode>(this)
val resultJson = mapper.writeValueAsString(sourceJsonNode)
return mapper.readValue(resultJson, type)
}
val pathTokensRaw: List<String> = propertyPath.split('/')
require(!pathTokensRaw.contains("")) {
"propertyPath must not contain empty parts"
}
val onlyDigitsRegex = """\d+""".toRegex()
val wordRegex = """\w+""".toRegex()
val pathTokens: List<PathToken> = pathTokensRaw.map {
PathToken(
stringValue = it,
type = when {
it.matches(onlyDigitsRegex) -> {
PathTokenType.ARRAY_INDEX
}
it.matches(wordRegex) -> {
PathTokenType.PROPERTY
}
else -> {
throw IllegalArgumentException("propertyPath must contain only [A-Za-z0-9] chars")
}
},
)
}
val sourceJsonNode = mapper.valueToTree<JsonNode>(this)
val newValueJsonNode = mapper.valueToTree<JsonNode>(newValue)
var parentNode: JsonNode = sourceJsonNode
pathTokens.dropLast(1).forEach {
parentNode = if (it.type == PathTokenType.ARRAY_INDEX) {
parentNode.get(it.intValue) ?: error("Bad index in propertyPath")
} else { // property
parentNode.get(it.stringValue) ?: error("Bad property in propertyPath")
}
}
val lastPathToken = pathTokens.last()
when (parentNode.nodeType) {
JsonNodeType.ARRAY -> {
check(lastPathToken.type == PathTokenType.ARRAY_INDEX) {
"Bad propertyPath. Expected array index at the end."
}
val parentArrayNode = parentNode as ArrayNode
val index = lastPathToken.intValue
if (index > parentArrayNode.size()) {
throw IndexOutOfBoundsException("Can't set/add/insert element at index $index. Check propertyPath.")
}
when (arrayModificationMode) {
REPLACE -> {
parentArrayNode.set(index, newValueJsonNode)
}
INSERT_APPEND -> {
parentArrayNode.insert(index, newValueJsonNode)
}
REMOVE -> {
parentArrayNode.remove(index)
}
}
}
JsonNodeType.OBJECT -> {
check(lastPathToken.type == PathTokenType.PROPERTY) {
"Bad propertyPath. Expected property name at the end."
}
(parentNode as ObjectNode).set(lastPathToken.stringValue, newValueJsonNode)
}
else -> error("Unexpected parent JsonNode type: ${parentNode.nodeType}, raw value: $parentNode")
}
val resultJson = mapper.writeValueAsString(sourceJsonNode)
return mapper.readValue(resultJson, type)
}
data class PathToken(
val type: PathTokenType,
val stringValue: String
) {
val intValue: Int
get() = if (type == PathTokenType.ARRAY_INDEX)
stringValue.toInt()
else
error("PathToken $stringValue is property, not array! Check propertyPath.")
}
enum class PathTokenType {
/** Property name, to be specific */
PROPERTY,
/** Integer, starting from 0 */
ARRAY_INDEX
}
enum class ArrayModificationMode {
REPLACE,
INSERT_APPEND,
REMOVE
}