Java 浅拷贝性能比较
source link: https://www.jitwxs.cn/a9fa88a0.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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 BeanCopierReflectasmUtils {
private static final Map<String, BeanCopier> BEAN_COPIER_MAP = new ConcurrentHashMap<>();
private static final Map<String, ConstructorAccess> CONSTRUCTOR_ACCESS_CACHE = new ConcurrentHashMap<>();
private static final int MAX_CACHE_SIZE = 512;
public static void copyProperties(Object source, Object target) {
BeanCopier copier = getBeanCopier(source.getClass(), target.getClass());
copier.copy(source, target, null);
}
public static <T> T copyProperties(T source, Class<T> targetClass) {
if (source == null) {
return null;
}
T target;
try {
ConstructorAccess<T> constructorAccess = getConstructorAccess(targetClass);
target = constructorAccess.newInstance();
} catch (RuntimeException e) {
try {
target = targetClass.newInstance();
} catch (InstantiationException | IllegalAccessException e1) {
throw new RuntimeException(String.format("Create new instance of %s failed: %s", targetClass, e.getMessage()));
}
}
copyProperties(source, target);
return target;
}
public static <T> List<T> copyProperties(List<?> sourceList, Class<T> targetClass) {
if (CollectionUtils.isEmpty(sourceList)) {
return Collections.emptyList();
}
ConstructorAccess<T> constructorAccess = getConstructorAccess(targetClass);
List<T> resultList = new ArrayList<>(sourceList.size());
for (Object source : sourceList) {
T target;
try {
target = constructorAccess.newInstance();
} catch (RuntimeException e) {
try {
target = targetClass.newInstance();
} catch (InstantiationException | IllegalAccessException e1) {
throw new RuntimeException(String.format("Create new instance of %s failed: %s", targetClass, e.getMessage()));
}
}
copyProperties(source, target);
resultList.add(target);
}
return resultList;
}
private static <T> ConstructorAccess<T> getConstructorAccess(Class<T> targetClass) {
ConstructorAccess<T> constructorAccess = CONSTRUCTOR_ACCESS_CACHE.get(targetClass.getName());
if(constructorAccess != null) {
return constructorAccess;
}
try {
constructorAccess = ConstructorAccess.get(targetClass);
if (CONSTRUCTOR_ACCESS_CACHE.size() > MAX_CACHE_SIZE) {
CONSTRUCTOR_ACCESS_CACHE.clear();
}
CONSTRUCTOR_ACCESS_CACHE.put(targetClass.getName(),constructorAccess);
} catch (Exception e) {
throw new RuntimeException(String.format("Create new instance of %s failed: %s", targetClass, e.getMessage()));
}
return constructorAccess;
}
private static BeanCopier getBeanCopier(Class<?> sourceClazz, Class<?> targetClazz) {
String key = generatorKey(sourceClazz, targetClazz);
BeanCopier copier;
if(BEAN_COPIER_MAP.containsKey(key)) {
copier = BEAN_COPIER_MAP.get(key);
} else {
copier = BeanCopier.create(sourceClazz, targetClazz, false);
BEAN_COPIER_MAP.put(key, copier);
}
return copier;
}
private static String generatorKey(Class<?> sourceClazz, Class<?> targetClazz) {
return sourceClazz + "_" + targetClazz;
}
}
如上所示,拷贝方法通过 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;
}
再来编写它的测试类:
public class CloneTest extends BaseCopyTest {
@Override
void warnUp() {
User source = prepareOne();
System.out.println(source);
System.out.println(source.clone());
}
@Override
void copyLogic(List<User> data) {
for(User source : data) {
Object target = source.clone();
}
}
@Override
String name() {
return "Java Clone";
}
}
四、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 速度也很快,并且写起来很方便,推荐使用
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK