Parcours de tableaux d'objets

Dans l’exercice précédent, vous avez parcouru des tableaux d’objets. Pour cela, on peut utiliser une boucle classique for (int i = 0; i < tab.length; ++i) mais on peut également utiliser des itérateurs. Avec le langage C++, on utiliserait l’une des syntaxes ci-dessous (utilisation du & si l’on souhaite une référence plutôt qu’une copie) :

for (auto element : tab) { .... }
for (const auto element : tab) { .... }
for (auto& element : tab) { .... }
for (const auto& element : tab) { .... }

On appelle souvent ce type de boucle des foreach ou des range-based for. En Java, la syntaxe est similaire, à ceci près que l’on n’a pas besoin de spécifier si l’on souhaite une référence ou non. On peut préciser le type des éléments, mais on peut également utiliser le mot-clef var :

for (int elt: tab) { .... }
for (var elt: tab) { .... }
for (final var elt: tab) { .... }

Un exemple avec un type primitif et un avec un type référence :

public static void parcours_tab() {
  int[] x = {1, 3, 4, 5};
  for (var elt: x) {
    System.out.println(elt); // affiche 1  3  4  5
  }
}
public static void test() {
  // ci-dessous, on aurait pu utiliser Integer[] x = {1, 3, 4, 5};
  Integer[] x = {  // ici, on a un tableau d'objets
      Integer.valueOf(1),
      Integer.valueOf(3),
      Integer.valueOf(4),
      Integer.valueOf(5)
  };
  for (var elt: x) {
    System.out.println(elt.floatValue()); // affiche 1.0  3.0  4.0  5.0
  }
}

Exercice 1 : Range-based MyArray   

Reprenez le code de votre classe MyArray et remplacez le boucles for que vous avez écrites par des boucles range-based.

Le constructeur de copie

À l’instar du C++, il est possible de créer des constructeurs de copie. Pour cela, il suffit de copier tous les attributs de la classe dans la nouvelle instance créée. Cela paraît simple mais cela nécessite quelques précautions. Voici un code Java tout simple pour copier une instance de SUV :

SUV.java
 1package fr.polytech.aco.vehicule.voiture;
 2import java.util.Date;
 3
 4public class SUV {
 5    String nom;
 6    Date dateAchat;
 7
 8    SUV(String nom, Date dateAchat) {
 9      this.nom = nom;
10      this.dateAchat = dateAchat;
11    }
12
13    // constructeur de copie
14    SUV(SUV from) {
15      this.nom = from.nom;
16      this.dateAchat = from.dateAchat;
17    }
18
19    void printSUV() {
20      System.out.println("Nom = " + this.nom + " achat = " + this.dateAchat);
21    }
22
23    public static void main(String args[]) {
24      SUV suv1 = new SUV("suv 1", new Date());
25      suv1.dateAchat.setTime(123456789);
26      suv1.printSUV();
27
28      SUV suv2 = new SUV(suv1);
29      suv2.nom = "suv 2";
30      suv2.dateAchat.setTime(1234);
31
32      suv1.printSUV();
33    }
34}

Dans ce code, le main crée d’abord une instance de SUV, suv1, et lui affecte un nom et une date d’achat. Ensuite on le copie dans suv2 et on modifie à la fois le nom et la date d’achat de suv2. On affiche le contenu de suv1 avant et après les modifiations opérées sur suv2. Voilà ce que l’on obtient :

Nom = suv 1 achat = Fri Jan 02 11:17:36 CET 1970
Nom = suv 1 achat = Thu Jan 01 01:00:01 CET 1970

Le nom n’a pas changé et c’est bien. En revanche, la date a été modifiée. Cela résulte du fait que le type String est non-mutable. Donc, lorsqu’on modifie le nom de suv2 à la ligne 29, on recrée une instance de String que l’on affecte à l’attribut nom de suv2. Cela ne change pas la valeur de suv1.nom. En revanche, le setTime() de la ligne 30 modifie la Date sauvegardée dans suv2. Or, Date est une classe mutable et, comme c’est une classe, c’est un type référence. Donc l’instruction de la ligne 10 a fait que suv1 et suv2 partagent la même référence de Date. Modifier la date de suv2 modifie donc aussi celle de suv1.

