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é A dépend d'une unité B si A utilise un type défini par B. Dans ce cas, le compilateur, après avoir lu l'unité A.java, doit chercher des informations sur B qui se trouvent dans le fichier de classe B.class. Si ce fichier de classe n'existe pas, mais que l'unité B.java existe, le compilateur doit procéder à sa compilation, pour produire B.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 C.class et de l'unité de compilation C.java dont il provient, il ne recompile C.java que si cette unité est plus récente que C.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é.