Słowa te to:
To jebane kurestwo nie działa!
Przynajmniej nie działa tak jak normalny człowiek, gdzie normalny równoważne jest programista java, się spodziewa. W czym rzecz. Na początek wygeneruj projekt w mavenie i skonfiguruj maven-build-plugin by korzystał z flag source i target do wersji 1.5.
UWAGA! Potrzebujesz zainstalowanej Javy 1.5 oraz 1.6+. Java 1.6+ musi być domyślną, a 1.5 nie powinno być możliwości wywołania inaczej niż przez podanie pełnej ścieżki do pliku java.exe.
Następnie piszemy prosty program:
Listing 1. Program testowy
public class App { public static void main(String[] args) { System.out.println("Hello World!".isEmpty()); } }
Metoda isEmpty() doszła w API 1.6. Zatem jeżeli damy odpowiednie flagi w trakcie kompilacji to…
Skompiluj program (mvn compile), następnie wejdź do target/classes i wywołaj:
Listing 2. Co mamy w środku klasy
$ javap -verbose pl.koziolekweb.blog.javac.App Compiled from "App.java" public class pl.koziolekweb.blog.javac.App extends java.lang.Object SourceFile: "App.java" minor version: 0 major version: 49 Constant pool: const #1 = Method #7.#16; // java/lang/Object."":()V const #2 = Field #17.#18; // java/lang/System.out:Ljava/io/PrintStream; const #3 = String #19; // Hello World! const #4 = Method #20.#21; // java/lang/String.isEmpty:()Z const #5 = Method #22.#23; // java/io/PrintStream.println:(Z)V const #6 = class #24; // pl/koziolekweb/blog/javac/App const #7 = class #25; // java/lang/Object const #8 = Asciz ; const #9 = Asciz ()V; const #10 = Asciz Code; const #11 = Asciz LineNumberTable; const #12 = Asciz main; const #13 = Asciz ([Ljava/lang/String;)V; const #14 = Asciz SourceFile; const #15 = Asciz App.java; const #16 = NameAndType #8:#9;// " ":()V const #17 = class #26; // java/lang/System const #18 = NameAndType #27:#28;// out:Ljava/io/PrintStream; const #19 = Asciz Hello World!; const #20 = class #29; // java/lang/String const #21 = NameAndType #30:#31;// isEmpty:()Z const #22 = class #32; // java/io/PrintStream const #23 = NameAndType #33:#34;// println:(Z)V const #24 = Asciz pl/koziolekweb/blog/javac/App; const #25 = Asciz java/lang/Object; const #26 = Asciz java/lang/System; const #27 = Asciz out; const #28 = Asciz Ljava/io/PrintStream;; const #29 = Asciz java/lang/String; const #30 = Asciz isEmpty; const #31 = Asciz ()Z; const #32 = Asciz java/io/PrintStream; const #33 = Asciz println; const #34 = Asciz (Z)V; { public pl.koziolekweb.blog.javac.App(); Code: Stack=1, Locals=1, Args_size=1 0: aload_0 1: invokespecial #1; //Method java/lang/Object." ":()V 4: return LineNumberTable: line 7: 0 public static void main(java.lang.String[]); Code: Stack=2, Locals=1, Args_size=1 0: getstatic #2; //Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #3; //String Hello World! 5: invokevirtual #4; //Method java/lang/String.isEmpty:()Z 8: invokevirtual #5; //Method java/io/PrintStream.println:(Z)V 11: return LineNumberTable: line 9: 0 line 10: 11 }
Linia major version: 49 oznacza ni mniej ni więcej, że klasa jest zgodna z API 1.5. Uruchommy program na Javie 1.6. Ok wynik spodziewany, czyli false, choć nie do końca, bo jednak w 1.5 nie ma isEmpty(). Teraz uruchommy ją na Javie 1.5:
Listing 3. Uruchomienie z wykorzystaniem 1.5
$ /c/Program\ Files/Java/jre1.5.0_22/bin/java pl.koziolekweb.blog.javac.App Exception in thread "main" java.lang.NoSuchMethodError: java.lang.String.isEmpty()Z at pl.koziolekweb.blog.javac.App.main(App.java:9)
o… popsuło się… Czyli jednak metody isEmpty() nie ma w 1.5.
Dlaczego tak się dzieje?
Otóż flagi source i target są sugestią dla kompilatora jak ma załatwić pewne sprawy. W przypadku tej pierwszej flagi kompilator będzie wiedział, że powinien zgłaszać błędy w przypadku użycia np. typów generycznych i enumów jeżeli jest ustawiona na wersję 1.4 i wcześniejsze. Ta druga flaga wymusza na kompilatorze tylko ustawienie odpowiedniej sygnatury w pliku class. Przy czym wartość przy target nie może być niższa niż ta przy source.
Kompilator nie sprawdza jednak czy dana metoda jest dostępna w API w danej wersji. W javadocu jest znacznik @since, ale jako, że javadoc jest usuwany z plików class to kompilator nie może sprawdzić czy metoda występuje w danej wersji API. Sprawdzane jest tylko czy metoda o danej sygnaturze jest dostępna w API aktualnie używanym do kompilacji. Sygnatury metod są sprawdzane w trakcie wywołania. Zresztą widzieliśmy to w przypadku uruchomienia programu w API 1.6. Pomimo, że wersja klasy wskazywała, że dana metoda nie powinna istnieć dla kodu w danej wersji to program uruchomił się prawidłowo.
Po co to wszystko
Ponieważ jest to pułapka, w którą bardzo łatwo wpaść pisząc integrację ze starszym systemem. Pomimo, że wersja klasy będzie prawidłowa to nadal istnieje szansa, że program wywali się z błędem NoSuchMethodError, a ty będziesz się zastanawiał dlaczego, bo przecież skompilowało się prawidłowo. Jeszcze bardziej będzie to bolało jeżeli używasz CI, bo wtedy problem wyjdzie dopiero u klienta w trakcie testów.