19. května 2010

Jak na porovnávání Comparable objektů či pomocí Comparatoru

Nemít přetěžování operátorů skutečně považuji za velký problém Javy. Proč? Např. proto, že porovnávání objektů pomocí instance třídy Comparator či porovnání objektů implementující rozhraní Comparable je boj, který pernamentně prohrávám. Mějme např. dva datumy d1 a d2. Pokud chci zjistit, zda platí d1 <= d2, pak mám následující možnosti:

  • !d1.after(d2)
  • d1.compareTo(d2) <= 0
Přiznám se, že z těchto dvou variant mi přijde čitelnější ta první (druhou mi mozek nějak nebere a vždy mě stojí hrozně přemýšlení co to znamená). Ovšem metody before a after má pouze třída java.util.Date nikoliv obecná třída implementující rozhraní Comparable. Pokud budeme objekty porovnávat pomocí instance Comparator pak musíme vystačit s metodou compare.

Z tohoto důvodu jsem si udělal jednoduchý enum:

public enum Relation {
   eq, ne, lt, le, gt, ge;

   public static <C extends Comparable<? super C>> boolean rel(C c1, Relation oper, C c2) {
      switch (oper) {
         case eq:
            return c1.compareTo(c2) == 0;
         case ne:
            return c1.compareTo(c2) != 0;
         case lt:
            return c1.compareTo(c2) < 0;
         case le:
            return c1.compareTo(c2) <= 0;
         case gt:
            return c1.compareTo(c2) > 0;
         case ge:
            return c1.compareTo(c2) >= 0;
      }
      throw new IllegalArgumentException("Unsupported operation " + oper);
   }

   public static <C> boolean rel(Comparator<C> comp, C c1, Relation oper, C c2) {
      switch (oper) {
         case eq:
            return comp.compare(c1, c2) == 0;
         case ne:
            return comp.compare(c1, c2) != 0;
         case lt:
            return comp.compare(c1, c2) < 0;
         case le:
            return comp.compare(c1, c2) <= 0;
         case gt:
            return comp.compare(c1, c2) > 0;
         case ge:
            return comp.compare(c1, c2) >= 0;
      }
      throw new IllegalArgumentException("Unsupported operation " + oper);
   }
}
Díky použití tohoto enumu mohu přepsat výše uvedený příklad porovnávání dvou datumů do podoby:
rel(d1, le, d2)
(pokud použijeme statické importy na metodu rel a instanci enumu le). Protože se mi tento zápis ještě stále nezdál dostatečně výmluvný, použil jsem další vylepšení (stačí jej přidat do výše uvedeného enumu):

   protected static final Map<String, Relation> S2R;

   static {
      Map<String, Relation> tmp = new TreeMap<String, Relation>();
      tmp.put("==", eq);
      tmp.put("!=", ne);
      tmp.put("<=", le);
      tmp.put("<", lt);
      tmp.put(">=", ge);
      tmp.put(">", gt);
      S2R = unmodifiableMap(tmp);
   }

   public static <C extends Comparable<? super C>> boolean rel(C c1, String oper, C c2) {
      return rel(c1, S2R.get(oper), c2);
   }

   public static <C> boolean rel(Comparator<C> comp, C c1, String oper, C c2) {
      return rel(comp, c1, S2R.get(oper), c2);
   }
Nyní dostaneme již poměrně elegantní zápis:
rel(d1, "<=", d2)
Je škoda, že Java nemá přetěžování operátorů, které nás vede k takovýmto vylepšením. Např. Groovy si umí poradit a operace porovnání umí zavolat nad objekty implementující Comparable. Scala je na tom podobně díky možnosti přetížení operátorů.