Morceaux de Java au fil des TP

Ce document est disponible à l'URL http://www.enseignement.polytechnique.fr/profs/informatique/Luc.Maranget/doc/java.html

Introduction

Ce document décrit des éléments de programmation Java, tels que vus dans l'ordre des TP. Il n'entend pas se substituer aux autres documentations et en particulier à l'annexe Java du poly.

1  Un langage plutôt classe

Nous choisissons de ne presque rien cacher: Java est un langage objet avec des classes. C'est à dire que les valeurs manipulées en Java (les ``trucs'') se scindent en deux catégories distinctes: Les objets sont des trucs particuliers qui contiennent:
  1. Des variables, ou champs, qui contiennent tout simplement des trucs, que l'on désigne par objet.nom.
  2. Des méthodes, qui sont plus ou moins des fonctions à n arguments, que l'on appelle par objet.nom(arg1,... ,argn).
Par exemple, une variable i déclarée comme int i, contient un bête entier. Les habitués de Caml noteront que les méthodes ne sont pas des valeurs.

Bon, on crée les objets en instanciant des classes, il n'est donc pas trop surprenant que les classes regroupent variables et méthodes tout comme les objets. En simplifiant, on crée un objet à partir d'une classe en en faisant une espèce de copie.

1.1  Programme de classe

Là où ça devient drôle c'est que les classes existent aussi en tant que telles. Essentiellement, les classes structurent les programmes que vous écrirez. Plus précisément, un programme se construit à partir de une ou plusieurs classes, dont une au moins contient une méthode main qui est le point d'entrée du programme.

Les variables des classes sont les variables globales du programme et leurs méthodes sont les fonctions du programme. Une variable ou une méthode qui existent dès que la classe existe est dite statique. Les autres existent à raison d'une variable ou méthode différente par objet quand on crée les objets, elle sont dites dynamiques. Pour s'y retrouver on a tendance à donner des noms qui commencent par une majuscule aux classes et par une minuscule aux objets.

Commençons par un programme fabriqué à l'aide d'une seule classe. Par exemple, la classe simple suivante est un programme qui affiche coucou ! sur la console:
class Simple {
  static String msg = "coucou !" ;

  public static void main (String []) // déclaration de méthode
  {
    System.out.println(msg) ;
  }
}
Cette classe ne sert pas à fabriquer des objets. Elle se suffit à elle même. Par conséquent tout ce qu'elle définit (variable msg et méthode main) est statique. Par re-conséquent, toutes les déclarations sont précédées du mot-clé static. En plus d'être statique, la méthode main doit impérativement être publique et prendre un tableau de chaîne en argument. Sinon, l'environnement d'exécution ne saura pas la lancer. Si le source est contenu dans un fichier Simple.java, il se compile par javac Simple.java et se lance par java Simple. C'est une bonne idée de mettre les classes dans des fichiers homonymes, ça permet de s'y retrouver.

En termes de programmation objet, la méthode main invoque la méthode println de l'objet System.out, avec l'argument msg. L'objet System.out étant rangé comme la variable out de la classe système. Notons que msg est en fait Simpl.msg, mais dans la classe Simple, on peut se passer de rappeler que msg est une variable de la classe Simple, alors autant en profiter.

Reste à se demander quel est l'objet rangé dans System.out. Eh bien, disons que c'est un objet d'une autre classe (La classe BufferedWriter) qui a été mis là par le système Java et ne nous en préoccupons plus. Ouf.

1.2  Complément : La méthode main

La déclaration de cette méthode doit obligatoirement être de la forme :
  public static void main (String [] arg)
ou encore :
  public static  int main (String [] arg)
Le sens du mot-clé public est expliqué plus loin. Le mot clé static est connu (cf. la section 1.1). Le reste des obligations portent sur le type de la valeur renvoyée, qui peut n'être rien (premier cas) ou un entier (second cas) et des arguments. Ces types assurent une petite collaboration avec Unix. En effet tous les programmes Unix renvoie un code de retour qui est un entier (ce code est accessible par $status une fois la commande exécutée). Ça ne sert presque à rien.

Les arguments sont plus intéressants, il s'agit des arguments données sur la ligne de commande. De sorte que l'on peut facilement écrire une commande echo en Java:
class Echo {
  public static void main (String [] arg) {
    for (int i = 0 ; i < arg.length ; i++) {
      System.out.println(arg[i]);
    }
  }
}
Ce qui nous donnera après compilation :
# java Echo coucou foo bar
coucou
foo
bar

1.3  Collaboration de classe

La classe-programme Simple utilisait d'autres classes, par exemple, la classe System. Le source de ces classes a été écrit par les auteurs du système Java, et on peut en profiter.

Pour structurer vos programmes à vous, vous pouvez aussi écrire plusieurs classes. Par exemple, réécrivons le programme simple à l'aide de deux classes. Le message est fourni par une classe Message
class Message {
  static String coucou = "coucou !" ;
}
Tandis que le programme est modifié ainsi :
class Simple {
  public static void main (String []) // déclaration de méthode
  {
    System.out.println(Message.coucou) ;
  }
}
Si l'on met la classe Message dans un fichier Message.java. Elle sera compilée automatiquement lorsque l'on compile le fichier Simple.java (par javac Simple.java). Encore une bonne raison pour mettre les classes dans des fichiers homonymes.

1.4  Méfiance de classe

Lorsque l'on fabrique un programme avec plusieurs classes, l'une d'entre elles contient la méthode main. Les autres fournissent des fonctionnalités, en général sous forme de méthodes accessibles à partir des autres classes. Prenons l'exemple du TD-1. La classe Counter fournit la fonctionnalité d'une table d'association des chaînes vers les entiers. Cela passe par 3 méthodes utilisables à partir d'autres classes, dont voici les déclarations:
    static void init (int size) // initialisation de la table d'association
    static void set (String key, int v) // création ou mise à jour d'une entrée
    static int get (String key) // récupérer une valeur