Attention

De ce que l’on a vu ci-dessus, si vous créez un constructeur de copie en copiant tous les attributs de l’objet, il n’y aura aucun problème avec les attributs de types primitifs ou bien de types référence si ces derniers sont non-mutables. En revanche, pour les types référence mutables, il faut agir avec précaution.

Voici un code qui pallie ce problème : pour tous les attributs de types référence mutables, il faut créer de nouvelles instances. Nous verrons plus tard comment utiliser le clonage pour parvenir à cela. En attendant, voici ce que vous pouvez faire :

SUV.java
 1package fr.polytech.aco.vehicule.voiture;
 2import java.util.Date;
 3
 4public class SUV {
 5  String nom;
 6  Date dateAchat;
 7
 8  SUV(String nom, Date dateAchat) {
 9    this.nom = nom;
10    this.dateAchat = dateAchat;
11  }
12
13  // constructeur de copie
14  SUV(SUV from) {
15    this.nom = from.nom;  // type référence non mutable, no problemo
16    this.dateAchat = new Date(from.dateAchat.getTime()); // copie
17  }
18
19  void printSUV() {
20    System.out.println("Nom = " + this.nom + " achat = " + this.dateAchat);
21  }
22
23  public static void main(String args[]) {
24    SUV suv1 = new SUV("suv 1", new Date());
25    suv1.dateAchat.setTime(123456789);
26    suv1.printSUV();
27
28    SUV suv2 = new SUV(suv1);
29    suv2.nom = "suv 2";
30    suv2.dateAchat.setTime(1234);
31
32    suv1.printSUV();
33  }
34}

Et, effectivement, si l’on exécute le code, on voit que suv1 n’est plus modifié :

Nom = suv 1 achat = Fri Jan 02 11:17:36 CET 1970
Nom = suv 1 achat = Fri Jan 02 11:17:36 CET 1970

SUV a deux attributs : nom est une String, donc une référence non mutable. Donc on n’a pas besoin de créer une nouvelle instance de String pour cet attribut. dateAchat est une Date, donc un type référence mutable. Il faut donc créer une nouvelle instance de cet attribut.

Attention

Ce que l’on vient de voir pour le constructeur de copie s’applique également aux autres constructeurs : quand vous affectez les attributs de votre instance, il faut bien réfléchir si vous voulez utiliser des références ou de nouvelles copies. Par exemple, la ligne 10 du code ci-dessus utilise une référence. Cela signifie que, si le main contient les instructions suivantes :

Date date = new Date();
SUV suv1 = new SUV("suv 1", date);
SUV suv2 = new SUV("suv 2", date);

les deux instances de SUV partageront la même date (même référence). Donc toute modification de la date de suv1 induira une modification de la date de suv2. Si ce n’est pas souhaitable, il faut écrire un code similaire à celui de la ligne 16.

Exercice 2 : La quadrature du cercle   

Écrivez un programme Java composé de deux classes Main et Cercle, toutes deux appartenant à un package fr.polytech.geometrie.

Un Cercle est caractérisé par une couleur (String), une date de dernière modification (Date), des coordonnées x,y pour le situer dans le plan 2D (double,double) et un rayon (double). La classe Cercle possède un constructeur de copie ainsi qu’une méthode (un getter) pour récupérer son rayon, et une méthode (un setter) pour le modifier. Il possède également un getter et un setter pour la date de dernière modification.

Le main crée un premier cercle c1, le copie dans un deuxième c2. Il modifie le rayon et la date de dernière modification de c2 et affiche leurs valeurs dans l’instance c1.

Indice 1 

