Université Claude Bernard Lyon 1 ISFA MASTER 2 – IR, 2015–2016 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 Attribut "Nom" @6ee5a6: String Attribut "Prenom" @a1378b: String Attribut "Naissance" @41ff21: GregorianCalendar ... e2 : Etudiant e3 : Etudiant Opérateur new @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