IFT2030 Langages de programmation TP3 par: Nicola Grenon GREN30077303 et Jean-François Delisle DELJ02057809 lundi dix-sept avril deux mille six I – Le fonctionnement des programmes. Les deux programmes que nous avons écrits sont basés sur un schème similaire. Les principales différences structurelles, mis à part évidemment ce que nécessite la syntaxe du langage, concernent surtout la structure de données sousjacente à la mémorisation des données. Là où le programme en C utilise des listes simplement chaînées, le programme en Java utilise lui des LinkedList, simplement parce que la structure était déjà disponible, simple d'utilisation tout en ayant une similitude avec notre liste simplement chaînée intéressante. Mais au niveau des objets Java ou des struct de C, la forme est identique. Ainsi, la parcelle d'information de base dans les deux environnements est la Chaîne définie comme contenant un string (au sens de suite de caractères) et un compteur de multiplicité. Chaque fois qu'on voudra ajouter une chaîne de caractère à notre base de données, on vérifiera donc si elle est déjà présente dans la liste de toutes celles déjà employées et si c'est le cas, on incrémentera simplement le compteur au lieu d'en allouer une nouvelle. De même, au moment d'en effacer une, on vérifiera si la multiplicité de celle-ci est supérieure à 1 avant de l'effacer complètement. Les individus de l'annuaire quant à eux se trouvent à n'être que des structures vides contenant uniquement des pointeurs/références vers les chaînes précédemment stockées. Il est à noter qu'on pourrait être tenté en Java de ne pas conserver de compteur dans la structure de la chaîne puisque de toute façon tant que l'élément est référencé il est conservé et dès qu'il ne l'est plus il est libéré. Or on s'aperçoit que parce qu'on a besoin de conserver dans une liste (...) les éléments afin de savoir si on a besoin de créer ou pas la nouvelle chaîne (ou d'en retrouver l'adresse mémoire) celle-ci resterait donc indéfiniment active si on ne savait pas quand l'exclure justement de ladite liste. Le problème qu'il reste à prévoir en C surtout est la lecture de l'information à partir de la console. Comme on ne sait pas à l'avance la longueur de la chaîne de caractère qui sera reçue, on doit donc la lire petit à petit, en affectant, par petit bloc d'espace, à nouveau dans une liste chaînée, les caractères lus. À la fin de la lecture d'une chaîne, l'adresse de celle-ci sera finalement stockée dans la liste des struct chaine. Voici trois petits diagrammes précisant la façon dont sont gérées les données dans notre programme C. La structure en Java est très proche pour les deux premiers, mais emploie deux LinkedList plutôt que des listes simplement chaînées. Structure de la liste des chaînes liste NULL dummy NULL n n s n: multiplicité n s s: texte s Structure de l'annuaire des individus annuaire NULL dummy NULL nom num rue ville nom num rue ville nom num rue ville Pointeurs vers des éléments de la liste des chaînes ... Structure de la liste chaînée (blocs) employée pour la lecture du stdin debut NULL c c c c c: *char Individus II – Comparaison des styles de programmation. La principale différence au niveau de la programmation entre les deux langages est bien entendu le haut niveau d'abstraction, permis par Java et les classes prédéfinies accessibles, comparativement à C. Bien sûr en C nous aurions sans doute pu tirer avantage de plus d'outils déjà prêts, mais il reste que dans l'ensemble, Java permet souvent d'exprimer au moyen d'une simple instruction ce qui nécessite en C plusieurs lignes de code. L'encapsulation du Java permet également une synthétisation et un groupement des informations qui rend la relecture du code beaucoup plus aisée. La gestion simplifiée des exceptions est aussi un incontournable. Dans le code du programme en C, le nombre de lignes que l'on est obligé d'ajouter pour gérer simplement les dépassements de mémoire est effarant. À chaque lecture de la console, on doit tester le retour, à chaque allocation d'un maillon de chaîne on doit tester le retour et à chaque allocation d'un élément de nos chaînes on doit tester le retour... sans compter les tests sur les retours eux-mêmes si on n'est pas déjà dans le main! Et en elle-même, la gestion automatisée du collecteur d'espace mémoire inutilisé (garbage collector) rend aussi caduque une bonne partie du code qui est nécessaire en C. 2.1) Les chaînes de longueur quelconque. Le problème de la lecture des chaînes de longueur quelconque en est un du paradoxe de l'œuf ou de la poule... Pour savoir combien allouer de mémoire à la structure de donnée qui contiendra ladite chaîne, il faudrait d'abord en connaître la taille finale... ce qu'on ne saura qu'une fois la chaîne complètement lue... et donc déjà stockée. Comme on est en l'absence d'une structure qui s'ajuste automatiquement selon le besoin, on a donc élaboré une stratégie de «morcelage». On a choisit de lire les sections de la chaîne petit bout par petit bout en les concaténant ensuite via une liste chaînée. On perd un peu d'espace dans le processus (pointeurs chaînant) et on a ensuite le choix entre traîner cette structure complexe ou la transférer dans une seule grande chaîne de caractère de la bonne longueur... nécessitant au passage des opérations coûteuses en espace ou en temps de calcul. La fonction Java elle permet de se libérer de cette complexité en faisant ce genre de gestion de façon automatique. 2.2) Le partage des chaînes / la récupération de l'espace utilisé. Notre solution au partage des chaînes utilise la même tactique dans les deux cas. Nous en avons déjà discuté plus haut, mais il s'agit en résumé que la structure ou l'objet définissant un individu (et donc ses informations) ne soit qu'un registre de références à des éléments existants ailleurs, dans une liste qui elle gère l'ensemble de toutes les chaînes connues et le compte du nombre de fois que chacune est utilisée. Il ne nous reste alors plus qu'à incrémenter ou décrémenter chacun des compteurs au besoin, en ajouter une nouvelle si elle n'est pas déjà connue ou à libérer celles dont le compte descend à 0. La récupération de l'espace, une fois cette structure établie, est donc simple. Il suffit de libérer la mémoire au moment ou on exclu l'élément de notre liste chaînée. 2.3) Le traitement des erreurs. Le traitement des erreurs (donc des exceptions) est très fastidieux en C, car il faut développer des automatismes. Pour le dépassement de mémoire par exemple, il faut, à chaque malloc, prévoir un test pour déterminer si l'allocation a réussi et prévoir un enchaînement d'actions à exécuter dans ce cas dans le corps même de l'application. Tandis qu'en Java, le tout peut être traité en dehors des limites des fonctions que nous tentons de définir et ainsi modulariser le traitement des exceptions a part, de façon claire et concise. Les autres types d'erreurs peuvent facilement être regroupées en Java et aussi traitées de façon générale là où en C il faudrait essayer de prévoir tous els cas de figure possibles et imaginables... Ce qui peut bien entendu être une grosse source d'erreurs d'inattention, d'oublis et autres impondérables. 2.4) La syntaxe. La syntaxe «âgée» de C en elle-même est également une source de problèmes. Souvent mineurs, mais quand même agaçants, citons par exemple l'obligation de déclarer avant le début du code toutes les variables qui seront utilisées. III – Forces et faiblesses. Après avoir ainsi décrit les problèmes rencontrés, il devient facile de conclure que le C a pour principale faiblesse, en ce qui nous concerne, une lourdeur d'implémentation due à l'obligation, pour faire un code performant et sécuritaire, d'ajouter un grand nombre de tests là où en Java on peut simplement prévoir le cas principal et traiter de façon modulaire, à part, les cas d'exception. Il est malheureusement très facile en C d'omettre des vérifications essentielles. De plus, le modèle C ne nous permet pas aussi facilement de modulariser les structures de données. On se trouve fréquemment, par exemple, à traiter dans le cœur même du programme des éventualités à couvrir découlant de l'intérieur d'une des fonctions. En Java, avec du bon code, quand on retourne au main, on n'a plus qu'à traiter l'information qui y est acheminée, les cas spéciaux ont pour la plupart été réglés avant. En Java par contre, on peut facilement perdre de vue la structure sous-jacente des informations et par conséquent générer du code d'une moins grande efficacité voire redondant ou inutile. Mais somme toutes, il est bien plus facile d'exprimer le même concept en Java qu'en C. Java supplante finalement clairement à notre avis C pour deux atouts qui sont une réelle révélation, la gestion automatique de la récupération de la mémoire et la gestion des exceptions. Deux outils qui nous ont fait sauver des heures et des heures de travail. IV – Hachage. Il pourrait exister un problème en Java (et d'ailleurs en C) en ce qui a trait à l'utilisation d'une table de hachage si cela nous faisait perdre de vue la gestion de la multiplicité d'une chaîne rencontrée que nous avons utilisée. En l'occurrence, en Java, si on utilisait le Hashtable disponible avec une vérification de collisions externes, on perdrait l'avantage qu'on a dans notre structure plus simple, à savoir la désallocation immédiate d'une chaîne qui n'est plus nécessaires. Dans la structure que nécessiterait la Hashtable, nous perdrions cet avantage à moins d'une implantation complètement manuelle (et coûteuse en temps de calcul) afin de ne plus référencer les chaînes caduques dès qu'elle le deviennent.