Le clonage profond d'objets est une exigence courante dans les applications Java, permettant de créer une copie indépendante d'un objet source et de tous les objets auxquels il fait référence. Cet article explore plusieurs méthodes populaires pour réaliser un clonage profond en Java, y compris des utilitaires de bean et la sérialisation, et évalue leurs performances relatives.
Environnement de Test
Les tests ont été réalisés dans un environnement Java 8 avec Maven 3.6 et l'IDE IntelliJ IDEA, sur un projet Spring Boot 2.2.x. La machine hôte est équipée d'un processeur Intel Core i7-9700 (8 cœurs/8 threads à 3.00 GHz) et de 16 Go de RAM.
Modèles de Données
Pour l'évaluation, nous utilisons les classes SourceData, TargetData et DetailArticle, définies ci-dessous. Les classes SourceData et TargetData ont une structure identique pour faciliter le mappage et incluent une liste imbriquée d'objets DetailArticle, simulant une structure de données complexe typique.
import lombok.Data;
import java.util.List;
@Data
public class SourceData {
private String nomProduit;
private Integer quantiteStock;
private Long referenceId;
private Double prixUnitaire;
private Float poidsTotal;
private Boolean estActif;
private Character codeType;
private Byte niveauPriorite;
private Short categorieProduit;
private List<DetailArticle> details;
}
import lombok.Data;
import java.util.List;
@Data
public class TargetData {
private String nomProduit;
private Integer quantiteStock;
private Long referenceId;
private Double prixUnitaire;
private Float poidsTotal;
private Boolean estActif;
private Character codeType;
private Byte niveauPriorite;
private Short categorieProduit;
private List<DetailArticle> details;
}
import lombok.Data;
@Data
public class DetailArticle {
private String codeArticle;
private Integer nombreUnites;
}
Génération de Données Exemple
Une méthode utilitaire est utilisée pour créer une instance de SourceData avec des valeurs d'exemple, y compris une liste de DetailArticle. Cette instance sera clonée pour les tests de performance.
import java.util.ArrayList;
import java.util.List;
public class DataGenerator {
public static SourceData createSampleSourceData() {
SourceData sampleData = new SourceData();
sampleData.setNomProduit("Ordinateur Portable Ultra");
sampleData.setQuantiteStock(25);
sampleData.setReferenceId(987654321L);
sampleData.setPrixUnitaire(1299.99);
sampleData.setPoidsTotal(1.8f);
sampleData.setEstActif(true);
sampleData.setCodeType('P');
sampleData.setNiveauPriorite((byte) 3);
sampleData.setCategorieProduit((short) 205);
List<DetailArticle> articleDetails = new ArrayList<>();
DetailArticle detailCpu = new DetailArticle();
detailCpu.setCodeArticle("CPU-XYZ-123");
detailCpu.setNombreUnites(1);
articleDetails.add(detailCpu);
DetailArticle detailRam = new DetailArticle();
detailRam.setCodeArticle("RAM-DDR5-32GB");
detailRam.setNombreUnites(2);
articleDetails.add(detailRam);
sampleData.setDetails(articleDetails);
return sampleData;
}
}
Chaque méthode de clonage sera testée sur une colllection de 1 million d'objets.
Utilisation de Spring BeanUtils.copyProperties
La classe BeanUtils de Spring Framework fournit une méthode copyProperties pour copier les propriétés d'un objet source vers un objet cible. Elle est basée sur la réflexion et utilise un mécanisme de mise en cache pour optimiser les performances, la rendant très efficace bien que n'étant pas toujours la plus rapide.
Dépendance Maven
Version utilisée : spring-beans 5.2.3-RELEASE (généralement incluse avec Spring Boot).
Performance : 1 million de copies en 1591 ms
Le code suivant illustre l'utilisation de BeanUtils.copyProperties et mesure le temps d'exécution pour le clonage d'une liste d'objets SourceData en TargetData.
import org.springframework.beans.BeanUtils;
import org.springframework.util.StopWatch;
import org.junit.jupiter.api.Test; // Ou Junit 4 @Test
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class CloningPerformanceTests {
private final int iterations = 1_000_000;
@Test
void measureSpringBeanUtilsCopy() {
List<SourceData> sourceRecords = new ArrayList<>();
for (int i = 0; i < iterations; i++) {
sourceRecords.add(DataGenerator.createSampleSourceData());
}
StopWatch timer = new StopWatch("Spring BeanUtils Copy Test");
timer.start("BeanUtils.copyProperties");
List<TargetData> copiedRecords = sourceRecords.stream()
.map(source -> {
TargetData target = new TargetData();
BeanUtils.copyProperties(source, target);
return target;
})
.collect(Collectors.toList());
timer.stop();
System.out.println("Temps écoulé pour Spring BeanUtils : " + timer.getTotalTimeMillis() + " ms");
}
}
Utilisation de Hutool BeanUtil.copyToList
Hutool est une boîte à outils Java qui regroupe de nombreuses utilitaires. Sa classe BeanUtil offre des méthodes pour la copie de propriétés, y compris copyToList pour les collections. Il est important de noter que, malgré des améliorations récentes (comme l'ajout de cache et la suppression de certains verrous dans les versions plus récentes), les performances de Hutool peuvent être inférieures à d'autres solutions dans des scénarios de haute concurrence ou avec un grand volume de données, en raison de son utilisation sous-jacente de la réflexion et de la gestion des verrous internes.
Dépendance Maven
Version utilisée : hutool-all 5.8.24.
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.24</version>
</dependency>
Performance : 1 million de copies en 8136 ms
L'exemple suivant mesure le temps nécessaire pour copier une liste d'objets SourceData vers TargetData en utilisant BeanUtil.copyToList.
import cn.hutool.extra.bean.BeanUtil; // Correction: Hutool's BeanUtil is in extra.bean
import org.springframework.util.StopWatch;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
// ... (CloningPerformanceTests class definition, iterations field)
public class CloningPerformanceTests {
private final int iterations = 1_000_000;
@Test
void measureHutoolBeanUtilCopy() {
List<SourceData> sourceRecords = new ArrayList<>();
for (int i = 0; i < iterations; i++) {
sourceRecords.add(DataGenerator.createSampleSourceData());
}
StopWatch timer = new StopWatch("Hutool BeanUtil Copy Test");
timer.start("BeanUtil.copyToList");
List<TargetData> copiedRecords = BeanUtil.copyToList(sourceRecords, TargetData.class);
timer.stop();
System.out.println("Temps écoulé pour Hutool BeanUtil : " + timer.getTotalTimeMillis() + " ms");
}
}
MapStruct pour la Conversion d'Objets
MapStruct est un générateur de code qui simplifie l'implémentation de mappers entre types Java. Au lieu d'utiliser la réflexion à l'exécution, MapStruct génère des implémentations de mappers lors de la compilation. Cela signifie que la conversion d'objets se fait via des appels de méthode directs, sans la surcharge de la réflexion, ce qui se traduit par des performances supérieures.
Dépendances Maven et Configuration de Build
MapStruct nécessite une dépendance pour l'API et une autre pour le processeur d'annotations. Il est également nécessaire de configurer le plugin Maven Compiler pour exécuter le processeur d'annotations durant la phase de compilation. L'exemple inclut également Lombok (version 1.18.6) pour simplifier les POJO.
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
<!-- Lombok pour simplifier les POJO -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
Définition de l'interface du mapper :
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
@Mapper(componentModel = "spring") // Optionnel: pour l'intégration Spring
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper(SourceTargetMapper.class);
TargetData mapSourceToTarget(SourceData source);
// MapStruct peut aussi générer des méthodes pour les listes
List<TargetData> mapSourceListToTargetList(List<SourceData> sourceList);
}
Performance : 1 million de copies en 998 ms
L'utilisation de MapStruct pour la conversion d'une liste est démontrée ci-dessous, affichant les meilleures performances parmi les méthodes testées.
import org.springframework.util.StopWatch;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
// ... (CloningPerformanceTests class definition, iterations field)
public class CloningPerformanceTests {
private final int iterations = 1_000_000;
@Test
void measureMapStructCopy() {
List<SourceData> sourceRecords = new ArrayList<>();
for (int i = 0; i < iterations; i++) {
sourceRecords.add(DataGenerator.createSampleSourceData());
}
StopWatch timer = new StopWatch("MapStruct Copy Test");
timer.start("SourceTargetMapper.mapSourceToTarget");
List<TargetData> copiedRecords = sourceRecords.stream()
.map(SourceTargetMapper.INSTANCE::mapSourceToTarget)
.collect(Collectors.toList());
timer.stop();
System.out.println("Temps écoulé pour MapStruct : " + timer.getTotalTimeMillis() + " ms");
}
}
Sérialisation/Désérialisation avec FastJSON
Une autre approche pour le clonage profond est de sérialiser l'objet source en une chaîne (par exemple, JSON) puis de désérialiser cette chaîne en un nouvel objet cible. Cette méthode garantit un clonage profond complet, car chaque élément de l'objet et de ses références est recréé. FastJSON est une bibliothèque populaire pour cette tâche. Il est important de prêter attention aux potenitelles références circulaires lors de la sérialisation, et de désactiver leur détection si nécessaire avec SerializerFeature.DisableCircularReferenceDetect.
Dépendance Maven
Version utilisée : fastjson 2.0.32.
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
Performance : 1 million de copies en 2229 ms
Le test suivant utilise FastJSON pour sérialiser et désérialiser une liste d'objets, mesurant ainsi le temps de clonage.
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject; // Pour parseObject/toJSONString si on ne veut pas utiliser la classe JSON plus générique
import org.springframework.util.StopWatch;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
// ... (CloningPerformanceTests class definition, iterations field)
public class CloningPerformanceTests {
private final int iterations = 1_000_000;
@Test
void measureFastJsonSerializationCopy() {
List<SourceData> sourceRecords = new ArrayList<>();
for (int i = 0; i < iterations; i++) {
sourceRecords.add(DataGenerator.createSampleSourceData());
}
StopWatch timer = new StopWatch("FastJSON Serialization Copy Test");
timer.start("JSON.toJSONString & parseObject");
List<TargetData> copiedRecords = sourceRecords.stream()
.map(source -> JSON.parseObject(JSON.toJSONString(source), TargetData.class))
.collect(Collectors.toList());
timer.stop();
System.out.println("Temps écoulé pour FastJSON (sérialisation) : " + timer.getTotalTimeMillis() + " ms");
}
}
Observations Générales sur le Clonage
Il est crucial de comprendre que la plupart des méthodes de copie de propriétés basées sur la réflexion (comme Spring BeanUtils ou Hutool BeanUtil) réalisent une copie superficielle des objets complexes et des collections imbriquées par défaut. Cela signifie que si votre objet source contient des références à d'autres objets ou des listes, ce sont les références elles-mêmes qui sont copiées, et non les objets référencés. Par conséquent, toute modification apportée aux objets ou éléments de liste dans la copie cible affectera également l'objet source (et vice versa).
MapStruct, quant à lui, permet un clonage plus profond si vous définissez explicitement des mappers pour les types imbriqués. Il générera alors le code nécessaire pour copier récursivement les propriétés. Pour notre exemple, la liste details est composée de DetailArticle qui sont aussi des POJO, MapStruct génère un code pour copier chaque DetailArticle. Cependant, si DetailArticle contenait lui-même des objets complexes, il faudrait un mapper dédié pour ceux-ci également.
La méthode de sérialisation/désérialisation (avec FastJSON ou une autre bibliothèque de sérialisation comme Jackson ou la sérialisation Java standard) est la seule qui garantit un clonage profondément complet par nature. Elle transforme l'objet en un flux de données (JSON, binaire, etc.), puis recrée un nouvel objet entièrement indépendant à partir de ce flux, coupant ainsi toutes les références à l'objet original.