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

Ρίξτε μια εις βάθος ματιά στο Java Reflection API

Στο "Java In-Depth" του περασμένου μήνα, μίλησα για ενδοσκόπηση και τρόπους με τους οποίους μια τάξη Java με πρόσβαση σε ακατέργαστα δεδομένα κλάσης θα μπορούσε να κοιτάξει "μέσα" σε μια τάξη και να καταλάβει πώς κατασκευάστηκε η τάξη. Επιπλέον, έδειξα ότι με την προσθήκη ενός φορτωτή τάξης, αυτές οι τάξεις θα μπορούσαν να φορτωθούν στο περιβάλλον λειτουργίας και να εκτελεστούν. Αυτό το παράδειγμα είναι μια μορφή στατικός ενδοσκόπηση. Αυτό το μήνα θα ρίξω μια ματιά στο Java Reflection API, το οποίο δίνει στις τάξεις Java τη δυνατότητα να εκτελούν δυναμικός introspection: η δυνατότητα να κοιτάξετε μέσα σε τάξεις που έχουν ήδη φορτωθεί.

Η χρησιμότητα της ενδοσκόπησης

Ένα από τα πλεονεκτήματα της Java είναι ότι σχεδιάστηκε με την υπόθεση ότι το περιβάλλον στο οποίο λειτουργούσε θα άλλαζε δυναμικά. Οι τάξεις φορτώνονται δυναμικά, η δέσμευση γίνεται δυναμικά και οι παρουσίες αντικειμένων δημιουργούνται δυναμικά εν κινήσει όταν είναι απαραίτητες. Αυτό που δεν ήταν πολύ δυναμικό ιστορικά είναι η ικανότητα χειρισμού «ανώνυμων» τάξεων. Σε αυτό το πλαίσιο, μια ανώνυμη κλάση είναι αυτή που φορτώνεται ή παρουσιάζεται σε μια τάξη Java κατά το χρόνο εκτέλεσης και του οποίου ο τύπος ήταν προηγουμένως άγνωστος στο πρόγραμμα Java.

Ανώνυμα μαθήματα

Η υποστήριξη ανώνυμων τάξεων είναι δύσκολο να εξηγηθεί και ακόμη πιο δύσκολο να σχεδιαστεί σε ένα πρόγραμμα. Η πρόκληση της υποστήριξης μιας ανώνυμης τάξης μπορεί να δηλωθεί ως εξής: "Γράψτε ένα πρόγραμμα που, όταν του δοθεί ένα αντικείμενο Java, μπορεί να ενσωματώσει αυτό το αντικείμενο στη συνεχιζόμενη λειτουργία του." Η γενική λύση είναι μάλλον δύσκολη, αλλά περιορίζοντας το πρόβλημα, μπορούν να δημιουργηθούν ορισμένες εξειδικευμένες λύσεις. Υπάρχουν δύο παραδείγματα εξειδικευμένων λύσεων σε αυτήν την κατηγορία προβλημάτων στην έκδοση 1.0 της Java: Java applets και η έκδοση γραμμής εντολών του Java διερμηνέα.

Οι μικροεφαρμογές Java είναι κλάσεις Java που φορτώνονται από μια τρέχουσα εικονική μηχανή Java στο πλαίσιο ενός προγράμματος περιήγησης Web και επικαλούνται. Αυτές οι τάξεις Java είναι ανώνυμες επειδή ο χρόνος εκτέλεσης δεν γνωρίζει εκ των προτέρων τις απαραίτητες πληροφορίες για να καλέσει κάθε μεμονωμένη τάξη. Ωστόσο, το πρόβλημα της επίκλησης μιας συγκεκριμένης τάξης επιλύεται χρησιμοποιώντας την κλάση Java java.applet.Applet.

