Java对象的类型转换和属性复制

1. 问题的源由

在J2EE项目开发中,会涉及很多领域模型对象,例如,

  • VO (View Object) 视图对象,也叫展示对象,用于前端页面渲染所需要的数据
  • DTO (Data Transfer Object) 数据传输对象,一般用于Service和Manager向外传输数据
  • PO (Persistent Object) 持久化对象,一般和数据库表结构会形成一一映射关系,通过DAO层向上传输数据源对象

更多领域分层模型,及其所在的应用分层,可见阿里巴巴Java开发手册中的工程规约定义。

这些VO/DTO/PO都是POJO对象,每个属性都有getter/setter方法的定义,其数据转换链条大概为,

数据库DB 》PO 》DTO 》VO

可以看到,在项目中不同的应用分层DB/DAO/Service/Manager/Web,经常会需要把一个数据对象转换为其它类型的数据对象,转换的同时复制部分对象属性,有时部分属性需要从多个数据对象获取进行组装。这些数据的转换有一个共同的需求,那就是从一个源对象变为目标对象

源对象 Source s 》目标对象 Target t

接下来我们就需要讨论这个转换过程。

2. 单个对象的转换

1)简单的Java对象转换,使用BeanUtils.copyProperties

使用Spring都知道Spring Beans中提供了一个BeanUtils,可以用于对象的属性复制,其使用方法为,

    Target t = new Target();
    BeanUtils.copyProperties(s, t);
    BeanUtils.copyProperties(s, t, 'password');

其中,第二个copyProperties方法中第三个参数是告知复制过程中忽略复制'password'这个属性。

2)单个对象的转换,指定目标类

BeanUtils.copyProperties不提供目标对象的创建,因此每次都需要new出一个目标对象。为了能够省去这一步,可以使用下面的方法。

实现代码

public static <S, T> T convert(S s, Class<T> clazz) {
  T t = null;
  
  try {
    t = clazz.newInstance();
    BeanUtils.copyProperties(s, t);
  } catch (Exception e) {
    e.printStackTrace();
  }
  
  return t;
}

上述方法将对象的生成封装起来返回。

使用样例

Target t = convert(s, Target.class);

使用中直接指定目标类,代码更加简洁易懂。

3. List/Set集合之间的转换

在很多时候,我们不仅处理一个对象的转换,而是面对集合的对象,这时需要能够对集合中的对象进行批量处理转换。

1)List集合转换,将列表中每一个实体转换为目标类

实现代码

public static <S, T> List<T> convert(Iterable<S> iterable, Class<T> clazz) {
  return StreamSupport.stream(iterable.spliterator(), false)
                      .map(s -> convert(s, clazz))
                      .collect(Collectors.toList());
}

使用样例

List<Target> targets = convert(srcCollection, Target.class);
2)List集合转换,提供一个mapper转换方法,将列表中每一个实体对象转换为目标类

实现代码

public static <S, T> List<T> convert(Iterable<S> iterable, 
           Function<? super S, ? extends T> mapper) {
  return StreamSupport.stream(iterable.spliterator(), false)
                      .map(mapper)
                      .collect(Collectors.toList());
}

使用样例

List<Target> targets = convert(srcCollection, mapper);

其中mapper的定义为Function<? super S, ? extends T>,即该函数需要一个源对象作为输入参数,返回一个目标对象

3)List集合转换,转换为指定的List/Set容器类,集合中的实体对象不变

实现代码

public static <S, U extends Collection<S>> U collect(Iterable<S> iterable, CollectionFactory<S, U> factory) {
  U collection = factory.createCollection();
  iterable.forEach(collection::add);
  return collection;
}

其中CollectionFactory为一个集合类工厂。

使用样例

Iterable<Integer> iterable = IntStream.range(0, 5).boxed().collect(Collectors.toList());
ArrayList<Integer> arrayList = collect(iterable, ArrayList::new);
HashSet<Integer> hashSet = collect(iterable, HashSet::new);
LinkedList<Integer> linkedList = collect(iterable, LinkedList::new);

