En ingénierie du logiciel, on cherche non seulement à produire des logiciels modulaires, mais aussi à réaliser leur production d'une façon modulaire. Typiquement, on ne doit pas être obligé de recompiler tout le programme quand une modification locale du source a été réalisée. La solution, qui existe depuis Fortran, est la compilation séparée.
Le Java Development Kit de Sun fait du fichier l'unité de compilation (on ne peut pas compiler une partie d'un fichier). Les définitions de types peuvent être réparties entre plusieurs fichiers, et chacun peut être compilé séparément. La spécification du langage n'impose pas comment les paquets et les unités de compilation sont stockés : 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 de classe dont le nom est suffixé en .class qui contient sa définition compilée. Par exemple, la classe projet.util.Liste a un fichier de classe Liste.class qui se trouve dans un sous-répertoire util d'un répertoire projet.
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 Linux :
linux% setenv CLASSPATH .:$(HOME)/java/classesRappelons que << . >> désigne le répertoire courant, vraisemblablement celui où le développement du programme s'effectue, et $(HOME) le répertoire personnel. Par exemple, le type projet.util.Liste sera alors recherché successivement dans
Pour faire exécuter une classe principale figurant dans un paquet, on doit utiliser le nom complet de la classe. Par exemple, pour charger dans la Machine Virtuelle Java la classe projet.Main, c'est-à-dire la classe Main du paquet projet, dont le fichier de classe est projet/Main.class, on exécute la commande suivante, sous Linux :
linux% java projet.Main
Une unité de compilation se compose de trois parties :
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 des paquets anonymes, commode pour développer de petites applications, 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 Linux, 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 (Integer, Exception, Cloneable, etc.) peuvent donc être désignés par leur nom simple dans tout programme.