一. data class 的 copy() 為淺拷貝
淺拷貝是按位拷貝物件,它會建立一個新物件,這個物件有著原始物件屬性值的一份精確拷貝。如果屬性是基本型別,拷貝的就是基本型別的值;如果屬性是記憶體地址(引用型別),拷貝的就是記憶體地址 ,因此如果其中一個物件改變了這個地址,就會影響到另一個物件。
深拷貝會拷貝所有的屬性,並拷貝屬性指向的動態分配的記憶體。當物件和它所引用的物件一起拷貝時即發生深拷貝。深拷貝相比於淺拷貝速度較慢並且花銷較大。
data class 的 copy() 是複製函式,能夠複製一個物件的全部屬性,也能複製部分的屬性。
例如下面的程式碼:
data class Address(var street:String)
data class User(var name:String,var password:String,var address: Address)
fun main(args: Array<String>) {
val user1 = User("tony","123456", Address("renming"))
val user2 = user1.copy()
println(user2)
println(user1.address===user2.address) // 判斷 data class 的 copy 是否為淺拷貝,如果二者的address指向的記憶體地址相同則為淺拷貝,反之為深拷貝
val user3 = user1.copy("monica")
println(user3)
val user4 = user1.copy(password = "abcdef")
println(user4)
}
複製程式碼
執行結果:
User(name=tony, password=123456, address=Address(street=renming))
true
User(name=monica, password=123456, address=Address(street=renming))
User(name=tony, password=abcdef, address=Address(street=renming))
複製程式碼
user1.address===user2.address 列印的結果是 true 表示二者記憶體地址相同。 如果物件內部有引用型別的變數,通過拷貝後二者指向的是同一地址,表示為淺拷貝。所以 data class 的 copy 為淺拷貝。
當然,如果想實現深拷貝可以有很多種方式,比如使用序列化反序列化、一些開源庫(例如:github.com/enbandari/K…
本文接下來要介紹的不是深拷貝,但跟深拷貝會有一些關係,是 Java Bean 到 Java Bean 的之間的對映。這樣類似的工具有:Apache 的 BeanUtils、Dozer、MapStruct 等等。
二. MapStruct 簡介
MapStruct 是一個基於JSR 269的 Java 註釋處理器。開發者只需要定義一個 Mapper 介面,該介面宣告任何所需的對映方法。在編譯期間 MapStruct 將生成此介面的實現類。
使用 MapStruct 可以在兩個 Java Bean 之間實現自動對映的功能,只需要建立好介面。由於它是在編譯時自動建立具體的實現,因此無需反射等開銷,在效能上也會好於 Apache 的 BeanUtils、Dozer 等。
三. Kotlin 中使用 MapStruct
在 github 上找到了一個 MapStruct Kotlin 實現的開源專案:github.com/Pozo/mapstr…
3.1 mapstruct-kotlin 的安裝:
新增 kapt 外掛
apply plugin: 'kotlin-kapt'
複製程式碼
然後在專案中新增如下依賴:
api("com.github.pozo:mapstruct-kotlin:1.3.1.2")
kapt("com.github.pozo:mapstruct-kotlin-processor:1.3.1.2")
複製程式碼
另外,還需要新增如下依賴:
api("org.mapstruct:mapstruct:1.4.0.Beta3")
kapt("org.mapstruct:mapstruct-processor:1.4.0.Beta3")
複製程式碼
3.2 mapstruct-kotlin 的基本使用
對於需要使用 MapStruct 的 data class,必須加上一個@KotlinBuilder
註解
@KotlinBuilder
data class User(var name:String,var password:String,var address: Address)
@KotlinBuilder
data class UserDto(var name:String,var password:String,var address: Address)
複製程式碼
通過新增@KotlinBuilder
註解會在編譯時生成 UserBuilder、UserDtoBuilder 物件,他們在 Mapper 的實現類中被使用,用於建立物件以及對物件的賦值。
再定義一個 Mapper:
@Mapper
interface UserMapper {
fun toDto(user: User): UserDto
}
複製程式碼
這樣,就可以使用了。MapStruct 會在編譯時自動生成好 UserMapperImpl 類,完成將 User 物件轉換成 UserDto 物件。
fun main() {
val userMapper = UserMapperImpl()
val user = User("tony","123456", Address("renming"))
val userDto = userMapper.toDto(user)
println("${user.name},${user.address}")
}
複製程式碼
執行結果:
tony,Address(street=renming)
複製程式碼
3.3 mapstruct-kotlin 的複雜應用
對於稍微複雜的類:
// domain elements
@KotlinBuilder
data class Role(val id: Int, val name: String, val abbreviation: String?)
@KotlinBuilder
data class Person(val firstName: String, val lastName: String, val age: Int, val role: Role?)
// dto elements
@KotlinBuilder
data class RoleDto(val id: Int, val name: String, val abbreviation: String, val ignoredAttr: Int?)
@KotlinBuilder
data class PersonDto(
val firstName: String,
val phone: String?,
val birthDate: LocalDate?,
val lastName: String,
val age: Int,
val role: RoleDto?
)
複製程式碼
Person 類中還包含有 Role 類,以及 Person 跟 PersonDto 的屬性並不完全一致的情況。在 Mapper 介面中,支援使用@Mappings
來做對映。
@Mapper(uses = [RoleMapper::class])
interface PersonMapper {
@Mappings(
value = [
Mapping(target = "role", ignore = true),
Mapping(target = "phone", ignore = true),
Mapping(target = "birthDate", ignore = true),
Mapping(target = "role.id", source = "role.id"),
Mapping(target = "role.name", source = "role.name")
]
)
fun toDto(person: Person): PersonDto
@Mappings(
value = [
Mapping(target = "age", ignore = true),
Mapping(target = "role.abbreviation", ignore = true)
]
)
@InheritInverseConfiguration
fun toPerson(person: PersonDto): Person
}
複製程式碼
在 PersonMapper 的 toDto() 中,對於 PersonDto 沒有的屬性,在 Mapping 時可以使用ignore = true
。
下面來看看,將 person 對映成 personDto,以及 personDto 再映射回 person。
fun main() {
val role = Role(1, "role one", "R1")
val person = Person("Tony", "Shen", 20, role)
val personMapper = PersonMapperImpl()
val personDto = personMapper.toDto(person)
val personFromDto = personMapper.toPerson(personDto)
println("personDto.firstName=${personDto.firstName}")
println("personDto.role.id=${personDto.role?.id}")
println("personDto.phone=${personDto.phone}")
println("personFromDto.firstName=${personFromDto.firstName}")
println("personFromDto.age=${personFromDto.age}")
}
複製程式碼
執行結果:
personDto.firstName=Tony
personDto.role.id=1
personDto.phone=null
personFromDto.firstName=Tony
personFromDto.age=0
複製程式碼
由於 Person 沒有 phone 這個屬性並且在 Mapping 時忽略了,因此轉換成 PersonDto 後personDto.phone=null
。
而 PersonDto 雖然有 age 屬性,但是在 Mapping 時忽略了,因此轉換成 Person 後personFromDto.age=0
。
這樣的結果達到了我們的預期。
總結
在使用 Kotlin 的 data class 時,如果需要做 Java Bean 之間的對映,使用 MapStruct 是一個很不錯的選擇。