De façon interne, la classe Counter utilise une table de hachage. Tout ce qui concerne cette table de hachage n'a pas besoin d'être vu des autres classes (ici Stat) mais doit quand même être là. Les variables et méthodes privées, accessibles seulement à partir de la classe Counter elle-même sont déclarées avec le mot-clef private. Par exemple :
class Counter {
/*************************************/
/*   Données et méthodes privées     */
/*************************************/

/* Deux tableaux pour la table de hachage */
    private static int [] count = null; // rien au de'part
    private static String [] name = null; // non plus
/* Taille de la table */
    private static int size = 0;

    // Fonction de hachage, qui transforme s en un entier
    private static int hash (String s) {
       ...
    }

/*************************************/
/*   Le reste est accessible         */
/*************************************/

    static void set (String key, int v) {
       ...
    }

    static int get (String key) {
       ...
    }

    static void init (int size) {
        count = new int [size];  // initialisation du tableau des compteurs
        name = new String [size]; // initialisation du tableau des chaînes
        Counter.size = size;     // Jeu subtil sur le nommage des variables
    }
}
Ainsi on le voit, la méthode init (non-privée) de la classe Counter a bien accès aux variables privées count, name et size. Et, si ni get, ni set ne bouleversent ces initialisation, on sait positivement que ces valeurs initiales ne changeront pas.

Pour fixer les idées, voici le source complet de la classe Counter.

Cette pratique, de restreindre autant que possible la visibilité des variables et méthodes structure fortement les programmes : On parle d'abstraction, la séparation en classe segmente le programme en unités plus petites, dont on n'a pas besoin de tout savoir. Au niveau de la classe elle-même, la démarche d'abstraction revient à écrire plusieurs méthodes, chaque méthode réalisant une tâche spécifique.

L'abstraction (découpage en classes et puis en méthodes, qui interagissent selon des conventions claires qui disent le quoi et cachent les détails du comment) est un fondement de la bonne programmation, c'est à dire de la production de programme compréhensibles et donc de programmes qui sont facilement mis au point, puis modifiés.

En Java, par défaut, les méthodes (et les variables) de classes sont visibles de tout le package de leur classe (les classes sont regroupés en packages, une notion sans intérêt pour le moment), Le mot-clé private restreint la visibilité des méthodes (et variables) à leur propre classe. À l'opposé, le mot-clé public étend la visibilité des méthodes aux autres packages. C'est pourquoi, la méthode main doit être publique, afin que l'environnement d'exécution Java (qui se trouve dans un autre package que les classes que vous écrivez, vous utilisateur de cet environnement) puisse la voir et l'invoquer.

Le mot-clef public s'applique aussi aux classe, il entraîne alors que cette classe peut être étendue sans distinction de package, si je ne me trompe pas.

1.5  Reproduction de classe par héritage

La programmation objet exprime toute sa puissance par le mécanisme de l'héritage. Nous n'examinons que l'héritage du point de vue de la classe. C'est en fait extrêment simple, si une classe Foo se déclare ainsi
class Foo extends Bar {
  ...
}
Alors, la classe Foo démarre dans la vie avec toutes les variables et toutes les méthode de la classe Bar, chouette non ?

Mais la classe Foo ne va pas se contenter de démarrer dans la vie, elle peut effectivement étendre la classe dont elle hérite et ceci de deux façons : Le TD-2 utilise extends pour construire une interface graphique à partir de la MacLib.

2  Constructions de base

En première approximation, Java c'est du C avec un emballage ``objet''. C'est à dire que les déclarations et initialisations des variables et les corps des méthodes ressemblent furieusement à du C. Si on connaît déjà C, c'est bien pratique, sinon on apprend C en même temps...

2.1  Types

Java (et C aussi d'ailleurs, mais moins) a l'ambition d'être un langage fortement typé, c'est à dire que si l'on écrit un programme incorrect du point de vue des types, alors le compilateur vous jette. Il s'agit non pas d'une contrainte irraisonnée imposée par des informaticiens fous, mais d'une aide extraordinaire à la mise au point des programmes : la majorité des erreurs stupides ne passe pas la compilation. Par exemple, le programme suivant contient au moins deux erreurs de typage (confusion entre entier et booléen, oubli d'un argument dans un appel de méthode) :
class MalType {

// la methode incr prend un entier i et renvoie i+1
   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
   }
}
Eh bien, 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
Néanmoins, le système de types de Java est plus puissant que celui de C. 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)) ;
Ça peut s'admettre 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 de types là où il sent que c'est utile.

Complément : Les types de type Il y a en java trois sortes de types, les types scalaires qui sont des mot-clefs (donc colorés par emacs), les classes qui sont le type de leurs objets (par exemple String type des chaînes, objets de la classe String), et les types des tableaux à la syntaxe plutôt particulière (le type de chaque tableau est paramétré par le type de ses éléments, le type générique des tableaux ne peut donc pas être une classe).

2.2  Déclarations

De façon générale, une déclaration établit une correspondance entre un nom et une construction nommable (valeur, méthode). En outre la déclaration réserve quelque part la place nécessaire pour ranger cette valeur ou cette méthode.

Les déclarations de variables sont de la forme suivante :
  modifiers type name ;
Les modifiers sont des mots-clés (genre, static pour les variables vivantes dans toute la classe, final pour les constantes), le type est un type (genre int, int[] ou String) et name est un nom de variable. Une bonne pratique est d'initialiser les variables dès leur déclaration, ça évite bien des oublis. Pour ce faire :
  modifiers type name = expression ;
expression est une expression du bon type. Par exemple, voici trois déclarations, de variables de type entier, chaîne et tableau de chaîne :
  static int i = 0;
  static String message = "coucou" ;
  static String [] jours =
    {"dimanche", "lundi", "mardi", "mercredi",
    "jeudi", "vendredi", "samedi"} ;
Les déclarations peuvent apparaître à pas mal d'endroits, dans une classe, un corps de fonction, l'initialisation d'une boucle. Nous y reviendront.

