Lihatlah API Refleksi Java secara mendalam

Dalam "Java In-Depth" bulan lalu, saya berbicara mengenai introspeksi dan cara di mana kelas Java dengan akses ke data kelas mentah dapat melihat "di dalam" kelas dan mengetahui bagaimana kelas itu dibina. Selanjutnya, saya menunjukkan bahawa dengan penambahan kelas loader, kelas tersebut dapat dimuat ke dalam lingkungan berjalan dan dijalankan. Contoh itu adalah bentuk introspeksi statik . Bulan ini saya akan melihat Java Reflection API, yang memberi kelas Java kemampuan untuk melakukan introspeksi dinamik : kemampuan untuk melihat ke dalam kelas yang sudah dimuat.

Utiliti introspeksi

Salah satu kekuatan Java adalah ia dirancang dengan asumsi bahwa lingkungan di mana ia berjalan akan berubah secara dinamis. Kelas dimuat secara dinamik, pengikatan dilakukan secara dinamik, dan keadaan objek dibuat secara dinamis dengan cepat ketika diperlukan. Apa yang tidak begitu dinamik dari segi sejarah ialah kemampuan memanipulasi kelas "tanpa nama". Dalam konteks ini, kelas tanpa nama adalah kelas yang dimuat atau disajikan ke kelas Java pada waktu berjalan dan yang jenisnya sebelumnya tidak diketahui oleh program Java.

Kelas tanpa nama

Menyokong kelas tanpa nama sukar untuk dijelaskan dan lebih sukar untuk dirancang dalam program. Cabaran untuk menyokong kelas tanpa nama dapat dinyatakan seperti ini: "Tulis program yang, ketika diberi objek Java, dapat memasukkan objek itu ke dalam operasi yang terus berlanjut." Penyelesaian umum agak sukar, tetapi dengan mengekang masalahnya, beberapa penyelesaian khusus dapat dibuat. Terdapat dua contoh penyelesaian khusus untuk kelas masalah ini dalam versi Java 1.0: applet Java dan versi baris perintah juru bahasa Java.

Java applet adalah kelas Java yang dimuat oleh mesin maya Java yang berjalan dalam konteks penyemak imbas Web dan dipanggil. Kelas Java ini tidak dikenali kerana masa larian tidak mengetahui lebih awal maklumat yang diperlukan untuk memanggil setiap kelas. Namun, masalah untuk memanggil kelas tertentu diselesaikan dengan menggunakan kelas Java java.applet.Applet.

Superclasses biasa, seperti Applet, dan antaramuka Java, seperti AppletContext, mengatasi masalah kelas tanpa nama dengan membuat kontrak yang telah dipersetujui sebelumnya. Khususnya, pembekal lingkungan waktu proses mengiklankan bahawa dia dapat menggunakan objek apa pun yang sesuai dengan antarmuka yang ditentukan, dan pengguna lingkungan runtime menggunakan antara muka yang ditentukan dalam objek apa pun yang ingin dia sampaikan ke waktu berjalan. Dalam kes applet, antara muka yang ditentukan dengan baik wujud dalam bentuk superclass biasa.

Kelemahan penyelesaian superclass yang biasa, terutama jika tidak ada banyak warisan, adalah bahawa objek yang dibina untuk dijalankan di persekitaran juga tidak dapat digunakan di beberapa sistem lain kecuali sistem itu menerapkan keseluruhan kontrak. Dalam kes Appletantaramuka, lingkungan hosting harus dilaksanakan AppletContext. Apa yang dimaksudkan dengan penyelesaian applet ialah penyelesaiannya hanya berfungsi semasa anda memuat applet. Sekiranya anda meletakkan contoh Hashtableobjek di halaman Web anda dan mengarahkan penyemak imbas anda ke sana, ia akan gagal dimuat kerana sistem applet tidak dapat beroperasi di luar jangkauan terhadnya.

Sebagai tambahan kepada contoh applet, introspeksi membantu menyelesaikan masalah yang saya sebutkan bulan lalu: mencari tahu bagaimana memulakan pelaksanaan di kelas yang baru dimuat versi baris perintah mesin maya Java. Dalam contoh itu, mesin maya harus menggunakan beberapa kaedah statik di kelas yang dimuatkan. Secara konvensional, kaedah itu dinamakan maindan mengambil satu argumen - pelbagai Stringobjek.

