Généralités sur JAVA

Spécification du langage JAVA

APPENDICES JAVA

Compléments JAVA

retour sommaire 5. Les Interfaces

Les interfaces sont des collections de méthodes, sans implémentation de leur corps. Les interfaces permettent d'encapsuler des méthodes, sans restreindre leur implémentation à un seul arbre d'héritage. Lorsqu'une classe implémente une interface, elle implémente en général toutes les méthodes contenues dans l'interface. Si la classe est abstraite, elle peut ne pas implémenter toutes les méthodes, et laisser ce soin à ses sous-classes.

L'utilisation d'interfaces permet ainsi de simuler, en quelque sorte, l'héritage multiple. Cependant, cela diminue quelque peu les performances du programme.

L'utilisation d'interfaces permet à plusieurs classes de partager une collection de méthodes, sans avoir à connaître la structure des autres classes. Ces classes ne sont donc pas forcement liées par un lien d'héritage. Un exemple plus concret de l'utilisation des interfaces sera donné plus loin. L'exemple ci-dessous montre comment implémenter une interface.

public interface Stocker {
void compacter(Stream s);
void reconstituer(Stream s);
}
public class Dessin implements Stocker {
...
void compacter(Stream s){
// Compactage au format JPEG
...
}
void reconstituer(Stream s){
// Reconstituer l'image JPEG
...
}
}

Les interfaces peuvent être, tout comme les classes, privées ou publiques. Par défaut, une interface est privée. Les méthodes d'une interface sont toujours publiques. Les variables d'une interface sont toujours publiques, statiques, et finales.

5.1. Les Interfaces comme Types

Les interfaces, tout comme les classes, peuvent être utilisées comme des types. Il est ainsi possible d'avoir une déclaration telle que celle ci-dessous.

Stocker ObjetQuiDoitStocker;

Ici, la variable ObjetQuiDoitStocker doit être une instance d'une classe qui implémente l'interface Stocker. Ainsi, grâce à ce type de déclaration (qui peut aussi être dans une liste de paramètres), le programmeur n'est pas obligé de spécifier le type de l'objet attendu. Tout ce qu'on sait sur cet objet, c'est qu'il implémente les méthodes contenues dans l'interface Stocker.

Contrairement à l'héritage classique, les différentes classes qui implémentent une interface n'ont plus à hériter d'une même classe abstraite.

5.2. Les Méthodes et les Variables d'une Interface

Les méthodes d'une interface sont déclarées de manière simple, sans aucun modificateur.

TypeRetourné NomDeMethode ( ListeDeParamètre );

Les variables déclarées dans une interface doivent être initialisées.

5.3. Combiner des Interfaces

Les interfaces peuvent elles-mêmes incorporer une ou plusieurs autres interfaces, en utilisant le mot réservé extends.

interface Exemple extends Stocker,AutreInterface {
...
}

L'interface contient les méthodes définies dans Exemple, plus les méthodes spécifiées dans les interfaces Stocker, et AutreInterface.

6. Les Packages

Les packages sont des groupes de classes, et d'interfaces. Ils représentent en fait des unités de compilation, et permettent de gérer un large espace de noms en évitant les conflits. Toute classe ou interface est contenue dans un package. Par convention, les noms des packages sont composés de différents noms séparés par des points. En général, le premier nom représente l'organisation qui a développé le package.

6.1. Les Packages comme Unités de Compilation

La déclaration d'un package se fait à l'aide du mot réservé package.

package MonPackage;

Lorsque cette ligne est présente dans une unité de compilation, ce doit être la première ligne qui ne soit pas un commentaire dans ce fichier. En d'autres termes, toute ligne précédant cette ligne doit être une ligne de commentaire.

Lorsqu'il n'existe pas de déclaration de package, l'unité de compilation est automatiquement placée dans un package par défaut qui n'a pas de nom.

6.2 Utiliser des Classes et des Interfaces d'un autre Package

La langage JAVA permet d'utiliser des classes ou des interfaces qui ont été écrites dans d'autres packages. Il existe pour cela deux moyens :

Il est possible de préfacer chaque référence à la classe ou à l'interface avec le nom de son package d'appartenance, comme dans l'exemple ci-dessous:

JAVA.lang.Thread MaTache=new JAVA.lang.Thread();

