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ů.

23 komentářů:

martiner řekl(a)...

Pěkný. Comparable taky nedávám ;)

A řekl(a)...

Ono je to trochu dvousečné.

Máš pravdu. Pro vlastní použití je možnost přetěžování operátorů úžasná, šetří čas a zjednodušuje zápis.

Ale přesto jsem strašně rád, že Java tuto možnost nemá. A u jiných jazyků jsem přetěžování operátorů lidem ve svém týmu důrazně nedoporučil.

Základní problém je ve spolupráci více lidí na jednom projektu. A v dlouhodobé údržbě jednoho projektu - a tím myslím skutečně dlouhodobé, máme tu kusy kódu z roku 1997, co se stále používají. Zdrojáky putují mezi členy týmu. Každý použije svou tvořivost... a když má každý k dispozici kreativní nástroje typu přetěžování operátorů, vede to k tomu, že při úpravě starých zdrojáků bezmála na každé řádce musí člověk koukat, co vlastně dané operace znamenají. Sčítá plus řetězce, nebo je přepisuje? Porovnává většítko podle abecedy nebo numericky? Tohle vše pak strašně zdražuje údržbu...

Čím je jazyk "ukecanější" a popisnější, tím méně je problémů. Z toho důvodu mi strašně pijou krev věci jako IDisposable a yield return v C#. Pokud není z kódu na první pohled zřejmé, co dělá, je to špatně.

maaartinus řekl(a)...

Ono to sice casto nehraje roli, ale tohle pouziti switche a Map muze zpomalit porovnavani celkem dost vyrazne. Zapis je to nicmene pekny.

Mozna trochu citelnejsi nez prvni verze se mi zda

public enum Relation {
EQ {
public > boolean rel(C c1, C c2) {
return c1.compareTo(c2) == 0;
}
},
...
public abstract > boolean rel(C c1, C c2);
}

s pouzitim

EQ.rel(d1, d2);

ale to pak radsi bez enumu jako staticka metoda volana jako

isLess("aaa", "abc")


Mozna bych namisto toho dekoroval Comparator metodama isLess, isLessOrEqual, atd., pricemz Comparable bych resil generovanym komparatorem. Volalo by se to pak

import static ExtendedComparator.*;
extend(myCmp).isLess(d1, d2);
extend().isEqual("abc", "cba");

ale taky to neni idealni:
- import static nesnasim, takze to bude taky dlouhy.
- ma to overhead vytvareni objektu ktery jde minimalizovat lokalni promennou, takze dalsim psanim.


Vyresil jsem si to tim, ze to neresim a naucil jsem se zit s c1.compareTo(c2) == 0;

Jira řekl(a)...

2 Maaartin> Na rychlost prdím a řeším ji až teprve v okamžiku, kdy je s ní problém, takže jsem ji zatím řešit nemusel. Co se týče metod isLess(d1, d2), tak ty my přijdou stejně špatně čitelné jako samotné metody compare a compareTo, proto jsem od nich utekl. Navíc jejich jméno je příšerně dlouhé a kód se pak zdlouhavě čte.

2 A> Přijde mi, že nemáš pravdu. To je to samé, jako bys truhláři zakázal používat šroubovák, protože se s ním občas zraní. A pak když by bylo zašroubovat šroub, tak by jej zatloukal kladivem. Pokud se někomu přetěžování operátorů nemá jej používat a nebo jiným způsobem zařídit, aby se používali jenom kde mají (např. interní směrnicí a nebo pomocí code review). Jak zabráníš programátorovi napsat metodu add, která je úplně stejně popisná jako operátor +?

maaartinus řekl(a)...

> Na rychlost prdím a řeším ji až teprve v okamžiku, kdy je s ní problém, takže jsem ji zatím řešit nemusel.