Κοινά σούπερ γυαλιά, όπως Applet, και Java interface, όπως AppletContext, αντιμετωπίστε το πρόβλημα των ανώνυμων τάξεων δημιουργώντας ένα συμβόλαιο που έχει συμφωνηθεί προηγουμένως. Συγκεκριμένα, ένας προμηθευτής περιβάλλοντος χρόνου εκτέλεσης διαφημίζει ότι μπορεί να χρησιμοποιήσει οποιοδήποτε αντικείμενο που συμμορφώνεται με μια καθορισμένη διεπαφή και ο καταναλωτής περιβάλλοντος χρόνου εκτέλεσης χρησιμοποιεί αυτήν την καθορισμένη διεπαφή σε οποιοδήποτε αντικείμενο σκοπεύει να παρέχει στον χρόνο εκτέλεσης. Στην περίπτωση των μικροεφαρμογών, υπάρχει μια καλά καθορισμένη διεπαφή με τη μορφή ενός κοινού superclass.

Το μειονέκτημα μιας κοινής λύσης superclass, ειδικά απουσία πολλαπλής κληρονομικότητας, είναι ότι τα αντικείμενα που έχουν κατασκευαστεί για εκτέλεση στο περιβάλλον δεν μπορούν επίσης να χρησιμοποιηθούν σε κάποιο άλλο σύστημα, εκτός εάν το σύστημα εφαρμόζει ολόκληρη τη σύμβαση. Στην περίπτωση του Applet διεπαφές, το περιβάλλον φιλοξενίας πρέπει να εφαρμόσει AppletContext. Αυτό σημαίνει για τη λύση applet είναι ότι η λύση λειτουργεί μόνο όταν φορτώνετε applet. Εάν βάλετε μια παρουσία ενός Hashtable Αντικείμενο στην ιστοσελίδα σας και δείξτε το πρόγραμμα περιήγησής σας σε αυτήν, θα αποτύχει να φορτώσει επειδή το σύστημα applet δεν μπορεί να λειτουργήσει εκτός του περιορισμένου εύρους του.

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

Το κίνητρο για μια πιο δυναμική λύση

Η πρόκληση με την υπάρχουσα αρχιτεκτονική Java 1.0 είναι ότι υπάρχουν προβλήματα που θα μπορούσαν να επιλυθούν με ένα πιο δυναμικό περιβάλλον ενδοσκόπησης - όπως στοιχεία φόρτωσης UI, προγράμματα οδήγησης συσκευών με δυνατότητα φόρτωσης σε λειτουργικό σύστημα Java και δυναμικά ρυθμιζόμενα περιβάλλοντα επεξεργασίας. Η "εφαρμογή killer" ή το ζήτημα που προκάλεσε τη δημιουργία του Java Reflection API, ήταν η ανάπτυξη ενός μοντέλου αντικειμένου αντικειμένου για την Java. Αυτό το μοντέλο είναι τώρα γνωστό ως JavaBeans.

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

Το Java Reflection API αναπτύχθηκε από τις ανάγκες του API στοιχείων διεπαφής χρήστη JavaBeans.

Τι είναι ο προβληματισμός;

Βασικά, το API Reflection αποτελείται από δύο στοιχεία: αντικείμενα που αντιπροσωπεύουν τα διάφορα μέρη ενός αρχείου κλάσης και ένα μέσο για την εξαγωγή αυτών των αντικειμένων με ασφαλή τρόπο. Το τελευταίο είναι πολύ σημαντικό, καθώς η Java παρέχει πολλές διασφαλίσεις ασφαλείας, και δεν θα είχε νόημα να παρέχουμε ένα σύνολο κατηγοριών που ακύρωσαν αυτές τις διασφαλίσεις.