Il est possible d'importer une classe, une interface, ou même le package qui contient les classes ou interfaces que l'on veut utiliser. Pour cela, on utilise le mot réservé import. Cela permet de rendre le nom de la classe ou de l'interface directement accessible dans le package où est faite l'importation. Lorsqu'on importe un package, toutes ses classes et ses interfaces publiques sont accessibles.

// Importation des classes de JAVA.lang
import JAVA.lang.*;
// Importation de la classe Applet
import JAVA.applet.Applet;

La spécification d'un nom de classe ambigu provoque une erreur de compilation. Cela se produit lorsqu'on importe une classe, et que, dans le même package, on définit une classe de même nom. Pour éviter ce genre d'erreur, il convient de spécifier la totalité du nom de la classe que l'on veut utiliser.

7. Les Expressions

Les expressions en langage JAVA ressemblent beaucoup aux expressions qui existent en langage C.

7.1. Les Opérateurs

Les opérateurs, de la priorité la plus forte à la priorité la plus faible, sont :

. [] ()
++ -- ! ~ instanceof
* / %
+ -
<< >> >>>
< > <= >=
== !=
&
^
|
&&
||
?:
= op=
,

7.1.1. Les Opérateurs sur les Entiers

Le résultat d'une opération sur deux entiers n'est jamais byte, short, ou char. Si l'une des opérandes est un long, alors le résultat sera un long. Sinon, le résultat sera un int. Ainsi, par exemple, si i est de type byte, i+1 sera de type int. Quand le résultat d'une opération dépasse les bornes d'un type, le résultat est réduit à modulo la borne du type.

Les opérateurs unaires sont :

      Opérateur              Opération
          -            Négation
          ~            Complémentaire du bit
         ++            Incrémentation
         --            Décrémentation

Les opérateurs binaires sont :

      Opérateur              Opération
          +            Addition
          -            Soustraction
          *            Multiplication
          /            Division
          %            Modulo
          &            ET logique
          |            OU logique
          ^            OU exclusif logique
         <<            Décalage à gauche
         >>            Décalage à droite en
                       gardant le signe
         >>>           Décalage à droite
                       remplaçant par des
                       zéros

Tous ces opérateurs produisent des résultats dont le type est un entier. Les seuls opérateurs qui ne respectent pas cette règle sont les opérateurs <,>,<=,>=,== et !=, qui donnent des booléens.

7.1.2. Les Opérateurs sur les Booléens

Les booléens peuvent être combinés à travers une opération pour donner un nouveau booléen en résultat. L'opérateur unaire ! est la négation logique. Les opérateurs binaires &,|, et ^ sont respectivement le ET logique, le OU logique, et le OU exclusif logique. L'utilisation de ces opérateurs force l'évaluation des deux opérandes. Pour éviter d'évaluer l'opérande de droite, on peut utiliser les opérateurs && et ||.

7.1.3. Les Opérateurs sur les Réels

Les opérateurs sur les réels sont, tout comme les entiers, les opérateurs usuels : +,-,*,/, et les opérateurs d'affectation +=,-=,*=, /=. Les opérateurs unaires ++ et -- sont aussi disponibles : ils incrémentent et décrémentent de 1.0. De même, % et %= sont disponibles.

a % b;
// c'est équivalent à :
a - ((int)(a/b)*b);

Il s'agit du reste après la division.

Les opérations qui contiennent au moins une opérande de double précision (Double), donnent un résultat en double précision. Dans les autres cas, le résultat est en simple précision (Float). Les opérations qui dépassent la borne inférieure génèrent un résultat nul. Les opérations qui dépassent le borne supérieure, génèrent la valeur Inf. La division par zéro génère la valeur Inf.

Les opérateurs relationnels sont aussi disponibles. Cependant, il convient de prendre certaines précautions : dans la comparaison, les valeurs ne sont pas pleinement prises en compte. Par exemple, si a<b n'est pas vrai, cela ne signifie pas forcement que a>=b.

7.1.4. Les Opérateurs sur les Tableaux

La syntaxe suivante :

<expression>[<expression>]

permet de récupérer la valeur d'un élément du tableau. Les bornes d'un tableau vont de 0 à la borne supérieure, définie lors de la déclaration, moins 1. Les erreurs de dépassement n'apparaissent que lors de l'exécution par le runtime JAVA.

