9 Cours ENPC, printemps 2000
Java, objets, interfaces et Internet

Paquets

Les programmes Java sont des collections de types. Certains de ces types sont publics, c'est-à-dire destinés à être accesibles à d'autres utilisateurs que leur auteur. Ces collections de types publics sont organisées en paquets, dans le but de garantir une désignation non-ambiguë de chaque type, si possible dans le monde entier. La spécification de Java propose d'utiliser le système des noms de domaine Internet pour assurer cette unicité. Par exemple, tous les types conçus à l'ENPC devraient figurer dans un paquet FR.enpc (en renversant le nom de domaine enpc.fr, et en écrivant le nom de premier niveau en majuscules). Ensuite, de façon interne, l'École pourrait décider de désigner le type public Liste, dont l'auteur est l'élève toto, par le nom FR.enpc.eleves.toto.Liste.

La modularité est donc assurée à deux échelles, localement par les hiérarchies de types (publics ou non) et globalement par la hiérarchie des paquets, concernant les types publics. Un type est accessible en dehors du paquet où il est défini s'il est déclaré public.

Un paquet peut être composé de sous-paquets et d'unités de compilation. Par exemple, le paquet java est composé des sous-paquets awt, applet, io, lang, net et util ; chacun de ses sous-paquets est désigné par son nom complet: java.awt, java.applet, etc.

Une unité de compilation se compose de définitions de types. Par exemple, le paquet java.util contient les unités de compilation contenant les définitions des classes Dictionary, Enumeration, Hashtable. L'usage est de désigner les paquets par des noms commençant par une minuscule, les classes par des noms commençant par une majuscule, et les membres d'une classe à nouveau par des noms commençant par une minuscule. Ainsi, le nom complet java.lang.System.out désigne le membre out de la classe System du paquet java.lang.

La spécification du langage n'impose pas comment les paquets et les unités de compilation sont stockées : dans une base de données, dans un système de fichiers, local ou distribué. Elle n'indique pas non plus quels sont les paquets accessibles.

L'implémentation couramment utilisée, sous Unix, représente chaque paquet comme un répertoire, les sous-paquets comme des sous-répertoires, et les unités de compilation comme des fichiers de nom suffixé en .java ; chaque définition de type figurant dans une unité de compilation donne lieu à un fichier suffixé en .class qui contient sa définition compilée (ou forme objet). Les types accessibles sont ceux situés sous l'un des répertoires spécifiés par la variable d'environnement CLASSPATH, ainsi que ceux contenant les types standards de l'environnement Java ; ces répertoires et leurs fichiers sont explorés dans l'ordre où ils figurent dans cette variable, pourvu qu'ils soient autorisés en lecture. Par exemple, supposons que l'on ait défini, sous Unix :

setenv CLASSPATH .:$(HOME)/java/classes

Rappelons que « . » désigne le répertoire courant, vraisemblablement celui où le développement du programme s'effectue, et $(HOME) le répertoire personnel ; la classe projet.util.Liste sera alors recherchée dans ./projet/util/Liste.class, puis dans $(HOME)/java/classes/util/Liste.class, et finalement dans les types standards (où elle ne devrait pas se trouver). La classe standard java.awt.Image sera trouvée dans java/awt/Image.class, relativement au répertoire contenant les types standards.

Pour faire exécuter une classe figurant dans un paquet (et contenant une méthode main), on doit utiliser le nom complet de la classe. Par exemple, si l'on a un fichier objet projet/Main.class, où projet est un sous-répertoire de l'un des répertoires figurant dans CLASSPATH, on exécute la commande, sous Unix :

java projet.Main

Unités de compilation

Une unité de compilation se compose de trois parties :

  • la déclaration du paquet, avec son nom complet
  • des déclaration d'importation
  • des définitions de type (classes et interfaces)

Une déclaration de paquet a la forme :

package projet.util;

Elle indique que l'unité de compilation appartient au paquet projet.util. Si l'unité ne commence pas par une déclaration de paquet, elle est considérée comme faisant partie d'un paquet anonyme. L'usage de ces paquets est commode pour développer de petites applications, mais est contraire aux ambitions du langage en matière de génie logiciel. C'est pourquoi la spécification de Java ne précise pas comment ces paquets anonymes doivent être traités. Sur les implémentations courantes, sous Unix, les unités de compilation sans déclaration de paquet d'un même répertoire constituent un même paquet anonyme. Il est recommandé que les types d'un paquet anonyme ne soient pas déclarés public, afin qu'ils ne puissent pas être importés par un autre paquet, même accidentellement.

Les déclarations d'importation permettent à un type public d'un autre paquet d'être désigné par son nom simple au lieu de son nom qualifié complet : cette déclaration doit spécifier le nom complet du paquet qui contient ce type. Il est possible d'importer un seul type :

import java.awt.Graphics;

ou tous les types publics d'un paquet :

import java.awt.*;

Cette forme étoilée n'importe pas les sous-paquets d'un paquet. Il est donc nécessaire de déclarer à la fois :

import java.awt.*;
import java.awt.event.*;

Toute unité de compilation importe implicitement le paquet java.lang : tous ses types (Boolean, Integer, Exception, Cloneable, etc) peuvent donc être désignés par leur nom simple dans tout programme.

L'implémentation courante impose qu'un type public, et plus généralement qu'un type utilisé par une autre unité de compilation (c'est-à-dire, un autre fichier), soit défini dans un fichier dont le nom est le nom du type suffixé par java.

Accessibilité

Les paquets permettent d'implémenter une notion de modularité basée sur la visibilité des noms, donc l'accessibilité des objets qu'ils désignent. Les noms dont la visibilité est contrôlable sont ceux des types (classes et interfaces) et ceux des membres et constructeurs de ces types : l'un des mots-clés public, protected, private ou l'absence de spécifiant détermine le degré de visibilité.