Les déclarations de fonctions (de méthodes en terminologie Java) ne peuvent se situer qu'au niveau des classes et suivent la forme générale suivante :
  modifiers type name (args)
type est le type du résultat de la fonction (void si il n'y en a pas), name est le nom de la fonction et args sont les déclarations des arguments de la fonction qui sont de bêtes déclarations de variables (sans le ``;'' final) séparées par des virgules ``,''. Suit ensuite le corps de la fonction, entre accolades ``{'' et ``}''. Il n'y a pas lieu d'initialiser les arguments et d'ailleurs cela n'aurait aucun sens puisque les arguments sont initialisés à chaque appel de fonction.

Complément : Déclarations sans initialisation. Les déclarations de variables qui sont aussi des déclarations de champs de classe (et d'objet, c'est pareil) sont un rien particulières. En effet, si ces déclarations ne comportent pas d'initialisation, alors les variables déclarées contiennent des valeurs conventionnelles, zéro pour un entier, false pour un booléen etc. et null pour un objet. On peut rapprocher cet effet, dont il fait savoir profiter, du cas des éléments des tableaux (cf. la section 6.7.2).

2.3  Principales instructions

Il y a une distinction assez jésuite entre expressions (qui produisent un résultat) et instructions (qui n'en produisent pas). C'est jésuite parce qu'en fait toute expression suivie de ``;'' devient une instruction.

Les expressions les plus basiques sont :
Constantes
Soit 1 (entier), true (booléen), "coucou !" (chaîne), etc. Une constante amusante est null, qui est un objet sans champs ni méthodes.
Usage de variable
Soit ``i'', où i est une variable déclarée par ailleurs. En fait, si la variable i est déclarée dans la classe Classe, alors on y accède normalement par ``Classe.i'', si la variable variable i est un champ de l'objet objet, alors on y accède par ``objet.i''. Heureusement pour nous, on peut utiliser la notation abrégée ``i'' à l'intérieur de la classe Classe et de l'objet objet. Ce qui fait que dans la pratique, il y a pas mal d'implicite dans les usages de variables.
Appel de méthode
C'est comme les variables :
statique
Soit Classe.f(i)f est une méthode de la classe Classe. par ailleurs, si Classe.g ne prend pas d'argument, on écrit Classe.g().
dynamique
Si objet est un objet possédant la méthode f, alors on invoque cette méthode par ``objet.f(...)''.
Notez bien que les mêmes notations abrégées que pour les variables s'appliquent à l'intérieur des classes et des objets. La notation abrégée est parfois source de surprises et ont doit de temps en temps revenir à la notation complète, au moins dans sa tête, pour comprendre ce qui se passe.

Variable this
C'est carrément spécial et peut être ignoré en première lecture, à l'intérieur des méthodes d'un objet, this est cet objet lui-même. De part la notation abrégée qui interprète ``variable'' comme ``this.variable en l'absence de liaison locale de la variable variable on a généralement pas besoin d'expliciter this.

Usage des opérateurs
Par exemple i+1 (addition) ou i == 1 || i == 2 (opérateur égalité et opérateur ou booléen).
Accès dans les tableaux
Par exemple t[i], où t est un tableau défini par ailleurs.
Affectation
Par exemple i = 1, l'expression à droite de = est calculée et le résultat est rangé dans la variable donnée à gauche de =. En fait on peut trouver autre chose qu'une variable à gauche de =, on peut trouver tout ce qui désigne une case de mémoire, par exemple, un élément de tableau t[i].

Le résultat de cette ``expression'' est la valeur affectée. Ce qui permet des trucs du genre i = j = 0, pour initialiser i et j à zéro. Cela se comprend si on lit cette expression comme i = (j = 0).

Expression parenthésée
Si e est une expression, alors (e) est aussi une expression. Cela permet essentiellement de contourner les priorités relatives des opérateurs, mais aussi de rendre un source plus clair. Par exemple, on peut écrire (i == 1) || (i == 2), c'est peut-être plus lisible que i == 1 || i == 2.
C contenait quelques expressions pas piquées des vers qui sont reprises en Java.
Incrément, décrément
Soit i variable de type entier. Alors l'expression i++ range i+1 dans i et renvoie l'ancienne valeur de i. L'expression i-- fait la même chose avec i-1. Enfin l'expression ++i (resp. --i) est similaire, mais elle renvoie la valeur incrémentée (resp. décrémentée) de i en résultat.

Affectations particulières
La construction i op= expression est sensiblement équivalente à i = i op expression. Par exemple:
  i *= 2
range deux fois le contenu de i dans i et renvoie donc ce contenu doublé. Les finauds noteront que ++i est aussi i += 1.
Ces expressions avancées sont géniales, mais il est de bon goût de les employer avec parcimonie. Que l'on songe seulement au sens de t[++i] *= i++ et l'on comprendra.

Les instructions les plus courantes sont les suivantes:
Expressions suivies de ;
Évidemment cette construction n'est utile que si e fait des effets de bords (c'est à dire fait autre chose que rendre son résultat). C'est bien sûr le cas d'une affectation par exemple.

Séquence
On peut grouper plusieurs instructions en une séquence d'instruction, les instructions s'exécutent dans l'ordre.
  i = i + 1;
  i = i + 1;


Déclarations
Une déclaration de variable (suivie de ``;'') est une instruction qui réserve de la place pour la variable déclarée et l'initialise éventuellement:
  int i = 0;
Il ne faut pas confondre affectation et déclaration, dans le premier cas, on modifie le contenu d'une variable qui existe déjà, dans le second on crée une nouvelle variable.

Bloc
On peut mettre une séquence d'instructions dans un bloc {...}. La portée des déclaration internes au bloc s'éteint à la sortie du bloc. Par exemple, le programme suivant:
  int i = 0 ;
  {
    int i = 1; // Déclaration d'un nouvel i
    System.out.println("i=" + i);
  }
  System.out.println("i=" + i);
affiche une première ligne i=1 puis une seconde i=0. Il faut bien comprendre que d'un affichage à l'autre l'usage de variable ``i'' ne fait pas référence à la même variable. Lorsque l'on veut savoir à quelle déclaration correspond un usage, la règle est de remonter le source du regard vers le haut, jusqu'à trouver la bonne déclaration.

Attention, seule la portée des variables est limitée par les blocs, en revanche l'effet des instruction passe allègrement les frontières de blocs. Par exemple, le programme suivant :
  int i = 0 ;
  {
    i = 1; // Affectation de i
    System.out.println("i=" + i);
  }
  System.out.println("i=" + i);
affiche deux lignes i=1.

Retour de méthode
On peut dans le corps d'une méthode retourner à tout moment par l'instruction return expression;, où expression est une expression dont le type est celui des valeurs retournées par la méthode.

Par exemple, la méthode double qui double son argument entier s'écrit :
  int double (int i) {
    return i+i ;
  }
Si la méthode ne renvoie rien, alors return n'a pas d'argument :
  void rien () {
    return ;
  }
À noter que, si la dernière instruction d'une méthode est return ; (sans argument), alors on peut l'omettre. De sorte que la méthode rien s'écrit aussi :
  void rien () { }


Conditionnelle
C'est l'instruction if:
  if (i % 2 == 0) { // opérateur modulo
    System.out.println("C'est pair") ;
  } else {
    System.out.println("C'est impair") ;
  }
Boucle while
C'est assez tradi également. Voici les entiers de zéro à neuf:
  int i = 0 ;
  while (i < 10) {
    System.out.println(i) ;
    i = i+1 ;
  }
Soit while (expression) bloc, on exécute expression, qui est une expression booléenne, si le résultat est false c'est fini, sinon on exécute bloc et on recommence.

Boucle do
C'est la même chose mais on teste la condition à la fin du tour de boucle, conclusion : on passe au moins une fois dans la boucle :
  i = 0 ;
  do {
    System.out.println(i) ;
    i = i+1 ;
  } while (i < 10)
(Les entiers de zéro à neuf).

Boucle for
C'est celle de C. Elle est un rien complexe.
  for (int i=0 ; i < 10 ; i = i+1)
    System.out.println(i);
C'est à dire que la syntaxe générale est
  for (einit ; econd ;  enext)
    bloc
Et que c'est quasiment la même chose que d'écrire :
  {einit ;
    while (econd) {
      bloc
      enext;
    }
  }
Gestion du contrôle
Certaines instructions permettent de ``sauter'' quelquepart de l'intérieur des boucles. Ce sont des formes polies de goto. Il s'agit de break, qui fait sortir de la boucle, et de continue qui commence l'itération suivante en sautant par dessus ce qui reste de l'itération en cours.

Ainsi on peu ecrire, pour rechercher si l'entier foo est présent dans le tableau t.
   boolean trouve = false ;

   for (int i = 0 ; i < t.length ; i++) {
     if (t[i] == foo) {
       trouve = true ;
       break ;
     }
   }
   // trouve == true ssi foo est dans t
Ou encore si on veut cette fois compter les occurences de foo dans t :
  int count = 0 ;

   for (int i = 0 ; i < t.length ; i++) {
     if (t[i] != foo) {
       contine ;
     }
     count++ ;
   }
Noter que dans les deux cas on fait des choses un peu compliquées. Dans le premier cas, on pourrait faire une méthode et utiliser return, ou une boucle while sur trouve. Dans le second cas, on pourrait écrire le test d'égalité. Bon, c'est pour expliquer car, dans certaines situations, ces instructions sont pratiques.

2.4  Types scalaires

2.4.1  Booléens

Les booléens sont les deux valeurs true et false, le type des booléen est boolean.

Les booléens s'introduisent discrètement dans les programmes par l'intermédiaire des opérateurs de comparaison (qui s'appliquent aux scalaires en général), on a l'égalité ==, la différence !=, les plus petit, plus grand, etc. <, >, <= et >=.
  boolean b1 = 1 < 2 ; // vrai
  boolean b2 = 1 != 1 ; // faux
Il existe des opérateurs sur les booléens, la négation ! (sisi), le ``et logique'' && et le ``ou logique'' ||. par exemple on exprime la condition i appartient à l'intervalle [0...9] de la façon assez élégante suivante :
  if (0 <= i && i <= 9) ...
Complément : Sémantique Les opérateurs && et || sont paresseux et s'évaluent de la gauche vers la droite, c'est à dire que dans l'expression ``e1 && e2'', l'expression e2 ne sera pas évaluée dans la cas où e1 s'évalue comme false car ce résultat est suffisant pour savoir que la conjonction vaut false. Il en va de même avec || et true. Ce truc peut servir dans une expression du genre :
  ((0 <= i && i < max) && a[i] < m)
max est la taille du tableau a. La sémantique de && évite toute erreur d'accès au tableau a.

2.4.2  Entiers

Rien de bien nouveau, le type scalaire des entiers s'appelle int. les entiers sont signés sur 32 bits (c'est à dire compris entre -231 et 231 - 1. Les entiers explicites sont en base dix et les opérations usuelles *, +, - et \ sont disponibles, l'opérateur reste de la division euclidienne (ou modulo) est noté %. Notons qu'il s'agit en fait d'arithmétique modulo 232. Les représentants des classes d'équivalence étant choisis un peu bizarrement (certains sont négatifs). Il existe aussi in type long sur 64 bits et une règle de promotion des entiers qui veut qu'une expression entière possède le type son sous-composant le plus gros. (Ainsi, si i est un int et l est un long, alors i+l est un long.)

2.4.3  Caractères

Là c'est plus original le type char des caractères est un entier sur 16 bits, ce qui permet de représenter le jeu de caractère Unicode qui regroupe les caractères de nombreuses langues. On note les caractères par 'a', 'b', ..., 'z', '0', '1', ...'9', '%', etc.

L'entier qui est le caractère est un code interne, conforme dans le cas des 128 premier au code ascii qui marche partout. Ça correspond plus ou moins aux caractères que vous pouvez entrer aux clavier sans vous poser de questions. Pour les autres c'est nettement moins clair et nous éviterons de les utiliser.

Donc les caractères sont des entiers, c'est à dire que les opérations entières fonctionnent avec eux, en particulier on peut les soustraire et les additionner. Ça devient utile quand on sait que les lettres de l'alphabet sont consécutives, le code suivant :
  char jMajuscule = 'j' - 'a' + 'A';
range donc le caractère 'J' dans la variable jMajuscule et ceci sans connaître la valeur exacte des caractères en termes d'entiers.

Une autre utilisation assez classique est de trouver la valeur d'un chiffre décimal rangé comme un caractère dans c, en sachant que les codes des chiffres sont consécutifs :
  int i ;
  if ('0' <= c && c <= '9')
    i := c - '0';
  else
     // Erreur
Complément : Changement de type Lorsque l'on affiche un caractère par System.out.println, il y a un miracle c'est bien le caractère qui s'affiche :
  for (int i = 0 ; i < 10 ; i++)
    System.out.print('a');
  System.out.println();
ce programme affiche ``aaaaaaaaaa''. Mais comment afficher caractères ``visibles'' du jeu ascii, dont les codes vont de 32 à 127. Il suffit de convertir l'entier i en un caractère, à l'aide d'une contrainte de type (type cast):
  for (int i = 32 ; i < 128 ; i++)
    System.out.print((char)i);
  System.out.println();
L'exemple ci-dessus est un peu gratuit, mais la contrainte de type est parfois nécessaire en raison de la règle de promotion et parce que la différence de deux caractères est (logiquement ?) de type int. Ainsi l'expression 'j' - 'a' 'A'+ est un int, et donc System.out.println('j' - 'a' + 'A') affichera le code ascii de ``J''. Ça se corrige par un cast bien senti, notons au passage que le système Java accepte de ranger un int dans une variable de type char sans râler, je ne suis pas capable d'expliquer pourquoi.

Pour l'anecdote une solution sans cast et sans entier du problème de l'affichage des caractères visibles est également possible :
for (char c = ' ' ; c <= '~' ; c++)
      System.out.print(c);
    System.out.println()

2.5  Passage par valeur

La règle générale de Java est que les arguments sont passés par valeur. Cela signifie que l'appel de méthode se fait par copie des valeurs passées en argument et que chaque appel de méthode dispose de sa propre version des paramètres. Par exemple, soit la méthode:
  static void dedans(int i) {
    i = i+2 ;
    System.out.println("i=" + i)
  }

  static void dehors(int i) {
     System.out.println("i=" + i) ;
     dedans(i) ;
     System.out.println("i=" + i) ;
   }
L'appel de dehors(0) se traduira par l'affichage de trois lignes i=0, i=2, i=0. C'est très semblable à l'exemple sur la structure de bloc. Ce qui compte c'est la portée des variables. Le résultat est sans doute moins inattendu si on considère cet autre programme rigoureusement équivalent :
  static void dedans(int j) { // Ca change ici
    j = j+2 ;
    System.out.println("j=" + j)
  }

  static void dehors(int i) {
     System.out.println("i=" + i) ;
     dedans(i) ;
     System.out.println("i=" + i) ;
   }
Mais ce que l'on doit bien comprendre c'est que le nom de l'argument de dedans, i ou j n'a pas d'importance, c'est une variable muette et heureusement.

La règle se nuance très nettement dans le cas des objets, qui eux sont passés par référence. C'est à dire que l'appelant et l'appelé partagent le même objet. (En chipotant c'est une référence sur l'objet qui est passée par valeur !). Ceci concerne en particulier les tableaux qui sont des sortes d'objets. L'exemple suivant montre l'effet du passage par référence :
  // Affichage d'un tableau (d'entiers) passé en argument
  static void affiche(int[] t) {
    for (int i = 0 ; i < t.length ; i++)
      System.out.print ("t[" + i + "] = " + t[i] + " ") ;
    System.out.println ("") ; // sauter une ligne
  }

// trois façons d'ajouter deux
  static void dedans (int[] t) {
    t[0] = t[0] + 2;
    t[1] += 2;
    t[2]++ ; t[2]++ ;
  }

  static void dehors () {
    int[] a = {1,2,3} ; // declaration et initialisation d'un tableau
    affiche (a) ;
    dedans (a) ;
    affiche (a) ;    
  }
L'affichage est le suivant:
1 2 3 
3 4 5 
Ce qui illustre bien qu'il n'y a, tout au cours de l'exécution du programme qu'un seul tableau, a, t étant juste des noms différents de ce même tableau. On peu aussi renommer l'argument t en a ça ne change rien.

3  Cet obscur objet

Dans cette section, nous examinons la difficile et dialectique question de la classe et de l'objet.

3.1  Utilisation des objets

Enfin, cette question sera examinée plus tard, pour le moment les seuls objets connus sont construits pour nous par le système (par exemple System.out) ou par nous sans nous en rendre compte (les chaînes, et les tableaux). Ajoutons-y l'objet null qui n'a ni champs, ni méthodes, ni rien, mais alors rien du tout et qui se permet donc d'appartenir à toutes les classes...

La MacLib expose pas mal d'objets. Commençons par le point. Le point est un objet de la classe Point. On crée un nouveau point par un appel de constructeur dont la syntaxe est :
Point p = new Point () ;
On peut aussi créer un point en donnant explicitement ses coordonnées (entières) au constructeur :
Point p = new Point (100,100) ;
On dit que le constructeur de la classe Point est surchargé, c'est à dire qu'il y a en fait deux constructeurs qui se distinguent par le type de leurs arguments.

On peut ensuite accéder aux champs d'un point à l'aide de la notation usuelle. Les points ont deux champs h et v qui sont leurs coordonnées horizontales et verticales :
if (p.h == p.v)
  System.out.println("Le point est sur le diagonale");

3.2  Fabrication des objets, que des champs

Un objet minimal n'a pas de méthodes, il sert uniquement à regrouper des valeurs ensemble.

On crée très facilement une classe des paires d'entiers de la façon suivante :
class IntPair {
  int x ;
  int y ;

  IntPair (int x0, int y0) {
    x = x0 ;
    y = y0 ;
  }
}
On remarque que les champs x et y ne sont pas introduits par le mot-clef static, ils sont dynamiques. Par conséquent, chaque objet crée aura ses propres champs. Le programme :
  IntPair p1 = new IntPair (0, 1) ;
  IntPair p2 = new IntPair (2, 3) ;

  System.out.println(p1.x + ", " + p2.y)  
Affichera ``0, 3''.

La structure de paire n'est pas très subtile, mais une définition à peine différente donne les listes. Ici le deuxième champ est simplement un autre objet de la même classe :
class IntList {
  int cont ;
  IntList rest ;

  IntList (int cont, IntList rest) {
    this.cont = cont ;
    this.rest = rest ;
  }
}
(Remarquez le truc du ``this.'' pour avoir des arguments de constructeur et des champs homonymes.)

Avec une classe aussi minimale (aucune méthode), la liste vide est null on réalise l'opération premier du cours par l.cont et l'opération reste par l.rest. Enfin on construit une nouvelle liste à partir d'une ancienne à l'aide du constructeur (unique) de la classe IntList, par new IntList (i,l).

Mais on peut bien tout faire d'un coup. Ainsi, on construira une liste à l'aide du constructeur :
  IntList l = new IntList (1, new IntList (2, new IntList (3,null))) ;
Autre par exemple, on affichera une liste l ainsi :
  static void print (IntList l) {
 
  while (l != null) {
    System.out.println(l.cont) ;
    l = l.rest ;
  }
Dans un tel programme il faut remarquer une chose absolument fondamentale : Avant que de faire l.cont ou l.rest, il faut vérifier que l n'est pas l'objet null. Sinon, ça plante.

3.3  Fabrication des objets, avec méthode

Il n'y a en fait plus grand chose à dire, on ajoute des méthodes (non-statiques) dynamiques à la classe. Voici par exemple une classe des piles d'entiers minimale et conforme à la spécification des piles du TD-3 :
class IntStack {
  final static int MAX=64 ;
  private int [] t ;
  private int sp ;

  IntStack () {
    t = new int [MAX] ;
    sp = 0 ;
  }

 private void error (String msg) {
    System.err.println (msg) ;
    System.exit (2) ;
  }

  boolean empty () {
    return sp == 0 ;
  }

  int pop () {
    if (empty ())
      error ("Pop sur pile vide") ;
   return t[--sp] ;
  }

  void push (int i) {
    if (sp >= MAX)
      error ("Push sur pile pleine") ;
   t[sp++] = i ;
  }

  public String toString () {
    StringBuffer r = new StringBuffer () ;

    r.append ("{") ;
    for (int i = 0 ; i < sp-1 ; i++) {
      r.append(t[i]) ;
      r.append(", ") ;
    }
    if (sp > 0)
      r.append(t[sp-1]);
    r.append ("}") ;
    return r.toString () ;
  }
}
Au passage, ce source (qui n'est pas la solution du TD-3, mais un exemple) appelle quelques remarques :

4  Exception

Les exceptions servent à rompre le flot normal de l'exécution. Une exception est un objet (On s'en serait douté !) d'une classe particulière (il y a plusieures classes d'exceptions). Par exemple la classe ``Error'' est conventionnellement utilisée pour signaler les situations d'erreur. C'est le cas, par exemple encore, d'une tentative de dépilage d'une pile vide, dans une quelconque classe Pile :
int depile() {
  if (vide())
    throw new Error ("Pile vide") ;
  ...
}
Observez la technique, un mot-clé, throw, puis un objet d'une classe appropriée. Lever l'exception Error modifie radicalement l'exécution en cours, en particulier le retour de la méthode depile est immédiat et aucune valeur n'est renvoyée. Plus généralement, le contrôle remonte à travers toutes les méthodes en attente, jusqu'à planter le programme lui-même.

Toutefois, on peut arrêter cette folle course en attrapant l'exception. Ici on pourra par exemple encapsuler l'appel à pile, dans une méthode qui renvoie 0 en cas de pile vide :
int depileZero (Pile p) {
  try {
   return p.depile () ;
  } catch (Error e) {
    System.err.println ("Erreur réparée : " + e.getMessage ()) ;
    return 0 ;
  }
}
Observez la technique, un mot-clé try, les instructions qui peuvent lever l'exception, un autre mot-clé (catch) avec l'exception attrapée en argument (entre (... )) et les instructions à exécuter en cas d'exception. Si Error n'est pas levée, tout se passe normalement, sinon le contrôle passe en System.err.println ("Er...

Les exceptions donnent la possibilité de traiter les cas d'erreur de l'extérieur des méthodes qui les détectent et qui ne savent pas faire autre chose que signaler l'erreur. En particulier on peut attraper l'exception n'importe où (c'est à dire pas immédiatement autour de la méthode qui la lève). Ceci permet, par exemple, un traitement personalisé des erreurs dans la méthode main :
public static void main (String [] arg) {
  try {
     ... 
  } catch (Error e) {
    System.err.println("Cher utilisateur, je suis au regret de devoir vous quitter") ;
    System.exit(2) ;
  }
}
Là où ça se complique encore, c'est que le système de type de Java impose de signaler qu'une méthode peut lever une exception, dans le cas de certaines exceptions. Ce n'est pas le cas de Error ci-dessus, c'est malheureusement le cas de IOException (cf la section 5 suivante). Les méthodes de librairie déclarent pouvoir lever de telles exceptions à l'aide de la déclaration throws (notez le ``s''). C'est par exemple le cas de la méthode read, qui lit un caractère et rend son code.
public int read() throws IOException
Plus raide encore, les méthodes qui appellent une méthode dont la déclaration contient ``throws e'' et qui n'attrapent pas les exceptions de la classe e, doivent également être déclarées comme pouvant lever l'exception e. C'est le cas, par exemple, d'une hypothétique méthode ``avalant'' les espaces avant de lire un caractère :
int eat_space_read() throws IOException {
  int c ;

  do {
    c = read () ;
    if (c != ' ')
      return c ;
  } while (true) ;
}

5  Entrées-sorties

Nous savons déjà écrire dans la sortie standard (System.out en Java) : il suffit d'utiliser les méthodes System.out.print et System.out.println. Pour lire dans l'entrée standard (System.in, comme vous pouviez vous en douter), c'est un rien plus complexe, mais sans plus.

Il faut tout d'abord fabriquer une entrée, objet de la classe, ``Reader'' que l'on peut traduire en ``lecteur'' à partir de System.in :
Reader entree =  new InputStreamReader(System.in) ;
Ensuite, on peut lire un caractère de l'entrée par l'appel ``entree.read()''. Cette fonction renvoie un entier (type int) avec le comportement un peu tordu suivant : Bref, voici Cat.java un filtre trivial dont la sortie est une copie de l'entrée :
import java.io.*; // La classe Reader est dans le package java.io

class Cat {

  public static void main(String [] args) {
    Reader entree =  new InputStreamReader(System.in) ;

    try {
      for (;;) {// Boucle infinie, on sort par break
        int c = entree.read ();
        if (c == -1)
          break;
        else
          System.out.print ((char)c);
      }
      entree.close();
    } catch (IOException e) { // En cas de malaise
      System.err.print("Malaise : ") ;  
      System.err.println(e.getMessage()) ;
      System.exit(-1) ; // Terminer le programme sur erreur
    }      
  }
}
Remarquez :

6  Quelques fonctions (méthodes) de librairie

6.1  Les entiers

Les entiers (type int) sont des valeurs de base du langage, mais il existe aussi une classe Integer des entiers vus commes des objets. Certaines des méthodes de cette classe sont utiles et notamment :

6.2  Un peu de maths

Les fonctions mathématiques usuelles sont définies dans la classe Math du package java.lang (ouvert par défaut).

Cette classe contient entre autres :

6.3  Les chaînes

Les chaînes sont traitées par la classe String du package java.lang.

Néamoins, il y a un peu de syntaxe spécialisée, les constantes chaînes s'écrivent entre doubles-quotes, par exemple ``"coucou"''. En outre, la concaténation de deux chaînes s1 et s2, s'écrit ``s1+s2''.

Les autres fonctions classiques sur les chaînes, sont généralement des méthodes dynamiques normales. Il y a entre autres :

6.4  Les tampons de caractères

En Java, on ne peut pas changer une chaîne (e.g. changer un caractère individuellement) de plus la longueur des chaîne est fixée une fois pour toutes. Or, on veut souvent construire une chaîne petit à petit, on peut le faire en utilisant la concaténation ``+'' mais il y a alors un prix à payer car une nouvelle chaîne est créée à chaque concaténation. En fait, lorsque l'on construit une chaîne de gauche vers la droite (pour affichage d'un tableau par ex.) on a besoin d'un autre type de structure de donnée : le tampon. Les tampons (Buffer) de caractères sont traitées par la classe StringBuffer du package java.lang. On trouvera un exemple d'utilisation classique de la classe StringBuffer à la section 3.3 : une création de tampon, des tas de ``append(...)'', et un ``toString()'' final.

6.5  Classe Reader

Cette classe Reader des lecteurs offre une vision d'un flot d'entrée très semblable à celle du langage C. Un exemple d'utlisation complet est en section 5. Elle est membre du package java.io qui n'est pas ouvert par défaut, de sorte qu'il faut écrire :
import java.io.* ;
quand on veut s'en servir. Une autre conséquence est que toutes les méthodes exportée sont publiques (cf. section 1.4). Les méthodes les plus utiles sont : La classe Reader est abstraite, elle sert seulement à décrire une fonctionnalité et il n'existe pas à proprement parler d'objets de cette classe. Par conséquent, il n'existe pas de constructeurs de la classe Reader.

En revanche, il existe diverses sous-classes de Reader qui permettent de construire concrètement des objets. qui peuvent se faire passer pour des Reader. Il s'agit par example des classes StringReader (lecteur sur une chaîne), InputStreamReader (lecteur sur un flot d'octets) et FileReader (lecteur sur un fichier).

