Java對象深拷貝淺拷貝總結


在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進行優化,兩者會有一個數量級的差距。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM