Versi apa kode Java Anda?

23 Mei 2003

Q:

SEBUAH:

kelas publik Halo {public static void main (String [] args) {StringBuffer salam = baru StringBuffer ("hello,"); StringBuffer siapa = StringBuffer baru (args [0]). Append ("!"); salam.append (siapa); System.out.println (salam); }} // Akhir kelas

Pada awalnya pertanyaan itu tampaknya agak sepele. Jadi sedikit kode yang terlibat di Hellokelas, dan apa pun yang ada hanya menggunakan fungsionalitas yang berasal dari Java 1.0. Jadi kelas harus berjalan di hampir semua JVM tanpa masalah, bukan?

Jangan terlalu yakin. Kompilasi menggunakan javac dari Platform Java 2, Edisi Standar (J2SE) 1.4.1 dan jalankan di versi Java Runtime Environment (JRE) yang lebih lama:

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Halo dunia Pengecualian di thread "main" java.lang.NoSuchMethodError di Hello.main (Hello.java:20 ) 

Alih-alih "hello, world!", Kode ini melontarkan error runtime meskipun sumbernya 100 persen kompatibel dengan Java 1.0! Dan kesalahan tersebut juga tidak persis seperti yang Anda harapkan: alih-alih versi kelas yang tidak cocok, kesalahan tersebut mengeluh tentang metode yang hilang. Bingung? Jika demikian, penjelasan lengkapnya akan Anda temukan nanti di artikel ini. Pertama, mari kita perluas pembahasannya.

Mengapa repot-repot dengan versi Java yang berbeda?

Java cukup platform-independen dan sebagian besar kompatibel ke atas, jadi adalah umum untuk mengkompilasi sepotong kode menggunakan versi J2SE yang diberikan dan mengharapkannya bekerja di versi JVM yang lebih baru. (Perubahan sintaks Java biasanya terjadi tanpa perubahan substansial pada set instruksi kode byte.) Pertanyaan dalam situasi ini adalah: Dapatkah Anda menetapkan beberapa jenis versi Java dasar yang didukung oleh aplikasi yang Anda kompilasi, atau apakah perilaku compiler default dapat diterima? Saya akan menjelaskan rekomendasi saya nanti.

Situasi lain yang cukup umum adalah menggunakan kompilator dengan versi yang lebih tinggi daripada platform penerapan yang dimaksudkan. Dalam kasus ini, Anda tidak menggunakan API yang baru ditambahkan, tetapi hanya ingin mendapatkan keuntungan dari peningkatan alat. Lihat cuplikan kode ini dan coba tebak apa yang harus dilakukannya pada waktu proses:

public class ThreadSurprise {public static void main (String [] args) melempar Exception {Thread [] threads = new Thread [0]; utas [-1] .sleep (1); // Haruskah lemparan ini? }} // Akhir kelas

Haruskah kode ini membuang ArrayIndexOutOfBoundsExceptionatau tidak? Jika Anda mengompilasi ThreadSurprisemenggunakan versi Sun Microsystems JDK / J2SDK (Platform Java 2, Kit Pengembangan Standar) yang berbeda, perilaku tidak akan konsisten:

  • Versi 1.1 dan kompiler sebelumnya menghasilkan kode yang tidak dibuang
  • Versi 1.2 melempar
  • Versi 1.3 tidak membuang
  • Versi 1.4 melempar

Poin halus di sini adalah itu Thread.sleep()adalah metode statis dan tidak memerlukan Threadcontoh sama sekali. Namun, Spesifikasi Bahasa Java mengharuskan compiler tidak hanya menyimpulkan kelas target dari ekspresi kiri threads [-1].sleep (1);, tetapi juga mengevaluasi ekspresi itu sendiri (dan membuang hasil evaluasi tersebut). Apakah indeks referensi -1 darithreadsbagian array dari evaluasi seperti itu? Kata-kata dalam Spesifikasi Bahasa Java agak kabur. Ringkasan perubahan untuk J2SE 1.4 menyiratkan bahwa ambiguitas akhirnya diselesaikan untuk mengevaluasi ekspresi tangan kiri sepenuhnya. Bagus! Karena kompiler J2SE 1.4 tampaknya merupakan pilihan terbaik, saya ingin menggunakannya untuk semua pemrograman Java saya meskipun platform runtime target saya adalah versi sebelumnya, hanya untuk mendapatkan keuntungan dari perbaikan dan peningkatan tersebut. (Perhatikan bahwa pada saat penulisan, tidak semua server aplikasi disertifikasi pada platform J2SE 1.4.)

Meskipun contoh kode terakhir agak dibuat-buat, ini berfungsi untuk menggambarkan suatu hal. Alasan lain untuk menggunakan versi J2SDK terbaru termasuk ingin memanfaatkan javadoc dan peningkatan alat lainnya.

Terakhir, kompilasi silang adalah cara hidup yang baik dalam pengembangan Java yang disematkan dan pengembangan game Java.

Puzzle kelas halo menjelaskan

The Hellocontoh yang mulai artikel ini adalah contoh dari salah lintas kompilasi. J2SE 1.4 menambahkan metode baru untuk StringBufferAPI: append(StringBuffer). Saat memutuskan cara menerjemahkan greeting.append (who)ke dalam kode byte, javac mencari StringBufferdefinisi kelas di jalur kelas bootstrap dan memilih metode baru ini, bukan append(Object). Meskipun kode sumber sepenuhnya kompatibel dengan Java 1.0, kode byte yang dihasilkan memerlukan runtime J2SE 1.4.

Perhatikan betapa mudahnya membuat kesalahan ini. Tidak ada peringatan kompilasi, dan kesalahan hanya dapat dideteksi pada waktu proses. Cara yang benar untuk menggunakan javac dari J2SE 1.4 untuk menghasilkan Hellokelas yang kompatibel dengan Java 1.1 adalah:

> ... \ jdk1.4.1 \ bin \ javac -target 1.1 -bootclasspath ... \ jdk1.1.8 \ lib \ class.zip Hello.java 

Mantra javac yang benar berisi dua opsi baru. Mari kita periksa apa yang mereka lakukan dan mengapa mereka perlu.

Setiap kelas Java memiliki cap versi

Anda mungkin tidak menyadarinya, tetapi setiap .classfile yang Anda buat berisi stempel versi: dua bilangan bulat pendek unsigned mulai dari byte offset 4, tepat setelah angka 0xCAFEBABEajaib. Mereka adalah nomor versi mayor / minor dari format kelas (lihat Spesifikasi Format File Kelas), dan memiliki utilitas selain hanya menjadi titik ekstensi untuk definisi format ini. Setiap versi platform Java menentukan berbagai versi yang didukung. Berikut adalah tabel rentang yang didukung pada saat penulisan ini (versi saya dari tabel ini sedikit berbeda dari data di dokumen Sun — saya memilih untuk menghapus beberapa nilai rentang yang hanya relevan untuk kompiler Sun versi sangat lama (sebelum 1.0.2)) :

Platform Java 1.1: 45.3-45.65535 Platform Java 1.2: 45.3-46.0 Platform Java 1.3: 45.3-47.0 Platform Java 1.4: 45.3-48.0 

JVM yang patuh akan menolak memuat kelas jika stempel versi kelas berada di luar rentang dukungan JVM. Perhatikan dari tabel sebelumnya bahwa JVM yang lebih baru selalu mendukung seluruh rentang versi dari tingkat versi sebelumnya dan juga memperluasnya.

Apa artinya ini bagi Anda sebagai pengembang Java? Dengan kemampuan untuk mengontrol cap versi ini selama kompilasi, Anda dapat menerapkan versi runtime Java minimum yang diperlukan oleh aplikasi Anda. Inilah tepatnya yang dilakukan -targetopsi kompilator. Berikut adalah daftar perangko versi yang dikeluarkan oleh kompiler javac dari berbagai JDK / J2SDK secara default (perhatikan bahwa J2SDK 1.4 adalah J2SDK pertama di mana javac mengubah target defaultnya dari 1.1 menjadi 1.2):

JDK 1.1: 45.3 J2SDK 1.2: 45.3 J2SDK 1.3: 45.3 J2SDK 1.4: 46.0 

Dan inilah efek dari menentukan berbagai -targets:

-target 1.1: 45.3 -target 1.2: 46.0 -target 1.3: 47.0 -target 1.4: 48.0 

Sebagai contoh, berikut ini menggunakan URL.getPath()metode yang ditambahkan di J2SE 1.3:

URL url = URL baru ("//www.javaworld.com/columns/jw-qna-index.shtml"); System.out.println ("jalur URL:" + url.getPath ());

Karena kode ini membutuhkan setidaknya J2SE 1.3, saya harus menggunakannya -target 1.3saat membuatnya. Mengapa memaksa pengguna saya untuk menghadapi java.lang.NoSuchMethodErrorkejutan yang terjadi hanya jika mereka keliru memuat kelas di 1.2 JVM? Tentu, saya dapat mendokumentasikan bahwa aplikasi saya memerlukan J2SE 1.3, tetapi akan lebih bersih dan lebih kuat untuk menerapkan hal yang sama pada tingkat biner.

I don't think the practice of setting the target JVM is widely used in enterprise software development. I wrote a simple utility class DumpClassVersions (available with this article's download) that can scan files, archives, and directories with Java classes and report all encountered class version stamps. Some quick browsing of popular open source projects or even core libraries from various JDKs/J2SDKs will show no particular system for class versions.

Bootstrap and extension class lookup paths

When translating Java source code, the compiler needs to know the definition of types it has not yet seen. This includes your application classes and core classes like java.lang.StringBuffer. As I am sure you are aware, the latter class is frequently used to translate expressions containing String concatenation and the like.

A process superficially similar to normal application classloading looks up a class definition: first in the bootstrap classpath, then the extension classpath, and finally in the user classpath (-classpath). If you leave everything to the defaults, the definitions from the "home" javac's J2SDK will take effect—which may not be correct, as shown by the Hello example.

To override the bootstrap and extension class lookup paths, you use -bootclasspath and -extdirs javac options, respectively. This ability complements the -target option in the sense that while the latter sets the minimum required JVM version, the former selects the core class APIs available to the generated code.

Remember that javac itself was written in Java. The two options I just mentioned affect the class lookup for byte-code generation. They do not affect the bootstrap and extension classpaths used by the JVM to execute javac as a Java program (the latter could be done via the -J option, but doing that is quite dangerous and results in unsupported behavior). To put it another way, javac does not actually load any classes from -bootclasspath and -extdirs; it merely references their definitions.

With the newly acquired understanding for javac's cross-compilation support, let's see how this can be used in practical situations.

Scenario 1: Target a single base J2SE platform

This is a very common case: several J2SE versions support your application, and it so happens you can implement everything via core APIs of a certain (I will call it base) J2SE platform version. Upward compatibility takes care of the rest. Although J2SE 1.4 is the latest and greatest version, you see no reason to exclude users who can't run J2SE 1.4 yet.

The ideal way to compile your application is:

\bin\javac -target -bootclasspath \jre\lib\rt.jar -classpath 

Yes, this implies you might have to use two different J2SDK versions on your build machine: the one you pick for its javac and the one that is your base supported J2SE platform. This seems like extra setup effort, but it is actually a small price to pay for a robust build. The key here is explicitly controlling both the class version stamps and the bootstrap classpath and not relying on defaults. Use the -verbose option to verify where core class definitions are coming from.

As a side comment, I'll mention that it is common to see developers include rt.jar from their J2SDKs on the -classpath line (this could be a habit from the JDK 1.1 days when you had to add classes.zip to the compilation classpath). If you followed the discussion above, you now understand that this is completely redundant, and in the worst case, might interfere with the proper order of things.

Scenario 2: Switch code based on the Java version detected at runtime

Here you want to be more sophisticated than in Scenario 1: You have a base-supported Java platform version, but should your code run in a higher Java version, you prefer to leverage newer APIs. For example, you can get by with java.io.* APIs but wouldn't mind benefiting from java.nio.* enhancements in a more recent JVM if the opportunity presents itself.

In this scenario, the basic compilation approach resembles Scenario 1's approach, except your bootstrap J2SDK should be the highest version you need to use:

\bin\javac -target -bootclasspath \jre\lib\rt.jar -classpath 

This is not enough, however; you also need to do something clever in your Java code so it does the right thing in different J2SE versions.

One alternative is to use a Java preprocessor (with at least #ifdef/#else/#endif support) and actually generate different builds for different J2SE platform versions. Although J2SDK lacks proper preprocessing support, there is no shortage of such tools on the Web.

Namun, mengelola beberapa distribusi untuk platform J2SE yang berbeda selalu menjadi beban tambahan. Dengan beberapa pandangan jauh ke depan, Anda bisa lolos dengan mendistribusikan satu build aplikasi Anda. Berikut adalah contoh cara melakukannya ( URLTest1adalah kelas sederhana yang mengekstrak berbagai bit menarik dari URL):