Université Claude Bernard Lyon 1 MASTER 2 IR, 2015 2016 ISFA Remise à Niveau Java TD4. Classe Object et comparaison d objets en Java André FABBRI Compétences indispensables à acquérir : Faire connaissance avec la classe Object Distinguer l égalité superficielle de celle profonde entre deux objets 1 La classe Object Dans l API Java, toute classe existante ainsi que toute classe que vous créez hérite automatiquement de la classe Object. Ainsi n importe quel objet créé a aussi accès aux méthodes proposés par cette classe. Ces méthodes correspondent à des fonctionnalités fondamentales de Java communes à tous les objets. Vous trouverez ci-dessous les signatures des principales méthodes de Object habituellement utilisées : public String tostring() : représentation de l objet sous la forme d un String ; public boolean equals(object obj) : vérifie si l objet sollicité est égal à l objet obj ; public int hashcode() : retourne une valeur de hashage pour cet objet ; protected Object clone() throws CloneNotSupportedException : retourne une copie de l objet. NB : throws CloneNotSupportedException signifie que la méthode clone est suceptible de retourner une exception Java. Nous n aurons pas le temps de couvrir cette fonctionnalité de Java mais pour plus d information veuillez vous reporter à : http: // jmdoudoux. developpez. com/ cours/ developpons/ java/ chap-exceptions. php 1.1 Des méthodes communes à tout objet Si les méthodes de Object sont communes à toutes les instances en Java, leur comportement par défaut est très simple, voire simpliste : public String tostring() : retourne l adresse mémoire de l objet ; public boolean equals(object obj) : compare les adresses mémoires des deux objets. Pour mieux saisir le comportement de ces méthodes en particulier, il est nécessaire de bien comprendre le fonctionnement interne de Java. 1
(rappel) - initialisation et affectation en Java En Java, il est important de distinguer une initialisation d une affection pour les instances d objets a. Une initialisation (opérateur new) réserve un nouvel espace mémoire pour l objet. L espace mémoire d un objet inclut une zone pour chacun ses attributs ainsi qu une adresse mémoire unique. Lors d une affectation (opérateur = tout seul), aucune zone mémoire n est réservée. La variable pointe simplement vers l adresse mémoire qui lui a été affectée. a. c.a.d qui ne correpsondent donc pas à des types primitifs (cf. TD2) 1.2 Exercice - mise en pratique L administration de l ISFA souhaite réaliser une base de donnée des anciens étudiants. Pour chaque étudiant, on souhaite conserver les informations minimales pour les identifier (nom, prénom date de naissance) ainsi que l année de leur première inscription, la formation initialement suivie et le dernier diplôme qu ils détenaient à cette époque là. Cependant la procédure d inscription génère chaque année une nouvelle fiche par étudiant inscrit ; risquant ainsi d introduire des doublons dans la base de données... Dans un nouveau projet Netbeans, vous allez créer une classe Etudiant avec les attributs privés et les méthodes publiques suivant : nom : le nom de l étudiant en String ; prenom : le prénom de l étudiant en String ; naissance : la date de naissance de l étudiant de type GregorianCalendar ; anneeinscription : année d inscription à une formation de type int ; formation : nom de la formation suivie en String ; dernierdiplome : nom du dernier diplôme obtenu en String ; get... () : différents accesseurs à chacun des attributs privés ; setdernierdiplome(...) : modificateur pour l attribut privé dernierdiplome ; Etudiant(...) : constructeur initisalisant l ensemble des attributs. Dans votre programme principal vous initialisez trois variables de type Etudiant comme cela est présenté ci-dessous : Etudiant e1 = new Etudiant(...); //intialisation d un etudiant e1 Etudiant e2 = e1; //affectation de e1 dans e2 //initialisation de e3 avec les memes parametres que e1 Etudiant e3 = new Etudiant(...); 2
Variables codées en Java Représentation dans l espace mémoire adresse mémoire e1 : Etudiant Opérateur = @7dd66aa :Etudiant @6ee5a6: String @a1378b: String @41ff21: GregorianCalendar Attribut "Nom" Attribut "Prenom" Attribut "Naissance"... e2 : Etudiant Opérateur new e3 : Etudiant @7d4b221 :Etudiant Figure 1 Illustration simplifiée de la représentation mémoire en Java Question 1 Observez le résultat de la méthode tostring() pour chacune des variables de type Etudiant. Comment est structuré l affichage par défaut? Question 2 Observez le résultat des appels suivant e1.equals(e2) et e1.equals(e3). Les résultats obtenus vous paraissent-ils logiques? Pourquoi? 2 Redéfinition de la méthode tostring Les méthodes présentes dans la classes Object sont souvent utilisées par des composantes de l API. La redéfinition de ces méthodes permet d adapter leur comportement aux objets des classes que l on développe. Pour savoir comment redéfinir une fonction existante dans la classe mère, merci de vous référez au TD1. En particulier, la méthode String tostring() est automatiquement appelée lorsque l objet manipulé doit être converti en une variable de type String. C est pour cette raison que vous pouvez fournir directement un objet quelconque à la méthode System.out.println(...). Dans la classe Etudiant développée précédemment, redéfinissez la méthode String tostring() de la classe Object afin d afficher le contenu des attributs de chaque objet. Question 1 Affichez les attributs des objets e1, e2 et e3 dans le terminal à partir de la méthode tostring. Modifiez à présent le dernier diplôme obtenu de l instance e2 pour un diplôme différent de celui initialement fourni aux instances e1 et e3. Question 2 Affichez les attributs des objets e1, e2 et e3 dans le terminal à partir de 3
la méthode tostring. Les résultats obtenus vous semblent-ils cohérents? Pourquoi? 3 Redéfinition de la méthode equals Comme nous l avons évoqué précédemment, la méthode equals par défaut compare uniquement l adresse en mémoire des différentes instances : on parle de comparaison superficielle des instances (shallow comparison en anglais). Il s agit d une relation d égalité a minima dans la mesure où l API ne dispose pas de plus amples informations sur l objet que vous souhaitez concevoir : deux variables partageant le même espace mémoire sont identiques par défaut mais, à l inverse, deux objets avec les mêmes attributs peuvent avoir des espaces mémoires différents. Une comparaison profonde (deep comparison en anglais) considère en revanche les valeurs de chacun des attributs. Suivant l application ou le modèle de données choisi, il ne sera pas nécessaire de comparer la totalité des attributs des instances deux à deux : c est dans ce genre de situation que la méthode equals prend tout son sens! Bonnes pratiques : redéfinition de boolean equals(object obj) Nous vous présentons ici les principales étapes à réaliser lorsque l on redéfinit la méthode equals. Les éléments présentés sont une synthèse rapide du très bon tutoriel accessible au lien suivant : http://technofundo.com/tech/java/equalhash.html. Pour de plus amples détails merci de vous référez à cette page. 1. Test de réflexivité : On vérifie tout d abord que l Object comparé ne correspond pas à la même adresse mémoire que l objet courant (this), auquel cas on retourne immédiatement true ; 2. Test du type : On vérifie ensuite que l objet comparé est bien initialisé (différent de null) et appartient à la même classe que l objet courant (.getclass()). Le cas échéant, il suffit de renvoyer false ; 3. Test des attributs : Une fois la nature de la classe vérifiée, on peut convertir l Object fournit en paramètre dans son véritable type (cast avec la notation (MaClasse)). On compare enfin les attributs que l on souhaite contrôler. On fera néanmoins attention aux deux points suivants pour comparer les attributs qui ne sont pas des types primitifs : vérifier qu ils soient bien initialisés avant de les comparer ; toujours utiliser la méthode equals pour les comparer! NB : Lorsque l on redéfinit la méthode equals, il est en général impératif de redéfinir la méthode hashcode comme nous le verrons par la suite. Redéfinissez à présent la méthode equals pour la classe Etudiant. Nous considérerons ici que deux étudiants sont identiques dès lors qu ils disposent des attributs nom,prenom et naissance identiques. 4
Créez à présent une quatrième instance e4 de la classe Etudiant initialisée avec les mêmes paramètres que e3 à la différence près du prenom qui sera différent. Question 1 Comparez les résultats par les appels des méthodes e1.equals(e2), e1.equals(e2), e1.equals(e3),e1.equals(e4) et enfin e3.equals(e4). Vérifiez que votre code s exécute correctement. 4 Redéfinition de la méthode hashcode La méthode int hashcode() est utilisée pour calculer une valeur dite de «hash» ou «hashage» associée à une instance. Cette valeur est généralement utilisée pour l identifier «de façon unique» dans une table de hash. Selon les spécifications de l API Java, la méthode hashcode est tenue de retourner la même valeur de hash lorsque deux objets sont considérés comme identiques, conformément à la méthode equals. C est pourquoi lorsque l on redéfinit il est aussi nécessaire de redéfinir la méthode hashcode afin d assurer le respect de cette propriété. En pratique, il suffit d utiliser dans le calcul de la valeur de hash, les mêmes attributs que ceux comparés dans la méthode equals. Bonnes pratiques : redéfinition de int hashcode() Nous vous présentons ici les principales étapes à réaliser lorsque l on redéfinit la méthode hashcode. Les éléments présentés sont une synthèse rapide du très bon tutoriel accessible au lien suivant : http://technofundo.com/tech/java/equalhash.html. Pour de plus amples détails merci de vous référez à cette page. 1. Initialisation : attribuer une valeur int initiale au hash de l objet. Il s agit en général d une valeur arbitraire (par exemple 7) ; 2. Hash des attributs : calculer une valeur de hash pour chacun des attributs en int (32 bits) : pour un type primitif : on le convertira éventuellement int ; pour un type objet : on utilisera la fonction hashcode de l objet ou une valeur de 0, lorsque l objet n a pas été initialisé (égal à null). 3. Intégration des hash : on ajoutera progressivement à la valeur de hash de l objet les valeurs de hash de chacun des attributs en multipliant, avant ou après chaque ajout, la valeur obtenue par un autre entier arbitraire (31 par exemple). L extrait de code ci-dessous illustre les pratiques présentées : int hash = 7; // a repeter pour chaque attribut hash = hash * 31 + hashdelattribut ; return hash; 5
Question 1 Redéfinissez à présent la méthode hashcode de la classe Etudiant afin de respecter les spécifications de l API Java. Vous vérifierez que des étudiants identiques selon equals retournent bien la même valeur de hash. Dans le programme principale instanciez à présent un objet de type HashSet<Etudiant> correspondant à la base des anciens élèves (cf. http://docs.oracle.com/javase/7/ docs/api/java/util/hashset.html pour de plus amples détails). Vous ajouterez ensuite les étudiants correspondant aux deux promotions ci-dessous. Question 2 Vérifiez que chaque étudiant est présent en un seul exemplaire et que les dates indiquées correpsondent à celles des premières inscriptions (vous passerez en revue les éléments du conteneur au moyen d itérateur cf. TD2). Comment expliquez-vous un tel résultat? // exemple de promotions pour tester votre programme ArrayList<Etudiant> promo2013 = new ArrayList<Etudiant>(); promo2013.add(new Etudiant("Nuzit","Lucie",new GregorianCalendar(1990,06,14),2013,"L3-SAF","Baccalauréat")); promo2013.add(new Etudiant("Tété","Hervé",new GregorianCalendar(1989,10,5),2013,"L3-SAF","Baccalauréat")); promo2013.add(new Etudiant("Tinmarre","Augustin",new GregorianCalendar (1990,02,20),2013,"L3-SAF","Deug-Info")); ArrayList<Etudiant> promo2014 = new ArrayList<Etudiant>(); promo2014.add(new Etudiant("Nuzit","Lucie",new GregorianCalendar(1990,06,14),2014,"M1-IR","L3-SAF")); promo2014.add(new Etudiant("Tinmarre","Augustin",new GregorianCalendar (1990,02,20),2014,"M1-IR","L3-SAF")); promo2014.add(new Etudiant("Detriage","Edgard",new GregorianCalendar (1989,12,21),2014,"M1-IR","L3-Info")); 6