Motivasi untuk penyelesaian yang lebih dinamik

Tantangan dengan seni bina Java 1.0 yang ada adalah ada masalah yang dapat diselesaikan oleh lingkungan introspeksi yang lebih dinamis - seperti komponen UI yang dapat dimuat, pemacu peranti yang dapat dimuat dalam OS berbasis Java, dan lingkungan penyuntingan yang dapat dikonfigurasi secara dinamis. "Aplikasi pembunuh," atau masalah yang menyebabkan Java Reflection API dibuat, adalah pengembangan model komponen objek untuk Java. Model itu kini dikenali sebagai JavaBeans.

Komponen antara muka pengguna adalah titik reka bentuk yang ideal untuk sistem introspeksi kerana mempunyai dua pengguna yang sangat berbeza. Di satu pihak, objek komponen dihubungkan bersama untuk membentuk antara muka pengguna sebagai sebahagian daripada beberapa aplikasi. Sebagai alternatif, perlu ada antara muka untuk alat yang memanipulasi komponen pengguna tanpa perlu mengetahui komponen apa, atau yang lebih penting lagi, tanpa akses ke kod sumber komponen.

Java Reflection API berkembang dari keperluan API komponen antara muka pengguna JavaBeans.

Apa itu refleksi?

Pada dasarnya, Reflection API terdiri daripada dua komponen: objek yang mewakili pelbagai bahagian fail kelas, dan alat untuk mengekstrak objek tersebut dengan cara yang selamat dan selamat. Yang terakhir sangat penting, kerana Java menyediakan banyak perlindungan keselamatan, dan tidak masuk akal untuk menyediakan sekumpulan kelas yang membatalkan perlindungan tersebut.

Komponen pertama API Refleksi adalah mekanisme yang digunakan untuk mengambil maklumat mengenai kelas. Mekanisme ini dibina ke dalam kelas yang dinamakan Class. Kelas khas Classadalah jenis sejagat untuk maklumat meta yang menerangkan objek dalam sistem Java. Pemuat kelas dalam sistem Java mengembalikan objek jenis Class. Sehingga kini tiga kaedah yang paling menarik di kelas ini adalah:

  • forName, yang akan memuat kelas nama tertentu, menggunakan pemuat kelas semasa

  • getName, yang akan mengembalikan nama kelas sebagai Stringobjek, yang berguna untuk mengenal pasti rujukan objek dengan nama kelas mereka

  • newInstance, yang akan memanggil konstruktor nol di kelas (jika ada) dan mengembalikan anda contoh objek kelas objek

Untuk ketiga kaedah yang berguna ini, Reflection API menambahkan beberapa kaedah tambahan ke kelas Class. Ini adalah seperti berikut:

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

Sebagai tambahan kepada kaedah ini, banyak kelas baru ditambahkan untuk mewakili objek yang akan dikembalikan oleh kaedah ini. Kelas-kelas baru kebanyakannya adalah sebahagian daripada java.lang.reflectpakej, tetapi beberapa kelas jenis asas baru ( Void, Byte, dan sebagainya) adalah dalam java.langpakej. Keputusan dibuat untuk meletakkan kelas baru di mana mereka berada dengan meletakkan kelas yang mewakili meta-data dalam paket refleksi dan kelas yang mewakili jenis dalam paket bahasa.

Oleh itu, Reflection API mewakili sejumlah perubahan pada kelas Classyang membolehkan anda mengemukakan soalan mengenai dalaman kelas, dan sekumpulan kelas yang mewakili jawapan yang diberikan oleh kaedah baru ini.

Bagaimana saya menggunakan Reflection API?

Soalan "Bagaimana saya menggunakan API?" mungkin soalan yang lebih menarik daripada "Apa itu refleksi?"

Refleksi API adalah simetrik , yang bermaksud bahawa jika anda memegang Classobjek, anda boleh bertanya tentang dalamannya, dan jika anda mempunyai salah satu dalaman, anda boleh bertanya kelas mana yang menyatakannya. Oleh itu, anda boleh bergerak pergi dan balik dari kelas ke kaedah ke parameter ke kelas ke kaedah, dan seterusnya. Salah satu penggunaan teknologi ini yang menarik adalah untuk mengetahui sebahagian besar saling bergantung antara kelas tertentu dan sistem yang lain.

