Προγραμματισμός

Ποια έκδοση είναι ο κώδικας Java;

23 Μαΐου 2003

Ε:

ΕΝΑ:

δημόσια τάξη Γεια σας {public static void main (String [] args) {StringBuffer Salam = νέο StringBuffer ("γεια,"); StringBuffer who = νέο StringBuffer (args [0]). Append ("!"); χαιρετισμός. προσθήκη (ποιος); System.out.println (χαιρετισμός); }} // Τέλος μαθήματος 

Στην αρχή η ερώτηση φαίνεται μάλλον ασήμαντη. Τόσο λίγος κώδικας εμπλέκεται στο γεια τάξη, και ό, τι υπάρχει χρησιμοποιεί μόνο λειτουργικότητα που χρονολογείται από το 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 Γεια σας κόσμος Εξαίρεση στο νήμα "main" java.lang.NoSuchMethodError στο Hello.main (Hello.java:20 ) 

Αντί για το αναμενόμενο "γεια, κόσμος!", Αυτός ο κώδικας ρίχνει σφάλμα χρόνου εκτέλεσης, παρόλο που η πηγή είναι 100 τοις εκατό συμβατή με Java 1.0! Και το σφάλμα δεν είναι ακριβώς αυτό που θα περίμενε κανείς: αντί για αναντιστοιχία έκδοσης κατηγορίας, κάπως παραπονιέται για μια μέθοδο που λείπει. Μπερδεμένος? Εάν ναι, θα βρείτε την πλήρη εξήγηση αργότερα σε αυτό το άρθρο. Πρώτον, ας διευρύνουμε τη συζήτηση.

Γιατί να ασχοληθείτε με διαφορετικές εκδόσεις Java;

Η Java είναι αρκετά ανεξάρτητη από την πλατφόρμα και ως επί το πλείστον συμβατή προς τα πάνω, οπότε είναι συνηθισμένο να συντάσσετε ένα κομμάτι κώδικα χρησιμοποιώντας μια δεδομένη έκδοση J2SE και να περιμένετε να λειτουργεί σε μεταγενέστερες εκδόσεις JVM. (Οι αλλαγές σύνταξης Java συμβαίνουν συνήθως χωρίς ουσιαστικές αλλαγές στο σύνολο εντολών κώδικα byte.) Το ερώτημα σε αυτήν την περίπτωση είναι: Μπορείτε να δημιουργήσετε κάποιο είδος βασικής έκδοσης Java που υποστηρίζεται από την μεταγλωττισμένη εφαρμογή σας ή είναι αποδεκτή η προεπιλεγμένη συμπεριφορά μεταγλωττιστή; Θα εξηγήσω τη σύστασή μου αργότερα.

Μια άλλη αρκετά κοινή κατάσταση είναι να χρησιμοποιήσετε έναν μεταγλωττιστή με υψηλότερη έκδοση από την πλατφόρμα ανάπτυξης που προορίζεται. Σε αυτήν την περίπτωση, δεν χρησιμοποιείτε API που προστέθηκαν πρόσφατα, αλλά απλώς θέλετε να επωφεληθείτε από τις βελτιώσεις εργαλείων. Κοιτάξτε αυτό το απόσπασμα κώδικα και προσπαθήστε να μαντέψετε τι πρέπει να κάνει κατά την εκτέλεση:

