IUT Aix en Provence - Dépt. Informatique
Java et les Interfaces Homme-Machine
 
Henri Garreta


Java
pour les programmeurs C++



Table des matières

1. Généralités

1.1 Introduction
1.2 Le jargon des langages orientés objets
1.3 Structure générale d'un fichier source Java
1.4 Paquets. Classes publiques et non publiques
1.5 Fichiers et répertoires
1.6 Pratiquement... comment fait-on ?
1.7 Conventions sur les noms

2. Variables, valeurs et références. Classes et objets.

2.1 Types primitifs
2.2 Références
2.3 Tableaux
2.4 Chaînes de caractères
2.5 Classes, variables et méthodes
2.6 Création, initialisation et destruction des objets
2.7 Les deux facettes de la notion de classe

3. Héritage

3.1 Héritage, super-classe, sous classe
3.2 Redéfinition des méthodes
3.3 Méthodes et classes abstraites. Interfaces
3.4 Exceptions

Index


1. Généralités

1.1 Introduction

Java est un langage à vocation générale, orienté objets, qui étend le langage C.

On pourrait donc s'attendre à ce que Java soit très proche de C++. En fait, il y a des différences notables entre ces deux langages, car Java a été défini bien plus récemment que C++ (début des travaux en 90, naissance officielle en 95) et sans aucune contrainte de compatibilité avec un langage préexistant. Cela a permis à ses concepteurs d'exclure des éléments jugés pénibles ou dangereux (pointeurs, gestion de la mémoire, etc.) et d'inclure des concepts qui semblaient trop osés il y a quelques années (interfaces graphiques portables, compilateurs "au dernier moment", etc.). Ainsi, Java est plus cohérent, plus moderne et plus agréable à utiliser que C++.

Rassurez-vous, la syntaxe de base, les expressions et les instructions de Java sont celles de C, donc de C++. D'autre part, les concepts de la programmation orientée objets sont ceux de la plupart des langages de la communauté OO. Ainsi, si vous connaissez C++, vous avez déjà fait le plus gros du travail pour apprendre Java.

Ces notes sont une description rapide de Java, destinée à des personnes ayant une expérience de la programmation et connaissant bien le langage C++. Elles ne sont ni équilibrées (on s'attache plus aux différences entre ces langages qu'aux concepts importants), ni progressives (les notions sont expliquées sans souci de leurs prérequis).

Toutes les suggestions et critiques sont les bienvenues à l'adresse henri.garreta@lim.univ-mrs.fr

1.2 Le jargon des langages orientés objets

On rappelle ici quelques notions fondamentales de la programmation orientée objets. C'est surtout un prétexte pour préciser la terminologie utilisée en Java, comparée à celle qu'on emploie en C++.

Classe, instance. La notion de classe est commune à tous les LOO : une classe est la description d'une sorte d'objets, appelés les instances de la classe. Notez qu'en Java les modèles (template) n'existant pas on n'est pas gêné pour parler d'instanciation à propos de la création des objets.

Variable d'instance. Un objet possède un état et un comportement. L'état est défini par les valeurs d'un ensemble de données membres [C++] ou variables d'instance [Java] ; on dit parfois attributs. Chaque objet possède son propre exemplaire de chaque variable d'instance, alloué lors de la création de l'objet.

Méthode. Le comportement d'un objet est défini par un ensemble de fonctions membres [C++] ou méthodes d'instance [Java]. On peut exprimer l'opération "appeler une méthode m sur un objet p" en disant "envoyer le message m à l'objet p". Une méthode d'instance est toujours appelée en association avec un objet (le destinataire du message) ; cela s'écrit p.m(arguments). En Java, la notation m(arguments) exprime toujours un message qu'un objet s'envoie à lui même (si m est une méthode d'instance) ou à sa propre classe (si m est une méthode de classe).

Variable de classe. Une donnée membre statique [C++] ou variable de classe [Java] est une variable dont il existe un unique exemplaire pour la classe, accédé par toutes les instances. Une variable de classe est créée lors du chargement de la classe, en toute indépendance de la création ultérieure d'instances.

Méthode de classe. Une fonction membre statique [C++] ou méthode de classe [Java] n'est pas associée à un objet particulier. Elle est définie dans une classe, ce qui influe sur son nom et sur sa visibilité, mais à part cela elle est comme une fonction autonome. En particulier, l'appel d'une méthode de classe ne peut pas être considéré comme l'envoi d'un message à un objet.

Héritage. La relation d'héritage entre une classe D et une classe B s'exprime en disant que D dérive de B [C++] ou que D étend B [Java]. B est dite la classe de base [C++] ou la super-classe [Java] de D, qui est appelée la classe dérivée [C++] ou sous-classe [Java] de B.

Les autres notions importantes liées à l'héritage (fonction virtuelle, polymorphisme, fonction ou classe abstraite, etc.) bénéficient des mêmes appellations en Java et en C++ (ouf !).

1.3 Structure générale d'un fichier source Java

Un fichier source Java apparaît plus "propre" qu'un texte source C++ :

Dans un fichier source Java on trouve donc :

Par exemple, voici un fichier source Java :

package collection;  

import java.awt.*; 
import java.util.Vector;  

class Maillon 
{
    déclaration des membres de la classe Maillon 
}

public class Liste 
{
    déclaration des membres de la classe Liste 
}

1.4 Paquets. Classes publiques et non publiques

Un programme Java est un ensemble de classes. Cela est vrai aussi bien pour le texte source que pour le résultat de la compilation.

Les classes sont regroupées en paquets (packages). C'est un arrangement arbitraire, sans lien avec l'organisation hiérarchique induite par l'héritage. Le principal effet du placement d'une classe dans un paquet est l'allongement de son nom. Par exemple, parce qu'elle est dans le paquet java.awt, le nom complet de la classe Button est java.awt.Button. On évite ainsi tout conflit avec une éventuelle autre classe qui se nommerait également Button.

Instruction package

On indique qu'une classe est placée dans un paquet en faisant commencer le fichier dans lequel elle est définie par l'instruction "package paquet". Par exemple, les classes Maillon et Liste montrées ci-dessus sont dans le paquet collection.

L'instruction package doit être la première expression, autre qu'un commentaire, du fichier dans lequel elle figure. Si un fichier ne comporte pas une telle instruction, alors les classes qu'il définit sont placées dans un "paquet par défaut" appelé le paquet sans nom (unnamed package).

Classes publiques et non publiques

Une définition de classe peut être précédée du qualifieur public, la classe est alors dite publique, ou bien ne pas avoir de qualifieur, on dit alors que la classe est non publique, ou qu'elle a le contrôle d'accès par défaut (on ne dit pas qu'une classe est privée, cela n'aurait pas de sens). La différence est la suivante :

Si vous n'utilisez jamais l'instruction package alors les classes que vous définissez sont toutes dans le paquet sans nom. Qu'elles soient publiques ou non revient donc pour vous au même.