上述将一个Collection集合转换为ArrayList/HashSet/LinkedList,集合中的整型变量对象保持不变。

4)List集合转换,转换为指定的List/Set容器类,并将集合中的实体对象转换为目标类

实现代码

public static <S, U extends Collection<T>, T> U collect(Iterable<S> iterable, Class<T> clazz, CollectionFactory<T, U> factory) {
  Iterable<T> list = convert(iterable, clazz);
  U collection = factory.createCollection();
  list.forEach(collection::add);
  return collection;
}

使用样例

Iterable<UserEntity> users = UserDao.SingleInstance.findAll();
ArrayList<UserVO> arrayList = collect(users, UserVO.class, ArrayList::new);
HashSet<UserVO> hashSet = collect(users, UserVO.class, HashSet::new);
LinkedList<UserVO> linkedList = collect(users, UserVO.class, LinkedList::new);

上述将一个Collection集合转换为ArrayList/HashSet/LinkedList,同时集合中的UserEntity变量对象转换为了UserVO。

4. List/Set/Map之间的转换

Java中Map的处理效率比List/Set高效不少,有些时候需要在List/Set/Map之间进行转换。

1)List集合转换,转换为Map集合,提供一个keyGenerator生成key,实体对象保存为value

实现代码

public static <S> Map<String, S> map(Iterable<S> iterable, Function<? super S, String> keyGenerator) {
  return StreamSupport.stream(iterable.spliterator(), false)
         .collect(Collectors.toMap(keyGenerator, Function.identity()));
}

使用样例

List<UserEntity> users = UserDao.SingleInstance.findAll();
Map<String, UserEntity> result = map(users, UserEntity::getName);

上述样例中由UserEntity::getName来生成对象的键值,其必须为保证唯一。

2)Map集合转换,转换为List集合,实体对象保持不变

实现代码

public static <K, S, U extends Collection<S>> U collect(Map<K, S> map, CollectionFactory<S, U> factory) {
  U collection = factory.createCollection();
  map.values().iterator().forEachRemaining(collection::add);
  return collection;
}

使用样例

Map<String, UserEntity> users;
List<UserEntity> list1 = collect(users, ArrayList::new);
Set<UserEntity> set = collect(users, HashSet::new);
List<UserEntity> list2 = collect(users, LinkedList::new);
Collection collection = users.values();

上述将一个用户的Map集合转换为List集合,可以看到一个简单获得List集合的方法就是使用map.values()方法。

3)Map集合转换,转换为指定的Map容器类

实现代码

public static <K, S, T, U extends Map<K, T>> U map(Map<K, S> srcMap, Class<T> clazz,  MapFactory<K, T, U> factory) {
  U u = factory.createMap();
  srcMap.entrySet()
        .forEach(ksEntry -> u.put(ksEntry.getKey(),
                          convert(ksEntry.getValue(), clazz)));
  return u;
}

使用样例

Map<String, UserEntity> users;
Map<String, UserVO> m1 = map(users, UserVO.class, HashMap<String, UserVO>::new);
Map<String, UserVO> m2 = map(users, UserVO.class, LinkedHashMap<String, UserVO>::new);
Hashtable<String, UserVO> m3 = map(users, UserVO.class, Hashtable<String, UserVO>::new);

上述将一个用户的Map集合转换为List集合,可以看到一个简单获得List集合的方法就是使用map.values()方法。

5. 源代码

演示代码仓库地址:https://gitee.com/pphh/blog,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/blog.git

本篇博文的程序代码样例在文件路径171201_java_object_copy\171201_java_object_copy_convert中,请使用JDK8及后续版本编译并运行项目代码。

6. 参考资料

  • POJO阿里巴巴Java开发手册(2017版本):应用分层和领域模型PO/DTO/VO
  • 百度百科:POJO简单Java对象

Java集合框架和各实现类性能测试

Java语言集合框架提供一系列集合接口类 (collection interface)和实现类,满足对集合中元素对象的各种集合抽象操作。

