3. Archives, chemin de recherche, classpath

3.1. Archives

La machine Java sait aller chercher des fichiers classe dans une structure de répertoires, que cette structure soit rangée dans un système de fichiers sur un disque dur ou dans un fichier archive . Le format le plus courant de tels fichiers est le format ZIP, et Java a son propre format, JAR, qui signifie Java Archive. Ces deux formats sont ouverts par les logiciels de gestion d’archives courants, tels que WinZip ou WinRar. Placer des dizaines de fichiers dans un seul, tout en conservant la bonne structure hiérarchique des répertoires, simplifie beaucoup les choses, notamment lors de la distribution d’une application. Notons que cette distribution peut se faire de façon classique, sous forme de diffusion de fichier, mais aussi de façon automatique au travers de l'Internet. Distribuer un code peut signifier autoriser son téléchargement au travers du net, en automatique. Ce qui commande le téléchargement peut très bien être un utilisateur, mais aussi une application distribuée. Enfin, cela permet de compresser les fichiers, et d’économiser de la place.

3.2. Variable CLASSPATH

La dernière question qui reste en suspend pour la machine java est : où se trouve la structure hiérarchique des fichiers classe ? Quel est son point de montage dans le système de fichiers ? Où se trouvent les fichiers archive ? La réponse est double. Tout d’abord, un certain nombres de fichiers archive sont connus par défaut, et se trouvent dans les répertoires d'installation de toute machine Java. C'est notamment le cas du fichier rt.jar ( rt signifie runtime), qui contient la librairie standard. Ensuite, la machine Java sait lire une variable d'environnement standard, appelée CLASSPATH, qui, comme son nom le suggère, contient un ensemble d’endroits où se trouvent des classes Java. Ces endroits peuvent être des répertoires, dans ce cas la machine Java va scanner cette arborescence à la recherche de fichiers .class, ou des fichiers portant l’extension .zip ou .jar. Enfin, il est aussi possible de lancer la machine Java avec l’option –classpath pour lui indiquer d’autres endroits où aller chercher les classes.

3.3. Notion de classloader

Chaque classe est chargée dans la machine Java par un objet particulier, instance de la classe ClassLoader. On parle alors de classloader d'une classe. La classe Class, qui modélise les classes en Java, possède une méthode qui permet d'accèder au classloader qui l'a chargée. Il est donc toujours possible, lorsque l'on possède un objet, de savoir quel classloader a chargé la classe de cet objet, ou encore dans quel classloader il vit. Les classloaders sont répartis dans une hiérarchie, au sommet de laquelle se trouve le bootstrap classloader . Ce classloader est le premier à être créé par la JVM. C'est ce classloader qui charge les classes de la hiérarchie java.*. Un classloader est capable de créer autant de sous-classloader qu'il veut, qui vont à leur tour charger leurs propres classes, et pouvoir exécuter leur propre code. La règle est que lorsqu'un classloader a besoin d'une classe qu'il ne possède pas, il la demande à son classloader père, qui peut répéter le processus. Donc, deux classloader créés par un même père ne se connaissent pas, et ne peuvent pas échanger leurs classes. Il est donc possible d'avoir deux classes portant le même nom complet, chargées dans deux classloader différents, sans pour autant générer de conflit. En particulier, ces deux classes peuvent être différentes, cas que l'on rencontre souvent lorsque l'on utilise une même librairie open source dans des versions différentes. Dans le fonctionnement standard de la JVM J2SE (par opposition à JEE), le bootstrap classloader crée un fils, l'extension classloader, qui prend en charge les classes se trouvant dans le répertoire lib/ext de l'installation standard du JRE. Enfin, l'extension classloader crée le classloader qui nous intéresse : le system-classpath classloader. C'est lui qui charge les JAR et les classes spécifiés par la variable CLASSPATH de notre application. Les classloaders fonctionnent sur deux principes en Java :
  • le principe de visibilité : un classloader enfant peut voir les classes chargées par son père. L'inverse n'est pas vrai.
  • le principe d'unicité : lorsqu'un père charge une classe, un fils ne la charge pas, il la demande au père.
Cette notion de classloader est au centre de la sécurité du code Java. Elle permet à la JVM de gérer de nombreuses applications différentes, sans que des conflits puissent éclater entre applications.

3.4. Bilan sur les classes chargées

Les classes chargées par la JVM sont donc :
  • celles qui se trouvent dans les répertoires de la distribution du JDK : rt.jar et i18n.jar par le bootstrap classloader, le contenu de lib/ext par l'extension classloader ;
  • les classes et JAR spécifiées par la variable d'environnement CLASSPATH, par la propriété système java.class.path et par l'option de lancement de la JVM -cp ou -classpath ;
  • de plus, si l'un des JAR chargés possède un fichier MANIFEST.MF dans son répertoire META-INF, et que ce fichier définit un attribut Class-Path, alors les JAR précisés par cet attribut sont aussi chargés.