Contoh yang berfungsi

Namun, pada tahap yang lebih praktikal, anda boleh menggunakan Reflection API untuk membuang kelas, seperti yang dilakukan oleh dumpclasskelas saya pada ruangan bulan lalu.

To demonstrate the Reflection API, I wrote a class called ReflectClass that would take a class known to the Java run time (meaning it is in your class path somewhere) and, through the Reflection API, dump out its structure to the terminal window. To experiment with this class, you will need to have a 1.1 version of the JDK available.

Note: Do not try to use a 1.0 run time as it gets all confused, usually resulting in an incompatible class change exception.

The class ReflectClass begins as follows:

import java.lang.reflect.*; import java.util.*; public class ReflectClass { 

As you can see above, the first thing the code does is import the Reflection API classes. Next, it jumps right into the main method, which starts out as shown below.

 public static void main(String args[]) { Constructor cn[]; Class cc[]; Method mm[]; Field ff[]; Class c = null; Class supClass; String x, y, s1, s2, s3; Hashtable classRef = new Hashtable(); if (args.length == 0) { System.out.println("Please specify a class name on the command line."); System.exit(1); } try { c = Class.forName(args[0]); } catch (ClassNotFoundException ee) { System.out.println("Couldn't find class '"+args[0]+"'"); System.exit(1); } 

The method main declares arrays of constructors, fields, and methods. If you recall, these are three of the four fundamental parts of the class file. The fourth part is the attributes, which the Reflection API unfortunately does not give you access to. After the arrays, I've done some command-line processing. If the user has typed a class name, the code attempts to load it using the forName method of class Class. The forName method takes Java class names, not file names, so to look inside the java.math.BigInteger class, you simply type "java ReflectClass java.math.BigInteger," rather than point out where the class file actually is stored.

Identifying the class's package

Assuming the class file is found, the code proceeds into Step 0, which is shown below.

 /* * Step 0: If our name contains dots we're in a package so put * that out first. */ x = c.getName(); y = x.substring(0, x.lastIndexOf(".")); if (y.length() > 0) { System.out.println("package "+y+";\n\r"); } 

In this step, the name of the class is retrieved using the getName method in class Class. This method returns the fully qualified name, and if the name contains dots, we can presume that the class was defined as part of a package. So Step 0 is to separate the package name part from the class name part, and print out the package name part on a line that starts with "package...."

Collecting class references from declarations and parameters

With the package statement taken care of, we proceed to Step 1, which is to collect all of the other class names that are referenced by this class. This collection process is shown in the code below. Remember that the three most common places where class names are referenced are as types for fields (instance variables), return types for methods, and as the types of the parameters passed to methods and constructors.

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

In the above code, the array ff is initialized to be an array of Field objects. The loop collects the type name from each field and process it through the tName method. The tName method is a simple helper that returns the shorthand name for a type. So java.lang.String becomes String. And it notes in a hashtable which objects have been seen. At this stage, the code is more interested in collecting class references than in printing.

The next source of class references are the parameters supplied to constructors. The next piece of code, shown below, processes each declared constructor and collects the references from the parameter lists.

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

As you can see, I've used the getParameterTypes method in the Constructor class to feed me all of the parameters that a particular constructor takes. These are then processed through the tName method.

An interesting thing to note here is the difference between the method getDeclaredConstructors and the method getConstructors. Both methods return an array of constructors, but the getConstructors method only returns those constructors that are accessible to your class. This is useful if you want to know if you actually can invoke the constructor you've found, but it isn't useful for this application because I want to print out all of the constructors in the class, public or not. The field and method reflectors also have similar versions, one for all members and one only for public members.

Langkah terakhir, ditunjukkan di bawah, adalah mengumpulkan rujukan dari semua kaedah. Kod ini mesti mendapatkan rujukan dari kedua-dua jenis kaedah (serupa dengan bidang di atas) dan dari parameter (serupa dengan pembangun di atas).

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

Dalam kod di atas, terdapat dua panggilan untuk tName- satu untuk mengumpulkan jenis pengembalian dan satu untuk mengumpulkan jenis parameter masing-masing.