Souhlasim s tim, ze optimalizovat neco predem nema zadny smysl, tady ovsem delas neco, co se muze projevit na rychlosti spousty trid a pritom ti to profiler treba nemusi ukazat (bo se to zainlajnuje a profilovat neoptimalizovany kod nema smysl - tady se ovsem muzu plest, nicmene jsem mel takovy dojem z myho posledniho profajlovani).

> Jak zabráníš programátorovi napsat metodu add, která je úplně stejně popisná jako operátor +?

Nijak, ale jmen je spousta o operatoru jen par, takze to neudela. Proste zretezeni bych pojmenoval concat, scitani cehokoliv pak add. Pro veci co lze porovnavat vice zpusoby (treba zrovna stringy) muzes napsat Comparator a nenechat je implementovat Comparable, tuhle moznost s operatory nemas (potreboval bys ternarni <= nebo moznost definovat vlastni kreace).

Jira řekl(a)...

2 Maaartin> No ono je to složitější s tou optimalizací, ale ja se tím nehodlám trápit.

Ono je to o tom, že nic není všemocné a všespásné a jde jenom o to, nalézt nějakou rozumnou cestu. Co to je rozumná cesta se musí nějak definovat. o uhlídat kód, že jde rozumnou cestou je složité ať jazyk přetěžování operátorů podporuje a nebo ne. Takže pro mě je jednoznačné, že mi chybí a nemám problém si ohlídat, že se používají v okamžicích, kdy to kódu prospívá, podobně jako si hlídám, aby se metody správně a výstižně jmenovali.

A co se týče comparátorů a porovnávacích operátorů, to by v Javě samosebou nefungovalo, ale ve Scale se to nechá krásně pořešit pomocí implicit parametrů ...

Anonymní řekl(a)...

Nepřijde mi na compare/compareTo nic nečitelného, ale možná je to tím, že jsem bývalý céčkař. Spíš se mi v začátcích pletlo, která z metod compare a compareTo patří ke kterému interfacu, ale to se nechá naučit z faktu, že ...To v angličtině uvozuje jediný objekt. A pak taky mnemotechnická pomůcka: v názvu ComparaTOr už je skryto TO, tak už nemůže být podruhé v compareTO :-).

Anonymní řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
Anonymní řekl(a)...

No osobně nevidím nic nepřehledného na těchto rozhraních. Ale pokud to někomu nevyhovuje, nechť to klidně dělá podle toho výčtu. Takový helper je docela pěkný.

aubi řekl(a)...

Je to o základech. Porovnání a la compareTo se používá v C, protože na to existuje instrukce CMP.

Ta dělá to, že odečte druhý argument od prvního a nastaví flagy. Takže se ví, jestli je výsledek kladný, nula, nebo záporný.

Jednoduché, elegantní.

Přetěžování operátorů je peklo přesně z toho důvodu, který uvedl Anonymní. V podstatě všechno, co Java při přechodu od C++ opustila (např. preprocesor nebo přetěžování operátorů), už způsobilo v mém týmu dost velké problémy - lidová tvořivost působí v dlouhodobém supportu velké problémy.

雅婷 řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
水慧 řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
Anonymní řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
Anonymní řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
紫倫妍勳 řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
Anonymní řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
Anonymní řekl(a)...

Úplne súhlasím s A. Veci ako preťažovanie operátorov a typedef z C++ tak zneprehľadnia kód, že nikto nevie, kde je sever. Otázka vytvorenia metódy add() - nie je problém do javadoc napísať čo robí, prípadne ju refaktorovať. Skúste refaktorovať výskyt operátora "+".

Bedo.

Jira řekl(a)...

2 Bedo> Nerozumim, kde je problem s refactorovanim??? Refactorovat vyskyt metody add je stejny problem jako refactorovat vyskyt operatoru +!

Anonymní řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
saa řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
靜如 řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
初蓉初蓉 řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.
Anonymní řekl(a)...
Tento komentář byl odstraněn administrátorem blogu.