La seule difficulté devrait résider dans le choix de copier ou non les arguments des constructeurs. Ici, seul Date est un type référence mutable et devrait nécessiter de créer de nouvelles instances. Le type double est primitif et n’en nécessite pas. Le type String est bien un type référence mais il est non mutable, donc il ne pose pas de problème non plus.

Clonage

La copie de tous les attributs d’une instance est une problématique classique que l’on rencontre dès lors qu’on manipule des types référence. Aussi, pour aider le programmeur à la résoudre, Java propose la notion de clonage.

Définition

Une classe est clonable si elle implémente l’interface Cloneable et si elle possède une méthode clone() qui permet de créer un nouvel objet, copie de l’instance qui a appelé la méthode clone().

Ci-dessous, voici un exemple de classe clonable. Sur les lignes 4 et 16, on vérifie bien les conditions ci-dessus. Donc la classe SUV est clonable. Nous allons détailler ci-dessous les différentes étapes à considérer pour le clonage.

SUV.java
 1package fr.polytech.aco.vehicule.voiture;
 2import java.util.Date;
 3
 4public class SUV implements Cloneable {
 5    String nom;
 6    Date dateAchat1;
 7    Date dateAchat2;
 8
 9    SUV(String nom, Date dateAchat) {
10        this.nom = nom;
11        this.dateAchat1 = new Date(dateAchat.getTime());
12        this.dateAchat2 = new Date(dateAchat.getTime());
13        System.out.println("SUV constructor");
14    }
15
16    public Object clone() {
17        try {
18            SUV new_obj = (SUV) super.clone();
19            new_obj.dateAchat1 = (Date) this.dateAchat1.clone();
20            System.out.println("Refs achat1: " +
21                    (new_obj.dateAchat1 == this.dateAchat1));
22            System.out.println("Refs achat2: " +
23                    (new_obj.dateAchat2 == this.dateAchat2));
24            return new_obj;
25        }
26        catch (CloneNotSupportedException e) {
27            System.out.println(e.getMessage());
28            return null;
29        }
30    }
31
32    // méthodes
33    void printSUV() {
34        System.out.println("Nom = " + this.nom +
35                " achat1 = " + this.dateAchat1 + " achat2 = " + this.dateAchat2);
36    }
37
38    // on peut créer des méthodes statiques, y compris le main:
39    public static void main(String args[]) {
40        SUV suv1 = new SUV("suv 1", new Date());
41        suv1.dateAchat1.setTime(123456789);
42        suv1.dateAchat2.setTime(123456789);
43        suv1.printSUV();
44
45        SUV suv2 = (SUV) suv1.clone();
46        suv2.nom = "suv 2";
47        suv2.dateAchat1.setTime(1234);
48        suv2.dateAchat2.setTime(1234);
49
50        System.out.println("==============================");
51        suv1.printSUV();
52        suv2.printSUV();
53    }
54}

Les étapes du clonage :

La première étape d’un clonage consiste évidemment à créer l’instance de SUV qui va contenir la copie. C’est ce que réalise l’instruction de la ligne 18 ci-dessus. L’idée consiste donc à exécuter, via super, le clonage de la classe parente. Celle-ci copiant tous ses attributs (super.clone()), il ne nous restera plus qu’à copier les attributs propres à la classe SUV

…Sur la ligne 4, on voit que SUV n’hérite d’aucune classe puisqu’il n’y a pas le mot-clef extends… wait, wait, wait… Mais à quoi fait référence super dans ce cas ? En fait, je vous ai menti : par défaut, toute classe hérite de la classe Object, qui a le bon goût de posséder une méthode de clonage spécifique. Celle-ci commence par déterminer quelle est la classe de l’instance qui a appelé la méthode clone(), SUV en l’occurrence dans l’exemple ci-dessus. Ensuite, si cette classe n’implémente pas l’interface Cloneable, une exception CloneNotSupportedException est levée. Dans le cas contraire, Object.clone() crée l’instance de la classe (SUV) et initialise ses attributs par des copies dites shallow, c’est-à-dire ce que l’on aurait obtenu en utilisant l’opérateur d’affectation :