δημόσια τάξη ThreadSurprise {public static void main (String [] args) ρίχνει την Εξαίρεση {Thread [] threads = new Thread [0]; νήματα [-1]. ύπνο (1); // Πρέπει να ρίξει αυτό; }} // Τέλος μαθήματος 

Εάν αυτός ο κώδικας ρίξει ένα ArrayIndexOutOfBoundsException ή όχι? Εάν συντάξετε ThreadSurprise χρησιμοποιώντας διαφορετικές εκδόσεις Sun Microsystems JDK / J2SDK (Java 2 Platform, Standard Development Kit), η συμπεριφορά δεν θα είναι συνεπής:

  • Η έκδοση 1.1 και οι παλαιότεροι μεταγλωττιστές δημιουργούν κώδικα που δεν ρίχνει
  • Έκδοση 1.2 ρίχνει
  • Η έκδοση 1.3 δεν ρίχνει
  • Έκδοση 1.4 ρίχνει

Το λεπτό σημείο εδώ είναι αυτό Thread.sleep () είναι μια στατική μέθοδος και δεν χρειάζεται Νήμα παράδειγμα σε όλα. Ωστόσο, η προδιαγραφή γλώσσας Java απαιτεί από τον μεταγλωττιστή να συμπεράνει όχι μόνο την κλάση στόχου από την αριστερή έκφραση του νήματα [-1]. ύπνο (1);, αλλά και να αξιολογήσετε την ίδια την έκφραση (και να απορρίψετε το αποτέλεσμα μιας τέτοιας αξιολόγησης). Είναι δείκτης αναφοράς -1 του νήματα αποτελεί μέρος μιας τέτοιας αξιολόγησης; Η διατύπωση στο Java Language Specification είναι κάπως ασαφής. Η σύνοψη των αλλαγών για το J2SE 1.4 υποδηλώνει ότι η ασάφεια επιλύθηκε τελικά υπέρ της αξιολόγησης της αριστερής έκφρασης πλήρως. Μεγάλος! Δεδομένου ότι ο μεταγλωττιστής J2SE 1.4 φαίνεται να είναι η καλύτερη επιλογή, θέλω να το χρησιμοποιήσω για όλο τον προγραμματισμό Java, ακόμη και αν η πλατφόρμα χρόνου εκτέλεσης στόχου μου είναι παλαιότερη έκδοση, απλώς για να επωφεληθώ από τέτοιες διορθώσεις και βελτιώσεις. (Σημειώστε ότι κατά τη στιγμή της σύνταξης δεν είναι πιστοποιημένοι όλοι οι διακομιστές εφαρμογών στην πλατφόρμα J2SE 1.4.)

Αν και το τελευταίο παράδειγμα κώδικα ήταν κάπως τεχνητό, χρησίμευσε για την απεικόνιση ενός σημείου. Άλλοι λόγοι για τη χρήση μιας πρόσφατης έκδοσης J2SDK περιλαμβάνουν την επιθυμία να επωφεληθείτε από το javadoc και άλλες βελτιώσεις εργαλείων.

Τέλος, η διασταύρωση είναι ένας τρόπος ζωής στην ενσωματωμένη ανάπτυξη Java και στην ανάπτυξη παιχνιδιών Java.

Γεια σας παζλ εξήγησε

ο γεια παράδειγμα που ξεκίνησε αυτό το άρθρο είναι ένα παράδειγμα λανθασμένης συλλογής. Το J2SE 1.4 πρόσθεσε μια νέα μέθοδο στο StringBuffer API: προσάρτημα (StringBuffer). Όταν ο javac αποφασίζει πώς να μεταφράσει χαιρετισμός.απόκριση (ποιος) σε κώδικα byte, αναζητά το StringBuffer ορισμός κλάσης στο bootstrap classpath και επιλέγει αυτήν τη νέα μέθοδο αντί προσθήκη (αντικείμενο). Παρόλο που ο πηγαίος κώδικας είναι πλήρως συμβατός με το Java 1.0, ο κωδικός byte που προκύπτει απαιτεί χρόνο εκτέλεσης J2SE 1.4.

Σημειώστε πόσο εύκολο είναι να κάνετε αυτό το λάθος. Δεν υπάρχουν προειδοποιήσεις συλλογής και το σφάλμα εντοπίζεται μόνο κατά το χρόνο εκτέλεσης. Ο σωστός τρόπος χρήσης του javac από το J2SE 1.4 για τη δημιουργία ενός Java 1.1 συμβατού γεια η τάξη είναι:

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

Η σωστή ανάδειξη javac περιέχει δύο νέες επιλογές. Ας εξετάσουμε τι κάνουν και γιατί είναι απαραίτητα.

Κάθε τάξη Java έχει μια σφραγίδα έκδοσης

Μπορεί να μην το γνωρίζετε, αλλά όλα .τάξη το αρχείο που δημιουργείτε περιέχει μια σφραγίδα έκδοσης: δύο χωρίς υπογραφή κοντούς ακέραιους που ξεκινούν από το byte offset 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 που απαιτείται από την εφαρμογή σας. Αυτό ακριβώς είναι το -στόχος η επιλογή μεταγλωττιστή. Ακολουθεί μια λίστα με γραμματόσημα έκδοσης που εκπέμπονται από μεταγλωττιστές 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 

Και εδώ είναι το αποτέλεσμα του καθορισμού διαφόρων -στόχοςμικρό:

-target 1.1: 45.3 -getget 1.2: 46.0 -getget 1.3: 47.0 -getget 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, πρέπει να το χρησιμοποιήσω - στόχος 1.3 όταν το χτίζετε. Γιατί να αναγκάσω τους χρήστες μου να αντιμετωπίσουν java.lang.NoSuchMethodError εκπλήξεις που συμβαίνουν μόνο όταν έχουν φορτώσει κατά λάθος την τάξη σε 1,2 JVM; Σίγουρα, θα μπορούσα έγγραφο ότι η εφαρμογή μου απαιτεί J2SE 1.3, αλλά θα ήταν πιο καθαρή και πιο ισχυρή επιβάλλω το ίδιο σε δυαδικό επίπεδο.

Δεν πιστεύω ότι η πρακτική καθορισμού του στόχου JVM χρησιμοποιείται ευρέως στην ανάπτυξη λογισμικού επιχειρήσεων. Έγραψα μια απλή τάξη χρησιμότητας DumpClassVersions (διατίθεται με τη λήψη αυτού του άρθρου) που μπορεί να σαρώσει αρχεία, αρχεία και καταλόγους με τάξεις Java και να αναφέρει όλα τα γραμματόσημα έκδοσης τάξης που συναντήθηκαν. Κάποια γρήγορη περιήγηση δημοφιλών έργων ανοιχτού κώδικα ή ακόμη και βασικές βιβλιοθήκες από διάφορα JDK / J2SDK δεν θα εμφανίζουν κανένα συγκεκριμένο σύστημα για εκδόσεις κατηγορίας.

Διαδρομές αναζήτησης τάσης εκκίνησης και επέκτασης

Κατά τη μετάφραση του πηγαίου κώδικα Java, ο μεταγλωττιστής πρέπει να γνωρίζει τον ορισμό των τύπων που δεν έχει ακόμη δει. Αυτό περιλαμβάνει τις κατηγορίες εφαρμογών σας και τις βασικές τάξεις όπως java.lang.StringBuffer. Όπως είμαι σίγουρος ότι γνωρίζετε, η τελευταία τάξη χρησιμοποιείται συχνά για τη μετάφραση εκφράσεων που περιέχουν Σειρά συνένωση και παρόμοια.

Μια διαδικασία επιφανειακά παρόμοια με την κανονική φόρτωση κλάσης εφαρμογών αναζητά έναν ορισμό κλάσης: πρώτα στο bootstrap classpath, μετά την επέκταση classpath και τέλος στο user classpath (-κατάπαθο). Εάν αφήσετε τα πάντα στις προεπιλογές, θα ισχύσουν οι ορισμοί από το J2SDK του javac "home" - κάτι που μπορεί να μην είναι σωστό, όπως φαίνεται από το γεια παράδειγμα.

Για να παρακάμψετε τις διαδρομές αναζήτησης του bootstrap και της κλάσης επέκτασης, χρησιμοποιείτε -το bootclasspath και -εξείδωτα επιλογές javac, αντίστοιχα. Αυτή η ικανότητα συμπληρώνει το -στόχος επιλογή με την έννοια ότι ενώ το τελευταίο ορίζει την ελάχιστη απαιτούμενη έκδοση JVM, η πρώτη επιλέγει τα βασικά API κλάσης διαθέσιμα στον δημιουργημένο κώδικα.

Θυμηθείτε ότι το ίδιο το javac γράφτηκε στην Java. Οι δύο επιλογές που μόλις ανέφερα επηρεάζουν την αναζήτηση τάξης για δημιουργία κώδικα byte. Το κάνουν δεν επηρεάζουν το bootstrap και τα classpath επέκτασης που χρησιμοποιεί το JVM για να εκτελέσει το javac ως πρόγραμμα Java (το τελευταίο θα μπορούσε να γίνει μέσω του - Τζ επιλογή, αλλά αυτό είναι αρκετά επικίνδυνο και οδηγεί σε μη υποστηριζόμενη συμπεριφορά). Για να το θέσω με άλλο τρόπο, το javac δεν φορτώνει πραγματικά κλάσεις από -το bootclasspath και -εξείδωτα; αναφέρεται απλώς στους ορισμούς τους.

Με την πρόσφατα ληφθείσα κατανόηση για την υποστήριξη cross-compilation του javac, ας δούμε πώς μπορεί να χρησιμοποιηθεί σε πρακτικές καταστάσεις.

Σενάριο 1: Στόχευση πλατφόρμας J2SE μίας βάσης

Αυτή είναι μια πολύ συνηθισμένη περίπτωση: αρκετές εκδόσεις J2SE υποστηρίζουν την εφαρμογή σας και συμβαίνει να μπορείτε να εφαρμόσετε τα πάντα μέσω βασικών API ορισμένων (θα το ονομάσω βάση) Έκδοση πλατφόρμας J2SE. Η ανοδική συμβατότητα φροντίζει τα υπόλοιπα. Αν και το J2SE 1.4 είναι η πιο πρόσφατη και καλύτερη έκδοση, δεν υπάρχει λόγος να αποκλείσετε χρήστες που δεν μπορούν να εκτελέσουν ακόμα το J2SE 1.4.

Ο ιδανικός τρόπος για τη σύνταξη της εφαρμογής σας είναι:

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

Ναι, αυτό σημαίνει ότι ίσως χρειαστεί να χρησιμοποιήσετε δύο διαφορετικές εκδόσεις J2SDK στο μηχάνημά σας κατασκευής: αυτή που επιλέγετε για το javac του και αυτή που είναι η βασική πλατφόρμα J2SE που υποστηρίζεται. Αυτό φαίνεται σαν μια επιπλέον προσπάθεια εγκατάστασης, αλλά στην πραγματικότητα είναι μια μικρή τιμή για να πληρώσετε για μια ισχυρή κατασκευή. Το κλειδί εδώ είναι να ελέγχετε ρητά τόσο τα γραμματόσημα έκδοσης της κατηγορίας όσο και το bootstrap classpath και να μην βασίζεστε στις προεπιλογές. Χρησιμοποιήστε το -πολύλογος επιλογή για επαλήθευση από πού προέρχονται οι ορισμοί της βασικής κλάσης.

Ως δευτερεύον σχόλιο, θα αναφέρω ότι είναι συνηθισμένο να περιλαμβάνονται οι προγραμματιστές rt.jar από τα J2SDK τους στο -κατάπαθο γραμμή (αυτό θα μπορούσε να είναι μια συνήθεια από το JDK 1,1 ημέρες όταν έπρεπε να προσθέσετε class.zip στο μονοπάτι συλλογής). Εάν ακολουθήσατε την παραπάνω συζήτηση, τώρα καταλαβαίνετε ότι αυτό είναι απολύτως περιττό, και στη χειρότερη περίπτωση, ενδέχεται να επηρεάσει τη σωστή σειρά πραγμάτων.

Σενάριο 2: Εναλλαγή κώδικα με βάση την έκδοση Java που εντοπίστηκε κατά το χρόνο εκτέλεσης

Εδώ θέλετε να είστε πιο εξελιγμένοι από ό, τι στο Σενάριο 1: Έχετε μια έκδοση πλατφόρμας Java που υποστηρίζεται από τη βάση, αλλά εάν ο κώδικάς σας εκτελείται σε μια υψηλότερη έκδοση Java, προτιμάτε να αξιοποιήσετε νεότερα API. Για παράδειγμα, μπορείτε να περάσετε με java.io. * API αλλά δεν θα πειράξει να επωφεληθούν java.nio. * βελτιώσεις σε ένα πιο πρόσφατο JVM εάν η ευκαιρία παρουσιάζεται.

Σε αυτό το σενάριο, η βασική προσέγγιση μεταγλώττισης μοιάζει με την προσέγγιση του σεναρίου 1, εκτός από το bootstrap J2SDK που θα πρέπει να είναι το ύψιστος έκδοση που πρέπει να χρησιμοποιήσετε:

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

Αυτό όμως δεν είναι αρκετό. πρέπει επίσης να κάνετε κάτι έξυπνο στον κώδικα Java σας, ώστε να κάνει το σωστό σε διαφορετικές εκδόσεις J2SE.

Μία εναλλακτική λύση είναι να χρησιμοποιήσετε έναν προεπεξεργαστή Java (με τουλάχιστον # ifdef / # else / # endif υποστήριξη) και πραγματικά δημιουργούν διαφορετικές εκδόσεις για διαφορετικές εκδόσεις πλατφόρμας J2SE. Αν και το J2SDK δεν διαθέτει κατάλληλη υποστήριξη προεπεξεργασίας, δεν υπάρχει έλλειψη τέτοιων εργαλείων στον Ιστό.

Ωστόσο, η διαχείριση πολλών διανομών για διαφορετικές πλατφόρμες J2SE είναι πάντα ένα επιπλέον βάρος. Με κάποια επιπλέον πρόβλεψη, μπορείτε να ξεφύγετε με τη διανομή μιας μόνο δόσης της εφαρμογής σας. Εδώ είναι ένα παράδειγμα για το πώς να το κάνετε αυτό (Δοκιμή URL1 είναι μια απλή τάξη που εξάγει διάφορα ενδιαφέροντα κομμάτια από μια διεύθυνση URL):