Το πρώτο συστατικό του Reflection API είναι ο μηχανισμός που χρησιμοποιείται για τη λήψη πληροφοριών σχετικά με μια τάξη. Αυτός ο μηχανισμός είναι ενσωματωμένος στην κλάση που ονομάζεται Τάξη. Η ειδική τάξη Τάξη είναι ο γενικός τύπος για τις μετα-πληροφορίες που περιγράφουν αντικείμενα στο σύστημα Java. Οι φορτωτές τάξης στο σύστημα Java επιστρέφουν αντικείμενα τύπου Τάξη. Μέχρι τώρα οι τρεις πιο ενδιαφέρουσες μέθοδοι σε αυτήν την τάξη ήταν:

  • για Όνομα, η οποία θα φορτώσει μια κλάση ενός δεδομένου ονόματος, χρησιμοποιώντας τον τρέχοντα φορτωτή κλάσης

  • getName, που θα επέστρεφε το όνομα της τάξης ως Σειρά αντικείμενο, το οποίο ήταν χρήσιμο για τον προσδιορισμό των αναφορών αντικειμένων με το όνομα της τάξης τους

  • newInstance, η οποία θα επικαλούσε τον μηδενικό κατασκευαστή στην κλάση (εάν υπάρχει) και θα σας επιστρέψει μια παρουσία αντικειμένου αυτής της κλάσης αντικειμένου

Σε αυτές τις τρεις χρήσιμες μεθόδους το Reflection API προσθέτει μερικές επιπλέον μεθόδους στην τάξη Τάξη. Αυτά είναι τα εξής:

  • getConstructor, getConstructors, getDeclaredConstructor
  • getMethod, getMethods, getDeclaredMethods
  • getField, getFields, getDeclaredFields
  • getSuperclass
  • getInterfaces
  • getDeclaredClasses

Εκτός από αυτές τις μεθόδους, προστέθηκαν πολλές νέες τάξεις για να αντιπροσωπεύσουν τα αντικείμενα που θα επέστρεφαν αυτές οι μέθοδοι. Τα νέα μαθήματα είναι κυρίως μέρος του java.lang.reflect πακέτο, αλλά μερικές από τις νέες κατηγορίες βασικού τύπου (Κενός, Ψηφιόλεξη, και ούτω καθεξής) βρίσκονται στο java.lang πακέτο. Η απόφαση λήφθηκε για να τοποθετηθούν οι νέες τάξεις όπου βρίσκονται τοποθετώντας τάξεις που αντιπροσώπευαν μετα-δεδομένα στο πακέτο προβληματισμού και τάξεις που αντιπροσώπευαν τύπους στο πακέτο γλώσσας.

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

Πώς χρησιμοποιώ το Reflection API;

Η ερώτηση "Πώς μπορώ να χρησιμοποιήσω το API;" είναι ίσως η πιο ενδιαφέρουσα ερώτηση από το "Τι είναι ο προβληματισμός;"

Το Reflection API είναι συμμετρικός, που σημαίνει ότι εάν κρατάτε ένα Τάξη αντικείμενο, μπορείτε να ρωτήσετε για τα εσωτερικά του και αν έχετε ένα από τα εσωτερικά, μπορείτε να το ρωτήσετε ποια τάξη το έχει δηλώσει. Έτσι μπορείτε να μετακινηθείτε εμπρός και πίσω από κλάση σε μέθοδο σε παράμετρο σε κλάση σε μέθοδο, και ούτω καθεξής. Μια ενδιαφέρουσα χρήση αυτής της τεχνολογίας είναι να ανακαλύψουμε τις περισσότερες από τις αλληλεξαρτήσεις μεταξύ μιας συγκεκριμένης κλάσης και του υπόλοιπου συστήματος.

Ένα λειτουργικό παράδειγμα

Σε πιο πρακτικό επίπεδο, ωστόσο, μπορείτε να χρησιμοποιήσετε το Reflection API για να εγκαταλείψετε μια τάξη, όπως και η δική μου χωματερή τάξη έκανε στη στήλη του περασμένου μήνα.

Για να δείξω το API προβληματισμού, έγραψα μια τάξη που ονομάζεται ReflectClass που θα έπαιρνε μια κλάση γνωστή στον Java χρόνο εκτέλεσης (που σημαίνει ότι βρίσκεται στη διαδρομή της τάξης σας κάπου) και, μέσω του Reflection API, απορρίψτε τη δομή του στο παράθυρο του τερματικού. Για να πειραματιστείτε με αυτήν την τάξη, θα πρέπει να έχετε μια έκδοση 1.1 του JDK διαθέσιμη.