6.6  Classe BufferedReader

Le principe des Reader va assez loin, car il est possible à partir d'un Reader donné de fabriquer un autre Reader qui possèdera une fonctionalité supplémentaire. C'est la cas de la classe BufferedReader, qui intercale un tampon mémoire (buffer) entre son entrée et sa sortie. Dans la pratique, cette technique regroupe les appels au système d'expoitation (notamment les accès au disque ou au réseau) et donne une meilleure efficacité.

6.7  Les tableaux

6.7.1  Les types

Les tableaux sont presques des objets, mais ils n'ont pas vraiment de classe, tout se passe plus ou moins comme si, pour chaque type type, un type des tableaux dont les éléments sont de type type tombait du ciel. Ce type (cette classe ???) des tableaux dont les éléments sont de type type, se note, comme en C, ``type[]''. En particulier on a :
  int [] ti ;        // tableau d'entiers
  String [] ts ;     // tableau de chaînes
  String [] [] tts ; // tableau de tableaux de chaînes

6.7.2  Valeurs tableaux

Contrairement à bien des langages, il est possible en Java, de fabriquer des tableaux directement. On distingue:

6.7.3  Opérations sur les tableaux

J'en vois deux :

7  La MacLib

La classe MacLib écrite par Philippe Chassignet fournit une service de fenêtre graphique dans le style de la librairie QuickDraw du Macintosh qui a suscité l'admiration de générations de programmeurs. Cette interface graphique présente l'amusante particularité que l'origine des coordonées est en haut et à gauche.