nouvelleInstance.nom = ancienneInstance.nom;
nouvelleInstance.dateAchat1 = ancienneInstance.dateAchat1;
nouvelleInstance.dateAchat2 = ancienneInstance.dateAchat2;

et, enfin, Object.clone() renvoie la nouvelle instance. Par conséquent, pour les attributs de types primitifs ou de types référence non mutables, on n’a aucune modification à apporter à la nouvelle instance. En revanche, pour les types référence mutables, il faut faire des « deep » copies. C’est précisément ce que fait la ligne 19 avec la date d’achat 1 en utilisant la méthode clone() de Date. On devrait le faire également pour la date d’achat 2 mais, ici, j’ai choisi de ne pas le faire pour vous montrer ce que cela implique. Quand on exécute le main(), voilà ce que l’on obtient :

SUV constructor
Nom = suv 1 achat1 = Fri Jan 02 11:17:36 CET 1970 achat2 = Fri Jan 02 11:17:36 CET 1970
Refs achat1: false
Refs achat2: true
==============================
Nom = suv 1 achat1 = Fri Jan 02 11:17:36 CET 1970 achat2 = Thu Jan 01 01:00:01 CET 1970
Nom = suv 2 achat1 = Thu Jan 01 01:00:01 CET 1970 achat2 = Thu Jan 01 01:00:01 CET 1970

Ici, on observe bien que la date d’achat 1 a été copiée profondément puisque les références sur cet attribut diffèrent entre this et la nouvelle instance(Refs achat1: false). C’est bien ce que l’on souhaite. En revanche, celles de la date d’achat 2 sont identiques, et l’on n’a donc pas bien copié cette date. On peut voir, sur l’avant dernière ligne qu’effectivement la modification de la date d’achat 2 dans l’instance suv2 a modifié celle de suv1, ce que l’on voulait éviter.

Que se passe-t-il si SUV hérite d'une autre classe ?

On va maintenant créer une classe Vehicule, une classe Voiture qui hérite de Vehicule et enfin la classe SUV qui hérite de Voiture. On va ainsi voir que le clonage de SUV implique l’appel à Voiture.clone() qui, lui-même, implique l’appel à Vehicule.clone().

Vehicule.java
package fr.polytech.aco.vehicule;
import java.util.Date;

public class Vehicule implements Cloneable {
    public String marque;
    public Date dateMiseEnCirculation;

    public Vehicule(String marque, Date dateMiseEnCirculation) {
        this.marque = marque;
        this.dateMiseEnCirculation = new Date(dateMiseEnCirculation.getTime());
        System.out.println("Vehicule constructor");
    }

    public Object clone() throws CloneNotSupportedException {
        System.out.println("Cloning Vehicule");
        var newObj = (Vehicule) super.clone();
        newObj.dateMiseEnCirculation = (Date) this.dateMiseEnCirculation.clone();
        return newObj;
    }
}
Voiture.java
package fr.polytech.aco.vehicule.voiture;
import fr.polytech.aco.vehicule.Vehicule;
import java.util.Date;

public class Voiture extends Vehicule implements Cloneable {
  public Voiture(String marque, Date dateMiseEnCirculation) {
    super(marque, dateMiseEnCirculation);
    System.out.println("Voiture constructor");
  }

