Java 浅拷贝性能比较
|字数总计:2.5k|阅读时长:12分钟|阅读量:
本文提供相关源码,请放心食用,详见网页侧边栏或底部,有疑问请评论或 Issue
一、前言
实际开发中,经常会遇到对象拷贝的需求,本文就结合日常开发过程中,使用到的浅拷贝技术,进行性能比较,看看谁更强。
重要: 下面将会花大量篇幅,列出各种类型浅拷贝的代码,你可以直接拖到文章末尾,看性能对比结果。然后再根据你中意的对象回过头来看它的代码,避免疲劳。
首先创建一个用于拷贝的 Bean,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| 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; }
|
然后编写一个模板类,给各个浅拷贝方法提供预热和耗时统计功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| 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
算是一个比较古老的工具类,其自身是存在性能问题的,阿里巴巴开发手册中也明确禁止使用该工具,本次对比仍然把它加进来把。
想要用它需要导入依赖包:
1 2 3 4 5
| <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.4</version> </dependency>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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 正好相反,要注意避免踩坑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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; } }
|
对应的,编写下它的测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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 需要引入依赖:
1 2 3 4 5
| <dependency> <groupId>com.esotericsoftware</groupId> <artifactId>reflectasm</artifactId> <version>1.11.9</version> </dependency>
|
改造 BeanCopierUtils 类代码后如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| 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
进行缓存,提高效率。编写下它对应的测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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 “原生类”参赛的选手:
3.1 new
咱们 java 面向对象编程学习的第一个关键字,非 new 莫属了。虽然浅拷贝用 new 未免太过于傻瓜,但还是把它请出来,看看它的性能咋样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| @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; }
|
再来编写它的测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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)
即可,编写它的测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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,有点傻瓜,但也把它列出来,看看性能咋样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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)。
类别 |
1K |
1W |
10W |
100W |
500W |
1000W |
Apache BeanUtils |
27 |
134 |
1331 |
12902 |
28231 |
128566 |
Spring BeanUtils |
4 |
21 |
217 |
1949 |
2004 |
19755 |
Spring BeanCopier |
1 |
6 |
60 |
546 |
528 |
5114 |
Spring BeanCopier Reflectasm |
2 |
8 |
72 |
569 |
563 |
5325 |
Java New |
0 |
3 |
21 |
47 |
44 |
192 |
Java Clone |
0 |
2 |
15 |
92 |
95 |
834 |
Lombok toBuilder |
0 |
1 |
10 |
40 |
42 |
263 |
Lombok newBuilder |
0 |
1 |
8 |
40 |
43 |
273 |
排除掉 BeanUtils 后,结果如下:
最后简单总结下:
- 禁止使用 Apache BeanUtils,性能差到离谱
- 不推荐使用 Spring BeanUtils,可以使用 Spring BeanCopier 替代
- Spring BeanCopier Reflectasm 和 Spring BeanCopier 相比提升不了性能,但是写起来更简便(不需要显式 new 对象)
- Java 原生的 new 和 clone 性能很高,可以使用 clone
- Lombok 的 toBuilder 速度也很快,并且写起来很方便,推荐使用