3.5. Visibilité

Les règles de visibilité des classes dépendent également de leur rangement dans les paquets. Revoyons les règles que nous avons déjà citées. Les membres de classes peuvent être :
  • public : toutes les autres classes y ont accès, quel que soit le paquet dans lequel elles se trouvent ;
  • protected : toutes les classes de ce paquet y ont accès, ainsi que les classes des autres paquets qui héritent de cette classe ;
  • aucune directive : ce membre est accessible du paquet, et d’aucun autre endroit ;
  • private : membre accessible uniquement de la classe même. Aucune autre classe ne peut voir ce membre.
Comme on peut le voir, la seule différence porte sur la directive protected, qui est un peu moins contraignante. On notera que la directive protected a un sens différent du C++, puisqu’elle étend l’accès à ce membre à toutes les classes qui se trouvent dans le même paquet. Enfin, si une classe n’est rangée dans aucun paquet (pas de déclaration package en première ligne, ce cas ne devrait jamais arriver !), elle est compilée dans un paquet anonyme, qui est le paquet par défaut. Les règles de visibilité s’appliquent alors, entre cette classe et les autres classes de ce paquet anonyme, et avec les autres classes rangées dans des paquets normaux. Il est fortement déconseillé de ne pas ranger ses classes dans un paquet.

3.6. Conseils d'écriture, bibliothèque standard

Il est très important de bien choisir sa structure de paquets lorsque l’on se lance dans le développement d’un projet important. Le nombre de classes et donc de fichiers augmente très rapidement, et plus ce nombre augmente, plus la structuration de l’ensemble doit être rigoureuse. On peut examiner sur quelques paquets principaux la façon dont la bibliothèque standard a été structurée, et s’en inspirer :
  • java.lang : classes relatives au langage, on y trouve entre autres String, Integer, Float etc…
  • java.util : classes utilitaires, telles que Array, Collection et ses dérivées, Date, Calendar ;
  • java.sql : classes du JDBC pour l’accès aux bases de données ;
  • java.text : classes utilitaires pour formater les chaînes de caractères et les textes ;
  • java.io : classes utilisées pour les entrées / sorties ;
  • java.awt : Abstract Windows Toolkit , ce sont les classes de couche basse pour la gestion des interfaces graphiques ;
  • java.applet : classes propres aux applets Java ;
  • java.net : classes permettant l’accès aux ressources réseaux.
