Java 淺拷貝性能比較

一、前言
實(shí)際開發(fā)中,經(jīng)常會(huì)遇到對(duì)象拷貝的需求,本文就結(jié)合日常開發(fā)過程中,使用到的淺拷貝技術(shù),進(jìn)行性能比較,看看誰更強(qiáng)。
重要:下面將會(huì)花大量篇幅,列出各種類型淺拷貝的代碼,你可以直接拖到文章末尾,看性能對(duì)比結(jié)果。然后再根據(jù)你中意的對(duì)象回過頭來看它的代碼,避免疲勞。
首先創(chuàng)建一個(gè)用于拷貝的 Bean,如下所示:
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.RandomUtils;
import java.util.Date;
@Data
@Builder
public class User {
private long id;
private int age;
private String name;
private boolean isMale;
private School school;
private Date createDate;
public static User mock() {
return User.builder()
.id(RandomUtils.nextLong())
.age(RandomUtils.nextInt())
.name(RandomStringUtils.randomAlphanumeric(5))
.isMale(RandomUtils.nextBoolean())
.school(new School(RandomStringUtils.randomAlphanumeric(5), RandomUtils.nextInt()))
.createDate(new Date())
.build();
}
}
@AllArgsConstructor
class School {
private String name;
private int code;
}然后編寫一個(gè)模板類,給各個(gè)淺拷貝方法提供預(yù)熱和耗時(shí)統(tǒng)計(jì)功能:
public abstract class BaseCopyTest {
public List<User> prepareData(int size) {
List<User> list = new ArrayList<>(size);
IntStream.range(0, size).forEach(e -> list.add(User.mock()));
return list;
}
public User prepareOne() {
return User.mock();
}
public void testCopy(List<User> data) {
warnUp();
long startTime = System.currentTimeMillis();
copyLogic(data);
System.out.println(name() + ": " + (System.currentTimeMillis() - startTime) + "ms");
}
abstract void warnUp();
abstract void copyLogic(List<User> data);
abstract String name();
}二、工具類
首先介紹下工具類這邊,代表“工具類”參賽的選手有:
Apache BeanUtils——廉頗老矣
Spring BeanUtils——夕陽紅
Spring BeanCopier——三十而立
Spring BeanCopier + Reflectasm——身強(qiáng)力壯
2.1 Apache BeanUtils
Apache BeanUtils 算是一個(gè)比較古老的工具類,其自身是存在性能問題的,阿里巴巴開發(fā)手冊(cè)中也明確禁止使用該工具,本次對(duì)比仍然把它加進(jìn)來把。
想要用它需要導(dǎo)入依賴包:
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.4</version>
</dependency>public class ApacheBeanUtilsTest extends BaseCopyTest {
@Override
void warnUp() {
User source = prepareOne();
try {
User target = new User();
System.out.println(source);
BeanUtils.copyProperties(target, source);
System.out.println(target);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
void copyLogic(List<User> data) {
for(User source : data) {
try {
BeanUtils.copyProperties(new User(), source);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
String name() {
return "Apache BeanUtils";
}
}2.2 Spring BeanUtils
Spring BeanUtils 和 Apache Utils API 很像,但是在效率上比 Apache 效率更高,目前使用的人也不少。引入 spring-beans 依賴包后即可使用。
Spring BeanUtils 的 copyProperties() 方法,第一個(gè)是源對(duì)象,第二個(gè)是目標(biāo)對(duì)象。和 Apache BeanUtils 正好相反,要注意避免踩坑。
public class SpringBeanUtilsTest extends BaseCopyTest {
@Override
void warnUp() {
User source = prepareOne();
User target = new User();
System.out.println(source);
BeanUtils.copyProperties(source, target);
System.out.println(target);
}
@Override
void copyLogic(List<User> data) {
for(User source : data) {
BeanUtils.copyProperties(source, new User());
}
}
@Override
String name() {
return "Spring BeanUtils";
}
}2.3 Spring BeanCopier
Spring 還為我們提供了一種基于 Cglib 的淺拷貝方式 BeanCopier,引入 spring-core 依賴包后即可使用,它被認(rèn)為是取代 BeanUtils 的存在。
讓我們編寫一個(gè)工具類來使用 BeanCopier,如下所示:
public class BeanCopierUtils {
private static final Map<String, BeanCopier> CACHE = new ConcurrentHashMap<>();
public static void copyProperties(Object source, Object target) {
BeanCopier copier = getBeanCopier(source.getClass(), target.getClass());
copier.copy(source, target, null);
}
private static BeanCopier getBeanCopier(Class<?> sourceClazz, Class<?> targetClazz) {
String key = generatorKey(sourceClazz, targetClazz);
BeanCopier copier;
if(CACHE.containsKey(key)) {
copier = CACHE.get(key);
} else {
copier = BeanCopier.create(sourceClazz, targetClazz, false);
CACHE.put(key, copier);
}
return copier;
}
private static String generatorKey(Class<?> sourceClazz, Class<?> targetClazz) {
return sourceClazz + "_" + targetClazz;
}
}對(duì)應(yīng)的,編寫下它的測(cè)試類:
public class BeanCopierUtilsTest extends BaseCopyTest {
@Override
void warnUp() {
User source = prepareOne();
User target = new User();
System.out.println(source);
BeanCopierUtils.copyProperties(source, target);
System.out.println(target);
}
@Override
void copyLogic(List<User> data) {
for(User source : data) {
BeanCopierUtils.copyProperties(source, new User());
}
}
@Override
String name() {
return "Spring BeanCopier";
}
}2.4 Spring BeanCopier + Reflectasm
在大量對(duì)象拷貝過程中,new 操作往往是耗時(shí)的,Spring BeanCopier 并沒有解決 new 這個(gè)動(dòng)作。Reflectasm 是一個(gè)高性能的反射工具包,可以利用它來解決 new 步驟的耗時(shí)。使用 Reflectasm 需要引入依賴:
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>reflectasm</artifactId>
<version>1.11.9</version>
</dependency>改造 BeanCopierUtils 類代碼后如下:
public class BeanCopierReflectasmUtilsTest extends BaseCopyTest {
@Override
void warnUp() {
User source = prepareOne();
try {
System.out.println(source);
System.out.println(BeanCopierReflectasmUtils.copyProperties(source, User.class));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
void copyLogic(List<User> data) {
for(User source : data) {
User target = BeanCopierReflectasmUtils.copyProperties(source, User.class);
}
}
@Override
String name() {
return "Spring BeanCopier Reflectasm";
}
}如上所示,拷貝方法通過 class 進(jìn)行反射創(chuàng)建對(duì)象,并對(duì) ConstructorAccess 進(jìn)行緩存,提高效率。編寫下它對(duì)應(yīng)的測(cè)試類:
public class BeanCopierReflectasmUtilsTest extends BaseCopyTest {
@Override
void warnUp() {
User source = prepareOne();
try {
System.out.println(source);
System.out.println(BeanCopierReflectasmUtils.copyProperties(source, User.class));
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
void copyLogic(List<User> data) {
for(User source : data) {
User target = BeanCopierReflectasmUtils.copyProperties(source, User.class);
}
}
@Override
String name() {
return "Spring BeanCopier Reflectasm";
}
}三、原生類
回過頭來介紹下代表 Java “原生類”參賽的選手:
new——祖師爺
clone——瘦死的駱駝比馬大
3.1 new
咱們 java 面向?qū)ο缶幊虒W(xué)習(xí)的第一個(gè)關(guān)鍵字,非 new 莫屬了。雖然淺拷貝用 new 未免太過于傻瓜,但還是把它請(qǐng)出來,看看它的性能咋樣。
public class NewTest extends BaseCopyTest {
@Override
void warnUp() {
User source = prepareOne();
try {
User target = new User();
System.out.println(source);
target.setId(source.getId());
target.setAge(source.getAge());
target.setName(source.getName());
target.setMale(source.isMale());
target.setSchool(source.getSchool());
target.setCreateDate(source.getCreateDate());
System.out.println(target);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
void copyLogic(List<User> data) {
for(User source : data) {
User target = new User();
target.setId(source.getId());
target.setAge(source.getAge());
target.setName(source.getName());
target.setMale(source.isMale());
target.setSchool(source.getSchool());
target.setCreateDate(source.getCreateDate());
}
}
@Override
String name() {
return "Java New";
}
}3.2 clone
clone 也是 Java 原生提供的拷貝方法,并且據(jù)說性能還不錯(cuò),我司項(xiàng)目里面就還有許多用 clone 的實(shí)現(xiàn)。咱們也拉出來比劃比劃:
使用 clone 咱們得先讓對(duì)象實(shí)現(xiàn) Cloneable 接口,修改 User:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User implements Cloneable {
private long id;
private int age;
private String name;
private boolean isMale;
private School school;
private Date createDate;
public static User mock() {
return User.builder()
.id(RandomUtils.nextLong())
.age(RandomUtils.nextInt())
.name(RandomStringUtils.randomAlphanumeric(5))
.isMale(RandomUtils.nextBoolean())
.school(new School(RandomStringUtils.randomAlphanumeric(5), RandomUtils.nextInt()))
.createDate(new Date())
.build();
}
@Override
public Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return null;
}
}
@AllArgsConstructor
class School {
private String name;
private int code;
}四、Lombok
最后咱們?cè)蹃斫榻B下 Lombok 的淺拷貝,代表 Lombok 出場(chǎng)的有兩位選手:
toBuilder——后起之秀
newBuilder——迅雷不及掩耳之勢(shì)
4.1 toBuilder
想要開啟 Lombok 的 toBuilder 功能,需要將 User 類上方的 @Builder 修改為 @Builder(toBuilder = true) 即可,編寫它的測(cè)試類:
public class ToBuilderTest extends BaseCopyTest {
@Override
void warnUp() {
User source = prepareOne();
System.out.println(source);
System.out.println(source.toBuilder().build());
}
@Override
void copyLogic(List<User> data) {
for(User source : data) {
User target = source.toBuilder().build();
}
}
@Override
String name() {
return "Lombok toBuilder";
}
}4.2 newBuilder
再來介紹下 Lombok 的 newBuilder,它有點(diǎn)類似于 new,有點(diǎn)傻瓜,但也把它列出來,看看性能咋樣:
public class NewBuilderTest extends BaseCopyTest {
@Override
void warnUp() {
User source = prepareOne();
System.out.println(source);
System.out.println(this.copy(source));
}
@Override
void copyLogic(List<User> data) {
for(User source : data) {
User target = this.copy(source);
}
}
private User copy(User source) {
return User.builder()
.id(source.getId())
.age(source.getAge())
.name(source.getName())
.isMale(source.isMale())
.school(source.getSchool())
.createDate(source.getCreateDate())
.build();
}
@Override
String name() {
return "Lombok newBuilder";
}
}五、測(cè)試
經(jīng)過漫長的選手出場(chǎng)介紹,咱們終于可以進(jìn)行性能對(duì)比了。首先介紹下本機(jī)器配置信息:
Win10 專業(yè)版 1909
AMD Ryzen 5 3600 6-Core
16GB RAM
測(cè)試均采用單線程測(cè)試,壓測(cè)不同數(shù)據(jù)量情況下各種方式的耗時(shí)結(jié)果,測(cè)試結(jié)果如下(單位ms)。


排除掉 BeanUtils 后,結(jié)果如下:

最后簡單總結(jié)下:
禁止使用 Apache BeanUtils,性能差到離譜
不推薦使用 Spring BeanUtils,可以使用 Spring BeanCopier 替代
Spring BeanCopier Reflectasm 和 Spring BeanCopier 相比提升不了性能,但是寫起來更簡便(不需要顯式 new 對(duì)象)
Java 原生的 new 和 clone 性能很高,可以使用 clone
Lombok 的 toBuilder 速度也很快,并且寫起來很方便,推薦使用
作者: Jitwxs
鏈接: https://jitwxs.cn/a9fa88a0.html
關(guān)注GitHub今日熱榜,專注挖掘好用的開發(fā)工具,致力于分享優(yōu)質(zhì)高效的工具、資源、插件等,助力開發(fā)者成長!
點(diǎn)個(gè)在看 你最好看