1. 集合接口类Collection/List/Set/Map

下图显示了在包java.util.*中主要的核心集合接口类,

上图中的Collection/List/Set/Map等都是泛型接口,各自的定义和作用如下:

  • Collection是集合的顶层接口定义,其它的集合类都继承于Collection(除Map),这个接口定义了对集合元素的增删改查,及其提供interator用于循环整个集合中的元素列表等等。
  • Set是一个不能有重复元素的集合。
  • List是一个有序元素集合,有序是指按照加入/插入数组位置的顺序,集合中可以有重复的元素,可以认为List就是一个数组,访问时可以通过位置index直接访问元素。
  • Map是对键值对(key/value)元素的集合,集合中不能有重复的key。
  • Queue/Deque是一个提供能够进行队列操作的集合,比如FIFO(先进先出)/ LIFO(后进先出)。
  • SortedSet/SortedMap是一个按照元素进行升序排列的集合,集合中的元素排序取决于Comparator的实现。

2. 集合实现类和工具类

集合的实现类很多,JDK中提供的实现类都在java.util.*包中,其中List/Set/Map有如下几个实现类,

  • List
    • ArrayList/LinkedList/Vector
  • Map
    • HashMap/LinkedHashMap/TreeMap/WeakHashMap
    • Hashtable/ConcurrentHashMap
  • Set
    • HashSet/LinkedHashSet/TreeSet

集合的工具类Collections/Arrays提供一些集合的复制、比较、排序和转换操作,

  • Utilities
    • Collections/Arrays

上述各个实现类和接口类的关系见下图,

3. 集合实现类的特点和数据结构

下面以表格形式列出各个实现类的特点和区别,方便进行对比,其中不同的数据结构决定了各个集合实现类的不同性能特点,详细的数据结构描述见后面注解。

集合接口 集合实现类 是否按插入顺序存放 是否按有序存放(1) 是否可以存放重复元素 是否线程安全 数据结构特性描述
List ArrayList Yes No Yes No 基于动态数组的数据结构,注2
LinkedList Yes No Yes No 基于双向链表的数据结构,查询慢,插入快,注3。
Vector Yes No Yes Yes* Deprecated,注4。
Map HashMap No No Yes No 基于哈希表的元素数据离散分布,注5。
LinkedHashMap No No Yes No 基于哈希表的元素数据离散分布,除此之外集合元素数据之间有双向链表指针,注6。
TreeMap No Yes Yes No 基于红黑树的数据结构,元素需要提供Comparator实现,用于有序存放,注7。
WeakHashMap No No Yes No
Hashtable No No Yes Yes 基于哈希表的元素数据分散分布,通过对象锁实现线程安全
ConcurrentHashMap No No Yes Yes 通过lock实现线程安全,在多线程环境中比Hashtable有更好的并发性能
Set HashSet No No No No 底层使用HashMap实现
LinkedHashSet Yes No No No 底层使用LinkedHashMap实现
TreeSet No Yes No No 底层使用TreeMap实现,元素需要提供Comparator实现,用于有序存放

注1:元素是否按有序存放,是指集合中元素根据Comparator进行比较后升序排列。

注2:ArrayList是基于动态数组的数据结构,数据插入时,会导致整个后端数据的往后移动一位,所以插入速度慢,但是根据位置索引可以直接访问元素数据,所以通过位置索引查询数据速度会很快。

注3:LinkedList是基于双向链表的数据结构,插入快,但是查询会比较慢。另外LinkedList支持getFirst/getLast/removeFirst/removeLast/addFirst/addLast操作,可以方便实现FIFO/LIFO队列操作。双向链表的数据结构如下图所示,

注4:Vector由于其在所有的get/set上进行了synchronize,导致难于在并发编程发挥作用,在很多时候可以使用List list = Collections.synchronizedList(new ArrayList())方法取代,目前不建议使用Vector来用于线程安全的编程。