Les types publics d'un paquet sont accessibles de tout paquet (à condition que le paquet contenant ces types publics soit lui-même accessible) ; les types ordinaires (non publics) ne sont accessibles que du paquet où ils sont définis.

Les membres ou constructeurs publics d'un type public sont accessibles de tout paquet ; en particulier, ils sont hérités par les sous-types. Une méthode publique ne peut être redéfinie, dans une sous-classe, que par une méthode publique. Les membres d'une interface sont implicitement déclarés publics. Il est inutile de spécifier des membres ou constructeurs publics dans un type non-public.

Les membres ou constructeurs privés ne sont accessibles qu'à l'intérieur de la classe où ils sont définis ; en particulier, ils ne sont pas hérités par les sous-types.

Les membres protégés d'une classe sont accessibles à partir d'une sous-classe, à travers une référence qui est un sous-type de cette sous-classe, ainsi que de toute classe du même paquet ; en particulier, ils sont hérités par les sous-classes. Une méthode protégée ne peut être redéfinie, dans une sous-classe, que par une méthode publique ou protégée.

Les membres ou constructeurs ordinaires (ni publics, ni privés, ni protégés) d'une classe sont accessibles de toute classe du même paquet ; en particulier, ils ne sont hérités que par les sous-classes du même paquet. Une telle méthode peut être redéfinie, dans une sous-classe, par une méthode qui ne doit pas être privée.

Exceptions

La notion d'exception décrit des situations où la procédure normale d'évaluation des expressions n'est pas pertinente. Cette procédure suppose que l'évaluation d'une expression (par exemple, d'un appel de fonction) résulte en la production d'une valeur. Il y a des cas où une valeur n'est pas obtenue, quand la procédure d'évaluation conduirait à exécuter une opération qui ne peut pas ou ne doit pas être réalisée :

  • parce qu'une ressource demandée n'est pas disponible : de la mémoire ne peut pas être allouée, un fichier ne peut pas être ouvert, le code d'une fonction ne peut pas être chargé, etc
  • parce qu'une opération n'est pas permise par la sémantique du langage : division entière par zéro, accès à un tableau hors de ses bornes
  • parce qu'une opération n'est pas permise par la sémantique de l'application : dépiler une pile vide

Il est donc possible que l'évaluation d'une expression, au lieu de retourner une valeur, déclenche une exception. Certaines de ces exceptions sont des erreurs, qui conduisent fatalement à une sortie du programme. D'autres peuvent être récupérées pour permettre la poursuite du programme.

En Java, les exceptions sont représentées par des objets de classe Throwable. La sous-classe Error est formée des exceptions qui ne sont pas considérées comme récupérables ; elles concernent les opérations de la machine virtuelle Java.

La sous-classe Exception est formée des exceptions considérées comme récupérables. Cependant, les objets de la sous-classe RuntimeException d'Exception ne sont pas obligatoirement récupérables : cette sous-classe comporte les exceptions AritmeticException, ArrayStoreException, NullPointerException, IndexOutOfBoundsException, etc. Les autres sous-classes d'Exception sont formées d'exceptions qui doivent être récupérées : par exemple, java.io.FileNotFoundException, java.net.UnknownHostException, ou InterruptedException Ces exceptions sont dites contrôlées parce que le compilateur vérifie comment elles sont traitées.

Nouvelles exceptions

Les exceptions définies par le programmeur sont des sous-classes d'Exception ; elles sont donc contrôlées.

Considérons l'implémentation d'une pile par un tableau. Les opérations d'empilage et de dépilage peuvent conduire à exécuter une écriture ou une lecture en dehors des bornes du tableau. Ces opérations ne seront pas réalisées et déclencheront une IndexOutOfBoundsException, qui n'est pas obligatoirement récupérable.

Cependant, une écriture en dehors du tableau, demandée par un empilage, n'est une erreur que dans la mesure où la taille du tableau, choisie a priori, n'est pas assez grande. Plutôt que de sortir brutalement du programme, on peut créer un nouveau tableau de taille double, copier le contenu du tableau précédent dans le nouveau, et continuer avec celui-ci ; cette exception est donc récupérable par la méthode d'empilage.

Par contre, la lecture en dehors du tableau (à l'indice -1), qui provoque la même exception, est due à une erreur de conception de l'algorithme utilisant la pile ; il ne revient donc pas aux opérations de la pile de récupérer cette exception ; par contre, la fonction qui demande un dépilage devrait, éventuellement, récupérer cette exception.

public class EmptyStackException extends Exception {
  
  EmptyStackException(Stackable s) {
    super("Empty stack");
  }
}

Traitement des exceptions

Une méthode dont le corps est susceptible de lever une exception doit :

  • soit intercepter l'exception et la traiter (par un try ... catch)
  • soit déclarer cette exception pour qu'elle soit propagée

La récupération minimale, qui gobe n'importe quelle exception, sans rien dire, est :

  try {
  ...
  }
  catch (Exception e) {}

Une version plus informative de cette récupération minimale consiste à imprimer la chaîne de caractères décrivant l'exception sur le flot de sortie en erreur standard :

  try {
    ...
  }
  catch (Exception e) {
    System.err.println(e);
  }

La déclaration d'une exception se fait dans l'en-tête de la méthode, à la fois dans sa déclaration, dans une interface :

  Object top() throws EmptyStackException;

et dans son implémentation :

  public Object top()  
    throws EmptyStackException {
    try {
      return content[height-1];
    } catch (ArrayIndexOutOfBoundsException e) {
      throw new EmptyStackException(this);
    }
  }


[Cours Java] [Notes 8]

Mise à jour :