
概述在Java开发中我们会看到各种各样的对象实体类包括POJOPlain Old Java Object普通Java对象VOView Object视图对象返回给前端用于展示层将某个页面组件的全部或部分数据封装到一个对象里有些公司将其定义为**Resp对象VO另有一说是Value Object值对象QOQuery Object查询对象一般用RequestBody注解的对象有些公司将其定义为**Req对象如OrderDetailReq用于查询订单详情信息的请求体DODomain Object领域对象常见于DDDDomain Driven Development开发中用于表示从真实业务场景中抽象出来的业务实体对象BOBusiness Object业务对象业务计算层的对象可能会增加金额汇率等业务逻辑字段POPersistent Object持久化对象一般情况下严格对应数据库表一个数据表对应一个PO类数据表里一个字段对应PO类的一个属性DAOData Access Object数据访问对象用于查询数据库的DAO模式DTOData Transfer Object数据传输对象用于在各个层之间传输数据尤其适用于展示层和服务层反思可能有很多人会有一个疑问为啥要搞这么多对象事实上我自己也会有这个顾虑这不没事找事么。一个对象从前端传输过来使用的是QO即查询对象然后我在业务层来处理、转换这个对象并用BO来承载加以封装然后处理逻辑来到领域层又需要转换为DO随后来到数据库交互层进行CRUD即增查改删操作需要转换为PO如果需要把数据再返回给前端上述4个对象很可能还需要反过来再封装一次从PO到DO到BO再到VO。这些对象基本上没有太大的差别字段几乎都是一样的。PO一般比VO多一个逻辑删除字段毕竟前端才不在乎你数据库的删除概念前端能看到的数据就是还没进行逻辑删除的数据。可能还有人会问能不能不要搞这么多对象QO、VO、和PO用一个对象不香么能少写很多类少写很多类转换方法即所谓的getter then setter。个人观点只能说可行可不行看公司编码规范。前端用不上的字段后端也给前端返回一来会给前端造成困扰这个字段啥意思用于展示或渲染什么数据二来多余的字段参与网络传输会降低性能其实影响真的微乎其微之所以会产生这么多对象一般都是因为随着业务逐渐发展项目越来越庞大前后端分离业务分层势在必行然后每一层都会定义很多POJO。但是对于新项目不应该过度设计应该根据项目发展的具体情况来适当分层重构。Lombok前面提到业务开发中可能会碰到各种各样的对象。事实上哪怕没有这么多对象几十张表也够烦的需要定义gettersetter构造器方法重写equals和hashcode方法等。对于JDK 14之前的版本可考虑使用Lombok提高生产力参考Java开发工具–Lombok深入实战。JDK 14版本后除Lombok可考虑使用JDK Record类型功能等价于Lombok。MapStruct对于对象之间的属性拷贝及转换可考虑使用MapStruct功能很强大参考Java对象拷贝MapStruct。拷贝对象拷贝(Object Copy)将一个对象的属性拷贝到另一个有着相同类类型的对象中去。主要有浅拷贝与深拷贝。Shallow Copy可翻译为浅拷贝浅复制浅克隆。Deep Copy可翻译为深拷贝深复制深克隆。另外还有延迟拷贝(Lazy Copy)。浅拷贝与深拷贝浅拷贝关于浅拷贝被复制对象的所有变量都含有与原来的对象相同的值而所有的对其他对象的引用仍然指向原来的对象浅复制仅复制所考虑的对象不复制所引用的对象只复制某个对象的指针而不复制对象本身两个引用指针指向被复制对象的同一块内存地址。深拷贝关于深拷贝被复制对象的所有变量都含有与原来的对象相同的值除去那些引用其他对象的变量。那些引用其他对象的变量将指向被复制过的新对象而不再是原有的那些被引用的对象。深复制把要复制的对象所引用的对象都复制一遍深拷贝会拷贝所有属性并且拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝需要创建被拷贝类的一个一模一样的新对象新对象和老对象不共享内存对新对象的修改不会影响老对象的值。深拷贝的实现方式包括所有对象都实现clone方法通过构造函数实现JDK字节流第三方工具如Apache Commons Lang提供的APIJSON序列化工具如FastJSON。Arrays.copyOf()问题Arrays.copyOf()是浅拷贝还是深拷贝答案Arrays.copyOf()及System.arraycopy()是浅拷贝。代码结论原型对象也发生改变。解读数组比较特殊其本身就是引用类型在使用Arrays.copyOf()时其实只是把引用地址复制给克隆对象。如果克隆对象修改引用对象那么指向引用地址的所有对象也会发生变化。来看看源码publicstaticT,UT[]copyOf(U[]original,intnewLength,Class?extendsT[]newType){T[]copy((Object)newType(Object)Object[].class)?(T[])newObject[newLength]:(T[])Array.newInstance(newType.getComponentType(),newLength);System.arraycopy(original,0,copy,0,Math.min(original.length,newLength));returncopy;}其中System.arraycopy()是一个native本地方法Object.clone()java.lang.Object的clone()方法的约定clone方法将对象复制一份并返回给调用者。一般而言clone()方法满足对任何对象x都有x.clone() ! x;克隆对象与原对象不是同一个对象对任何对象x都有x.clone().getClass() x.getClass();克隆对象与原对象的类型一样对任何对象x都有x.clone().equals(x)使用equals()方法比较数值是相同的。提到Object.clone()方法想到原型模式。优点是性能较高通过直接拷贝内存中的二进制流实现复制缺点是在对象层级嵌套较深时复制的代码实现难度较大。对比无论是深拷贝还是浅拷贝都需要实现Cloneable接口并且重写clone方法。深拷贝相比于浅拷贝速度较慢并且花销较大。两者主要区别在于是否支持引用类型的属性拷贝。面试题几个常见的面试题java.lang.Object.clone()方法的约定有哪些Arrays.copyOf()是浅拷贝还是深拷贝深拷贝的实现方式有几种拷贝为啥要设计成1.实现Cloneable接口2.重写Object.clone()方法值传递与引用传递Java中对于基本型变量采用的是值传递而对于对象传递采用的是引用传递即地址传递实际上是对对象作浅拷贝。方法调用(call by)根据参数传递的情况又分为值调用(call by value)和引用调用(call by reference)。传递值的是值调用传递地址的是引用调用。Java的方法对象参数传递仍然是值调用。序列化实现深拷贝的方式实现Cloneable接口在clone()方法里面重写克隆逻辑对克隆对象内部的引用变量再进行一次克隆序列化将整个对象图写入到一个持久化存储文件中并且当需要时把它读取回来这意味着当你需要把它读取回来时你需要整个对象图的一个拷贝。这就是当你深拷贝一个对象时真正需要的东西。必须确保对象图中所有类都是可序列化的序列化的限制和问题transient无法序列化拷贝transient变量性能问题创建socket序列化对象通过socket传输然后反序列化它性能比较差延迟拷贝两种的组合实际上很少会使用。当最开始拷贝一个对象时会使用速度较快的浅拷贝还会使用一个计数器来记录有多少对象共享这个数据。当程序想要修改原始的对象时它会决定数据是否被共享通过检查计数器并根据需要进行深拷贝。延迟拷贝看起来就是深拷贝但是只要有可能它就会利用浅拷贝的速度。当原始对象中的引用不经常改变的时候可使用延迟拷贝。由于存在计数器效率下降很高但只是常量级的开销。而且在某些情况下循环引用会导致一些问题。如何选择如果对象的属性全是基本类型的可使用浅拷贝但是如果对象有引用属性那就要基于具体的需求来选择。如果对象引用任何时候都不会被改变那么没必要使用深拷贝只需要使用浅拷贝就行。如果对象引用经常改变就要使用深拷贝。继承自java.lang.Object.clone()方法是浅复制。工具上面提到深拷贝需要拷贝所有依赖的引用对象。而对象引用关系往往非常复杂形成引用链或叫对象图Apache BeanUtils使用org.apache.commons.beanutils.BeanUtils进行对象深入复制时主要通过向BeanUtils框架注入新的类型转换器BeanUtils对复杂对象的复制默认是引用。Apache PropertyUtilsorg.apache.commons.beanutils.PropertyUtils.copyProperties()方法几乎与BeanUtils.copyProperties()相同主要区别在于后者提供类型转换功能即发现两个JavaBean的同名属性为不同类型时在支持的数据类型范围内进行转换PropertyUtils不支持这个功能所以说BeanUtils使用更普遍一点犯错的风险更低一点。而且它仍然属于浅拷贝。Apache提供SerializationUtils.clone(T)T对象需要实现Serializable接口属于深克隆。Spring BeanUtilsSpring中的BeanUtils对两个对象中相同名字的属性进行简单get/set仅检查属性的可访问性。成员变量赋值是基于目标对象的成员列表并且会跳过ignore的以及在源对象中不存在的所以这个方法是安全的不会因为两个对象之间的结构差异导致错误但是必须保证同名的两个成员变量类型相同。Apache提供的BeanUtils和Spring的BeanUtils中拷贝方法的原理都是先用JDK中java.beans.Introspector类的getBeanInfo()方法获取对象的属性信息及属性get/set方法接着使用反射Method. invoke(Objectobj, Object...args)方法进行赋值。Apache支持名称相同但类型不同的属性的转换Spring支持忽略某些属性不进行映射都设置缓存保存已解析过的BeanInfo信息。CGLib BeanCopierCGLib的BeanCopier原理不是利用反射对属性进行赋值而是直接使用ASM的MethodVisitor直接编写各属性的get/set方法具体过程可见BeanCopier类的generateClass(ClassVisitor)方法生成class文件然后进行执行。由于是直接生成字节码执行所以BeanCopier的性能较采用反射的BeanUtils有较大提高。DozerDozer基于反射来实现对象深拷贝反射调用set/get或直接对成员变量赋值。该方式通过invoke执行赋值实现时一般会采用beanutilJavassist等开源库。Dozer支持简单属性映射、复杂类型映射、双向映射、隐式映射以及递归映射支持定制化的属性字段映射可使用xml或注解进行映射的配置支持自动类型转换。Orika深拷贝不用担心原始类和克隆类指向同一个对象的问题。高效开源GitHub1.3K Star271 ForkJava对象映射库基于Java反射机制和字节码生成技术专门用于在Java应用程序中简化对象之间的转换。通过自动化和优化的方式将一个对象的属性映射到另一个对象中减少手动编写重复代码的需求官方文档。底层采用javassist类库生成Bean映射的字节码之后直接加载执行生成的字节码文件在速度上比使用反射进行赋值会快很多。工作原理反射机制使用Java反射机制来分析源对象和目标对象的属性能动态地确定哪些属性需要映射以及如何进行映射字节码生成为了提高性能在运行时生成字节码来执行映射操作。比传统的反射调用更快因为生成的字节码可直接操作对象的属性而不需要通过反射进行间接访问。MapperFactory和MapperFacade使用MapperFactory来配置和创建映射器MapperFacade核心接口提供对象映射的功能。开发者通过调用MapperFacade的map方法来执行对象之间的映射自动映射和自定义映射支持自动映射即如果源对象和目标对象的属性名称和类型匹配它会自动进行映射。允许定义自定义映射逻辑内置转换器内置转换器用于处理常见的数据类型转换。这些转换器可在映射过程中自动应用确保不同类型之间的兼容性场景DTO与实体转换在分层架构中通常需要在DTO和实体对象之间进行转换。可自动处理这些转换减少手动代码的编写微服务架构在微服务架构中不同服务之间的数据交换需要对象转换可高效地完成这些任务数据迁移在数据迁移或数据同步过程中需要将数据从一种结构转换为另一种结构API集成在集成第三方API时通常需要将API响应转换为应用程序内部的对象结构可帮助实现这一目标实战Maven项目引入依赖dependencygroupIdma.glasnost.orika/groupIdartifactIdorika-core/artifactIdversion1.5.4/version/dependency入门案例importma.glasnost.orika.MapperFacade;importma.glasnost.orika.impl.DefaultMapperFactory;publicclassOrikaExample{publicstaticvoidmain(String[]args){DefaultMapperFactorymapperFactorynewDefaultMapperFactory.Builder().build();MapperFacademappermapperFactory.getMapperFacade();SourceObjectsourcenewSourceObject();source.setId(1);source.setName(John Doe);DestinationObjectdestinationmapper.map(source,DestinationObject.class);}}classSourceObject{privateintid;privateStringname;}classDestinationObject{privateintid;privateStringname;}添加JVM选项来打开对java.lang模块的访问--add-opens java.base/java.langALL-UNNAMEDJSON对象拷贝可使用序列化来实现真实业务开发中有很大一部分时间是和前端打交道而不仅仅是提供一个微服务应用或SOA服务提供给其他的微服务SOA调用API Callor Service Call。现在前后端几乎都是使用JSON来传输数据因此后端经常需要将JSON ObjectPOJO对象转换成JSON String或从JSON String反序列化得到JSON Object。此时可使用的工具类就不要太多FastJsonJacksonGson等自研工具类很多公司都有自研工具的习惯传统自研有不少好处如稳定性和性能。因为会根据公司或团队的具体项目的业务需求不用考虑一套大而全的脚手架只实现简单的转换模板。性能BenchmarkTODO对比与选型工具这么多怎么选主要考虑两点稳定性文档丰富度GitHub活跃度未关闭的issue多少性能对比不过绝大多数情况下真的没有必要一是没有大对象二是对于消耗的时间真的没有那么敏感。参考