注5:HashMap基于哈希表的元素数据离散分布,是指数据按照一定规则进行离散,比如数值按16取模,各自落入不同的子集合,因此数据元素排列插入后无序,见下图所示,

注6:LinkedHashMap在集合元素数据之间有双向链表指针,数据的删除和插入快,数据元素排列后无序,见下图所示,

注7:TreeMap基于红黑树(近平衡二叉树)的数据结构,这个数据结构最大的特点是各个元素数据达到平衡分布,最远和最近叶子节点离根节点距离相差不超1,使得元素的查找和插入速度达到O(log N)级别。

4. 集合实现类的插入操作

我们可以尝试为各个集合实现类进行插入数据操作,然后查看数据元素在集合中的数据排列,下面主要观察数据排列是否有序,是否按照插入顺序排列。通过观察,可以更加深入地了解各个实现类的数据结构和特性。

List的数据插入

下面的代码新建了三个List实现类实例,然后依次插入10个随机数,最后打印出列表数据。

List<Integer> list1 = new ArrayList<>();
List<Integer> list2 = new LinkedList<>();
List<Integer> list3 = new Vector<>();

for (int i = 0; i < 10; i++) {
 //产生一个随机数,并将其放入List中
 int value = (int) (Math.random() * 100);
 App.logMessage("第 " + i + " 次产生的随机数为:" + value);
 list1.add(value);
 list2.add(value);
 list3.add(value);
}

App.logMessage("ArrayList:" + list1);
App.logMessage("LinkedList:" + list2);
App.logMessage("Vector:" + list3);

结果如下,请观察元素插入和排列顺序,

第 0 次产生的随机数为:41
第 1 次产生的随机数为:68
第 2 次产生的随机数为:62
第 3 次产生的随机数为:4
第 4 次产生的随机数为:18
第 5 次产生的随机数为:38
第 6 次产生的随机数为:97
第 7 次产生的随机数为:9
第 8 次产生的随机数为:19
第 9 次产生的随机数为:1
ArrayList:[41, 68, 62, 4, 18, 38, 97, 9, 19, 1]
LinkedList:[41, 68, 62, 4, 18, 38, 97, 9, 19, 1]
Vector:[41, 68, 62, 4, 18, 38, 97, 9, 19, 1]

可以看到,各个List的数据元素排列和插入顺序一致,这也是由于动态数组的数据结构带来的特性。

Set的数据插入

下面的代码新建了三个Set实现类实例,然后依次插入10个随机数,最后打印出列表数据。

Set<Integer> set1 = new HashSet<>();
Set<Integer> set2 = new LinkedHashSet<>();
Set<Integer> set3 = new TreeSet<>();

for (int i = 0; i < 10; i++) {
 //产生一个随机数,并将其放入Set中
 int value = (int) (Math.random() * 100);
 App.logMessage("第 " + i + " 次产生的随机数为:" + value);
 set1.add(value);
 set2.add(value);
 set3.add(value);
}

App.logMessage("HashSet:" + set1);
App.logMessage("LinkedHashSet:" + set2);
App.logMessage("TreeSet :" + set3);

结果如下,请观察元素插入和排列顺序,

第 0 次产生的随机数为:51
第 1 次产生的随机数为:86
第 2 次产生的随机数为:24
第 3 次产生的随机数为:66
第 4 次产生的随机数为:76
第 5 次产生的随机数为:59
第 6 次产生的随机数为:13
第 7 次产生的随机数为:34
第 8 次产生的随机数为:89
第 9 次产生的随机数为:21
HashSet:[66, 34, 51, 21, 86, 24, 89, 59, 76, 13]
LinkedHashSet:[51, 86, 24, 66, 76, 59, 13, 34, 89, 21]
TreeSet :[13, 21, 24, 34, 51, 59, 66, 76, 86, 89]

可以看到,HashSet/LinkedHashSet无序,TreeSet按大小依次排列。这是由于HashSet/LinkedHashSet/TreeSet底层实现用的是HashMap/LinkedHashMap/TreeMap,因此继承了各自的特点。

Map的数据插入