Ce n'est pas une classe de librairie à proprement parler, puisqu'elle ne fait pas partie de la distribution Java standard. Mais c'est une partie de l'installation de Java à l'X et c'est standard dans ce sens : tout le monde à l'X peut l'utiliser (et en effet c'est une classe publique).

7.1  Objets graphiques

L'implantation Java de la MacLib utilise des objets graphiques, les points les rectangles les polygones etc.

7.1.1  La classe Point

La classe des points est un grand classique. Elle sert d'exemple dans la présentation des objets de ce même document à la section 3.1. Les points n'ont pas tellement de méthodes utiles, ils servent plutôt d'arguments aux autres méthodes de la MacLib.

En particulier les polygones sont des tableaux de points, de sorte que l'on définit un triangle dont les trois sommets sont les points p1, p2 et p3 par :
Point p1 = ... ;
Point p2 = ... ;
Point p3 = ... ;
Point [] triangle = {p1, p2, p3} ;

7.1.2  La classe Rect

La classe des rectangle est celle des objets graphiques qui sont rectangles. Comme la classe des points, elle sert surtout à construire des objets passés en argument aux méthodes de la MacLib.

7.2  Méthodes de la MacLib

La MacLib elle même fournit des méthodes qui contrôlent l'interface graphique. Citons :

8  Quelques exemples tordus et trucs

Cette section regroupe des astuces de programmation ou corrige des erreurs rencontrées lors des TD.

8.1  Portée des variables

On a parfois recours à la notation complète des noms de variables pour contourner la portée des variables (voir la section 2.3). Considérons encore une fois la classe Counter du TD-1 et plus particulièrement sa méthode init :
class Counter {

    private static int [] count = null; // rien au de'part
    private static String [] name = null; // non plus
    private static int size = 0;

     ...

    static void init (int size) {
        count = new int [size];
        name = new String [size];
        Counter.size = size; // Oups !!!
    }
}
Dans cette méthode, donc, l'argument size cache le champ size de la classe, Il convient alors de désigner ce champ à l'aide de la notation complète Counter.size.

Notons que l'on aurrait très bien pu écrire :
    static void init (int x) {
        count = new int [size];
        name = new String [size];
        size = x;
    }
Mais je trouve ça moins clair.

De même à l'intérieur d'une méthode non-statique, (ou d'un constructeur), on peut cacher les champs d'un objet par des liaisons locale et toujours avoir accès aux champs en utilisant this qui est l'objet courant.
class IntList {
  int cont ;
  IntList rest ;

  IntList (int cont, IntList rest) {
    this.cont = cont ;
    this.rest = rest ;
  }
}

8.2  Sur les caractères

Les codes des caractères de '0' à '9' se suivent, de sorte que les deux trucs suivants s'appliquent : Des astuces analogues s'appliquent aux cas des lettres minuscules et majuscules.

Index

  • abs, 6.2
  • append(int), 6.4
  • append(Object), 6.4
  • append(String), 6.4

  • BufferedReader (classe), 6.6
  • BufferedReader, 6.6
  • backColor, 7.2
  • blackColor, 7.2
  • blueColor, 7.2
  • boolean (type scalaire), 2.4.1
  • break (mot-clé), 2.3

  • catch (mot-clé), 4
  • char (type scalaire), 2.4.3, 8.2
  • charAt, 6.3
  • close, 6.5
  • compareTo, 6.3
  • continue (mot-clé), 2.3
  • cos, 6.2
  • cyanColor, 7.2

  • do (mot-clé), 2.3

  • else (mot-clé), 2.3
  • equals, 6.3
  • erasePolygon, 7.2
  • eraseRect, 7.2
  • extends (mot-clé), 1.5

  • final (mot-clé), 2.2
  • for (mot-clé), 2.3
  • foreColor, 7.2
  • framePolygon, 7.2
  • frameRect, 7.2

  • greenColor, 7.2

  • h, 7.1.1
  • héritage, 1.5
  • Integer (classe), 6.1
  • IntList (classe), 3.2
  • IntPair (classe), 3.2
  • if (mot-clé), 2.3
  • initQuickDraw, 7.2
  • int (type scalaire), 2.4.2

  • length, 6.3
  • lineTo, 7.2
  • liste, 3.2
  • long (type scalaire), 2.4.2

  • MacLib (classe), 7
  • Math (classe), 6.2
  • magentaColor, 7.2
  • main, 1.2
  • max, 6.2
  • min, 6.2
  • moveTo, 7.2

  • null, 3.1

  • objet, 3
  • overriding, 1.5

  • PI, 6.2
  • Point (classe), 7.1.1
  • Point (classe), 3.1
  • Point(), 7.1.1
  • Point(int, int), 7.1.1
  • paintPolygon, 7.2
  • paintRect, 7.2
  • paire, 3.2
  • parseInt, 6.1
  • polygone, 7.1.1, 7.2
  • private (mot-clé), 1.4
  • public (mot-clé), 1.4

  • Reader (classe), 6.5
  • Rect (classe), 7.1.2
  • Rect(), 7.1.2
  • Rect(int, int, int, int), 7.1.2
  • Rect(Point, Point), 7.1.2
  • random, 6.2
  • read, 6.5
  • redColor, 7.2
  • return (mot-clé), 2.3
  • round, 6.2

  • String (classe), 6.3
  • StringBuffer (classe), 6.4
  • StringBuffer(), 6.4
  • setRect, 7.1.2
  • sin, 6.2
  • sous-classe, 1.5
  • static (mot-clé), 1.1, 2.2
  • structure de bloc, 2.3
  • surcharge, 3.1, 6.4

  • tableaux, 6.7
  • this (mot-clé), 2.3
  • throw (mot-clé), 4
  • throws (mot-clé), 4
  • toLowerCase, 6.3
  • toString(), 6.4, 7.1.1
  • toUpperCase, 6.3
  • try (mot-clé), 4
  • type, 2.1

  • v, 7.1.1

  • while (mot-clé), 2.3
  • whiteColor, 7.2

  • yellowColor, 7.2

Ce document a été traduit de LATEX par HEVEA.