En combinant des identificateurs et des points on peut créer un ensemble de noms de paquets avec une structure arborescente. Par exemple, la bibliothèque Java comporte les paquets :

java.applet
java.awt
java.awt.color
java.awt.event

etc.

Note. Toutes les classes de Java (livrées avec le compilateur et la machine) sont dans des paquets dont le premier élément du nom est "java.").

Instruction import

L'effet principal du placement d'une classe dans un paquet est l'allongement de son nom. Parce qu'elle se trouve dans le paquet java.awt, la classe Button se nomme en réalité java.awt.Button. Nous dirons que Button est le nom court de la classe java.awt.Button. L'instruction

import nom_de_classe; 

(précédant l'utilisation de la classe) permet de désigner la classe indiquée par son nom court. Exemple :

import java.awt.Button;
...
Button bOk = new Button("Ok");

On peut autoriser l'utilisation du nom court de toutes les classes d'un paquet par l'instruction :

import nom_de_paquet.*;

(l'étoile * doit être comprise comme remplaçant des noms de classes, non de paquets). Exemple :

import java.awt.*;
import java.awt.event.MouseListener;       // ceci ne découle pas de
                                           // la ligne précédente
...
Button bOk = new Button("Ok");
Checkbox cBox = new Checkbox("Lecture seule");

Note 1. L'instruction import n'est pas l'analogue de la directive #include de C++. L'instruction import ne sert pas à rendre visibles des classes qui sans cela auraient été invisibles (le compilateur voit toujours toutes les classes), ni à donner des droits d'accès sur des classes qui sans cela auraient été interdites (toutes les classes publiques sont accessibles partout). L'instruction import vous apporte uniquement le droit de nommer des classes par leur nom court, vous dispensant de préciser à chaque utilisation le paquet auquel elles appartiennent.

Note 2. Les classes du paquet java.lang étant des éléments indissociables du langage lui-même, il n'est pas nécessaire d'écrire une instruction import pour ce paquet, dont les classes peuvent donc en toute circonstance être appelées par leurs noms courts.

1.5 Fichiers et répertoires

Fichiers sources

Une classe publique doit être définie dans un fichier ayant le même nom que la classe et l'extension ".java". Par exemple, le fichier donné en exemple à la section 1.3 doit se nommer Liste.java, car il définit la classe publique Liste.

Conséquence : un fichier source peut contenir au plus une classe publique.

Lorsque le compilateur compile un fichier source, il consulte (et même compile, si nécessaire) les autres fichiers sources contenant des classes mentionnées dans le premier fichier. Le compilateur prend notamment en charge la question des références croisées (une classe A référence une classe B, alors que B référence A) pour laquelle le programmeur n'a à prendre aucune mesure particulière.

N.B. Dans tous les systèmes d'exploitation qui nous intéressent, y compris Windows, les outils Java (compilateur, machine, etc.)

Fichiers produits par le compilateur

Quel que soit le nom du fichier source dans lequel elle est écrite, la compilation d'une classe produit un fichier ayant le nom de la classe et l'extension ".class". Par exemple, la compilation du fichier donné en exemple à la section 1.3 produit les deux fichiers Maillon.class et Liste.class

Si une classe appartient au paquet sans nom, le fichier .class correspondant doit se trouver dans le répertoire de travail. Sinon, le fichier .class doit se trouver dans un répertoire dont le chemin, relatif au répertoire de travail, reproduit le nom du paquet en question. Par exemple, les fichiers produits par la compilation du fichier montré plus haut doivent se trouver dans le répertoire ./collection, puisqu'il s'agit de classes du paquet collection.

Le compilateur s'appuie sur cette convention de placement des fichiers pour trouver les classes mentionnées dans un fichier qu'il est en train de compiler : si les fichiers .class ne sont pas placés comme indiqué, les classes ne seront pas trouvées et la compilation échouera.

La hiérarchie des noms de paquets est reproduite dans la hiérarchie des répertoires (et devient donc un moyen d'organiser les fichiers .class). Par exemple, le fichier produit par la compilation d'une certaine classe Cylindre d'un paquet nommé images.3d.simples doit avoir le chemin ./images/3d/simples/Cylindre.class

Notez enfin que :

1. Sans que ce soit une obligation absolue (quoique...) les compilations se passent mieux si les fichiers sources sont eux aussi placés dans des répertoires correspondant aux paquets auxquels appartiennent les classes.

2. Les classes de Java appartiennent à des paquets qui commencent tous par "java.". Elles sont donc dans des sous-répertoires d'un répertoire nommé java dont la localisation est connue du compilateur.

3. La variable d'environnement CLASSPATH permet d'ajouter d'autres répertoires de base pour la recherche des classes.

1.6 Pratiquement... comment fait-on ?

Il faut savoir que le compilateur Java ne produit pas un code pour une machine physique, comme le Pentium ou le PowerPC, mais un code (appelé parfois bytecode pour le distinguer du langage machine ordinaire) destiné à une machine "virtuelle", la JVM (Java Virtual Machine) ou interprète Java, qui est un programme capable de décoder les instructions du bytecode et de les exécuter sur une machine donnée. Ainsi, un programme source Java compilé une fois à l'aide d'une machine quelconque donne un programme compilé (bytecode) qui pourra s'exécuter sur toute machine pour laquelle existe un interprète Java.

L'exécution d'un programme interprété est plus lente que celle d'un programme complètement compilé (jusqu'à dix fois plus lente, dans le cas de Java). Pour pallier cet inconvénient on ajoute à la machine virtuelle un compilateur "au dernier moment" ou JIT (just in time) compiler, qui traduit en langage machine les fragments de bytecode la première fois qu'il les interprète.

Il faut savoir aussi que Java permet de réaliser deux sortes d'exécutables : les applications et les applets. Les applications sont des programmes "indépendants", comme ceux que l'on écrit en C ou C++, sauf qu'ils sont destinés à être interprétés par la machine Java, non par l'ordinateur sous-jacent.

Les applets sont destinées à figurer dans une page HTML et donc à être exécutés par un navigateur web. Elles sont chargées comme une image ou un morceau de musique lorsque la page est affichée et elles s'exécutent automatiquement lorsque leur chargement est fini. Bien entendu, le navigateur en question doit contenir un interprète Java.

Saisie et compilation des programmes

Il existe un certain nombre d'environnements de développement, comme Visual Café, J++, J Builder, etc., dans lesquels on édite les classes avec beaucoup de facilité, puis on les compile et on les exécute en cliquant sur des boutons.

On peut renoncer à ces outils (surtout si on ne les a pas) et utiliser le JDK (Java Development Kit), développé par Sun Microsystems, la maison mère de Java, qui a les avantages d'être "officiel", tenu à jour, abondamment documenté, et... gratuit. Dans ce cadre :

Edition. On utilise n'importe quel éditeur de textes pour composer son programme, c'est-à-dire pour écrire des fichiers .java, chacun contenant une (recommandé) ou plusieurs classes.

Compilation. La forme la plus utile de la commande de compilation est

javac  -d  répertoire_racine_des_classes_produites  fichier_source

Exemple. Supposons que le fichier créé contient la classe MaClasse et s'appelle MaClasse.java. On en obtient la compilation en tapant la commande :

javac -d . MaClasse.java

Notez qu'il faut donner l'extension .java. Si elle réussit, la commande ci-dessus crée dans le répertoire de travail (ou dans un sous-répertoire, si MaClasse appartient à un paquet nommé) un fichier nommé MaClasse.class. On en obtient l'exécution en tapant

java MaClasse

Exécution. Mais qu'est-ce qu'on entend par "exécuter une classe" ? Une classe est exécutable si elle possède une méthode de classe, publique, nommée main, ayant pour argument un tableau de chaînes. Autrement dit, la commande ci-dessus est légitime si et seulement si MaClasse possède une méthode ayant la signature :

public static void main(String args[])

(les éléments soulignés ne sont pas imposés). Notez bien que l'argument args doit être déclaré même si vous n'envisagez pas de vous en servir dans la fonction main.

L'exécution de la classe commence par l'appel de cette méthode ; la suite dépend de ce que vous y avez écrit.

Par exemple, voici la version Java de l'inévitable programme PRINT "HELLO". Fichier source :

class MaClasse 
{
    public static void main(String args[])
    {
        System.out.println("Bonjour à tous !");
    } 
}

Compilation et exécution :

$ javac -d . MaClasse.java
$ java MaClasse
Bonjour à tous ! $

1.7 Conventions sur les noms

Lorsqu'un nom est formé en mettant bout à bout plusieurs mots courants, chacun de ces mots, sauf peut-être le premier, commence par une majuscule : e.hasMoreElements()

Les noms des classes commencent par une majuscule : Vector

Les noms des paquets commencent par une minuscule : java.util.Vector

Les noms des variables et des méthodes commencent par une minuscule : Point.x, Object.toString()

Les noms des variables publiques finales (constantes) sont souvent entièrement écrits en majuscules, surtout lorsqu'elles sont de types primitifs : Label.CENTER

 


2. Classes et objets

2.1 Types primitifs

La question des types est extrêmement claire en Java. Nous avons

Voici les types primitifs :

type
description
taille
codage
valeur initiale
Nombres entiers
byte Octet
8 bits
binaire, codage en complément à 2
0
short Entier court
16 bits
int Entier
32 bits
long Entier long
64 bits
Nombres flottants
float Flottant en simple précision
32 bits
Format IEE 754
0.0
double Flottant en double précision
64 bits
Autres types
char Caractère
16 bits
Unicode
'\u0000'
boolean Valeur booléenne
au moins 1 bit
false, true
false

En Java toute variable est initialisée lors de sa création. S'il n'y a pas d'autre indication, la valeur de la dernière colonne du tableau précédent est utilisée.

Toutes les conversions entre nombres (entiers, flottants et caractères) sont possibles. Pour éviter d'éventuelles pertes d'information, les conversions vers des tailles inférieures ou des précisions moindres doivent être explicitement demandées :

int i;
float x;
i = x; // ERREUR (possible perte d'information)
i = (int) x; // Ok

Les valeurs booléennes ne peuvent pas être converties vers les autres types. On peut toujours convertir vers le type booléen une valeur x d'un autre type, il suffit d'écrire x != 0.

Classes encapsulant les types primitifs

On a parfois besoin d'utiliser une valeur d'un type primitif dans un contexte où un objet est requis. A cet effet, Java fournit huit classes, chacune associée à un type primitif, nommées respectivement  : Byte, Short, Integer, Long, Float, Double, Character et Boolean.

Chacune de ces classes comporte une variable d'instance pour mémoriser une valeur v du type primitif TP en question et offre, au moins :

Exemple : un objet Vector tel que

Vector v = new Vector();

permet de mémoriser une collection d'objets. Pour y ranger la valeur d'une variable i de type int il faut écrire :

v.add(new Integer(i));

Ces classes comportent également des méthodes de classe qui implémentent des opérations diverses en rapport avec le type primitif en question. Se référer à la documentation en ligne.

Sémantique de l'accès aux données

La question est : que signifie une expression réduite au nom d'une variable x de type T ?

Comme dans les autres langages, si T est un type primitif alors x signifie la valeur d'une certaine donnée, le "contenu courant de la variable". On dit qu'une variable d'un type primitif a la sémantique des accès par valeur.

2.2 Références

Si une variable x n'est pas d'un type primitif alors x est d'un type classe et sa signification et celle d'une référence vers un objet, c'est-à-dire un pointeur géré de manière interne par le compilateur. Autrement dit, en Java toute variable d'un type tableau ou classe a la sémantique des accès par référence.

Par exemple, si Point est une classe définie par ailleurs, l'expression

Point p;

définit une variable p qui sera une référence sur une instance de Point. Pour le moment aucune instance n'a été créée ; comme en Java une variable est toujours initialisée, p vaut null, la référence qui ne référence rien.

L'instanciation de Point permet de rendre p plus intéressante :

p = new Point(10, 20);

désormais p est une référence sur un objet Point nouvellement créé et initialisé avec les valeurs 10 et 20 (probablement les coordonnées du point). Par la suite, une éventuelle affectation avec p au membre gauche transformera cette variable en une référence sur un autre objet.

Attention. Bien noter qu'il y a des ressemblances mais aussi des différences entre les références de Java et celles de C++ :

1. La principale ressemblance est l'aspect "pointeur géré automatiquement par le compilateur", c'est-à-dire le fait qu'il n'y a pas besoin d'un opérateur * pour distinguer l'accès à la référence de l'accès à l'objet référencé.

2. Une différence est le caractère obligatoire des références en Java. Il n'y a pas le choix : un type primitif est toujours accédé par valeur, un objet est toujours accédé par référence.

3. Une autre différence importante est le caractère modifiable des références en Java. Une fois initialisée, une référence peut être changée, alors qu'une référence de C++ ne le peut pas (de ce point de vue, une référence de Java ressemble plus à un pointeur qu'à une référence de C++).

Résumons la situation en disant qu'en Java une occurrence solitaire d'une variable d'un type classe désigne la référence elle-même, alors qu'une occurrence accompagnée de l'opérateur . implique un accès à l'objet référencé.

Point p, q;
...
affectation de valeurs à p et q
...
p.x = 0; // accès à l'objet référencé par p
...
p = q; // p est modifié (et référence désormais le même objet que q)

...

Références et copie des objets

La première conséquence (parfois souhaitée, parfois gênante) de l'accès par référence est le caractère superficiel de l'affectation. L'expression

p = q;

ne fait pas une duplication de l'objet référencé par q, mais met uniquement une nouvelle référence dessus :

Cette manière de concevoir l'affectation n'est viable que si les objets manipulés sont invariables. En effet, si l'objet référencé par q n'est pas susceptible de changer dans le futur, pour en avoir une copie dans p il n'est pas utile d'en faire une duplication effective. Mais si l'objet référencé par q peut changer dans la suite du programme, alors la "copie superficielle" précédente est trompeuse : on croit avoir sauvegardé l'état actuel de q dans p, mais il n'en est rien, chaque modification ultérieure de q se répercute dans p.

Pour remédier à ce problème, chaque objet offre la méthode Object clone(). Définie une première fois dans Object (la super-classe de toutes les classes) chaque classe peut en donner sa propre redéfinition, implémentant la version adaptée de l'opération de duplication. Notre copie s'écrit alors :

p = (Point) q.clone();

et on obtient (si la méthode clone a été redéfinie comme il faut dans la classe Point) :

Références et égalité

Une autre conséquence de l'accès par référence concerne l'opérateur d'égalité : il ne suffit pas que les objets référencés par p et q soient égaux pour que l'expression

p == q

soit vraie, il faut pour cela que p et q soient deux références sur le même objet (il faut qu'il y ait "égalité des adresses"). Ce test est très rapide, mais dans certaines situations il est trop contraignant. Pour cette raison, chaque objet offre la méthode boolean equals(Object x). Définie une première fois dans Object, chaque classe peut en donner sa propre redéfinition, implémentant la version adaptée de la comparaison. Dans notre exemple, cela s'écrira alors :

p.equals(q)

2.3 Tableaux

Il y a deux syntaxes pour déclarer un tableau à un indice. Supposons que nous ayons besoin de deux tableaux tab1 et tab2 dont les éléments sont d'un certain type TypElem (qui peut être un type primitif ou une classe). Première manière :

TypElem tab1[], tab2[];

Deuxième manière :

TypElem[] tab1, tab2;

Les tableaux peuvent être vus comme des objets. Ainsi, l'une ou l'autre des déclaration précédentes installe deux variables qui, pour le moment, valent null, la référence nulle.

Un tableau de n éléments, chacun initialisé avec sa valeur par défaut, commence à exister lorsqu'on écrit :

tab1 = new TypElem[n];

On utilise ensuite le tableau selon la notation habituelle : tab1[i].

Bien entendu, on peut aussi créer le tableau en même temps qu'on déclare la variable :

TypElem[] tab1 = new TypElem[n];

Un tableau connaît le nombre de ses éléments (le nombre qui a été mis dans l'expression new), c'est la valeur d'une variable d'instance finale (c'est-à-dire une constante d'instance) publique nommée length. Par exemple, voici un programme qui liste les arguments de la commande qui l'a lancé :

public class Echo
{
    public static void main(String args[])
    {
        int n = args.length;
        System.out.println(n + " arguments:");
        for (int i = 0; i < n; i++)
            System.out.println("   " + (i + 1) + " - " + args[i]);
    }
}
 

Compilation et exécution (Windows) :

C:> javac -d . Echo.java
C:> java Echo bonjour "est-ce que ca va ?" 1+2=3
3 arguments:
   1 - bonjour
   2 - est-ce que ca va ?
   3 - 1+2=3
C:>

Accidents liés aux tableaux

Tous les programmeurs le savent, les indices des tableaux aiment la liberté. Leur propension tenace à sortir des bornes des tableaux est une des principales causes de bugs dans les programmes.

Voici donc une excellente nouvelle : en Java le débordement d'un indice d'un tableau ne passe jamais inaperçu. Lorsqu'un tel phénomène se produit, une exception IndexOutOfBoundsException est lancée qui, si elle n'est pas attrapée, finira par produire l'arrêt du programme.

Un autre incident qui peut survenir dans l'utilisation d'un tableau est l'échec de son allocation (l'expression new). Une exception OutOfMemoryError est alors lancée qui, si elle n'est pas attrapée, terminera également le programme.

Tableaux à plusieurs indices

Examinons le cas de deux indices. Une matrice mat de NL lignes et NC colonnes, dont les éléments sont d'un certain type TypElem, se déclare par n'importe laquelle des expressions suivantes :

TypElem mat[][];

ou

TypElem[] mat[];

ou encore

TypElem[][] mat;

Les déclarations précédentes créent une variable qui, pour le moment, vaut null. On peut initialiser cette variable en écrivant, par exemple :

mat = new TypElem[NL][NC];

L'expression précédente initialise mat comme une référence sur un tableau nouvellement crée, formé de NL références sur des tableaux nouvellement crées, chacun formé de NC éléments de type TypElem. L'effet est exactement le même que si on avait exécuté les instructions suivantes (d'ailleurs parfaitement légales) :

mat = new TypElem[NL][];          // un tableau de références
for (int i = 0; i < NL; i++)
    mat[i] = new TypElem[NC];

Quelle que soit l'expression par laquelle on l'a construite, on accède à la matrice selon les notations habituelles :

mat référence sur la matrice
mat[i] référence sur la ième ligne de la matrice
mat[i][j] valeur ou référence (cela dépend de TypElem) sur le jème élément de la ième ligne de la matrice

On notera que cette manière de traiter les matrices leur permet d'avoir des lignes de longueurs différentes. Par exemple, voici la création d'un tableau triangulaire inférieur  : la première ligne a un élément, la deuxième deux, et ainsi de suite :

mat = new TypElem[NL][];
for (int i = 0; i < NL; i++)
    mat[i] = new TypElem[i + 1];

2.4 Chaînes de caractères

Il y a deux sortes de chaînes de caractères en Java :

A propos des String

Quelques méthodes (se référer à la documentation en ligne pour une liste complète) :

Noter l'absence de la méthode clone() : puisque les String sont invariables, il n'y a jamais besoin de les dupliquer.

Tout objet est capable de donner une représentation de lui-même sous forme de chaîne de caractères. Cela est accompli par la méthode String toString(), qui est définie une première fois dans la classe Object et qui peut être redéfinie dans chaque classe où la chose est utile.

Par exemple, les méthodes void print(Object x) et void println(Object x) (deux méthodes de la classe PrintStream, la classe de out, une variable de classe de la classe System) affichent une chaîne obtenue en appelant la méthode toString de l'objet passé en argument. Ainsi, l'expression :

System.out.println(x);

équivaut à :

System.out.println(x.toString());

L'utilisation des String est grandement facilitée par l'opérateur "+", qui exprime la concaténation des chaînes. Cet opérateur a la propriété suivante : il suffit que l'un de ses opérandes soit de type String pour provoquer la conversion vers ce type de l'autre opérande (par l'appel de la méthode toString). Dans l'exemple suivant, le flottant x est converti en chaîne avant sa concaténation :

float x;
...
String s = "distance: " + x + " Km";  

La ligne précédente se lit :

String s = "distance: " + x.toString() + " Km"; 

Java peut minimiser l'espace mémoire occupé par les chaînes de caractères. Puisqu'elles sont invariables, si deux chaînes sont égales il suffit d'en allouer un seul exemplaire. Cela apporte un gain de place et aussi une augmentation des performances du programme, puisque cela permet de remplacer le test "s1.equals(s2)" par le test, bien plus rapide, "s1 == s2".

Les chaînes apparaissant dans le programme (les constantes littérales) sont allouées de cette manière. On peut obtenir une gestion analogue des chaînes créés durant l'exécution à l'aide de la méthode String intern() : si s est une variable String, l'expression

s.intern()

renvoie une chaîne égale à s et garantie ne pas faire double emploi avec aucune autre chaîne précédemment créée. Autrement dit, si les valeurs de s1 et s2 proviennent toutes deux d'appels de intern(), alors s1.equals(s2) équivaut à s1 == s2.

A propos des StringBuffer

Quelques méthodes (se référer à la doc en ligne pour une liste complète) :

2.5 Classes, variables et méthodes

La syntaxe générale d'une définition de classe est à peu près la même qu'en C++.

En Java, il n'y a pas :

Accessibilité des membres

Les membres (variables et méthodes) des classes disposent de quatre niveaux d'accessibilité :

Variables et méthodes d'instance

Une donnée membre dont la déclaration n'est pas précédée du qualifieur static est appelée variable d'instance. Propriété fondamentale : il en existe un exemplaire distinct dans chaque instance de la classe. Une telle variable est initialisée lors de la création de l'instance :

Par exemple, les instances de la classe Fraction sont créées avec un numérateur 0 et un dénominateur 1 :

public class Fraction
{
    int numer = 0, denom = 1;
    ...
    autres membres
}

Une fonction membre dont la déclaration n'est pas précédée du qualifieur static est appelée méthode d'instance. Propriété fondamentale : une telle fonction ne peut être appelée qu'à travers une instance de la classe. La notation, pour un appel d'une telle méthode, est

unObjet.uneMethode(arguments)

Dans le corps d'une méthode d'instance, l'objet à travers lequel la méthode a été appelée est accessible à travers la constante d'instance this.

Variables et méthodes de classe

Une donnée membre dont la déclaration est précédée du qualifieur static est appelée variable de classe. Propriété fondamentale : il en existe un seul exemplaire pour la classe. Une telle variable est initialisée lors du chargement de la classe :

Une fonction membre dont la déclaration est précédée du qualifieur static est appelée méthode de classe. Propriété fondamentale : une telle fonction ne dispose pas de la variable this, car elle n'est pas appelée en association avec un objet. La notation, pour un appel d'une telle méthode, est :

UneClasse.uneMethode(arguments)

Variables finales (constantes)

Une variable d'instance ou de classe dont la déclaration est précédée du qualifieur final est une constante. Elle peut recevoir une valeur initiale (elle peut notamment être affectée dans le constructeur de la classe) mais par la suite sa valeur ne peut plus être changée. Exemple :

public class Bidule
{
    int valeurDuBidule;                   // variable d'instance
    final int rangDuBidule;               // constante d'instance
    static int nombreDeBidules = 0;       // variable de classe
    static final int MAX_BIDULES = 100;   // constante de classe

    public Bidule(int valeur) throws Exception   // constructeur 
    {
        if (nombreDeBidules >= MAX_BIDULES)
            throw new Exception("Trop de bidules"); 
        valeurDuBidule = valeur;
        rangDuBidule = nombreDeBidules++; 
    }
    ...
    autres membres
}

N.B. Les méthodes et les classes peuvent bénéficier aussi du qualifieur final. Nous verrons cela plus loin.

A propos de la surcharge des méthodes

La surcharge des méthodes consiste dans le fait que deux méthodes peuvent avoir le même nom si leurs signatures sont différentes.

(La signature d'une méthode n'englobe pas le résultat : on ne peut pas donner le même nom à deux méthodes qui ne différent que par le type du résultat.)

Le mécanisme de la surcharge va plus loin en Java qu'en C++, au moins sur deux points :

1. En Java on considère qu'une méthode et une variable sont de signatures différentes, et peuvent donc avoir le même nom. Exemple :

public class Point 
{ 
    int x, y;
    int x() { return x; } 
    int y() { return y; }
}

2. (Attention, point délicat). Considérons une méthode définie dans une classe, ayant le même nom qu'une méthode héritée d'une super-classe. Si ces deux méthodes ont la même signature, la première constitue une redéfinition de la seconde. Mais si ces deux méthodes n'ont pas la même signature :

2.6 Création, initialisation et destruction des objets

Une variable d'un type classe est une référence. Par défaut, elle est initialisée avec la valeur null. Utiliser une telle variable pour accéder à un membre, avant qu'elle n'ait reçu une valeur utile, est une erreur que la machine Java signale en lançant une exception NullPointerException.

La plupart du temps, on donne une valeur à une variable de type classe en créant un objet. Pour cela un seul moyen : l'opérateur new. Syntaxe :

uneVariable = new UneClasse(arguments);

(Les arguments sont facultatifs, mais la paire de parenthèses non.) L'instruction précédente est exécutée en plusieurs temps :

Comme en C++, un constructeur d'une classe est une méthode qui a le même nom que la classe, n'indique pas de type de résultat et ne comporte pas d'instruction return. Un constructeur peut être public, privé, protégé ou avoir l'accessibilité par défaut.

En Java un constructeur d'une classe UneClasse ne peut être appelé que de deux manières :

Une classe peut posséder plusieurs constructeurs (la surcharge des méthodes s'applique). En Java, un constructeur d'une classe peut en appeler un autre (cela n'est pas possible en C++) en utilisant la pseudo-méthode this(arguments). Exemple :

class Point
{
    int x, y;

    Point(int x, int y)
    {
        validation des valeurs de x et y
        this.x = x;
        this.y = y;
    }

    Point(int u)
    {
        this(u, 0);      // le constructeur à un argument
    }                    // appelle celui à deux arguments
    ...
    autres membres
} 

L'appel this(arguments), lorsqu'il est présent, doit être la première instruction du constructeur.

Si, pour une classe donnée, le programmeur n'a écrit aucun constructeur, alors le compilateur synthétise un constructeur sans arguments qui se réduit à ne rien faire (n'oubliez pas que chaque variable d'instance a reçu par ailleurs une valeur par défaut). Si le programmeur a écrit un constructeur, quel qu'il soit, alors aucun constructeur n'est synthétisé automatiquement.

La destruction des objets et la gestion de l'espace

En Java le programmeur n'a pas à se soucier de gérer la mémoire avec économie. Quand il a besoin d'un objet il le crée (à l'aide de l'opérateur new). Quand il n'en a plus besoin, il l'oublie purement et simplement :

Point p = new Point(1, 2);
...
p = new Point(3, 4);    // si aucune autre variable ou objet ne le
...                     // référence par ailleurs, le point (1,2)
                        // est désormais "déconnecté" de tout

C'est le rôle de la machine Java, quand la mémoire disponible vient à manquer, de passer en revue tous les objets existants et de détecter ceux qui n'ont plus aucune chance d'être accédés par la suite du programme (c'est-à-dire, les objets qu'aucune référence ne pointe). La mémoire occupée par ces objets "déconnectés" est alors récupérée et utilisée pour les nouvelles allocations. Ce mécanisme s'appelle traditionnellement le garbage collector (nettoyeur de mémoire).

Cette gestion automatique de la mémoire est extrêmement commode, mais elle a une petite conséquence déroutante : on ne peut pas savoir à quel moment les objets sont effectivement détruits. L'information suivante n'est donc pas très importante :

Destructeur. Si une classe possède une méthode finalize, avec la signature (les énoncés throws... seront expliqués plus loin) :

protected void finalize() throws Throwable

alors cette méthode sera appelée par Java chaque fois qu'un objet de cette classe sera effectivement détruit. Dans un programme qui ne fait pas une grosse consommation de mémoire cette méthode risque de n'être jamais appelée. On l'aura compris, il est plutôt rare qu'on définisse cette méthode.

Initialisations statiques

Les variables de classe (qualifiées static) peuvent souvent être initialisées par une affectation incorporée à leur déclaration. C'est le cas notamment des constantes (qualifiées final), comme dans :

public class Point
{
    static final Point ORIGINE = new Point(0, 0);
    ...
    autres membres
} 
Lorsque l'initialisation à faire est trop complexe pour s'en sortir de cette manière, on peut écrire un code d'initialisation statique, qui sera exécuté, une seule fois, lors du chargement de la classe. Dans l'exemple suivant, l'initialisation de la variable de classe tousLesPoints (une structure contenant constamment tous les points qui ont été créés) est trop complexe pour pouvoir la faire dans sa déclaration  :
public class Point
{
    int x, y;                          // les coordonnées du point
    static final Point ORIGINE;        // origine des coordonnées
    static Vector tousLesPoints;       // contient tous les points créés

    static                             // bloc d'initialisations statiques
    {
        ORIGINE = new Point(0, 0);
        tousLesPoints = new Vector();
        tousLesPoints.add(ORIGINE);
    }

    Point(int a, int b)
    {
        validation des valeurs de a et b
        x = a;
        y = b;
        tousLesPoints.add(this);
    }
    ...
    autres membres
} 

Constructeurs et héritage

L'héritage est traité plus loin dans cette note. Mais, puisque nous en sommes à la construction des objets, voici des explications sur l'initialisation des objets hérités.

La construction d'un objet de la sous-classe commence toujours par l'appel d'un constructeur de la super-classe. Si le programmeur n'a rien prévu à ce propos, alors il s'agit du constructeur par défaut (sans arguments) de la super-classe, qui doit donc exister. Par exemple, le constructeur de la classe Pixel suivante ne passe pas la compilation, car la classe Point n'a pas de constructeur sans arguments :

public class Pixel extends Point
{
    Color couleur;
    Pixel(int a, int b, Color c)        // VERSION ERRONEE
    {
        x = a; y = b; couleur = c;
    }
    ...
    autres membres 
}

On appelle explicitement un constructeur de la super-classe par une expression de la forme super(arguments). Voici la version correcte du constructeur précédent :

public class Pixel extends Point
{
    Color couleur;
    Pixel(int a, int b, Color c)
    {
        super(a, b);
        couleur = c;
    }
    ...
    autres membres 
}

L'appel de super comme ci-dessus remplace une syntaxe bizarre de C++, où il aurait fallu écrire :

    Pixel(int a, int b, Color c) : Point(a, b)    // ceci est du C++
    { 
        couleur = c; 
    }

L'appel de super, lorsqu'il est présent, doit être la première instruction du constructeur.

2.7 Les deux facettes de la notion de classe

A l'heure qu'il est on l'aura compris, la notion de classe possède deux facettes, surtout en Java :

Le premier aspect est plus ou moins présent, selon la nature de la classe, alors que le second est toujours évident, quel que soit le rôle d'une classe dans un programme.

Ainsi, une classe ne possédant ni variables d'instance ni méthodes d'instance ne doit en général pas être vue comme un type (les instances n'auraient ni état ni comportement). Or de telles classes existent et sont fort utiles ; composées de méthodes de classe et de variables de classe, presque toutes finales, elles remplissent le même rôle que les modules ou unités de certains langages non orientés objets. Par exemple, la classe java.lang.Math comporte deux constantes de classe (java.lang.Math.PI et java.lang.math.E) et une collection de méthodes de classe (java.lang.Math.log(double), java.lang.Math.sin(double), etc.)


3. Héritage


3.1 Héritage, super-classe, sous-classes

L'arbre des classes

Tous les langages OO qui nous intéressent pratiquent l'héritage des classes selon des modalités voisines. Voici les deux principales différences entre Java et C++ relativement à cette question :

Ainsi la relation "hérite de" définit sur l'ensemble de toutes les classes (les classes de la bibliothèque et celles de l'utilisateur) une unique structure arborescente ayant la classe Object pour racine. En particulier, toute classe est sous-classe directe ou indirecte de Object.

Si la classe Pixel est sous-classe de la classe Point, on dit que Pixel étend Point (en C++ on dirait que Pixel dérive de Point). Cela s'écrit :

class Pixel extends Point
{
    ...
    déclaration des membres que la classe Pixel
    ajoute à ceux de la classe Point
    ...
} 

Une classe finale est une classe qui ne peut pas avoir de sous-classes. Exemple (de la bibliothèque) :

public final class Integer extends Number
{
    ...
}
Rendre finale une classe a d'obscures conséquences surtout techniques. La plupart du temps, les classes que vous définissez n'ont pas de raison d'être finales.

Notes

1. Contrairement à C++, il n'y a pas en Java plusieurs sortes d'héritage (public, privé, protégé, etc.), mais une seule :

2. L'expression "extends Object" est facultative. En effet, une classe dont la définition ne comporte aucune formule "extends..." est comprise comme sous-classe directe de Object (c'est le cas de la plupart des classes dont nous avons parlé précédemment).

3.2 Redéfinition des méthodes

En Java comme en C++, lorsque dans une classe on définit une méthode qui a exactement la même signature qu'une méthode définie dans une super-classe (directe ou indirecte) on dit qu'on a affaire à une redéfinition de cette méthode. Dans la sous-classe, la redéfinition masque la définition héritée : lors d'un appel à travers une instance de la sous-classe, c'est la définition donnée dans la sous-classe qui sera exécutée.

La redéclaration d'une méthode ne peut pas restreindre son accessibilité. Par exemple, si la méthode était publique dans la super-classe, elle doit être qualifiée public dans la sous-classe.

Super

Dans une méthode d'une classe D, l'identificateur super est une constante d'instance qui référence l'objet à travers lequel on a appelé la méthode, mais considéré comme ayant pour type la super-classe directe B de D. Autrement dit, super et this sont la même adresse, mais this est de type D et super est de type B.

Le nom super permet d'appeler la version héritée d'une méthode redéfinie. Exemple :

class Pixel extends Point
{
    Color couleur;
    ...
    String toString()
    {
        return super.toString() + "[" + couleur + "]";
    }
}

Ici, la notation super.toString() est le moyen d'appeler dans Pixel la version de toString définie dans Point.

Note. L'identificateur super ne permet d'atteindre que la super-classe directe. Pour "aller plus haut" il faut utiliser l'opérateur de conversion. Exemple :

class A
{ 
    int x;
    ...
}

class B extends A
{ 
    int x;
    ...
}

class C extends B
{ 
    int x;
    ...
    void afficherLesTroisX()
    {
        System.out.println( x );            // affiche x de la classe C
        System.out.println( super.x );      // affiche x de la classe B
        System.out.println( ((A)this).x );  // affiche x de la classe A
    }
}

Une méthode finale est une méthode qui ne peut pas être redéfinie dans une classe dérivée :

class Point
{
    ...
    public final int distance(Point p)
    {
        return Math.abs(x - p.x) + Math.abs(y - p.y);
    }
    ...
}

Rendre finale une méthode a des conséquences techniques assez obscures.

Les conversions super-classe <--> sous-classe. Généralisation et (re-)particularisation.

Soit B une classe et D une sous-classe de B. Un objet de D possède tous les membres de la classe B ; par conséquent, la conversion du type D vers le type B est une opération possible et naturelle que Java effectue chaque fois que c'est nécessaire, sans qu'il faille le demander explicitement.

[Attention, remarque savante] De plus, puisqu'en Java les objets sont accédés par référence et que l'héritage est simple, cette conversion est toujours une opération transparente, qui ne demande aucun travail de la part de Java. Elle se résume à ceci : sans modification de sa valeur, une expression qui est une référence sur un D est vue comme étant une référence sur un B.

Puisque la sous-classe est une particularisation de la super-classe, cette conversion peut être appelée généralisation : vu comme un B, l'objet est tenu pour plus général que le D qu'il est en réalité. Exemple :

class Pixel extends Point
{
    Color couleur;
    ...
    autres membres
}

Pixel pix = new Pixel(a, b, Color.red);
...
Point pt = pix;

Dans cet exemple un seul objet est créé, c'est un Pixel. Aussi longtemps qu'il est accédé à travers la variable pt, il est vu comme un Point et sa variable d'instance couleur est inaccessible (mais la conversion n'affecte en rien la valeur de cette variable).

La conversion réciproque, du type super-classe vers le type sous-classe, n'est jamais automatiquement prise en charge par Java. Le programmeur doit la prescrire explicitement :
pix = pt;            // ERREUR
pix = (Pixel) pt;    // Ok

La conversion pix = (Pixel) pt est aussi transparente et sans travail que la généralisation pt = pix ; bien entendu, elle ne se justifie que si la valeur courante de pt est en fait un Pixel.

En C++ l'expression "(A) unB" (où unB est un objet de la classe B) est entièrement placée sous la responsabilité du programmeur, aucun contrôle n'est fait ni à la compilation ni à l'exécution. En Java la situation est bien meilleure puisque

Polymorphisme

En Java, toutes les méthodes sont virtuelles.

Le service rendu par les méthodes virtuelles est le même en Java qu'en C++. Rappelons-le brièvement, sur un cas particulier : soit C une classe possédant des sous-classes directes et/ou indirectes, et m une méthode définie dans C et redéfinie dans les sous-classes de C. La déclaration

C x;

introduit une variable de type C, qui sera ultérieurement affectée par un objet de la classe C ou d'une sous-classe de C. Dire que m est une méthode virtuelle c'est dire que la méthode effectivement appelée par l'expression

x.m(arguments)

n'est pas déterminée par le type de la variable x, mais par celui de l'objet qui est la valeur effective de x.

Voici un exemple typique montrant l'utilité des méthodes virtuelles. Proposons-nous d'écrire une méthode qui calcule et renvoie le nombre de fois qu'un objet x figure dans un tableau t. Elle sera utile pour différentes sortes d'objets précis, mais nous pouvons l'écrire une fois pour toutes, en nous plaçant à un niveau de généralité maximum (la méthode virtuelle en jeu ici est equals, déclarée dans la classe Object) :

public class MesOutils
{
    ...
    public static int combien(Object x, Object t[])
    {
        int r = 0;
        for (int i = 0; i < t.length; i++)
            if ( x.equals(t[i]) )
                r++;
        return r;
    }
    ...
}

A méditer, les deux aspects essentiels du programme ci-dessus :

L'opérateur instanceof

L'expression

unObjet instanceof uneClasse 

teste l'appartenance de l'objet indiqué à la classe indiquée. Cette expression est rejetée s'il est certain dès la compilation que le résultat du test sera false :

class A { ... }

class B { ... }

    ...
    A a;
    ...
    if (a instanceof B)              // ERREUR, A ne peut
        System.out.println("oui");   // être instance de B
    ...

Pour que l'expression "unObjet instanceof uneClasse" soit légale il faut que le type statique de unObjet (c'est-à-dire le type tel que le compilateur peut le déduire des types déclarés pour les variables et les méthodes qui apparaissent dans l'expression de unObjet) soit une sous-classe directe ou indirecte de uneClasse (auquel cas la réponse sera oui) ou réciproquement (la réponse sera alors intéressante). Exemple :

class Européen { ... }

class Français extends Européen { ... }

class Espagnol extends Européen { ... }

    ...
    Européen unEtudiant;
    ...
    instructions donnant une valeur à la variable unEtudiant
    ...
    if (unEtudiant instanceof Français)
        System.out.println("oui");
    ...

N.B. L'opérateur instanceof peut être utilisé également pour savoir si un objet implémente une interface (notion expliquée plus loin). La règle disant quand l'utilisation de instanceof est légale se complique en présence d'interfaces, mais la version "naïve" suivante reste valable : si la question est intéressante alors elle est valide. Autrement dit : si la valeur de "unObjet instanceof uneClasse" ne peut pas être calculée à la compilation, alors l'expression est légale.

3.3 Méthodes et classes abstraites. Interfaces

Méthodes et classes abstraites

En Java comme en C++, une méthode abstraite est une méthode déclarée mais non définie dans une classe C, et destinée à être définie dans les sous-classes directes ou indirectes de C.

Une méthode abstraite doit être signalée par le qualifieur abstract ; le corps de la méthode doit alors être absent, remplacé par un point-virgule :

public abstract void seDessiner(Graphics g);

Une classe abstraite est une classe qui ne doit pas avoir d'instances. Cela peut provenir :

Un classe abstraite est signalée par le qualifieur abstract :

abstract class ObjetGraphique
{
    public abstract void seDessiner(Graphics g);
    ...
    autres membres
}

L'utilisation habituelle d'une classe abstraite passe par la déclaration de variables ayant cette classe pour type :

ObjetGraphique p;

La valeur effective de p ne sera pas une instance directe de ObjetGraphique, mais d'une sous-classe dans laquelle la méthode seDessiner aura nécessairement été définie. Conséquence : l'appel suivant est légitime :

p.seDessiner(g);

Les méthodes (concrètes) peuvent être vues comme des services qu'une classe offre à ses utilisateurs. Les méthodes abstraites, au contraire, créent plutôt des corvées pour les utilisateurs de la classe. Déclarer une méthode abstraite c'est engager ceux qui écriront des sous-classes à définir ces méthodes "promises" dans la super-classe abstraite. Par exemple, chaque sous-classe de ObjetGraphique destinée à posséder des instances devra donner une définition concrète de la méthode seDessiner.

Une classe peut avoir des méthodes abstraites et des méthodes concrètes ; elle peut même utiliser les méthodes abstraites pour définir les méthodes concrètes. Par exemple, pour effacer un objet graphique il suffit de le dessiner avec la couleur du fond :

abstract class ObjetGraphique
{
    public abstract void seDessiner(Graphics g);

    public void sEffacer(Graphics g)
    {
        Color c = g.getColor();
        g.setColor(getBackground());
        seDessiner(g);
        g.setColor(c);
    }
    ...
    autres membres
}

(Attention, il y a une légère tricherie dans la méthode sEffacer)

Interfaces

Une interface est une classe qui ne possède que des méthodes publiques abstraites et des variables statiques finales (c'est-à-dire des constantes de classe).

Pour définir une interface on utilise le mot interface au lieu de class. Les conséquences en sont les suivantes

Dans une classe qui implémente une interface, chacune des méthodes de l'interface doit être explicitement qualifiée public. Dans le cas contraire il y aurait restriction de l'accessibilité de ces méthodes (puisque dans l'interface elles sont implicitement qualifiées publiques)

Exemple :

public interface Pile
{
    boolean vide();
    void empiler(Object x);
    Object depiler();
}

public interface Queue
{
    boolean vide();
    void entrer(Object x);
    Object sortir();
}

public class Table extends Vector implements Pile, Queue
{
    public boolean vide()
    {
        return size() == 0;
    }
    public void empiler(Object x)
    {
        add(x);
    }
    public Object depiler()
    {
        int k = size() - 1;
        Object r = elementAt(k);
        removeElementAt(k);
        return r;
    }
    ...
    etc.
}

Exemple d'utilisation :

public class Test
{
    public static void main(String args[])
    {
        Pile p = new Table();
		
        for (int i = 0; i < 10; i++)
            p.empiler(new Integer(i));
		
        while ( ! p.vide())
            System.out.print(p.depiler() + " ");
        System.out.println();
    }
}

3.4 Exceptions

La notion d'exception est la même en Java et en C++ : un mécanisme par lequel une fonction "profonde" notifie à la fonction qui l'a appelée, ou à la fonction qui a appelé celle-là, etc., la survenue d'un incident qui rend la poursuite du travail impossible ou inutile.

Lancement et capture d'une exception.

Les constructions syntaxiques employées en Java et en C++ sont très voisines. Lancement d'une exception de type ClasseException :

throw new ClasseException(argumentsEventuels);

Exemple (la clause throws est expliquée plus loin) :

public class Point
{
    public Point(int x, int y) throws Exception
    {
        if (x < O || x >= XMAX || y < 0 || y >= YMAX)
            throw new Exception("Coordonnées illégales");
        this.x = x;
        this.y = y;
    }
    ...
    autres membres
}

Capture :

    try
    {
        suite d'instructions dont on veut
        rattraper les exception lancées
    }
    catch (typeException1 e)
    {
        actions à faire si une exception compa-
        tible avec typeException1 a été lancée
    }
    catch (typeException2 e)
    {
        actions à faire si une exception compa-
        tible avec typeException2 a été lancée
    }
    ...
    finally
    {
        actions à faire dans tous les cas (même
        si aucune exception n'a été lancée) 
    }

Déclaration des exceptions

Si une méthode contient une expression pouvant lancer une exception d'un certain typeException, deux choses sont possibles :

La syntaxe est :
prototype_de_la_fonction throws typeException 

Exemple. La classe Lecture suivante offre deux méthodes de classe pour lire un entier. Toutes deux utilisent la méthode readLine de la classe BufferedReader, qui est déclarée

public String readLine() throws IOException

ainsi que la méthode de classe parseInt de la classe Integer, déclarée

public static int parseInt(String s) throws NumberFormatException

Toute méthode appelant les deux précédentes doit donc soit attraper ces exceptions (c'est ce que fait entierCuit) soit les déclarer à son tour (comme entierCru) :

import java.io.*;

public class Lecture
{
    static public int entierCru(String prompt) throws IOException
    {
        System.out.print(prompt);
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        return Integer.parseInt(in.readLine());
    }

    static public int entierCuit(String prompt)
    {
        int r = 0;
        try
        {
            System.out.print(prompt);
            BufferedReader in = 
                new BufferedReader(new InputStreamReader(System.in));
            r = Integer.parseInt(in.readLine());
        }
        catch (IOException e)
        {
            System.out.println("Problème d'entrée/sortie");
            System.exit(1);
        }
        catch (NumberFormatException e)
        {
            System.out.println("Format de nombre incorrect");
            System.exit(2);
        }
        catch (Throwable e)
        {
            System.out.println("Erreur lors d'une lecture");
            System.exit(3);
        }
    return r;
    }
}

La hiérarchie Throwable

Tout objet lancé comme exception doit être une instance, directe ou indirecte, de la classe Throwable. Un certain nombre de sous-classes de cette classe sont prédéfinies :

Throwable   classe de base de toutes les exceptions
        Error
*
des erreurs graves (qu'il n'est en général pas raisonnable de chercher à récupérer)
        Exception   les exception méritant d'être détectées et traitées par le programme.
Vos propres exceptions seront des sous-classes de Exception
                RuntimeException
*
incidents pouvant survenir durant le fonctionnement normal de la machine Java. Exemples :
        - indice de tableau hors des bornes
        - accès à un membre d'une référence null
        - division par 0
        - etc.
                IOException   incidents pouvant survenir durant une opération d'entrée/sortie


Exceptions non contrôlées. Les exceptions marquées d'une étoile dans le tableau ci-dessus sont dites "non contrôlées" car elles échappent à l'obligation de les déclarer dans l'en-tête de toute méthode susceptible de les lancer (autrement, il faudrait les déclarer dans les en-têtes de toutes le méthodes).

Index

Les mots réservés sont en caractères gras, les éléments de la bibliothèque en caractères penchés.

abstract (classe)
abstract (méthode)
boolean
byte
catch
char

ClassCastException
clone
double
equals
Error
Exception
extends
final (classe)
final (méthode)
final (variable)
finally
float
implements
import
IndexOutOfBoundsException
instanceof
int
interface
IOException
java.lang
long
main
new
NullPointerException
Object
OutOfMemoryError
package
private
protected
public (classe)
public (variable ou méthode)
RuntimeException
short
static (bloc d'initialisations)
static (variable ou methode)
super (constante d'instance)
super (méthode)
String
StringBuffer
this (constante d'instance)
this (méthode)
throw
throws
Throwable
toString
try