Previous Up Next

B.3.2  Types

Généralement un type est un ensemble de valeurs. Ce qui ne nous avance pas beaucoup ! Disons plutôt qu’un type regroupe des valeurs qui vont naturellement ensemble, parce qu’elles ont des représentations en machine identiques (byte occupe 8 bits en machine, int en occupe 32), ou surtout parce qu’elles vont naturellement ensemble (un objet Point est un point du plan, un objet Object est un objet).

B.3.2.1  Typage statique

Java est un langage typé statiquement, c’est-à-dire que si l’on écrit un programme incorrect du point de vue des types, alors le compilateur refuse de le compiler. Il s’agit non pas d’une contrainte irraisonnée imposée par des informaticiens fous, mais d’une aide à la mise au point des programmes : la majorité des erreurs stupides ne passe pas la compilation. Par exemple, le programme suivant contient deux erreurs de type  :

class MalType {

   static int incr (int i) { return i+1 ; }

   static void mauvais() {
     System.out.println(incr(true)) ; // Mauvais type
     System.out.println(incr()) ;     // Oubli d'argument
   }
}

La compilation de la classe MalType échoue :

% javac MalType.java
MalType.java:9: Incompatible type for method. Can't convert boolean to int.
     System.out.println(incr(true)) ; // Mauvais type
                             ^
MalType.java:10: No method matching incr() found in class MalType.
     System.out.println(incr()) ;     // Oubli d'argument
                            ^
2 errors

Le système de types de Java assez puissant et les classes permettent certaines audaces. La plus courante se voit très bien dans l’utilisation de System.out.println (afficher une ligne sur la console), on peut passer n’importe quoi ou presque en argument, séparé par des « + » :

  System.out.println ("booléen :" + (10 < 11) + "entier : " + (314*413)) ;

Cela peut se comprendre si on sait que + est l’opérateur de concaténation sur les chaînes, que System.out.println prend une chaîne en argument et que le compilateur insère des conversions là où il sent que c’est utile.

Il y a huit types scalaires, à savoir d’abord quatre types « entier », byte, short, int et long. Ces entiers sont en fait des entiers modulo 2p (avec p respectivement égal à 8, 16, 32 et 64) les représentants des classes d’équivalence modulo 2p sont centrés autour de zéro, c’est-à-dire compris entre −2p−1 (inclus) et 2p−1 (exclu). On dit aussi que les quatre types entiers correspondent aux entiers représentables sur 8, 16, 32 et 64 chiffres binaires, selon la technique dite du complément à la base (l’opposé d’un entier n est 2pn).

Les autres types scalaires sont les booléens boolean (deux valeurs true et false), les caractères char et deux sortes de nombres flottants, simple précision float et double précision double. L’économie de mémoire réalisée en utilisant les float (sur 32 bits) à la place des double (64 bits) n’a d’intérêt que dans des cas spécialisés.

Les tableaux sont un cas à part (voir B.3.6). Les classes sont des types pour les objets. Mais attention, les types sont en fait bien plus une propriété du source des programmes, que des valeurs lors de l’exécution. Nous essayons d’éviter de parler du « type » d’un objet. En revanche, il n’y a aucune difficulté à parler du type d’une variable qui peut contenir un objet, ou du type d’un argument objet.

B.3.2.2  Conversion de type

La syntaxe de la conversion de type est simple

  (type)expression

La sémantique est un peu moins simple. Une conversion de type s’adresse d’abord au compilateur. Elle lui dit de changer son opinion sur expression. Par exemple, comme nous l’avons déjà vu en section B.2.3

  Pair p = … ;
  System.out.println((Object)p) ;

dit explicitement au compilateur que l’expression p (normalement de type Pair) est vue comme un Object. Comme Pair est une sous-classe de Object (ce que sait le compilateur), ce changement d’opinion est toujours possible et (Object)p ne correspond à aucun calcul au cours de l’exécution. En fait, dans ce cas d’une conversion vers un sur-type, on peut même omettre la conversion explicite, le compilateur saura changer son opinion tout seul si besoin est.

Il n’en va pas de même dans l’autre sens

   Object o = … ;
   Pair p = (Pair)o ;
   System.out.println(p.x) ;

Le compilateur accepte ce source, la conversion est nécessaire pour le faire changer d’opinion sur la valeur d’abord rangée dans o, puis dans p : cet objet est finalement une paire, et possède donc une variable d’instance x. Mais ici, rien ne le garantit, et l’expression (Pair)o correspond à une vérification lors de l’exécution. Si cette vérification échoue, alors le programme échoue aussi (en lançant l’exception ClassCastException). Il est malheureusement parfois nécessaire d’utiliser de telles conversions (vers un sous-type) voir 3.3.2, même quand on programme proprement en ne mélangeant pas des objets de classes distinctes.

