在java開發的過程中我們很多時候會有深拷貝需求,比如將一個請求體拷貝多次,修改成多個不同版笨,分別發給不同的服務,在比如維護不同的緩存時。還有些時候並不需要深拷貝,只是簡單的類型轉換,比如到將do對象轉換為dto對象返回給前端,其中兩者的字段基本相同,只是類名不一樣。本文主要羅列了下自己總結的拷貝方式和適合的場景(深淺拷貝原理文章很多,本文不再解釋)。
拷貝過程中用到的Bean定義:
@Data
public class Source {
String a;
Filed1 filed1;
Filed1 filed2;
List<Filed1> fileds;
@NoArgsConstructor
@AllArgsConstructor
@Data
public static class Filed1 {
String id;
}
}
深拷貝
1. 手動new
Source source = getSource();
Source target = new Source();
target.setFiled1(new Source.Filed1(source.getFiled1().getId()));
target.setFiled2(new Source.Filed1(source.getFiled2().getId()));
if (source.getFileds() != null) {
ArrayList<Source.Filed1> fileds = new ArrayList<>(source.getFileds().size());
for (Source.Filed1 filed : source.getFileds()) {
fileds.add(new Source.Filed1(filed.getId()));
}
target.setFileds(fileds);
}
手動new非常簡單,但是非常繁瑣不利於后期的維護,每次修改類定義的時候需要修改相應的copy方法,不過性能非常高。
2. clone方法
// Source類
public Source clone() {
Source clone = null;
try {
clone = (Source) super.clone();
clone.setFiled1(filed1.clone());
clone.setFiled2(filed2.clone());
//列表的克隆
if (fileds != null) {
ArrayList<Filed1> target = new ArrayList<>(this.fileds.size());
for (Filed1 filed : this.fileds) {
target.add(filed.clone());
}
clone.setFileds(target);
}
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
// Filed1類
public Filed1 clone() throws CloneNotSupportedException {
return (Filed1) super.clone();
}
在重寫clone方法的時候,如果類的字段類型是String和Integer等不可變類型,那么source實例對應的字段是可以復用的,以為這個字段值不能被修改。如果字段類型是可變類型則也需要重寫,如Source中Filed1字段類型不是不可變類型,則也需要重寫clone方法,另外注意重寫clone方法的類必須實現Cloneable類(public class Source implements Serializable
),否則會拋出CloneNotSupportedException。
3. java自帶序列化
ByteArrayOutputStream out = new ByteArrayOutputStream();
new ObjectOutputStream(out).writeObject(source);
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray()));
Source target = (Source) in.readObject();
// spring中封裝了下可以直接使用
// Source target = (Source) SerializationUtils.deserialize(SerializationUtils.serialize(source));
這個方法很多書中都有提起,因為序列化來實現深度拷貝代碼比較簡單,可擴展性好,后期添加字段無需修改實現,不過類需要繼承標記接口Serializable(public class Source implements Serializable
)。不過這個方法沒有什么實際用途,因為確實性能非常低。
4. json序列化
public class JsonCopy {
private static ObjectMapper mapper = new ObjectMapper();
public static String encodeWithoutNull(Object obj) throws Exception {
return mapper.writeValueAsString(obj);
}
public static <T> T decodeValueIgnoreUnknown(String str, Class<T> clazz) throws Exception {
return mapper.readValue(str, clazz);
}
// 一千萬次 15.3秒
public static <T> T copy(T source, Class<T> tClass) throws Exception {
return decodeValueIgnoreUnknown(encodeWithoutNull(source), tClass);
}
}
一個簡單的工具類,利用了Jackson庫,性能一般,不過擴展性好,比java自帶序列化很大提升。
性能測試
我在自己的機器上用每種方法實現Source對象的一千萬次拷貝,測試了時間。結果如下:
類型 | 測試結果 |
---|---|
手動new | 一千萬次 774毫秒 |
clone方法 | 一千萬次 827毫秒 |
java自帶序列化 | 一千萬次 109.7秒 |
json序列化 | 一千萬次 15.3秒 |
深拷貝總結
從可擴展性和性能方面的考慮,如果注重性能,那么使用手動new和clone方法,如果注重擴展性那么使用java自帶序列化和json序列化。平時的使用中,優先使用json序列化,因為大部分場景下cpu不是瓶頸,在一些熱點代碼中改用重寫clone方法。使用clone方法和手動New兩個性能和可維護性都類似,只不過看你的喜好,我是認為clone比較符合Java風格,將對象的clone方法寫在那個類中。
淺拷貝
1. spring BeanUtils(Apache BeanUtils)
Source source = getSource();
Source target = new Source();
BeanUtils.copyProperties(source, target);
spring的BeanuUtils和Apache BeanUtils原理都類似,都是利用反射獲取了對象的字段,逐個賦值,性能方面其實也是比較好了,雖然利用了反射,但是內部緩存了反射的結果,后面在復制的時候可以直接取緩存的結果。反射的性能損耗在獲取Class信息那一塊,在調用的開銷和普通調用的類似,Jvm也會使用Jit進行優化。
2. mapstruct
@Mapper
public interface SourceMapper {
SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
Source copy(Source car);
}
Source target = SourceMapper.INSTANCE.copy(source);
mapstruct和lombok的原理類型,在編譯期根據你的注解生成所需要的方法,所以他的性能理論上和手寫是一樣的,現在springboot也可以和他很好的結合,如果遇到了對象拷貝的性能瓶頸可以考慮用下這個類庫。不過遺憾的是他並不支持深拷貝。https://github.com/mapstruct/mapstruct/issues/695
性能測試
類型 | 測試結果 |
---|---|
BeanUtils | 一千萬次 1825毫秒 |
mapstruct | 一千萬次 235毫秒 |
淺拷貝總結
淺拷貝也可以看到可以復制不同對象的實例字段,這是序列化和Clone方法等不具備的優勢,在轉化Bean的時候十分有用。在一般情況下,推薦使用Spring的BeanUtils類,不用引入額外的依賴,性能也夠用。如果在高並發的場景下,可以考慮通過mapstruct進行優化,兩者會有一個數量級的差距。