1) To add to Makoto post, I once used a method to control programmatically the correctness of a cloning. But it's something hard and long.
I give you the code I've used as an unit test. It's surely perfectible:
/**
* Vérification du bon clonage d'un objet.
* @param o Objet original.
* @param clone Objet qui se dit clone du premier.
* @param champsExclus Champs qu'il faut exclure de la vérification d'assertNotSame().
*/
public void controlerClonage(Object o, Object clone, String... champsExclus) {
assertNotNull(o, MessageFormat.format("L''objet de classe {0} dont le clonage doit être contrôlé ne peut pas valoir null.", o.getClass().getName()));
assertNotNull(clone, MessageFormat.format("L''objet de classe {0} dont le clonage doit être contrôlé ne peut pas valoir null.", o.getClass().getName()));
// Cloner l'objet puis vérifier qu'il est égal à son original.
assertEquals(o, clone, MessageFormat.format("L''objet cloné {0} et son original devraient être égaux.", o.getClass().getSimpleName()));
assertNotSame(o, clone, MessageFormat.format("L''objet cloné {0} et son original ne devraient pas partager le même pointeur.", o.getClass().getSimpleName()));
// Enumérer toutes les variables membres de l'objet.
Field[] champs = o.getClass().getDeclaredFields();
for(Field champ : champs) {
// Si le champ est parmi la liste de ceux à exclure, ne pas en tenir compte.
if (isChampExclu(champ, champsExclus)) {
continue;
}
// Si l'objet est un type primitif ou un String, l'assertNotSame() ne pourra pas s'appliquer.
// En revanche, l'assertEquals(), lui, toujours.
champ.setAccessible(true);
// Lecture de la valeur originale.
Object valeurOriginal = null;
try {
valeurOriginal = champ.get(o);
}
catch(IllegalArgumentException | IllegalAccessException e) {
fail(MessageFormat.format("L''obtention de la valeur du champ {0} dans l''objet original {1} a échoué : {2}.", champ.getName(),
o.getClass().getSimpleName(), e.getMessage()));
}
// Lecture de la valeur clonée.
Object valeurClone = null;
try {
valeurClone = champ.get(clone);
}
catch(IllegalArgumentException | IllegalAccessException e) {
fail(MessageFormat.format("L''obtention de la valeur du champ {0} dans l''objet cloné {1} a échoué : {2}.", champ.getName(),
clone.getClass().getSimpleName(), e.getMessage()));
}
assertEquals(valeurOriginal, valeurClone, MessageFormat.format("La valeur de la variable membre {0} de l''objet {1} et de son clone devrait être égales.", champ.getName(), clone.getClass().getSimpleName()));
// Les types primitifs, les chaînes de caractères et les énumérations, ne voient pas leurs pointeurs vérifiés.
// Et cela n'a de sens que si les valeurs de ces pointeurs sont non nuls.
if (valeurOriginal != null && valeurClone != null) {
if (champ.getType().isPrimitive() == false && champ.getType().equals(String.class) == false
&& Enum.class.isAssignableFrom(champ.getType()) == false)
assertNotSame(valeurOriginal, valeurClone, MessageFormat.format("La variable membre {0} de l''objet {1} et de son clone ne devraient pas partager le même pointeur.", champ.getName(), clone.getClass().getSimpleName()));
}
}
}
/**
* Déterminer si un champ fait partie d'une liste de champs exclus.
* @param champ Champ.
* @param champsExclus Liste de champs exclus.
* @return true, si c'est le cas.
*/
private boolean isChampExclu(Field champ, String[] champsExclus) {
for (String exclus : champsExclus) {
if (exclus.equals(champ.getName()))
return true;
}
return false;
}
/**
* Alimenter un objet avec des valeurs par défaut.
* @param objet Objet.
* @return Objet lui-même (alimenté).
*/
public Object alimenter(Object objet) {
double nDouble = 1.57818;
int nInt = 1;
long nLong = 10000L;
Class<?> classe = objet.getClass();
while(classe.equals(Object.class) == false && classe.getName().startsWith("java.") == false) {
for(Field field : classe.getDeclaredFields()) {
// Ignorer certains types
if (field.getType().equals(Class.class) || field.getType().equals(ArrayList.class)
|| field.getType().equals(List.class)|| field.getType().equals(Set.class)
|| field.getType().equals(HashSet.class) || field.getType().equals(HashMap.class)) {
continue;
}
// Ecarter les champs statiques.
if (Modifier.isStatic(field.getModifiers())) {
continue;
}
// Champs de type texte.
if (field.getType().equals(String.class)) {
setValue(field, objet, "*" + field.getName() + "*");
continue;
}
// Champs de type LocalDate.
if (field.getType().equals(LocalDate.class)) {
setValue(field, objet, LocalDate.now());
continue;
}
// Champs de type LocalDateTime.
if (field.getType().equals(LocalDateTime.class)) {
setValue(field, objet, LocalDateTime.now());
continue;
}
// Champs de type URL.
if (field.getType().equals(URL.class)) {
try {
URL url = new URL("http://a.b.c/test");
setValue(field, objet, url);
}
catch(MalformedURLException e) {
throw new RuntimeException("Mauvaise préparation d'URL pour test toString : " + e.getMessage());
}
continue;
}
// Champs de type ZonedDateTime.
if (field.getType().equals(ZonedDateTime.class)) {
setValue(field, objet, ZonedDateTime.now());
continue;
}
// Champs de type texte.
if (field.getType().equals(Double.class) || field.getType().equals(Double.TYPE)) {
setValue(field, objet, nDouble ++);
continue;
}
// Champs de type integer.
if (field.getType().equals(Integer.class) || field.getType().equals(Integer.TYPE)) {
setValue(field, objet, nInt ++);
continue;
}
// Champs de type long.
if (field.getType().equals(Long.class) || field.getType().equals(Long.TYPE)) {
setValue(field, objet, nLong ++);
continue;
}
// Champs de type énumération
if (Enum.class.isAssignableFrom(field.getType())) {
@SuppressWarnings("unchecked")
Class<Enum<?>> enumeration = (Class<Enum<?>>)field.getType();
Enum<?>[] constantes = enumeration.getEnumConstants();
setValue(field, objet, constantes[0]);
// On en profite pour vérifier toutes ses constantes.
for(Enum<?> constante : constantes) {
System.out.println(MessageFormat.format("{0}.{1}.{2} = {3}", classe.getName(), field.getName(), constante.name(), constante.toString()));
}
continue;
}
if (field.getType().getName().startsWith("java.") == false) {
try {
field.setAccessible(true);
Object membre = field.get(objet);
// Ecarter les champs statiques et abstraits.
if ((Modifier.isStatic(field.getModifiers()) && Modifier.isAbstract(field.getModifiers())) == false) {
// Si l'objet n'est pas initialisé, tenter de le faire.
if (membre == null) {
try {
membre = field.getType().getDeclaredConstructor().newInstance();
}
catch(@SuppressWarnings("unused") InstantiationException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
// On laisse passer, on ne pourra pas l'attribuer.
}
}
// Si l'on a obtenu le membre ou si on l'a créé, on l'alimente et on l'affecte.
if (membre != null && membre != objet) {
alimenter(membre);
setValue(field, objet, membre);
}
}
continue;
}
catch(IllegalArgumentException | IllegalAccessException e) {
System.err.println(MessageFormat.format("{0}.{1}.{2} n'a pas pu être assigné : {3}", objet.getClass().getName(), field.getName(), e.getMessage()));
}
}
// Indiquer les champs que l'on a pas pu assigner.
System.err.println(MessageFormat.format("non assigné : {0}.{1}", field.getName(), field.getType().getName()));
}
classe = classe.getSuperclass();
}
return objet;
}
/**
* Fixer une valeur à un champ.
* @param field Champ dans l'objet cible.
* @param objet Objet cible.
* @param valeur Valeur à attribuer.
*/
private void setValue(Field field, Object objet, Object valeur) {
field.setAccessible(true);
try {
field.set(objet, valeur);
}
catch(IllegalArgumentException | IllegalAccessException e) {
System.err.println(MessageFormat.format("{0}.{1} n'a pas pu être assigné : {2}", objet.getClass().getName(), field.getName(), e.getMessage()));
}
}
2) This isn't about automatic control by U.T., but for develop time, inside the IDE:
These days, I removed all the clone()
methods I had, having learnt that they are a bad practice, to replace them with copy constructors.
I noticed that if an IDE like IntelliJ detects in a class MyClass
a constructor of the form MyClass(MyClass source) {...}
it will warn if one assignation is missing.
The class name is surrounded in yellow, with the message: Copy constructor does not copy field 'population'
.
null
is a perfectly acceptable value in that scenario. – Zoosporenull
is the default field value when it's unassigned... – Zoospore