Σημείωση: Να δεν Προσπαθήστε να χρησιμοποιήσετε έναν χρόνο εκτέλεσης 1,0 καθώς μπερδεύεται, συνήθως με αποτέλεσμα μια ασυμβίβαστη εξαίρεση αλλαγής τάξης.

Η τάξη ReflectClass ξεκινά ως εξής:

εισαγωγή java.lang.reflect. *; εισαγωγή java.util. *; δημόσια τάξη ReflectClass { 

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

 public static void main (String args []) {Κατασκευαστής cn []; Κατηγορία cc []; Μέθοδος mm []; Πεδίο ff []; Κατηγορία c = null; Κατηγορία supClass; Συμβολοσειρά x, y, s1, s2, s3; Hashtable classRef = νέο Hashtable (); if (args.length == 0) {System.out.println ("Προσδιορίστε ένα όνομα κλάσης στη γραμμή εντολών."); System.exit (1); } δοκιμάστε {c = Class.forName (args [0]); } catch (ClassNotFoundException ee) {System.out.println ("Δεν ήταν δυνατή η εύρεση κλάσης '" + args [0] + "'"); System.exit (1); } 

Η μέθοδος κύριος δηλώνει πίνακες κατασκευαστών, πεδίων και μεθόδων. Αν θυμάστε, αυτά είναι τρία από τα τέσσερα βασικά μέρη του αρχείου τάξης. Το τέταρτο μέρος είναι τα χαρακτηριστικά, στα οποία δυστυχώς το Reflection API δεν σας δίνει πρόσβαση. Μετά τους πίνακες, έχω κάνει κάποια επεξεργασία γραμμής εντολών. Εάν ο χρήστης έχει πληκτρολογήσει ένα όνομα κλάσης, ο κωδικός προσπαθεί να το φορτώσει χρησιμοποιώντας το για Όνομα μέθοδος τάξης Τάξη. ο για Όνομα Η μέθοδος λαμβάνει ονόματα κλάσης Java, όχι ονόματα αρχείων, έτσι ώστε να κοιτάξουμε μέσα στο java.math.BigInteger τάξη, απλά πληκτρολογείτε "java ReflectClass java.math.BigInteger", αντί να επισημάνετε πού είναι πραγματικά αποθηκευμένο το αρχείο κλάσης.

Προσδιορισμός του πακέτου της τάξης

Υποθέτοντας ότι βρέθηκε το αρχείο τάξης, ο κώδικας προχωρά στο Βήμα 0, το οποίο φαίνεται παρακάτω.

 / * * Βήμα 0: Εάν το όνομά μας περιέχει τελείες, είμαστε σε ένα πακέτο, οπότε βάλτε το * πρώτα. * / x = c.getName (); y = x.substring (0, x.lastIndexOf (".")); if (y.length ()> 0) {System.out.println ("πακέτο" + y + "; \ n \ r"); } 

Σε αυτό το βήμα, το όνομα της κλάσης ανακτάται χρησιμοποιώντας το getName μέθοδος στην τάξη Τάξη. Αυτή η μέθοδος επιστρέφει το πλήρως αναγνωρισμένο όνομα και εάν το όνομα περιέχει τελείες, μπορούμε να υποθέσουμε ότι η κλάση ορίστηκε ως μέρος ενός πακέτου. Έτσι, το Βήμα 0 είναι να διαχωρίσετε το τμήμα ονόματος πακέτου από το τμήμα ονόματος κλάσης και να εκτυπώσετε το μέρος ονόματος πακέτου σε μια γραμμή που ξεκινά με "πακέτο ...."

Συλλογή αναφορών κλάσης από δηλώσεις και παραμέτρους

Με τη δήλωση πακέτου που έχει ληφθεί υπόψη, προχωράμε στο Βήμα 1, το οποίο είναι η συλλογή όλων άλλα ονόματα τάξεων που αναφέρονται από αυτήν την τάξη. Αυτή η διαδικασία συλλογής εμφανίζεται στον παρακάτω κώδικα. Να θυμάστε ότι τα τρία πιο κοινά μέρη όπου αναφέρονται τα ονόματα τάξεων είναι ως τύποι για πεδία (μεταβλητές παρουσίας), τύποι επιστροφής για μεθόδους και ως τύποι παραμέτρων που μεταβιβάζονται σε μεθόδους και κατασκευαστές.

 ff = c.getDeclaredFields (); για (int i = 0; i <ff.length; i ++) {x = tName (ff [i] .getType (). getName (), classRef); } 

Στον παραπάνω κώδικα, ο πίνακας στ αρχικοποιείται ως πίνακας Πεδίο αντικείμενα. Ο βρόχος συλλέγει το όνομα τύπου από κάθε πεδίο και το επεξεργάζεται μέσω του t Όνομα μέθοδος. ο t Όνομα Η μέθοδος είναι ένας απλός βοηθός που επιστρέφει το σύντομο όνομα για έναν τύπο. Έτσι java.lang.String γίνεται Σειρά. Και σημειώνει σε ένα hashtable ποια αντικείμενα έχουν δει. Σε αυτό το στάδιο, ο κώδικας ενδιαφέρεται περισσότερο για τη συλλογή αναφορών τάξεων παρά για την εκτύπωση.

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

 cn = c.getDeclaredConstructors (); για (int i = 0; i 0) {για (int j = 0; j <cx.length; j ++) {x = tName (cx [j] .getName (), classRef); }}} 

Όπως μπορείτε να δείτε, έχω χρησιμοποιήσει το τύποι getParameter μέθοδος στο Κατασκευαστής τάξη για να με τροφοδοτήσει όλες τις παραμέτρους που παίρνει ένας συγκεκριμένος κατασκευαστής. Αυτά στη συνέχεια υποβάλλονται σε επεξεργασία μέσω του t Όνομα μέθοδος.

Ένα ενδιαφέρον πράγμα που πρέπει να σημειωθεί εδώ είναι η διαφορά μεταξύ της μεθόδου getDeclaredConstructors και τη μέθοδο getConstructors. Και οι δύο μέθοδοι επιστρέφουν μια σειρά κατασκευαστών, αλλά το getConstructors Η μέθοδος επιστρέφει μόνο τους κατασκευαστές που είναι προσβάσιμοι στην τάξη σας Αυτό είναι χρήσιμο εάν θέλετε να μάθετε εάν μπορείτε πραγματικά να επικαλεστείτε τον κατασκευαστή που έχετε βρει, αλλά δεν είναι χρήσιμο για αυτήν την εφαρμογή, επειδή θέλω να εκτυπώσω όλους τους κατασκευαστές της τάξης, δημόσιο ή όχι. Οι ανακλαστήρες πεδίου και μεθόδου έχουν επίσης παρόμοιες εκδόσεις, μία για όλα τα μέλη και μία μόνο για δημόσια μέλη.

Το τελικό βήμα, που φαίνεται παρακάτω, είναι η συλλογή των αναφορών από όλες τις μεθόδους. Αυτός ο κωδικός πρέπει να λαμβάνει αναφορές τόσο από τον τύπο της μεθόδου (παρόμοια με τα παραπάνω πεδία) όσο και από τις παραμέτρους (παρόμοια με τους παραπάνω κατασκευαστές).

 mm = c.getDeclaredMethods (); για (int i = 0; i 0) {για (int j = 0; j <cx.length; j ++) {x = tName (cx [j] .getName (), classRef); }}} 

Στον παραπάνω κώδικα, υπάρχουν δύο κλήσεις προς t Όνομα - ένα για τη συλλογή του τύπου επιστροφής και ένα για τη συλλογή του τύπου κάθε παραμέτρου.

$config[zx-auto] not found$config[zx-overlay] not found