Une application Java consiste en un ensemble de fichiers de
classe, qui peuvent être d'origines diverses :
produits par le programmeur de l'application, membres d'une API standard
et documentée, ou obtenus par d'autres voies. Quand les programmes
sources sont disponibles, la construction de l'application se fait en
compilant l'ensemble des sources. Cette compilation doit respecter la
relation de dépendance entre unités
: on dit qu'une unité dépend d'une unité
si
utilise un type
défini par
. Dans ce cas, le compilateur, après avoir lu l'unité
, doit chercher des informations sur
qui se
trouvent dans le fichier de classe
.class. Si ce fichier de
classe n'existe pas, mais que l'unité
.java existe, le
compilateur doit procéder à sa compilation, pour produire
.class. Le compilateur est capable de suivre ces
relations de dépendance pour chercher les fichiers de classe nécessaires
et éventuellement pour les produire, si les unités de compilation
correspondantes sont disponibles. Dans le cas où plusieurs unités
forment un circuit pour la relation de dépendance (c'est-à-dire, sont
mutuellement dépendantes), ces unités, après avoir été lues
successivement, sont compilées ensemble.
Si l'une des unités de compilation est modifiée, il faut seulement
recompiler toutes les unités qui dépendent de celle-ci. Si le
compilateur dispose à la fois du fichier de classe .class et
de l'unité de compilation
.java dont il provient, il ne
recompile
.java que si cette unité est plus récente que
.class.
Cette recompilation n'est pas toujours possible quand le programmeur ne dispose pas de tous les programmes sources. C'est toujours le cas dès qu'il ou elle utilise une API, ou des fichiers de classe distribués sur l'Internet. L'auteur d'une API ne peut pas exiger que tous les utilisateurs d'une API recompilent leurs application à chaque fois qu'une nouvelle version de l'API est distribuée. En fait, le même problème se pose aussi quand des applications de très grande taille sont développées, car il n'est pas réaliste de recompiler une partie, voire l'ensemble de l'application dès qu'une modification mineure est faite à un fichier. Il faut donc se résoudre à travailler avec un ensemble de fichiers de classe qui n'est pas nécessairement cohérent. C'est pourquoi la spécification du langage Java décrit un certain nombre de modifications qui préservent la compatibilité binaire. Cette propriété garantit que la liaison du fichier de classe modifié se fera sans erreur si c'était le cas avant la modification. Elle ne garantit pas que le comportement ou le résultat de l'application seront identiques. Ces modifications, considérées comme sûres, sont les transformations suivantes :
L'idée générale de ces transformations est que l'on peut toujours modifier un type pour en faire un « sous-type » (en ajoutant des champs, par exemple), mais que si l'on en fait un « sur-type » (en supprimant des champs, par exemple), il faut alors mettre à jour les types pour lesquelles ces modifications sont visibles : ainsi, il faut mettre à jour tous les types du même paquet si on supprime un champ sans indication de visibilité, mais il n'y a pas de mise à jour nécessaire si on supprime un champ privé.