7.1.5. Les Opérateurs sur les Chaînes de Caractères

Les chaînes sont des objets String. L'opérateur + permet de concaténer deux chaînes de caractères. Si l'une des opérandes n'est pas de type String, la conversion est automatiquement faite. Si l'une des opérandes est une instance d'une classe, cette classe peut avoir préalablement défini une méthode ToString() qui retourne la chaîne de caractères correspondante.

float a=1.0;
print ("La valeur de a est "+a+"\n");
String s="a= "+a;

L'opérateur += fonctionne aussi sur les objets String.

7.1.6. Les Opérateurs sur les Objets

L'opérateur binaire instanceOf permet de déterminer si un objet est d'une certaine classe, ou de l'une de ses sous-classes.

// Est-ce que a est instance de la classe Exemple ?
if (a instanceOf Exemple) {
// Oui : on peut effectuer les opérations
Exemple b=(Exemple)a;
}

7.2. Les Conversions par les Casts

Les conversions en langage JAVA sont volontairement limitées, pour éviter certaines erreurs de corruption du système. Les entiers ne peuvent pas être convertis en tableaux, ou en objets. Les objets ne peuvent pas être convertis en types de base. Un objet peut être converti en un objet de sa superclasse sans perte, mais la conversion en une sous-classe entraîne une vérification lors de l'exécution. Si l'objet n'est pas converti en une sous-classe, n'est pas une instance de cette sous-classe, le runtime JAVA provoque une exception ClassCastException.

8. La Syntaxe

8.1. Les Déclarations

Les déclarations peuvent être effectuées dans n'importe quelle partie du code où la variable à déclarer est utilisée. Ainsi, on peut très bien déclarer le compteur d'une boucle for, dans la boucle elle-même.

for (int i=1;i<5;i++) {
...
}

La portée de la variable déclarée est le bloc dans lequel elle a été déclarée. Ainsi, dans l'exemple précédent, la variable i n'est valable que dans la boucle for. Cela est équivalent à :

{
int i=1;
for (;i<5;i++){
...
}
}

8.2. Les Instructions de Contrôle

Les instructions de contrôle sont, comme en langage C, les suivants :

if (booléen) instructions
else instructions
switch (expression) {
case expression: instructions
default: instructions
}
break [label];
continue [label];
return [label];
for ([expression];[expression];[expression]) instructions
while (booléen) instructions
do instructions while (booléen);
label:instructions

L'utilisation de labels peut se faire pour toute boucle. De même, il est possible d'interrompre toute boucle par l'utilisation de labels. Toute suite d'instructions peut avoir un label. Si une instruction break, est suivie d'un label, ce doit être le label d'un bloc qui englobe cette instruction. Si une instruction continue est suivie d'un label, ce doit être le label d'une boucle englobante.

8.3. Les Exceptions

Lorsqu'une erreur survient dans un programme JAVA, il est possible, pour le programme qui détecte l'erreur, de lancer une exception. Par défaut, lorsqu'une exception est lancée, elle est suivie de la fin de l'exécution de la tâche correspondante, et de l'apparition d'un message d'erreur sur l'écran. Cependant, les programmes peuvent récupérer ces exceptions, et les traiter pour continuer l'exécution sans risque.

Des exceptions sont lancées par défaut par le runtime JAVA. Cependant, toute classe peut définir ses propres exceptions, et les lancer à l'aide de l'instruction throw.

throw <NomException>;

Par convention, NomException est le nom d'une instance de la classe Exception, ou de l'une de ses sous-classes. L'instruction throw oblige le programme à exécuter la séquence de code de traitement de l'exception correspondante. L'exemple ci-dessous montre comment créer une exception, et comment l'appeler.

class MonException extends Exception {
}
class Exemple {
void Erreur() {
if (/* Pas d'erreur */) {
...
}
else // Une erreur!
{
throw new MonException();
}
}
}

Pour définir un morceau de code qui gérera l'erreur appropriée, le programme doit d'abord définir la partie de code qui est concernée par l'erreur. Cela se fait à l'aide du mot réservé try. Cette instruction définit un bloc de code, qui sera suivi par une ou plusieurs instructions catch. Ces instructions catch définissent le type d'exception qui doit être traité, et ce qui doit être fait dans ce cas-là. L'exemple ci-dessous illustre tout ceci.

