Le problème est le suivant : on veut comparer deux instances de la classe T (pour un tri par exemple) en comparant un de leurs attributs de classe E. On va écrire un comparateur générique pour mutualiser un maximum de chose. Évidemment, nos "vrais" comparateur en hériteront.

Notre comparateur générique implémente logiquement l'interface Comparator<T>. Pour cela, il implémente la méthode compare qui compare deux objets de classe T. L'idée générique est de comparer un attribut particulier (le même sur les deux objets) de classe E en utilisant un comparateur de E particulier. Deux problèmes se posent alors à nous.

Comment récupérer l'attribut sur lequel porte la comparaison ? La méthode qui récupère l'attribut qui nous intéresse est forcément spécifique à la classe T et à l'attribut. Pour se mettre d'accord sur la méthode qui renvoi l'attribut, le mieux serait d'imposer le nom de la méthode en héritant d'une interface. Cette méthode serait implémentée dans les "vrais" comparateurs. Là, comme le constructeur générique implémente déjà certaine méthode, il me semble que le mieux est de faire une méthode abstraite valueOf (ce qui doit être sensiblement la même chose qu'une interface...).

/**
 * @param obj
 * @return comparative value of the object
 */
abstract E valueOf(final T obj);
D'où sort ce comparateur de E ? L'idéal aurait certainement été de pouvoir passer la classe de ce comparateur en tant que paramètre de notre comparateur générique. Malheureusement, je n'ai pas réussi un tel exploit. Les voix des générics java sont impénétrables ;-). La solution que je propose, c'est simplement de passer l'instance du comparateur de E au constructeur de notre comparateur générique.
/** * Intermediate comparator.
 */
private Comparator<E> comparator;

/**
 * Constructor.
 * @param c intermediate comparator
 */
public AbstractComparator(final Comparator<E> c) {
   comparator = c;
}

On a maintenant tout ce qu'il nous faut pour implémenter la méthode compare de l'interface Comparator<T>.

public int compare(final T o1, final T o2) {
   return comparator.compare(valueOf(o1), valueOf(o2));
}

Bien, ça semble déjà pas mal. Il manque tout de même un petit truc. Ce genre de comparateur, ça ne change pas beaucoup au cours de l'exécution. Ce serait dommage de créer une multitude d'instances d'un même comparateur. Bref, on va faire l'effort de mettre en place un beau design pattern Singleton ! :D

Pour cela, on commence par passer le constructeur en protected. Ensuite, les comparateurs implémenteront une méthode de classe (static) getInstance. Cette méthode crée l'instance du comparateur si elle n'existe pas encore et la retourne. Rien que du classique... Trop classique même ! On ne va pas s'embêter à récrire tout ça à chaque fois ? Allez hop ! On va mutualiser le code dans le comparateur générique.

L'idée est la suivante. Dans mon "vrai" comparateur, la méthode getInstance va appeler la méthode getInstance du comparateur générique dont il hérite avec en paramètre, le nom de sa classe (histoire que le comparateur générique s'y retrouve ;-). N'oubliez pas que du coup, le comparateur générique stocke toutes les instances de tous les comparateurs qui en dérivent !! Et pour les stocker, on va utiliser une simple map avec comme clé, la class des comparateurs. Cette map est forcément un attribut de classe.

private static Map<Class, Comparator> comparators;

Ensuite, la méthode getInstance, méthode de classe également, prend une classe en paramètre et renvoi le comparateur correspondant. Cette méthode de classe est donc forcément générique, et son paramètre U implémente l'interface Comparator. Comme c'est la classe (et non une instance) qui est passée en argument, on va utiliser la méthode newInstance pour construire l'instance. En cas de problème, cette méthode lance des exceptions de type InstantiationException ou IllegalAccessException.

protected static <U extends Comparator> U getInstance(final Class<U> c)    throws InstantiationException, IllegalAccessException {
    if (comparators == null) {
        comparators = new HashMap<Class, Comparator>();
    }
    if (!comparators.containsKey(c)) {
        comparators.put(c, c.newInstance());
    }
    return (U) comparators.get(c);
}

Voilà notre super comparateur générique !

public abstract class AbstractComparator<T, E> implements Comparator<T> {    /**
     * comparators.
     */
    @SuppressWarnings("unchecked")
    private static Map<Class, Comparator> comparators;
    
    /**
     * Intermediate comparator.
     */
    private Comparator<E> comparator;
    
    /**
     * Bean constructor.
     * @param c comparator
     */
    protected AbstractComparator(final Comparator<E> c) {
        comparator = c;
    }
    
    /**
     * @param <U> generic parameter of the class
     * @param c class of the researched comparator
     * @return the researched comparator
     * @throws InstantiationException
     * @throws IllegalAccessException
     */
    @SuppressWarnings("unchecked")
    protected static <U extends Comparator> U getInstance(final Class<U> c)
        throws InstantiationException, IllegalAccessException {
        if (comparators == null) {
            comparators = new HashMap<Class, Comparator>();
        }
        if (!comparators.containsKey(c)) {
            comparators.put(c, c.newInstance());
        }
        return (U) comparators.get(c);
    }

    /**
     * @param o1
     * @param o2
     * @return comparison between a1 and o2
     * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
     */
    public int compare(final T o1, final T o2) {
        return comparator.compare(valueOf(o1), valueOf(o2));
    }
    
    /**
     * @param obj
     * @return comparative value of the object
     */
    abstract E valueOf(final T obj);

}