  public Object clone() throws CloneNotSupportedException {
    System.out.println("Cloning voiture");
    return super.clone();
  }
}
SUV.java
 1package fr.polytech.aco.vehicule.voiture;
 2
 3import java.util.Date;
 4
 5public class SUV extends Voiture implements Cloneable {
 6  String nom;
 7  Date dateAchat1;
 8  Date dateAchat2;
 9
10  SUV(String nom, Date dateAchat, String marque, Date miseEnCirculation) {
11    super(marque, miseEnCirculation);
12    this.nom = nom;
13    this.dateAchat1 = new Date(dateAchat.getTime());
14    this.dateAchat2 = new Date(dateAchat.getTime());
15    System.out.println("SUV constructor");
16  }
17
18  public Object clone() {
19    try {
20      System.out.println("Cloning SUV");
21      SUV new_obj = (SUV) super.clone();
22      new_obj.dateAchat1 = (Date) this.dateAchat1.clone();
23      System.out.println("Refs achat1: " +
24          (new_obj.dateAchat1 == this.dateAchat1));
25      System.out.println("Refs achat2: " +
26          (new_obj.dateAchat2 == this.dateAchat2));
27      System.out.println("Refs mise en circulation: " +
28          (new_obj.dateMiseEnCirculation == this.dateMiseEnCirculation));
29      return new_obj;
30    }
31    catch (CloneNotSupportedException e) {
32      System.out.println(e.getMessage());
33      return null;
34    }
35  }
36
37  void printSUV() {
38    System.out.println("Nom = " + this.nom +
39        " achat1 = " + this.dateAchat1 + " achat2 = " + this.dateAchat2);
40  }
41
42  public static void main(String args[]) {
43    SUV suv1 = new SUV("suv 1", new Date(), "Kia", new Date());
44    suv1.dateAchat1.setTime(123456789);
45    suv1.dateAchat2.setTime(123456789);
46    suv1.printSUV();
47
48    SUV suv2 = (SUV) suv1.clone();
49    suv2.nom = "suv 2";
50    suv2.dateAchat1.setTime(1234);
51    suv2.dateAchat2.setTime(1234);
52
53    System.out.println("==============================");
54    suv1.printSUV();
55    suv2.printSUV();
56  }
57}

L’exécution du main() résulte en l’affichage suivant :

Vehicule constructor
Voiture constructor
SUV constructor
Nom = suv 1 achat1 = Fri Jan 02 11:17:36 CET 1970 achat2 = Fri Jan 02 11:17:36 CET 1970
Cloning SUV
Cloning voiture
Cloning Vehicule
Refs achat1: false
Refs achat2: true
Refs mise en circulation: false
==============================
Nom = suv 1 achat1 = Fri Jan 02 11:17:36 CET 1970 achat2 = Thu Jan 01 01:00:01 CET 1970
Nom = suv 2 achat1 = Thu Jan 01 01:00:01 CET 1970 achat2 = Thu Jan 01 01:00:01 CET 1970

On voit donc bien l’enchaînement des appels des clone(). On voit également que le fait d’avoir appelé Vehicule.clone() implique que la copie de la date de mise en circulation est bien profonde, comme on le souhaitait. Enfin, il est à noter que la méthode clone() de Vehicule fait elle-même appel à Object.clone().

Que se passe-t-il si des attributs sont non clonables ?

Dans la dernière classe SUV ci-dessus, la ligne 21 permettait de cloner l’attribut dateAchat1 et, pour tout type référence mutable, on est censé utiliser cette méthode. Mais il se peut que certains de ces types n’implémentent pas l’interface Cloneable. Dans ce cas, une exception CloneNotSupportedException est levée et c’est pourquoi le code du clonage est encapsulé à l’intérieur d’un try / catch. Quand on arrive dans le catch, on n’a pas réussi à cloner l’instance. Dans ce cas, soit on considère que l’application peut quand même continuer à s’exécuter et on renvoie une référence null, soit cet échec est rédhibitoire et on lève une exception.

Exercice 3 : MyArray clonable   

Dans votre classe MyArray<T>, écrivez une nouvelle méthode setElement qui prend en arguments un index et un élément elt de type T et qui remplace l’élément de MyArray<T> situé à l’indice index par elt.

Faites en sorte que votre classe soit clonable. Clonez une de vos instances de MyArray<T>. Utilisez la méthode setElement pour modifier l’instance clonée et affichez les contenus de l’instance d’origine et de l’instance clonée (afin de vérifier que la modification du clone n’induit aucune modification sur l’instance d’origine).

 
© C.G. 2007 - 2024