try {
p.a=10;
} catch (NullPointerException e) {
println("p est null!");
} catch (Exception e) {
println("Une autre erreur est arrivée!");
} catch (Object obj) {
println("Ce n'est pas une exception !");
}

Le mot réservé catch peut aussi être suivi d'un nom d'interface. Lorsqu'une erreur survient, le couple try/catch est recherché avec un paramètre qui correspond à l'exception qui a été levée. La correspondance se fait lorsque le paramètre :

est de la même classe que l'exception,

est une superclasse de l'exception,

si c'est une interface, la classe de l'exception doit implémenter cette interface.

Le premier couple try/catch qui correspond est exécuté. Lorsque les instructions contenues dans le catch sont exécutées, l'exécution du programme reprend après le couple try/catch. Il n'est pas possible de faire reprendre l'exécution du programme à l'endroit où l'exception a été levée.

print("Ceci ");
try {
print ("est ");
throw new MonException();
print ("un ");
} catch (MonException e) {
print ("le ");
}
print("programme de démo");
// Le message qui apparaîtra sera :
// Ceci est le programme de démo

Il est possible de lancer une autre exception à l'intérieur du traitement d'une exception. Cela permet notamment de traiter les exceptions de manière plus générale, et d'éviter d'avoir à réécrire plusieurs fois le même traitement d'exception. Néanmoins, il ne faut pas oublier que l'exécution du programme ne reprendra pas au point où l'exception a été levée.

8.3.1. L'Instruction Finally

L'instruction finally permet d'exécuter des lignes de programme, qu'une exception soit levée ou non. Dans l'exemple ci-dessous,

try {
p.a=10;
} catch (NullPointerException e) {
} finally {
print("Instruction finally");
}

la ligne "Instruction finally" sera affichée quoiqu'il arrive. Les lignes de code comprises dans le bloc de finally seront exécutées même si le try contient des instructions return, break, continue ou throw. Dans l'exemple suivant, le mot finally sera toujours imprimé, alors que après le try ne sera imprimé que si a est différent de 10.

try {
if (a==10) {
return;
}
} finally {
print("finally\n");
}
print ("après le try\n");

8.3.2. Les Exceptions du Runtime JAVA

Cette section contient les différentes exceptions levée par le runtime JAVA lorsqu'il rencontre des erreurs.

ArithmeticException

Cette exception est levée uniquement lorsqu'un programme JAVA fait une division par zéro, ou lorsqu'une opération modulo par zéro est effectuée.

NullPointerException

Tenter d'accéder à une variable ou à une méthode dans un objet Null, ou tenter d'accéder à un élément d'un tableau Null lève l'exception NullPointerException. Ce sera le cas pour les instructions o.length et a[0] dans l'exemple suivant :

class Null {
public static void main(){
String o=null;
int a[]=null;
o.length();
a[0]=0;
}
}

IncompatibleClassChangeException

Cette exception est en général levée lorsqu'une définition de classe a été changée, mais que d'autres classes qui font référence à cette classe n'ont pas été recompilées. Il existe 4 changements particuliers qui sont surtout concernés :

Une des variables de classe est devenue une variable d'instance,

Une des variables d'instance est devenue une variable de classe,

Un champ qui est déclaré dans la classe est supprimé,

Une méthode de la classe est supprimée.

ClassCastException

Cette exception est levée lorsqu'un programme tente de convertir un objet O en un objet de classe C, et que O n'est ni de classe C, ni une instance d'une sous-classe de C. La déclaration suivante provoque une telle exception lors de son exécution.

class Cast(){
public static void main(){
Object o = new Object();
String s = (String)o; // Erreur de cast
s.length();
}
}

NegativeArraySizeException

Cette exception survient lorsqu'un tableau est initialisé avec une taille négative.

OutOfMemoryException

Cette exception est levée lorsque le système ne peut plus fournir de mémoire suffisante à l'exécution du programme. Cela peut survenir notamment lors de la création d'un objet avec l'opérateur new. C'est le cas du programme suivant.

class Lien {
int a[]=new int[1000000];
Lien l;
}
class OutOfMem {
public static void main() {
Lien racine = new Lien();
Lien courant = racine;
while (true) {
courant.l=new Lien();
courant=courant.l;
}
}
}

