Une programmation modulaire peut être obtenue d'une part au moyen des règles d'accessibilité concernant les paquets, les classes et leurs membres, et d'autre part grâce au système de types.
Rappelons d'abord que les classes de Java ont trois rôles :
On peut regretter que Java confonde ces rôles en une seule construction syntaxique et laisse au programmeur le soin de les utiliser à bon escient. Il est possible, par un usage judicieux des classes abstraites (qui remplissent le premier, et éventuellement, le second de ces trois rôles), de programmer de façon assez modulaire. Aucune instance d'une classe abstraite ne peut être construite, mais on peut déclarer une variable ou des paramètres de méthode d'un tel type.
Java offre cependant à côté des classes une autre catégorie de types, les interfaces qui, ne remplissant que le premier de ces trois rôles, permet une meilleure modularité. Une interface est un type purement abstrait au sens où il ne définit aucune implémentation et ne comporte pas de constructeur : une interface déclare uniquement des méthodes publiques (comme dans les classes abstraites, le corps de la méthode est remplacé par un << ; >>). Les interfaces portent souvent des noms se terminant en able. Par exemple, l'interface Comparable du paquet java.lang déclare une méthode de comparaison des objets :
interface Comparable { int compareTo(Object o); }
La documentation indique que la méthode compareTo(), appliquée à un objet x, avec pour argument un objet y, compare les objets x et y et retourne un entier <0, ou 0 ou un entier >0, selon que xest plus petit que y, lui est égal ou lui est supérieur. La documentation ne dit pas comment cette comparaison s'effectue, ce qui n'aurait aucun sens pour des objets quelconques : une telle méthode de comparaison n'est d'ailleurs définie que pour certaines classes, dont on dit qu'elles implémentent l'interface Comparable. Par exemple, les classes Character, Double, String, Integer implémentent cette interface, mais la classe Object ne l'implémente pas.
Une classe implémente une interface si elle contient une implémentation publique pour chacune des méthodes de l'interface. Une classe dont la définition spécifie implements, suivi de noms d'interfaces, doit implémenter ces interfaces ; elle est alors considérée comme un sous-type de ces interfaces.
Toute interface est un sous-type d'Object. Une classe qui implémente une interface est un sous-type de cette interface. On peut donc affecter à une variable de type l'interface une expression d'une classe l'implémentant, par exemple :
Comparable c = new Integer(3);
On peut aussi passer à une méthode un argument de type Integer si le paramètre correspondant est déclaré de type Comparable. Par exemple, si nous voulons définir une fonction min() qui calcule le minimum de deux objets, nous devons supposer que son premier argument est comparable à son second argument : il suffit de déclarer le premier argument de type Comparable :
static Object min(Comparable x, Object y) { return x.compareTo(y) <=0 ? x : y; }
On pourra alors invoquer cette fonction, par exemple, sur des instances d'Integer3.1 :
Object m = min(new Integer(3), new Integer(2));
ou, si l'on veut obtenir un Integer, à l'aide d'un transtypage :
Integer m = (Integer) min(new Integer(3), new Integer(2));