Каква версия е вашият Java код?

23 май 2003 г.

В:

A:

публичен клас Здравейте {public static void main (String [] args) {StringBuffer поздрав = нов StringBuffer ("здравей,"); StringBuffer кой = нов StringBuffer (аргументи [0]). Append ("!"); поздрав.append (кой); System.out.println (поздрав); }} // Край на класа

Отначало въпросът изглежда доста тривиален. В Helloкласа участва толкова малко код и каквото и да има, използва само функционалност, датираща от Java 1.0. Така че класът трябва да работи в почти всеки JVM без проблеми, нали?

Не бъдете толкова сигурни. Компилирайте го с помощта на javac от Java 2 Platform, Standard Edition (J2SE) 1.4.1 и го стартирайте в по-ранна версия на Java Runtime Environment (JRE):

> ... \ jdk1.4.1 \ bin \ javac Hello.java> ... \ jdk1.3.1 \ bin \ java Hello world Изключение в нишка "main" java.lang.NoSuchMethodError at Hello.main (Hello.java:20 ) 

Вместо очакваното „здравей, свят!“, Този код извежда грешка по време на работа, въпреки че източникът е 100 процента съвместим с Java 1.0! И грешката не е точно това, което бихте могли да очаквате: вместо несъответствие на версията на класа, по някакъв начин се оплаква от липсващ метод. Озадачен? Ако е така, ще намерите пълното обяснение по-нататък в тази статия. Първо, нека разширим дискусията.

Защо да се занимавам с различни версии на Java?

Java е доста независима от платформата и най-вече съвместима нагоре, така че е обичайно да се компилира парче код, като се използва дадена версия на J2SE и се очаква да работи в по-късните версии на JVM. (Синтаксисните промени в Java обикновено се извършват без съществени промени в набора от инструкции за байт код.) Въпросът в тази ситуация е: Можете ли да установите някаква основна версия на Java, поддържана от вашето компилирано приложение, или поведението на компилатора по подразбиране е приемливо? Ще обясня препоръката си по-късно.

Друга доста често срещана ситуация е използването на компилатор с по-висока версия от планираната платформа за внедряване. В този случай не използвате наскоро добавени API, а просто искате да се възползвате от подобренията на инструментите. Погледнете този кодов фрагмент и се опитайте да познаете какво трябва да прави по време на изпълнение:

публичен клас ThreadSurprise {public static void main (String [] args) хвърля изключение {Thread [] thread = new Thread [0]; нишки [-1] .sleep (1); // Трябва ли това да хвърли? }} // Край на класа

Трябва ли този код да хвърли ArrayIndexOutOfBoundsExceptionили не? Ако компилирате ThreadSurpriseс помощта на различни версии на Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit), поведението няма да бъде последователно:

  • Версия 1.1 и по-стари компилатори генерират код, който не изхвърля
  • Версия 1.2 хвърля
  • Версия 1.3 не хвърля
  • Версия 1.4 хвърля

Фината точка тук е, че Thread.sleep()е статичен метод и изобщо не се нуждае от Threadекземпляр. И все пак, спецификацията на езика Java изисква компилаторът не само да изведе целевия клас от левия израз на threads [-1].sleep (1);, но и да оцени самия израз (и да отхвърли резултата от такава оценка). Има препратка към индекс -1 наthreadsмасив част от такава оценка? Формулировката в спецификацията на езика Java е малко неясна. Обобщението на промените за J2SE 1.4 предполага, че неяснотата е окончателно разрешена в полза на пълното оценяване на левия израз. Страхотен! Тъй като компилаторът J2SE 1.4 изглежда най-добрият избор, искам да го използвам за цялото си програмиране на Java, дори ако целевата ми среда за изпълнение е по-ранна версия, само за да се възползвам от такива корекции и подобрения. (Имайте предвид, че по време на писането не всички сървъри за приложения са сертифицирани на платформата J2SE 1.4.)

Въпреки че последният пример за код беше донякъде изкуствен, той служи за илюстриране на точка. Други причини да използвате скорошна версия на J2SDK включват желанието да се възползвате от javadoc и други подобрения на инструментите.

И накрая, кръстосаното компилиране е доста начин на живот във вградената Java разработка и разработка на Java игри.

Здравейте клас пъзел обяснено

В Helloпримера, който започна тази статия е пример за неправилно кръстосано компилация. J2SE 1.4 добавя нов метод за StringBufferAPI: append(StringBuffer). Когато javac решава как да преведе greeting.append (who)в байтов код, той търси StringBufferдефиницията на класа в началната пътека на началния клас и избира този нов метод вместо append(Object). Въпреки че изходният код е напълно съвместим с Java 1.0, полученият байтов код изисква изпълнение на J2SE 1.4.

Обърнете внимание колко лесно е да направите тази грешка. Няма предупреждения за компилация и грешката се открива само по време на изпълнение. Правилният начин за използване на javac от J2SE 1.4 за генериране на съвместим с Java 1.1 Helloклас е:

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

Правилното заклинание на javac съдържа две нови опции. Нека разгледаме какво правят и защо са необходими.

Всеки Java клас има печат на версията

Може да не сте наясно с това, но всеки .classфайл, който генерирате, съдържа печат на версията: две неподписани къси цели числа, започващи с отместване на байта 4, веднага след 0xCAFEBABEмагическото число. Те са основните / второстепенни номера на версията на формата на класа (вижте Спецификацията на формата на файла на класа) и те имат полезност, освен че са просто точки за разширение за тази дефиниция на формат. Всяка версия на платформата Java определя набор от поддържани версии. Ето таблицата на поддържаните диапазони по това време на писане (моята версия на тази таблица се различава леко от данните в документите на Sun - избрах да премахна някои стойности на диапазона, отнасящи се само за изключително стари (преди 1.0.2) версии на компилатора на Sun) :

Платформа Java 1.1: 45.3-45.65535 Платформа Java 1.2: 45.3-46.0 Платформа Java 1.3: 45.3-47.0 Платформа Java 1.4: 45.3-48.0 

Съвместим JVM ще откаже да зареди клас, ако печатът на версията на класа е извън обхвата на поддръжка на JVM. Забележете от предишната таблица, че по-късните JVM винаги поддържат целия диапазон на версиите от предишното ниво на версията и също го разширяват.

Какво означава това за вас като разработчик на Java? Като се има предвид възможността да се контролира този печат на версията по време на компилацията, можете да приложите минималната версия на Java среда за изпълнение, изисквана от вашето приложение. Точно това прави -targetопцията на компилатора. Ето списък с печати на версиите, издавани от компилаторите на javac от различни JDK / J2SDK по подразбиране (обърнете внимание, че J2SDK 1.4 е първият J2SDK, където javac променя целта си по подразбиране от 1.1 на 1.2):

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

И тук е ефектът от посочване на различни -targets:

-цел 1.1: 45.3 -цел 1.2: 46.0 -цел 1.3: 47.0 -цел 1.4: 48.0 

Като пример, следното използва URL.getPath()метода, добавен в J2SE 1.3:

URL адрес на URL = нов URL ("//www.javaworld.com/columns/jw-qna-index.shtml"); System.out.println ("URL път:" + url.getPath ());

Тъй като този код изисква поне J2SE 1.3, трябва да го използвам, -target 1.3когато го изграждам. Защо да принуждавам потребителите си да се справят с java.lang.NoSuchMethodErrorизненади, които се случват само когато погрешно са заредили класа в 1.2 JVM? Разбира се, бих могъл да документирам, че моето приложение изисква J2SE 1.3, но би било по-чисто и по-стабилно да се налага същото на двоично ниво.

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.

Управлението на няколко дистрибуции за различни платформи на J2SE обаче винаги е допълнителна тежест. С известна прозорливост можете да се измъкнете с разпространението на една компилация на вашето приложение. Ето пример за това как да направите това ( URLTest1е прост клас, който извлича различни интересни битове от URL):