下面的代码新建了五个Map实现类实例,然后依次插入10个随机数(随机数为key),最后打印出列表数据。

Map map1 = new HashMap();
Map map2 = new TreeMap();
Map map3 = new LinkedHashMap();
Map map4 = new WeakHashMap();
Map map5 = new Hashtable();
Map map6 = new ConcurrentHashMap();

for (int i = 0; i < 10; i++) {
 //产生一个随机数,并将其放入map中
 int value = (int) (Math.random() * 100);
 App.logMessage("第 " + i + " 次产生的随机数为:" + value);
 if (!map1.containsKey(value)){
 map1.put(value, i);
 map2.put(value, i);
 map3.put(value, i);
 map4.put(value, i);
 map5.put(value, i);
 map6.put(value, i);
 }
}

App.logMessage("产生的随机数为key,index为value");
App.logMessage("HashMap:" + map1);
App.logMessage("TreeMap:" + map2);
App.logMessage("LinkedHashMap:" + map3);
App.logMessage("WeakHashMap:" + map4);
App.logMessage("Hashtable:" + map5);
App.logMessage("ConcurrentHashMap:" + map5);

结果如下,请观察元素插入和排列顺序,

第 0 次产生的随机数为:48
第 1 次产生的随机数为:86
第 2 次产生的随机数为:81
第 3 次产生的随机数为:19
第 4 次产生的随机数为:90
第 5 次产生的随机数为:74
第 6 次产生的随机数为:55
第 7 次产生的随机数为:29
第 8 次产生的随机数为:89
第 9 次产生的随机数为:65
产生的随机数为key,index为value
HashMap:{48=0, 81=2, 65=9, 19=3, 86=1, 55=6, 89=8, 90=4, 74=5, 29=7}
TreeMap:{19=3, 29=7, 48=0, 55=6, 65=9, 74=5, 81=2, 86=1, 89=8, 90=4}
LinkedHashMap:{48=0, 86=1, 81=2, 19=3, 90=4, 74=5, 55=6, 29=7, 89=8, 65=9}
WeakHashMap:{90=4, 74=5, 89=8, 29=7, 65=9, 55=6, 81=2, 86=1, 48=0, 19=3}
Hashtable:{90=4, 89=8, 65=9, 19=3, 86=1, 81=2, 55=6, 29=7, 74=5, 48=0}
ConcurrentHashMap:{90=4, 89=8, 65=9, 19=3, 86=1, 81=2, 55=6, 29=7, 74=5, 48=0}

可以看到,TreeMap按key大小升序排列,LinkedHashMap按value大小升序排列,其它无序。

集合实现类的性能比较

为了比较各个集合实现类的性能,对各个集合实现类进行如下三个操作,

  • 插入:在集合中插入0-N个数据元素,N为指定性能测试要达到的数组大小
  • 查询:在集合中一一查询0-N个数据元素,N为指定性能测试的数组大小,换句话说轮询集合中所有元素
  • 删除:在集合中一一删除0-N个数据元素,N为指定性能测试的数组大小,换句话说删除集合中所有元素

测试数组大小分别为1万、5万、10万、15万,线性倍增,然后观察各个集合实现类在不同数组大小下的性能表现。

注:在Map中查询分别添加了通过key/value查询的测试。

测试环境:Java版本为1.8.0_111,主机环境:Win7 SP1 x64, intel i5-6600K 4 cores 3.5GHz CPU, 16G memory。

可以看到,

  • List集合实现类在插入、查询、删除操作上,随着数组变大有明显的性能开销增加,性能开销的增加倍数超过数组大小的增加倍数。
  • Map集合实现类在通过value查询性能开销很大,在实际编程中,尽量避免此类操作
  • 表中有些操作时间开销都在10毫秒内,随着数组倍增,性能表现不错,这些操作可以在编程中多加利用。

测试程序代码

演示代码仓库地址:https://gitee.com/pphh/blog,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/blog.git

上述测试程序代码样例在文件路径171117_java_collection\demo中。

参考资料