JAVA exPress logo Published on JAVA exPress (http://www.javaexpress.pl)

Express killers, cz. II

Damian Szczepanik

Issue 3 (2009-03-08)

W drugim odcinku przyjrzyjmy się dwóm przykładom, które kryją pułapki związane z upraszczaniem kodu, skracaniem zapisu oraz chęcią pochopnego przyspieszenia procesu implementacji (i późniejszemu nadrabianiu podczas testowania).

Upraszczanie kodu niesie zwykle jeden cel: jest krótszy w zapisie. Proces ten ma jednak tę wadę, że kod wynikowy jest trudniejszy w analizie – szczególnie przez osoby trzecie. Nasuwa się zatem pytanie, jaki jest cel tak zwanej "optymalizacji" skoro w obu przypadkach bytecode jest identyczny?

Przyjrzyjmy się następującemu przykładowi, w którym programista wykorzystał funkcję log(), aby nie pisać tego samego kodu dwa razy. Sam pomysł godny polecenia, ale implementacja ma jedno ale:

        public class Instance
        {
          private Instance[] inst = { new Instance() {},  new Instance() {} };

          public static void main(String[] args)
          {
            System.out.println(new Instance());
          }

          private String log()
          {
            StringBuffer sb = new StringBuffer();
            for (Instance i : inst)
              sb.append(i);
            return (sb.toString());
          }

          public String toString()
          {
            return "["+log()+"]";
          }
        }
        

Jakie? Co zostanie wydrukowane na wyjście, biorąc pod uwagę fakt, że tablica inst zawiera dwa elementy?

Przyjrzyjmy się kolejnemu kawałkowi kodu. Tym razem programista zastosował operator warunkowy, by wartość jego mogła być obliczona na etapie kompilacji. Czy poniższa klasa się skompiluje, czy tylko skomplikuje? Spójrzmy:

        public class Echo
        {
          static class MyThrowable extends Throwable
          {
            public int echo()
            {
              return 2;
            }
          }


          static class MyException extends Exception
          {
            public int echo()
            {
              return 1;
            }
          }

          public static void main(String[] args)
          {
            System.out.println((true ? new MyThrowable() : new MyException()).echo());
          }
        }
        

Metoda main() próbuje stworzyć dwa obiekty i wywołać na jednym z nich metodę echo(). Powinien zostać zwrócony wynik w postaci liczby 2. Okazuje się jednak, że pomimo iż obie klasy implementują metodę echo(), kompilator protestuje i nie potrafi skompilować kodu właśnie z powodu tej metody. Gdzie jest źródło problemu?

Odpowiedzi:

Przykład pierwszy:

W metodzie log() użyto klasy StringBuffer, która pozwala optymalnie dodawać łańcuchy znakowe. Niestety zastosowano metodę append() tejże klasy, która wywołuje metodę toString() obiektu przekazanego jako referencję. W konsekwencji metoda log() wywołuje w pętli for metodę toString(), która znowu wywołuje metodę log(). Powstaje rekurencja nieskończona. Uruchomienie zakończy się wyjątkiem StackOverFlow().

Przykład drugi:

Kompilator nie pozwoli skompilować tak napisanego kodu, gdyż operator warunkowy będzie niejawnie rzutował wynik swojej operacji na pierwszy wspólny typ, jaki znajdzie w hierarchii dziedziczenia. Wynika to z faktu, że dopiero na etapie uruchomienia wiadomo będzie, co będzie wynikiem operacji, a tym samym, jaki typ będzie zwrócony przez operator. Kompilator jest zbyt leniwy i nie policzy wyniku tej operacji (mimo, że mógłby) – decyzję pozostawi maszynie wirtualnej.

Przykładowo wspólnym przodkiem dla klasy Float i Integer jest klasa Number, dla klasy String i Serializable interface Serializable, ale klasa String i Integer nie mają wspólnego przodka, zatem rzutowanie będzie na klasę Object.

Biorąc pod uwagę, że pierwszym, a zarazem jedynym przodkiem klas MyThrowable i MyException jest klasa Object, ten typ będzie właśnie wynikiem operacji. I tutaj pojawia się problem – klasa Object nie implementuje metody echo(). Zatem zapis można by rozbić na dwie linijki:

        System.err.println(o.echo());
        Object o = true ? new MyThrowable() : new MyException();
        

Teraz widać dokładnie, dlaczego kompilator zgłosił błąd. Gdyby klasa MyException rozszerzała klasę MyThrowable, wtedy kompilator nie zgłosiłby błędu, a wynikiem operacji byłaby wspomniana liczba 2.

W zaprezentowanym przykładzie w prosty sposób można wykryć błąd, gdyż zrobił to kompilator. Nadużywanie operatora warunkowego może jednak spowodować, że program nie zachowa się w sposób, który byłby od niego oczekiwany.

Source: http://www.javaexpress.pl/article/show/Express_killers_cz_II