czwartek, 8 listopada 2012

Java: parsowanie enuma ze Stringa

Od jakiegoś czasu na blogu nie było posta z poradą programistyczną, co strasznie i okropnie kłóci się z jego misją. Jako że to nie jest telewizja publiczna, to nie będę nikogo przekonywał, że było inaczej. Po prostu przeglądanie tapet z samochodami i słodkimi kotami jest przyjemniejsze i łatwiejsze niż kodowanie. Wróciłem do pisania "projektu mojego życia" i znowu natrafiłem na problem, który już kiedyś rozwiązywałem. Pozwolę sobie podywagować nieco nad rozwiązaniami problemu parsowania enumów przez nazwę w postaci tekstu.

Zasadniczo, to Sun (bo to jeszcze był Sun, a nie Oracle) w Javie 5 (1.5 tak na prawdę) dostarczył nam uniwersalne narzędzie do parsowania enumeratów po nazwie. Poniżej mały przykład kodu:

JakisEnum eJakis;
String strName;
try
{
  eJakis = Enum.valueOf(JakisEnum.class, strName);
}
catch (Exception ex) { }

Niestety osobiście mam z tym kodem drobny problem. Po pierwsze jest to 'masa' linii kodu. W zasadzie, to z 5 linii kodu interesuje mnie tylko jedna. Druga sprawa to pojawiający się tu wyjątek. Jak widać zupełnie mnie on nie interesuje. Do tego stopnia, że nie interesuje mnie jego rzeczywista klasa. Po prostu ignoruję go i przechodzę nad nim do porządku dziennego. Jakiś czas temu ktoś mnie nauczył, że rzucanie wyjątków jest dość kosztowne. Nie dotyczyło to tylko wywoływania wyjątków w systemie przez brak sprawdzeń, ale też o rzucanie własnych wyjątków. Sprawa dotyczy głównie call stacka, ale jest jeszcze parę innych powodów. Tak więc na StackOverflaw znalazłem inne rozwiązanie, które jest już nieco lepsze, bo nie powoduje rzucenia wyjątku. Poniżej kod zamieszczony przez jednego z użytkowników. Istotna jest deklaracja enuma.

public enum Blah {
  A("text1"),
  B("text2"),
  C("text3"),
  D("text4");

  private String text;

  Blah(String text) {
    this.text = text;
  }

  public String getText() {
    return this.text;
  }

  public static Blah fromString(String text) {
    if (text != null) {
      for (Blah b : Blah.values()) {
        if (text.equalsIgnoreCase(b.text)) {
          return b;
        }
      }
    }
    return null;
  }
}

Po części jest to coś czego potrzebowałem. Nie pojawia się żaden wyjątek, jest to zgodne ze wzorcem fabryki i możemy łatwo w jednej linii sprawdzić, czy nie uzyskaliśmy wartości null. Jeśli wartość null jest dla nas w porządku, to możemy nawet ją tutaj zostawić. Powyższa konstrukcja ma jednak pewne wady. Po pierwsze wymaga zadeklarowania nieco bardziej złożonego enuma. Zawsze możemy w projekcie zadeklarować jednego takiego, a potem z niego dziedziczyć. Drugim problemem jest różnica pomiędzy nazwą wartości, a tekstem do identyfikacji. Oczywiście możemy skorzystać z nazwy wartości dla pola text, jednak oznacza to powtarzanie tego samego 2 razy i łatwiej tylko o błąd. To mi się już tak bardzo nie podoba. Ostatni problem to liniowa złożoność. W zasadzie, to nawet wyższa, ponieważ wchodzi w grę porównywanie tekstów. Mając mały enum i nie parsując masy wartości w krótkim czasie, możemy sobie na coś takiego pozwolić. Skoro jednak chcę zrezygnować z wyjątków na rzecz wydajności, to nie chcę jej tracić na rzecz porównań i pętli.

StackOverflow dał mi jednak nieco do myślenia. Jedna z rzeczy, które w języku Java kocham, to fakt że jej kod jest otwarty. Poświęciłem chwilę i postanowiłem sprawdzić jak wygląda metoda valueOf.

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                              String name) {
   T result = enumType.enumConstantDirectory().get(name);
   if (result != null)
       return result;
   if (name == null)
       throw new NullPointerException("Name is null");
   throw new IllegalArgumentException(
       "No enum constant " + enumType.getCanonicalName() + "." + name);
}

Oczywiście nie trudno zauważyć, co można stąd wykorzystać. Wystarczy zwrócić uwagę na metodę enumConstantDirectory zwracającą Mapę, z której możemy pobrać element dla nazwy. Niestety ta metoda jest prywatna pakietowa, a my nie mamy do dyspozycji cudów takich jak rozszerzenia klas w C#. Na szczęście istnieje dostępna metoda publiczna w celu pobrania wszystkich wartości danego enuma. Możemy w ten sposób sami stworzyć taką mapę i na niej wyszukać nazwę. Będzie to zdecydowanie mniej kosztowne jak w metodzie drugiej. Jedyne czego wtedy trzeba się wystrzegać to wyjątków przy operacjach na mapie.

@SuppressWarnings("rawtypes")
public static <T extends Enum<T>> T fromString(Class<T> type, String name)
{
    if(name == null)
        return null;
  
    T[] universe = type.getEnumConstants();
    Map m = new HashMap();
    for(T constant : universe)
        m.put(((Enum)constant).name(), constant);
  
    return m.get(name);
}

Ja powyższą metodę zadeklarowałem w klasie EnumExt ExtEnum w moim pakiecie Utils. Jej użycie jest dokładnie takie samo jak w przypadku Enum.valueOf, ale mam pewność, że nie rzuci ona żadnym wyjątkiem. Zadeklarowałem metodę pomocniczą pozwalającą na przekazanie domyślnej wartości. W efekcie zamiast null mogę dostać na przykład wartość JakisEnum.unknown, jeśli taka jest mi wygodniejsza od null.

Tym razem tekst nie miał na celu wyłącznie podawania gotowych rozwiązań. Pragnę wskazać, jak można poszukiwać rozwiązań dla problemów jeśli wybraliśmy język Java. Warto zawsze poszukać jak coś zostało wcześniej zrobione.

Czytaj też:

Brak komentarzy:

Prześlij komentarz