Passons à l'implémentation d'un comparateur ABComparator héritant de ce comparateur générique. Je dispose des ingrédients suivants. Une classe A possède un attribut value de type B sur lequel repose la comparaison. Elle possède également une méthode getValue qui permet de récupérer cet attribut. Enfin, je dispose d'un comparateur de B appelé BComparator.

public class ABComparator extends AbstractComparator<A, B> {    
    /**
     * Constructor.
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    protected ABComparator() throws InstantiationException, IllegalAccessException {
        super(getInstance(BComparator.class));
    }

    /**
     * @see org.esupportail.ca.domain.beans.comparators.AbstractComparator#valueOf(java.lang.Object)
     */
    @Override
    public B valueOf(final A a) {
        if (a == null) {
            return null;
        }
        return a.getValue();
    }

    /**
     * @return the ABComparator instance
     * @throws InstantiationException
     * @throws IllegalAccessException
     */
    public static ABComparator getInstance() throws InstantiationException, IllegalAccessException {
        return getInstance(ABComparator.class);
    }
}

Simple et rapide à écrire ! Maintenant, les comparateurs, ça dépote ! :D Enfin presque... Encore faut-il avoir le comparateur de B sous la main. Par exemple, si B est String ou un type de base, il faut écrire les comparateurs correspondants...

Le pire, c'est que la plupart de ces types de base implémente l'interface Comparable. C'est-à-dire qu'il possède une méthode compareTo pour comparer une instance à une autre passée en argument. Du coup, on se retrouve à récrire pour toutes ces classes la même méthode compare qui teste si la première instance est nulle, et si elle ne l'est pas, elle retourne le résultat de la méthode compareTo appliquée à cette instance. Bref, c'est laborieux. Simplifions nous la vie avec les génériques !! :D

On va donc écrire un comparateur générique pour les classes implémentant l'interface Comparable.

public class ComparableComparator<T extends Comparable<T>> implements Comparator<T> {    /**
     * Constructor.
     */
    public ComparableComparator() {
        super();
    }

    /**
     * @param o1 first object to compare
     * @param o2 second object to compare
     * @return comparison result
     * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
     */
    public int compare(final T o1, final T o2) {
        if (o1 == null) {
            if (o2 == null) {
                return 0;
            }
            return -1;
        }
        return o1.compareTo(o2);
    }
}

Parfait, maintenant je peux initialiser par exemple mon comparateur de String : new ComparableComparator<String>(). Super, mais il se repose le problème de l'unicité de l'instance pour ces comparateurs. "Singleton, singleton, singleton !" hurlent mes compagnons ! Sous la pression populaire, je m'exécute ! :D

Vous avez compris le coup de la map pour stocker les instances. Le problème avec les génériques en Java, c'est qu'on ne peut pas instancier une classe générique comme tout à l'heure avec le newInstance (ou alors je ne sais pas faire ;-)). Par exemple, je ne peux écrire ComparableComparator<String>.newInstance. De même, ComparableComparator<String>.class n'est pas correct et je ne peux pas le passer en argument d'une méthode. Du coup, la seule solution que j'ai trouvé pour le moment, c'est d'instancier les comparateurs un par un à la main, dans une zone static du ComparableComparator... Comme je disais, je ne suis pas un grand spécialiste de ce langage mais il me semble que ça fait parti des limitations un peu embêtantes des génériques en Java... Dommage...

/** * comparators.
 */
@SuppressWarnings("unchecked")
private static Map<Class, ComparableComparator> comparators = new HashMap<Class, ComparableComparator>();
static {
    comparators.put(Date.class, new ComparableComparator<Date>());
    comparators.put(String.class, new ComparableComparator<String>());
}

Ensuite, la méthode getInstance prend la classe "Comparable" en argument. Si le comparateur correspondant n'est pas dans la map, une InstantiationException est lancée.

public static <E extends Comparable<E>> ComparableComparator getInstance(final Class<E> c)    throws InstantiationException {
    if (!comparators.containsKey(c)) {
        throw new InstantiationException(
            "No ComparableComparator could be initialized for the class " + c);
    }
    return comparators.get(c);
}

Voilà encore un joli exemple de l'utilité et des limites des génériques en Java ! :D

/** * Generic comparator for class which implements the Comparable interface.
 * @param <T> the generic comparable class
 */
public class ComparableComparator<T extends Comparable<T>> implements Comparator<T> {
    
    /**
     * comparators.
     */
    @SuppressWarnings("unchecked")
    private static Map<Class, ComparableComparator> comparators = new HashMap<Class, ComparableComparator>();
    static {
        comparators.put(Date.class, new ComparableComparator<Date>());
        comparators.put(String.class, new ComparableComparator<String>());
    }

    /**
     * Constructor.
     */
    private ComparableComparator() {
        super();
    }
    
    /**
     * @param <E>
     * @param c
     * @return the corresponding ComparableComparator
     * @throws InstantiationException
     */
    @SuppressWarnings("unchecked")
    public static <E extends Comparable<E>> ComparableComparator getInstance(final Class<E> c)
        throws InstantiationException {
        if (!comparators.containsKey(c)) {
            throw new InstantiationException(
                "No ComparableComparator could be initialized for the class " + c);
        }
        return comparators.get(c);
    }

    /**
     * @param o1 first object to compare
     * @param o2 second object to compare
     * @return comparison result
     * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
     */
    public int compare(final T o1, final T o2) {
        if (o1 == null) {
            if (o2 == null) {
                return 0;
            }
            return -1;
        }
        return o1.compareTo(o2);
    }

}