O kompatybilności wstecznej javac słów kilka…
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."<init>":()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 <init>;
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;// "<init>":()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."<init>":()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
}</init></init></init></init>
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.