NoClassDefFoundException

Cette exception est levée lorsque une classe est référencée sans que le runtime JAVA puisse la trouver.

IncompatibleTypeException

Cette exception est levée lorsqu'on tente d'instancier une interface qui n'est pas implémentée. C'est le cas du programme suivant.

interface I {
}
class IcompatibleType {
public static void main () {
I r=(I)new("I");
}
}

ArrayIndexOutOfBoundsException

Cette exception survient lorsqu'un programme tente d'accéder à un élément non valide d'un tableau. C'est le cas de l'exemple ci-dessous.

class Tableau{
public static void main(){
int a[]=new int[2];
a[10]= 0;
}
}

UnsatisfiedLinkException

Cette exception est levée lorsqu'une méthode est déclarée native, et qu'elle ne peut pas être trouvée par le runtime JAVA.

InternalException

Ce type d'exception ne doit jamais se produire à priori. Cela arrive seulement si une vérification du runtime JAVA a échoué. Dans ce cas, la seule solution est de le faire savoir aux auteurs du langage JAVA eux-mêmes, en leur envoyant un mail à l'adresse java@java.sun.com.

Appendice A : Quelques Précisions sur les Réels

L'utilisation des réels avec le langage JAVA nécessite de prendre quelques précautions. Cette section précise quelques points concernant les réels et leur utilisation. De plus, le langage JAVA possède quelques différences par rapport à la norme IEEE 754, qui seront expliquées ici. L'utilisation des réels ne produit aucune exception en langage JAVA.

A.1. Valeurs Spéciales

Il existe en JAVA un zéro positif, et un zéro négatif. Le zéro négatif peut se distinguer du positif dans les cas suivants :

le dépassement de la borne minimale, lors d'une multiplication, ou d'une division de termes de signes différents,

l'addition du zéro négatif à lui-même, ou la soustraction du zéro positif au zéro négatif,

la racine carré du zéro négatif

Transformer un zéro négatif en une chaîne de caractères donnera un '-'. Mis à part ces cas, les deux zéros sont indifférenciables.

Certains calculs qui produiraient une valeur au delà des bornes des réels, donneraient comme résultat un nombre signé fixe, nommé Inf (infini). Ce nombre ne peut être défini, ni utilisé comme les autres. Deux nombres Inf ne peuvent pas être distingués. Ainsi, on a :

(1./0.)+(1./0.) == (1./0.)

La division d'un nombre fini par Inf donne zéro comme résultat.

Certains autres calculs qui ne produiraient pas de valeur numérique significative donneraient comme résultat une valeur spéciale appelée Not A Number ou NaN. Toute opération ayant un NaN comme opérande produirait un NaN comme résultat. NaN n'est ni signé, ni ordonné. La division d'un infini par un autre donne un NaN comme résultat.

A.2. Ordre des Réels

Tous les opérateurs relationnels peuvent être appliqués aux réels. Toutes les valeurs réelles, à l'exception de NaN, sont ordonnées : -inf< Valeurs Finies < inf. Les relations d'ordre sont transitives, et l'égalité et l'inégalité sont réflectifs.

La valeur NaN n'est pas ordonnée. Cela signifie donc que le résultat de toute opération de relation entre un NaN et une autre valeur donnera false comme résultat. Seule la relation "Nan != n'importe quoi" est vraie.

A.3. Les Différences avec la norme IEEE-754

Voici un rapide aperçu des différences qui existent par rapport à la norme IEEE-754 :

Le langage JAVA ne lève aucune exception dans les opérations concernant les réels: il ne signale pas une opération invalide, la division par zéro, les dépassements de bornes.

Les arrondis : lorsqu'un résultat n'est pas exact, le langage JAVA l'arrondit à la valeur représentable la plus proche. C'est le mode par défaut de l'IEEE. Cependant, lors de la conversion d'un réel vers un entier, JAVA arrondit en se rapprochant de la valeur zéro. -6.3 deviendra -6, et 6.3 deviendra 6. Il n'existe pas de possibilités de définir la manière dont JAVA arrondit les valeurs : à l'entier supérieur, inférieur, ou vers zéro.

La langage JAVA ne supporte aucune extension des formats qui existent. La seule exception est l'extension des réels à simple précision (float) vers les réels à double précision (double).