On peut aussi convertir le type des scalaires, cette fois les conversions entraînent des transformations des valeurs converties, car les représentations internes des scalaires ne sont pas toutes identiques. Par exemple, si on change un int (32 bits) en long (64 bits), la machine réalise un certain travail. Ce calcul n’est pas ici une vérification, mais un changement de représentation. La plupart des conversions de types entre scalaires restent implicites et sont effectuées à l’occasion des opérations. Si par exemple on écrit 1.5 + 2, alors le compilateur arrive à comprendre 1.5 + (double)2, afin d’effectuer une addition entre double. Il y a un cas où on insère soit-même ce type de conversions.

   // a et b sont des int, on veut calculer le pourcentage a/b
   double p = 100 * (((double)a)/b) ;

(Notez l’abondance de parenthèses pour bien spécifier les arguments de la conversion et des opérations.) Si on ne change pas explicitement un des arguments de la division en double, alors « / » est la division euclidienne, alors que l’on veut ici la division des flottants. On aurait d’ailleurs pu écrire :

   double p = (100.0 * a) / b ;

En effet, le compilateur change alors a en double, pour avoir le même type que l’autre argument de la multiplication. Ensuite, la division est entre un flottant et un int, et ce dernier est converti afin d’effectuer la division des flottants.

Le compilateur n’effectue jamais tout seul une conversion qui risque de faire perdre de l’information. À la place, quand une telle conversion est nécessaire pour typer le programme, il échoue. Par exemple, prenons la partie entière d’un double.

  // d est une variable de type double, on veut prendre sa partie entière
  int e = d ;

Le compilateur, assez bavard, nous dit :

T.java:4: possible loss of precision
found   : double
required: int
    int e = d ;
            ^

Dans ce cas, on doit prendre ses responsabilités et écrire

   int e = (int)d ;

Il faut noter que nous avons effectivement pris le risque de faire n’importe quoi. Si d est trop gros pour que sa partie entière tienne dans 32 bits (supérieure à 231−1), alors on a un résultat étrange. Le programme

  double d = 1e100 ; // 10100
  System.out.println(d + ", " + (int)d) ;

conduit à afficher 1.0E100, 2147483647.

B.3.2.3  Complément : caractères

Les caractères de Java sont définis par deux normes internationales synchronisées ISO/CEI 10646 et Unicode. Le nom générique le plus approprié semblant être UTF-16. En simplifiant, une valeur du type char occupe 16 chiffres binaires et chaque valeur correspond à un caractère. C’est simplifié parce qu’en fait Unicode définit plus de 216 caractères et qu’il faut parfois plusieurs char pour faire un caractère Unicode. Dans la suite nous ne tenons pas compte de cette complexité supplémentaire introduite notamment pour représenter tous les idéogrammes chinois.

Un char a une double personnalité, est d’une part un caractère (comme 'a', 'é' etc.) et d’autre part un entier sur 16 bits (disons le « code » du caractère), qui contrairement à short est toujours positif. La première personnalité d’un caractère se révèle quand on l’affiche, la seconde quand on le compare à un autre caractère. Les 128 premiers caractères (c’est-à-dire ceux dont les codes sont compris entre 0 et 127) correspondent exactement à un autre standard international bien plus ancien, l’ASCII.

L’ASCII regroupe notamment les chiffres de '0' à '9' et les lettres (non-accentuées) minuscules et majuscules, mais aussi un certain nombre de caractères de contrôle, dont les plus fréquents expriment le « retour à la ligne ». Une petite digression va nous montrer que la standardisation du jeu de caractères n’est malheureusement pas suffisante pour tout normaliser. En Unix un retour à la ligne s’exprime par le caractère line feed noté '\n', en Windows c’est la séquence d’un carriage return noté '\r' et d’un line feed, et en Mac OS, c’est un carriage return tout seul ! Le plus affligeant est que ces différences s’expliquent par la conception de la machine à écrire mécanique, dont est dérivé le télétype, le premier terminal pratique des ordinateurs. En effet (visualisez une machine à écrire), pour passer à la ligne, il faut à la fois renvoyer le chariot de la machine à fond vers la droite (carriage return) et tourner le rouleau (line feed). On peut certes imaginer des effets spéciaux, écrire en escalier (Unix) ou écraser la ligne en cours (Mac OS), mais bon. Les concepteurs de la machine à écrire étaient d’ailleurs conscient de ce que les deux opérations sont souvent associées, puisque ces machines offrent un levier qui permet d’effectuer ensemble les deux opérations de pousser le chariot et de tourner le rouleau. Alors, pourquoi pas nous ?

Le plus souvent ces détails restent cachés, par exemple System.out.println() effectue toujours un retour à la ligne sur l’affichage de la console, c’est le code de bibliothèque qui se charge de fournir les bons caractères à la console selon le système sur lequel le programme est en train de s’exécuter. Toutefois des problèmes peuvent surgir en cas de transfert de fichiers d’un système à l’autre…


Previous Up Next