Une des exigences de l'ingénierie du logiciel est la production de
composants assemblables et utilisables dans plusieurs
applications, comme dans toute autre activité industrielle. La mise en
uvre de cette exigence dans un langage de programmation est
permise, d'une part par la notion de module, d'autre part par des
règles d'écriture et d'usage de ces modules. Plusieurs langages,
notamment Modula 2 et Ada, ont été conçus autour de la notion de module.
L'idée est qu'un programme est formé à partir de plusieurs modules ; les
entités définies dans chaque module sont classées en deux catégories :
publiques ou privées. Les entités publiques d'un module
sont déclarées dans son interface et sont exportables vers
les autres modules, lesquels déclarent dans leurs interfaces les entités
qu'ils importent. Les modules exportant des entités offrent des
services dont sont clients les modules qui les importent.
En Java, les interfaces sont des types qui permettent de représenter l'interface d'un module. Les interfaces portent souvent des noms se terminant en able.
package java.lang; public interface Comparable { int compareTo(Object o); }
La documentation indique que la méthode compareTo, appliquée à
un objet , avec pour argument un objet
, compare les objets
et
et retourne un entier
, ou
ou un entier
, selon que
est plus petit que
, lui est égal ou lui est supérieur ; elle
déclenche l'exception
ClassCastException
si le type de
ne permet pas la comparaison avec
. La
documentation ne dit pas comment cette comparaison s'effectue, ce qui
n'aurait évidemment aucun sens pour des objets quelconques : une telle
méthode de comparaison n'est d'ailleurs définie que dans des classes qui
réalisent l'interface Comparable. On dit alors qu'une
telle classe est munie d'un ordre naturel. Ainsi, les classes
Character, Double, String, Integer
réalisent cette interface, mais la classe Object ne la réalise
pas.
Par exemple, la classe Integer pourrait réaliser l'interface Comparable grâce à la définition suivante de compareTo :
package java.lang; public class Integer { // ... public int compareTo(Object o) { int n = ((Integer)o).intValue(); int m = this.intValue(); return (m<n ? -1 : (m==n ? 0 : 1)); } }
Toute interface est un sous-type d'Object. Une classe qui réalise une interface est un sous-type de cette interface. On peut donc affecter à une variable de type l'interface une expression d'une classe la réalisant, 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'Integer5.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));
Ainsi, il arrive qu'une classe ne soit pas naturellement ordonnée, mais que diverses relations d'ordre puissent être utilisées. Dans ce cas, on recourt à l'interface Comparator :
package java.lang; public interface Comparator { public int compare(Object o1, Object o2); public boolean equals(Object obj); // définie dans Object }
On peut alors définir une autre fonction min qui, au lieu de supposer que ses deux arguments appartiennent à un type naturellement ordonné, accepte un troisième argument de type Comparator :
static Object min(Object x, Object y, Comparator c) { return c.compare(x,y) <=0 ? x : y; }
Par exemple, considérons la classe Personne, composée des champs nom et prénom de type String (on pourrait y ajouter la date de naissance, le domicile, etc.) :
class Personne { private String prénom, nom; String getNom() {return nom;} String getPrénom() {return prénom;} public Personne(String prénom, String nom) { if (prénom==null || nom==null) throw new NullPointerException(); this.prénom = prénom; this.nom = nom; } // ... }
Il est facile de lui donner un ordre naturel en lui ajoutant une méthode compareTo, par exemple pour l'ordre lexicographique sur les noms :
class Personne implements Comparable { // ... public int compareTo(Object o) { Personne n = (Personne)o; int compNom = nom.compareTo(n.nom); return compNom!=0 ? compNom : prénom.compareTo(n.prénom); } public boolean equals(Object o) { return o instanceof Personne && ((Personne)o).prénom.equals(prénom) && ((Personne)o).nom.equals(nom); } public int hashCode() { return 31*prénom.hashCode() + nom.hashCode(); } }
On notera que les méthodes equals et compareTo se comportent différemment si l'objet n'a pas le type requis, ici Personne : le test o instanceof Personne permet à equals(Object) de retourner false, tandis que compareTo(Object), ne procédant pas à ce test, peut déclencher l'exception ClassCastException due au transtypage (Personne)o.
D'autre part, toute classe qui redéfinit equals doit aussi redéfinir hashCode ; en effet, deux objets égaux par equals doivent avoir la même valeur de hachage par hashCode. Ces contraintes (sur equals, compareTo, hashCode, etc.) doivent être respectées, afin d'assurer à l'utilisateur que les méthodes qui les utilisent (par exemple, Collections.sort, etc.) font bien ce qu'elles sont censées faire.
Il arrive que des instances de Personne doivent être comparées selon d'autres relations d'ordre : parfois, selon le nom, parfois selon le prénom, etc. On est alors conduit à réaliser l'interface Comparator sera par exemple, pour comparer selon le prénom (et pour deux prénoms égaux, selon le nom) :
class PrénomComparator implements Comparator { public int compare(Object o1, Object o2) { Personne r1 = (Personne) o1; Personne r2 = (Personne) o2; int prénomComp = r1.getPrénom().compareTo(r2.getPrénom()); if (prénomComp != 0) return prénomComp; else return r1.getNom().compareTo(r2.getNom()); } }On peut alors créer un objet comparateur, et le passer en argument à certaines méthodes qui l'utilisent, par exemple :
class TestComparaison { public static void main(String[] args) { Comparator prénomComparator = new PrénomComparator(); Nom turing = new Personne("Alan", "Turing"), neumann = new Personne("John", "von Neumann"); Personne m = (Personne) min(turing, neumann, new Prénomcomparator()); } }
L'utilisation de l'interface Comparator serait lourde si l'on devait définir une classe de réalisation pour chaque méthode de comparaison ; ceci peut être évité, car Java permet d'instancier des classes anonymes :
class TestComparaison { // ... public static void main(String[] args) { Comparator prénomComparator = new Comparator() { public int compare(Object o1, Object o2) { Personne r1 = (Personne) o1; Personne r2 = (Personne) o2; int prénomComp = r1.getPrénom().compareTo(r2.getPrénom()); if (prénomComp != 0) return prénomComp; else return r1.getNom().compareTo(r2.getNom()); } }; // ... Personne m = (Personne) min(turing, neumann, prénomComparator); } }
L'expression new Comparator() { ... }, qui utilise le nom de l'interface, permet à la fois de créer une instance et de définir la méthode compare().