|
Conventions d'écriture et portage |
Conventions
d'écriture
Portage de programmes écrits en C/C++
Ce chapitre vous indique les conventions généralement utilisées dans l'écriture des programmes et des suggestions pour vous aider à récupérer des programmes écrits en C ou C++.
Ces suggestions et les exemples qui sont donnés peuvent vous servir aussi comme complément d'information avant de passer à l'étude de la bibliothèque Java.
Par convention, l'écriture de vos classes devrait respecter les conventions suivantes :
- Si un fichier .java définit une classe ou une interface public, cette classe et le nom du fichier doivent avoir le même nom.
- Le nom des classes est une suite d'un ou plusieurs mots en minuscules, dont la première lettre est en majuscule (par exemple MaClasse).
- Le nom des interfaces utilise la convention des classes et utilise généralement un adjectif qualificatif (se terminant souvent par able).
- Le nom des méthodes commence par une minuscule, et représente une action :
- Les méthodes utilisées pour interroger ou modifier la valeur d'un champ var, doivent s'appeler getVar () et setVar () respectivement, ou éventuellement isVar () et setVar () si var est du type boolean. Cette convention n'est utile que si vous voulez utiliser facilement une classe comme JavaBean.
- Le nom des méthodes convertissant un objet en autre chose, commence par to (par exemple toString ()).
- Le nom des champs non final commence par une minuscule. Ce type de variable est rarement déclaré public ou protected ; il vaut mieux utiliser des variables private accessibles par des méthodes comme expliqué précédemment.
- Le nom des champs final (constantes) est une suite d'un ou plusieurs mots en majuscules, séparés par le caractère _ (par exemple MAX_PRIORITY).
- Le nom des packages représentant des sous-répertoires est en minuscules, et java est un nom de package réservé pour les classes standards Java.
Comme vous pouvez le voir, ces conventions ne laissent que peu de choix sur la langue à utiliser dans un programme Java. Mais, comme toute convention, vous pouvez l'ignorer et créer votre propre convention d'écriture.
Documentation des fichiers .java
Java définit des règles de syntaxe pour l'écriture des commentaires destinés à des fins de documentation. Si vous les respectez, vous pourrez les extraire pour fabriquer un fichier HTML, en utilisant la commande javadoc.
Vous avez développé un certains nombres de routines en C ou de classes en C++, et vous aimeriez les porter en Java... Les remarques signalées par les symboles et des chapitres précédents et les suggestions qui suivent devraient vous guider pour mener à bien cette tâche qui peut prendre un certain temps suivant les fonctionnalités du C/C++ que vous avez utilisé.
Les suggestions décrites ici ne concernent que les routines C/C++ utilisant la bibliothèque standard du C (stdio.h, stdlib.h,...) et pas les applications utilisant les routines de l'Interface Graphique de tel ou tel système (Windows, XWindow/Motif,...).
Elles ne représentent pas forcément la solution idéale à tel ou tel problème, mais plutôt une solution pragmatique et rapidement réalisable.
Rien ne vous empêche de revoir complètement l'architecture de vos routines pour qu'elles respectent les principes de la programmation orientée objets à la lettre.Conception des classes
Tout d'abord, essayez d'éventuellement modifier et de rassembler vos routines pour qu'elles puissent former des classes cohérentes.
Si la conception de vos routines n'utilise pas de principes de la programmation orientée objets comme l'encapsulation, n'ayez pas de scrupules à transposez toutes vos variables globales et vos fonctions en champs et méthodes de classe static. Une fois votre première version de portage terminée, vous pourrez éventuellement utiliser plus amplement la programmation objet.
N'oubliez d'ajouter le nom de la classe et l'opérateur point (.) devant les champs et les méthodes static d'une classe, quand vous voulez y accéder en dehors de cette classe (comme par exemple System.arraycopy ()).Java n'utilisant pas de fichiers header .h, il faut rassembler les déclarations de constantes et de types avec les fonctions dans des classes déclarées dans un ou plusieurs fichiers .java.
Remplacement des définitions de type typedef
typedef peut être utilisé en C de deux manières différentes :
#define s'utilise de deux manières différentes en C, l'une pour déclarer des constantes, l'autre pour créer des macros :
#ifdef s'utilise en C pour indiquer au compilateur qu'il doit
compiler ou non une partie du code selon qu'un symbole est défini
ou non. Il est très souvent utilisé pour ajouter des instructions
spéciales de suivi lorsqu'un programme est en phase de mise au point
(ou de debugging).
L'utilisation de constantes Java booléennes (champs static
final boolean) permettent de réaliser un effet comparable
comme dans l'exemple suivant :
// Définit un symbole DEBUG #define DEBUG void methode () { //... #ifdef DEBUG printf ("OK"); #endif } |
class Classe1 { static final boolean DEBUG = true; void methode () { //... if (DEBUG) System.out.println ("OK"); } } |
Les énumérations enum, disponible uniquement à partir de Java 5.0, définissent un ensemble cohérent de constantes. Vous pouvez les remplacer soit par des classes déclarant un ensemble de constantes public, soit par des interfaces implémentées dans les classes qui en ont besoin.
En C, les variables d'un type union permettent de typer de manière différente leur zone mémoire. Dans l'exemple :
union { double x; int a; } varUnion;
varUnion a pour taille, la taille du plus grand de ses éléments, ici la taille d'un double (8 octets), et mémorise soit un double par varUnion.x ou soit un int par varUnion.a.
Le portage le plus rapide consiste à remplacer les variables C de type union par une référence de classe Object. Une instance de classe Object permet de désigner n'importe quel objet Java, car toutes les classes héritent de cette classe. De plus, l'opérateur instanceof permet de tester qu'elle est la classe de l'objet mémorisé par une telle référence. Si comme dans l'exemple précédent l'union C utilise des types primitifs, utilisez éventuellement à la place les classe d'emballages Java (Boolean, Character, Integer, Long, Float, Double).
Voici un programme C et un programme Java mis en parallèle pour illustrer une manière de transformer une union de cette manière. Ces programmes permettent d'évaluer une expression comportant des nombres double et les opérateurs +, -, * et /, mémorisée sous forme d'un arbre :
#include <stdio.h> #include <stdlib.h> struct _Expression; /* Structure Operation mémorisant */ /* une expression avec opérateur binaire */ typedef struct { char operateur; struct _Expression *expr1, *expr2; } Operation; #define TYPE_VALEUR 0 #define TYPE_OPERATION 1 /* Structure _Expression mémorisant */ /* une union entre un double et une */ /* valeur de type Operation */ /* et un champ type permettant de */ /* connaître le type dans l'union */ typedef struct _Expression { int type; union { double valeur; Operation operation; } expr; } *Expression; double calculerOperation (Operation oper); double calculerExpression (Expression expr) { /* Calcul du résultat suivant le type */ /* d'expression mémorisée dans l'union */ switch (expr->type) { case TYPE_VALEUR : return expr->expr.valeur; case TYPE_OPERATION : return calculerOperation (expr->expr.operation); } return 0; } /* Calcul d'une opération binaire */ double calculerOperation (Operation oper) { /* Evaluation des deux opérandes */ double val1 = calculerExpression (oper.expr1); double val2 = calculerExpression (oper.expr2); switch (oper.operateur) { case '+' : return val1 + val2; case '-' : return val1 - val2; case '*' : return val1 * val2; case '/' : return val1 / val2; } return 0; } void main () { /* Allocation de 5 expressions */ Expression expr = malloc (5 * sizeof (struct _Expression)); /* Construction de l'arbre représentant */ /* l'expression 2 + 3 / 4 */ expr [0].type = expr [1].type = TYPE_OPERATION; expr [0].expr.operation.operateur = '+'; expr [0].expr.operation.expr1 = &expr [2]; expr [0].expr.operation.expr2 = &expr [1]; expr [1].expr.operation.operateur = '/'; expr [1].expr.operation.expr1 = &expr [3]; expr [1].expr.operation.expr2 = &expr [4]; expr [2].type = expr [3].type = expr [4].type = TYPE_VALEUR; expr [2].expr.valeur = 2; expr [3].expr.valeur = 3; expr [4].expr.valeur = 4; printf ("Resultat de 2 + 3/4 = %g", calculerExpression (&expr [0])); } |
// Fichier TestExpression.java // A exécuter par la commande // java TestExpression // Classe Operation mémorisant // une expression avec opérateur binaire class Operation { char operateur; Object expr1, expr2; } // Pas besoin d'une classe Expression : // une expression est mémorisée par une // référence de classe Object // contenant soit un objet de classe // Double, soit une objet de classe // Operation // Classe TestExpression public class TestExpression { static double calculerExpression (Object expr) { // Calcul du résultat suivant la classe // de l'objet expr if (expr instanceof Double) return ((Double)expr).doubleValue (); if (expr instanceof Operation) return calculerOperation ((Operation)expr); throw new IllegalArgumentException (); } // Calcul d'une opération binaire static double calculerOperation (Operation oper) { // Evaluation des deux opérandes double val1 = calculerExpression (oper.expr1); double val2 = calculerExpression (oper.expr2); switch (oper.operateur) { case '+' : return val1 + val2; case '-' : return val1 - val2; case '*' : return val1 * val2; case '/' : return val1 / val2; } throw new IllegalArgumentException (); } public static void main (String [] arg) { // Construction de l'arbre représentant // l'expression 2 + 3 / 4 Operation expr1 = new Operation (); expr1.operateur = '/'; expr1.expr1 = new Double (3); expr1.expr2 = new Double (4); Operation expr0 = new Operation (); expr0.operateur = '+'; expr0.expr1 = new Double (2); expr0.expr2 = expr1; System.out.println ( "Resultat de 2 + 3/4 = " + calculerExpression (expr0)); } } |
Enfin, voici une version plus orientée objet du programme TestExpression.java :
// Déclaration de l'interface Calculable // (Aurait pu être aussi une classe abstract) interface Calculable { double calculer (); } // Classe Nombre mémorisant un double // Le résultat de calculer () est ce nombre class Nombre implements Calculable { private double nombre; // Constructeur Nombre (double nombre) { this.nombre = nombre; } // Implémentation de la méthode calculer () public double calculer () { return nombre; } } // Classe Operation mémorisant une expression avec opérateur binaire // Le résultat de calculer () est l'évaluation de cette expression class Operation implements Calculable { private char operateur; private Calculable expr1, expr2; // Constructeur Operation (char operateur, Calculable expr1, Calculable expr2) { this.operateur = operateur; this.expr1 = expr1; this.expr2 = expr2; } // Implémentation de la méthode calculer () public double calculer () { // Evaluation des deux opérandes double val1 = expr1.calculer (); double val2 = expr2.calculer (); switch (operateur) { case '+' : return val1 + val2; case '-' : return val1 - val2; case '*' : return val1 * val2; case '/' : return val1 / val2; } throw new IllegalArgumentException ("Operateur inconnu"); } } // Classe de test TestExpression public class TestExpression { public static void main (String [] arg) { // Création de l'expression 2 + 3 / 4 Calculable expr = new Operation ('+', new Nombre (2), new Operation ('/', new Nombre (3), new Nombre (4))); System.out.println ("Resultat de 2 + 3/4 = " + expr.calculer ()); } }
Comme il est signalé au chapitre sur la création des classes, tous les paramètres des fonctions sont passés par valeur en Java. Si certaines de vos fonctions requièrent de leur passer des paramètres d'un type primitif par adresse, comme dans l'exemple suivant :
void fonction1 (int *param1); int fonction2 (double *param1);
vous devrez transformer vos fonctions d'une des trois manières suivantes :
Voici un programme C et un programme Java mis en parallèle pour illustrer ces trois types de transformation :
void fonction1 (int *param1) { *param1 = 1; /* ... */ } int fonction2 (double *param1) { *param1 = 1; /* ... */ return 0; } void fonction3 (float *param1) { *param1 = 1; /* ... */ return 0; } void fonctionAppelante () { int argInt = 0; double argDouble = 0; float argFloat = 0; int valRetour; fonction1 (&argInt); valRetour = fonction2 (&argDouble); fonction3 (&argFloat); /* ... */ } |
class VotreClasse { int fonction1 (int param1) { param1 = 1; // ... return param1; } int fonction2 (DoubleValue param1) { param1.value = 1; // ... return 0; } int fonction3 (float [] param1) { param1 [0] = 1; // ... return 0; } void fonctionAppelante () { int argInt = 0; DoubleValue argDouble = new DoubleValue (0); int valRetour; float [] argFloat = {0}; argInt = fonction1 (argInt); valRetour = fonction2 (argDouble); fonction3 (argFloat); // ... } } // Classe d'emballage du type double // avec champ d'accès public class DoubleValue { public double value; public DoubleValue (double value) { this.value = value; } } |
|
Les classes d'emballage des types primitifs Boolean,
Character, Integer,
Long, Float,
Double ne permettent pas
de modifier la valeur de type primitif qu'elle mémorise, donc
elles ne peuvent pas être utilisées dans ce cas. |
Java ne permet de créer dynamiquement que des instances de classes,
grâce à l'opérateur new.
Tous les appels au fonctions malloc () et calloc () doivent
être remplacés par new Classe1 () pour créer
une instance de Classe1 et par new Classe1 [nbreElements]
pour créer un tableau mémorisant
nbreElements références de classe Classe1
(il est rappelé que vous devez individuellement créer ou affecter
chacun des nbreElements références du tableau).
Java n'a pas d'équivalent de la fonction realloc () et ne
permet pas d'agrandir un tableau existant
: il faut créer un second tableau, puis copier le premier dans le
second grâce à la méthode arraycopy
() de la classe System.
La classe Vector peut éventuellement
vous servir pour utiliser des tableaux de taille variables, mais ceci peut
vous obliger à transformer beaucoup d'instructions de votre programme
: Comme Java n'autorise pas la surcharge de l'opérateur [ ],
l'accès aux éléments d'une instance de Vector
ne se fait que par des méthodes.
Tous les appels à free (ptr) peuvent être directement supprimés ou éventuellement remplacés par ptr = null; dans les cas où vous voulez que l'objet désigné par ptr ne soit plus référencé par ptr.
Plus vous maîtriserez le fonctionnement du Garbage Collector et la notion de référence, plus vous n'hésiterez plus à supprimer les instructions free () (C'est un des plus grand plaisirs du portage !).
En Java, les chaînes de caractères sont représentées par les classes String et StringBuffer. Il faut remplacer tous les tests de caractère nul ('\0') permettant de savoir si on a atteint la fin d'une chaîne de caractères en C, par des appels aux méthodes length () de ces classes.
L'arithmétique des pointeurs est une notion absente en Java. Vous devrez remplacer son usage par l'utilisation d'un indice courant associé à un tableau, ou pour les chaînes de caractères utiliser éventuellement les méthodes de la classe String comme substring (), pour créer des chaînes de caractères à la volée sans se soucier de la libération de ces chaînes puisque le Garbage Collector est là pour ça.
Voici un programme C et un programme Java mis en parallèle pour illustrer une manière de transformer une fonction chercherSousChaine (str1, str2) , qui recherche dans la chaîne de caractères str1 une chaîne str2 (équivalent de la fonction C strstr (char *str1, char *str2) et de la méthode Java indexOf (String str) de la classe String) :
#include <string.h> char *chercherSousChaine (char *str1, char *str2) { while (strlen (str1) >= strlen (str2)) if (!strncmp (str1, str2, strlen (str2))) return str1; else str1++; return NULL; // Pas trouvé } |
class ClasseChaine { public int chercherSousChaine (String str1, String str2) { int i = 0; while (str1.length () >= str2.length ()) if (str1.startsWith (str2)) return i; else { str1 = str1.substring (1); i++; } return -1; // Pas trouvé } } |
En C, les pointeurs sur fonctions sont très pratiques et beaucoup de bibliothèques C en font l'usage. Son utilisation se regroupe principalement en deux catégories :
/* Déclaration d'un type pointeur sur */ /* fonction prenant en paramètre un int */ typedef void (* Action) (int param); /* Déclaration de deux fonctions */ /* du même type que Action () */ void action1 (int param) { /* Corps de action1 () */ } void action2 (int param) { /* Corps de action2 () */ } /* Déclaration d'un tableau mémorisant */ /* différents pointeurs de type Action */ Action tabAction [] = {action1, NULL, action2, action1}; void ExecuterAction (int etat, int param) { /* Appel de fonction suivant etat */ if (tabAction [etat] != NULL) tabAction [etat] (param); } /* ... */ |
// Déclaration d'une interface déclarant une // méthode prenant en paramètre un int interface Action { void action (int param); } // Déclaration de deux classes implémentant // la méthode action () de cette interface class Action1 implements Action { public void action (int param) { /* Corps de action () */ } } class Action2 implements Action { public void action (int param) { /* Corps de action () */ } } // Déclaration de la classe Automate class Automate { // Déclaration d'un tableau mémorisant // différents objets dont la classe // implémente l'interface Action Action tabAction [] = {new Action1 (), null, new Action2 (), new Action1 ()}; void ExecuterAction (int etat, int param) { // Appel de action () en fonction de etat if (tabAction [etat] != null) tabAction [etat].action (param); } // ... } |
soit vous associez une constante à chacune des fonctions, vous remplacez les pointeurs du tableau par ces constantes et choisissez la bonne fonction à appeler grâce à l'instruction switch (), comme dans l'exemple suivant :
/* Déclaration d'un type pointeur sur */ /* fonction prenant en paramètre un int */ typedef void (* Action) (int param); /* Déclaration de deux fonctions */ /* du même type que Action () */ void action1 (int param) { /* Corps de action1 () */ } void action2 (int param) { /* Corps de action2 () */ } /* Déclaration d'un tableau mémorisant */ /* différents pointeurs de type Action */ Action tabAction [] = {action1, NULL, action2, action1}; void ExecuterAction (int etat, int param) { /* Appel de fonction suivant etat */ if (tabAction [etat] != NULL) tabAction [etat] (param); } /* ... */ |
// Déclaration de la classe Automate class Automate { // Déclaration de deux fonctions // d'action void action1 (int param) { /* Corps de action1 () */ } void action2 (int param) { /* Corps de action2 () */ } // Déclaration des deux constantes // représentant les deux méthodes final static int ACTION1 = 1; final static int ACTION2 = 2; // Déclaration d'un tableau mémorisant // différentes constantes d'action int tabAction [] = {ACTION1, 0, ACTION2, ACTION1}; void ExecuterAction (int etat, int param) { // Appel de la bonne méthode // en fonction de etat switch (etat) { case ACTION1 : action1 (param); break; case ACTION2 : action2 (param); break; } } } |
Java ne permet pas l'héritage multiple du C++. Si vous voulez néammoins conserver une architecture de classes qui utilise l'héritage multiple, vous pouvez utiliser les interfaces, en utilisant par exemple la solution suivante :
class Type1 { public : void methode1 () { /* ... */ } }; class Type2 { public : void methode2 () { /* ... */ } }; class ClasseDerivee : public Type1, public Type2 { // La classe ClasseDerivee hérite des // méthodes methode1 () et methode2 () }; void main () { Type1 *objet1 = new Type1 (); Type2 *objet2 = new Type2 (); ClasseDerivee *objet3 = new ClasseDerivee (); objet3->methode1 (); objet3->methode2 (); // Cast implicite vers les super classes objet1 = objet3; objet2 = objet3; } |
class Type1 { public void methode1 () { /* ... */ } } // Utilisation d'une interface à la place // de la seconde classe interface Type2 { void methode2 (); } class Classe2 implements Type2 { public void methode2 () { /* ... */ } } public class ClasseDerivee extends Type1 implements Type2 { // Création d'une instance de Classe2 // pour remplacer le second lien d'héritage Type2 objetClasse2 = new Classe2 (); // Implémentation de methode2 () pour // qu'elle appelle la méthode de Classe2 public void methode2 () { objetClasse2.methode2 (); } public static void main (String [] args) { Type1 objet1 = new Type1 (); Type2 objet2 = new Classe2 (); ClasseDerivee objet3 = new ClasseDerivee (); objet3.methode1 (); objet3.methode2 (); // Cast implicite vers la super classe // ou l'interface Type2 objet1 = objet3; objet2 = objet3; } } |
Cette solution peut être longue à programmer si les super classes définissent un nombre important de méthodes. Par contre, cette solution permet de modifier aisément le programme qui utilise les classes transformées en interfaces : vous n'avez qu'à modifier le nom de la classe utilisée à l'instantiation des objets (ici transformer new Type2 () en new Classe2 ()).
|