Dans tous les cas, les arborescences java.* et javax.* sont de bonnes sources d’inspiration pour structurer ses classes.
Java langage & API
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
Introduction : un peu d'histoire
1. Java : genèse d'un nouveau langage
Programmer en Java
1. Un premier exemple
1.1. Aperçu général, cycle de vie
1.2. Un premier programme
1.3. Programmes, applets, servlets, etc...
2. Une première classe
2.1. Écriture d'une classe
2.2. Instanciation de cette classe
3. Types de base, classes et objets
3.1. Types de base
3.2. Classes et objets
Classes Object et String
1. Introduction
2. La classe Object
2.1. La méthode toString()
2.2. La méthode clone()
2.3. La méthode equals()
2.4. La méthode hashCode()
2.5. La méthode finalize()
2.6. La méthode getClass()
3. La classe String
3.1. Introduction
3.2. Construction d'un objet de type String
3.3. Concaténation, StringBuffer et StringBuilder
3.4. Concaténations de chaînes de caractères depuis Java 8
3.5. Concaténations de chaînes de caractères depuis Java 11
3.6. Extraction d'une sous-chaîne de caractères
3.7. Comparaison de deux chaînes de caractères
3.8. Méthodes de comparaisons lexicographiques
3.9. Méthode de recherche de caractères
3.10. Méthode de modification de chaîne
3.11. Méthode de duplication
3.12. Support de l'unicode, internationalisation
Structure d'une classe
1. Introduction
2. Classes
2.1. Classes publiques
2.2. Classes internes
2.3. Classe membre
2.4. Classes locales
2.5. Classes anonymes
2.6. Le mot-clé this
3. Éléments statiques
3.1. Champ statique
3.2. Cas des constantes
3.3. Bloc statique
3.4. Classe membre statique
4. Membres d'une classe, visibilité
4.1. Bloc non statique
4.2. Accès à un membre, visibilité
4.3. Les champs
4.4. Signature d'une méthode
4.5. Les méthodes
4.6. Getters et Setters
5. Constructeur, instanciation
5.1. Chargement d'une classe
5.2. Constructeurs d'une classe
5.3. Instanciation d'un objet
5.4. Destruction d'objets
5.5. Le mot-clé final
6. Énumérations
6.1. Déclaration d'une énumération
6.2. Classe énumération
6.3. Méthode toString()
6.4. Méthode valueOf()
6.5. Méthode values()
6.6. Méthode ordinal()
6.7. Méthode compareTo()
6.8. Constructeurs privés
6.9. Classe utilitaire : EnumSet
Noms, opérateurs, tableaux
1. Introduction
2. Identificateurs, noms et expressions
2.1. Identificateurs et noms
2.2. Expressions
3. Opérateurs, ordre d'exécution
3.1. Ordre d'exécution
3.2. Les opérateurs
3.3. Les opérateurs ++ et --
3.4. Les opérateurs % et /
3.5. Les opérateurs <<, >> et >>>
3.6. L'opérateur instanceof
3.7. Les opérateurs &, | et ^
3.8. Les opérateurs && et ||
3.9. L'opérateur ? ... :
3.10. Les opérateurs d'affectation
4. Tableaux
4.1. Création d'un tableau
4.2. Initialisation d'un tableau
4.3. Utilisation d'un tableau comme un Object
4.4. Tableaux de tableaux
4.5. Copie de tableaux
5. Blocs, boucles et contrôles
5.1. Blocs
5.2. Mots-clés réservés
5.3. Tests : if et switch
5.4. Boucles : for, while, do ... while
5.5. Commandes continue et break
5.6. Commandes return et goto
Nombres, précision, calculs
1. Introduction
2. Calculs
2.1. Précision
2.2. Codage des nombres flottants
2.3. Le mot-clé strictfp
2.4. Conversion de types
3. Dépassements de capacité
3.1. Cas des entiers
3.2. Cas des flottants
3.3. Bibliothèques BigInteger et BigDecimal
4. Fonctions mathématiques
4.1. Fonctions usuelles
4.2. Générateurs aléatoires
5. Classes enveloppe
5.1. Associer les types de base à des objets
5.2. Auto-boxing
Héritage, abstraction, interfaces
1. Introduction
2. Abstraction et encapsulation
2.1. Abstraction
2.2. Encapsulation
3. Héritage
3.1. Définition de l'héritage
3.2. Conséquences pour les membres
3.3. Polymorphisme
3.4. Empêcher l'héritage
4. Classes abstraites
5. Interfaces
5.1. Introduction
5.2. Définition
5.3. Java 8 et les interfaces
5.4. Utilisation des interfaces
5.5. Définition de constantes dans les interfaces
5.6. Utilité des interfaces
Packages
1. Introduction
2. Notion de paquet
2.1. Déclaration d’appartenance à un paquet
2.2. Chargement d’une classe
2.3. Choix de nom
3. Archives, chemin de recherche, classpath
3.1. Archives
3.2. Variable CLASSPATH
3.3. Notion de classloader
3.4. Bilan sur les classes chargées
3.5. Visibilité
3.6. Conseils d'écriture, bibliothèque standard
Exceptions
1. Introduction
2. Erreurs et Exceptions
2.1. Classe Throwable, notion de stack trace
2.2. Classe Error
2.3. Classe RuntimException
2.4. Classe Exception
3. Déclenchement d'une exception
3.1. Exceptions déclenchées par la JVM
3.2. Exceptions déclenchées par l'application
4. Capter une exception
4.1. Traiter une exception localement
4.2. Code de captage
5. Créer ses propres exceptions
Entrées / sorties
1. Introduction
2. Notion de fichier
2.1. Introduction
2.2. La classe File
2.3. Construction d'une instance de File
2.4. Méthodes exposées
3. Flux de sortie
3.1. Introduction, notion de flux
3.2. Écriture de caractères, classe Writer
3.3. Bufferisation, construction d'un flux sur un autre
3.4. Utilisation de PrintWriter
3.5. Écriture d'octets, OuputStream
3.6. Écriture de types primitifs : DataOutputStream
3.7. Écriture d'objets : ObjectOutputStream
4. Flux d'entrée
4.1. Introduction
4.2. Lecture de caractères, classe Reader
4.3. Bufferisation, lecture ligne par ligne
4.4. Lecture d'octets : InputStream
4.5. Lecture de types primitifs : DataInputStream
4.6. Lecture d'objets : ObjectInputStream
5. Lecture et écriture de flux croisés
5.1. Introduction
5.2. Lire des caractères sur un flux binaire : classe InputStreamReader
5.3. Écrire des caractères sur un flux binaire : classe OutputStreamWriter
5.4. Remarque sur les jeux de caractères
6. Serialization d'objets
6.1. Enjeu de la sérialization d'objets
6.2. Serialization d'un objet
6.3. Sérialization d'une grappe d'objets
6.4. Première surcharge : méthode writeObject() readObject()
6.5. Deuxième surcharge : utilisation d'un externalizer
6.6. Troisième surcharge : utilisation d'un objet proxy
7. Flux compressés
7.1. Introduction
7.2. Flux de type gzip
7.3. Flux de type zip