Java 浅拷贝性能比较
一、前言
实际开发中,经常会遇到对象拷贝的需求,本文就结合日常开发过程中,使用到的浅拷贝技术,进行性能比较,看看谁更强。
重要:下面将会花大量篇幅,列出各种类型浅拷贝的代码,你可以直接拖到文章末尾,看性能对比结果。然后再根据你中意的对象回过头来看它的代码,避免疲劳。
首先创建一个用于拷贝的 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;
}
然后编写一个模板类,给各个浅拷贝方法提供预热和耗时统计功能:
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——身强力壮
2.1 Apache BeanUtils
Apache BeanUtils 算是一个比较古老的工具类,其自身是存在性能问题的,阿里巴巴开发手册中也明确禁止使用该工具,本次对比仍然把它加进来把。
想要用它需要导入依赖包:
<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() 方法,第一个是源对象,第二个是目标对象。和 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 依赖包后即可使用,它被认为是取代 BeanUtils 的存在。
让我们编写一个工具类来使用 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;
}
}
对应的,编写下它的测试类:
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
在大量对象拷贝过程中,new 操作往往是耗时的,Spring BeanCopier 并没有解决 new 这个动作。Reflectasm 是一个高性能的反射工具包,可以利用它来解决 new 步骤的耗时。使用 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 进行反射创建对象,并对 ConstructorAccess 进行缓存,提高效率。编写下它对应的测试类:
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 面向对象编程学习的第一个关键字,非 new 莫属了。虽然浅拷贝用 new 未免太过于傻瓜,但还是把它请出来,看看它的性能咋样。
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 原生提供的拷贝方法,并且据说性能还不错,我司项目里面就还有许多用 clone 的实现。咱们也拉出来比划比划:
使用 clone 咱们得先让对象实现 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
最后咱们咱来介绍下 Lombok 的浅拷贝,代表 Lombok 出场的有两位选手:
toBuilder——后起之秀
newBuilder——迅雷不及掩耳之势
4.1 toBuilder
想要开启 Lombok 的 toBuilder 功能,需要将 User 类上方的 @Builder 修改为 @Builder(toBuilder = true) 即可,编写它的测试类:
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,它有点类似于 new,有点傻瓜,但也把它列出来,看看性能咋样:
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";
}
}
五、测试
经过漫长的选手出场介绍,咱们终于可以进行性能对比了。首先介绍下本机器配置信息:
Win10 专业版 1909
AMD Ryzen 5 3600 6-Core
16GB RAM
测试均采用单线程测试,压测不同数据量情况下各种方式的耗时结果,测试结果如下(单位ms)。
排除掉 BeanUtils 后,结果如下:
最后简单总结下:
禁止使用 Apache BeanUtils,性能差到离谱
不推荐使用 Spring BeanUtils,可以使用 Spring BeanCopier 替代
Spring BeanCopier Reflectasm 和 Spring BeanCopier 相比提升不了性能,但是写起来更简便(不需要显式 new 对象)
Java 原生的 new 和 clone 性能很高,可以使用 clone
Lombok 的 toBuilder 速度也很快,并且写起来很方便,推荐使用
作者: Jitwxs
链接: https://jitwxs.cn/a9fa88a0.html
关注GitHub今日热榜,专注挖掘好用的开发工具,致力于分享优质高效的工具、资源、插件等,助力开发者成长!
点个在看 你最好看