Perhatikan Java Reflection API secara mendalam

Dalam "Java In-Depth" bulan lalu, saya berbicara tentang introspeksi dan cara di mana kelas Java dengan akses ke data kelas mentah dapat melihat "di dalam" kelas dan mencari tahu bagaimana kelas itu dibangun. Lebih lanjut, saya menunjukkan bahwa dengan tambahan class loader, class tersebut dapat dimuat ke dalam lingkungan yang sedang berjalan dan dieksekusi. Contoh itu adalah bentuk introspeksi statis . Bulan ini saya akan melihat Java Reflection API, yang memberi kelas Java kemampuan untuk melakukan introspeksi dinamis : kemampuan untuk melihat ke dalam kelas yang sudah dimuat.

Kegunaan introspeksi

Salah satu kelebihan Java adalah ia dirancang dengan asumsi bahwa lingkungan tempat Java dijalankan akan berubah secara dinamis. Kelas dimuat secara dinamis, pengikatan dilakukan secara dinamis, dan instance objek dibuat secara dinamis dengan cepat saat diperlukan. Apa yang tidak terlalu dinamis secara historis adalah kemampuan untuk memanipulasi kelas "anonim". Dalam konteks ini, kelas anonim adalah kelas yang dimuat atau disajikan ke kelas Java pada waktu proses dan yang tipenya sebelumnya tidak dikenal oleh program Java.

Kelas anonim

Mendukung kelas anonim sulit untuk dijelaskan dan bahkan lebih sulit untuk dirancang dalam sebuah program. Tantangan untuk mendukung kelas anonim dapat dinyatakan seperti ini: "Tulis program yang, ketika diberi objek Java, dapat memasukkan objek itu ke dalam operasi berkelanjutannya." Solusi umum agak sulit, tetapi dengan membatasi masalah, beberapa solusi khusus dapat dibuat. Ada dua contoh solusi khusus untuk kelas masalah ini di Java versi 1.0: applet Java dan interpreter Java versi baris perintah.

Applet Java adalah kelas Java yang dimuat oleh mesin virtual Java yang sedang berjalan dalam konteks browser Web dan dipanggil. Kelas-kelas Java ini bersifat anonim karena waktu proses tidak mengetahui sebelumnya informasi yang diperlukan untuk memanggil setiap kelas individu. Namun, masalah pemanggilan kelas tertentu diselesaikan dengan menggunakan kelas Java java.applet.Applet.

Superkelas umum, like Applet, dan antarmuka Java, like AppletContext, mengatasi masalah kelas anonim dengan membuat kontrak yang telah disepakati sebelumnya. Secara khusus, pemasok lingkungan runtime mengiklankan bahwa dia dapat menggunakan objek apa pun yang sesuai dengan antarmuka yang ditentukan, dan konsumen lingkungan runtime menggunakan antarmuka yang ditentukan dalam objek apa pun yang ingin dia suplai ke waktu proses. Dalam kasus applet, antarmuka yang ditentukan dengan baik ada dalam bentuk superclass umum.

Kelemahan dari solusi superclass umum, terutama dengan tidak adanya multiple inheritance, adalah bahwa objek yang dibangun untuk berjalan di lingkungan juga tidak dapat digunakan di beberapa sistem lain kecuali sistem tersebut mengimplementasikan seluruh kontrak. Dalam kasus Appletantarmuka, lingkungan hosting harus diimplementasikan AppletContext. Apa artinya bagi solusi applet adalah solusinya hanya bekerja saat Anda memuat applet. Jika Anda meletakkan sebuah instance dari suatu Hashtableobjek di halaman Web Anda dan mengarahkan browser Anda ke situ, itu akan gagal untuk dimuat karena sistem applet tidak dapat beroperasi di luar jangkauan terbatasnya.

Selain contoh applet, introspeksi membantu memecahkan masalah yang saya sebutkan bulan lalu: mencari tahu cara memulai eksekusi di kelas yang baru saja dimuat versi baris perintah dari mesin virtual Java. Dalam contoh itu, mesin virtual harus memanggil beberapa metode statis di kelas yang dimuat. Secara konvensi, metode itu dinamai maindan mengambil satu argumen - sebuah array Stringobjek.

Motivasi untuk solusi yang lebih dinamis

Tantangan dengan arsitektur Java 1.0 yang ada adalah terdapat masalah yang dapat diselesaikan dengan lingkungan introspeksi yang lebih dinamis - seperti komponen UI yang dapat dimuat, driver perangkat yang dapat dimuat dalam OS berbasis Java, dan lingkungan pengeditan yang dapat dikonfigurasi secara dinamis. "Aplikasi pembunuh", atau masalah yang menyebabkan Java Reflection API dibuat, adalah pengembangan model komponen objek untuk Java. Model itu sekarang dikenal sebagai JavaBeans.

Komponen antarmuka pengguna adalah titik desain yang ideal untuk sistem introspeksi karena mereka memiliki dua konsumen yang sangat berbeda. Di satu sisi, objek komponen dihubungkan bersama untuk membentuk antarmuka pengguna sebagai bagian dari beberapa aplikasi. Alternatifnya, perlu ada antarmuka untuk alat yang memanipulasi komponen pengguna tanpa harus mengetahui apa komponennya, atau, yang lebih penting, tanpa akses ke kode sumber komponen.

Java Reflection API tumbuh dari kebutuhan API komponen antarmuka pengguna JavaBeans.

Apakah refleksi itu?

Pada dasarnya, Reflection API terdiri dari dua komponen: objek yang mewakili berbagai bagian file kelas, dan sarana untuk mengekstrak objek tersebut dengan cara yang aman dan terjamin. Yang terakhir ini sangat penting, karena Java menyediakan banyak pengaman keamanan, dan tidak masuk akal untuk menyediakan sekumpulan kelas yang membatalkan pengaman tersebut.

Komponen pertama dari Reflection API adalah mekanisme yang digunakan untuk mengambil informasi tentang sebuah kelas. Mekanisme ini dibangun ke dalam kelas bernama Class. Kelas khusus Classadalah tipe universal untuk informasi meta yang mendeskripsikan objek dalam sistem Java. Pemuat kelas dalam sistem Java mengembalikan objek bertipe Class. Sampai saat ini tiga metode yang paling menarik di kelas ini adalah:

  • forName, yang akan memuat kelas dari nama tertentu, menggunakan pemuat kelas saat ini

  • getName, yang akan mengembalikan nama kelas sebagai Stringobjek, yang berguna untuk mengidentifikasi referensi objek dengan nama kelasnya

  • newInstance, yang akan memanggil konstruktor null pada kelas (jika ada) dan mengembalikan Anda sebuah instance objek dari kelas objek tersebut

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

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

Selain metode ini, banyak kelas baru ditambahkan untuk mewakili objek yang akan dikembalikan metode ini. Kelas-kelas baru sebagian besar adalah bagian dari java.lang.reflectpaket, tetapi beberapa jenis kelas dasar baru ( Void, Byte, dan sebagainya) berada di java.langpaket. Keputusan diambil untuk menempatkan kelas-kelas baru di tempat mereka berada dengan meletakkan kelas-kelas yang mewakili meta-data dalam paket refleksi dan kelas-kelas yang mewakili tipe-tipe dalam paket bahasa.

Dengan demikian, API Refleksi mewakili sejumlah perubahan pada kelas Classyang memungkinkan Anda mengajukan pertanyaan tentang internal kelas, dan sekumpulan kelas yang mewakili jawaban yang diberikan metode baru ini kepada Anda.

Bagaimana cara menggunakan Reflection API?

Pertanyaan "Bagaimana cara menggunakan API?" mungkin pertanyaan yang lebih menarik daripada "Apakah refleksi itu?"

Reflection API bersifat simetris , yang berarti bahwa jika Anda memegang sebuah Classobjek, Anda dapat menanyakan tentang internalnya, dan jika Anda memiliki salah satu internal, Anda dapat menanyakannya kelas mana yang menyatakannya. Dengan demikian Anda dapat berpindah-pindah dari kelas ke metode ke parameter ke kelas ke metode, dan seterusnya. Salah satu penggunaan yang menarik dari teknologi ini adalah untuk mengetahui sebagian besar interdependensi antara kelas tertentu dan sistem lainnya.

Contoh kerja

Pada tingkat yang lebih praktis, bagaimanapun, Anda dapat menggunakan API Refleksi untuk membuang kelas, seperti yang dumpclassdilakukan kelas saya di kolom 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, yang ditunjukkan di bawah, adalah mengumpulkan referensi dari semua metode. Kode ini harus mendapatkan referensi dari kedua jenis metode (mirip dengan bidang di atas) dan dari parameter (mirip dengan konstruktor 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 kode di atas, ada dua panggilan ke tName- satu untuk mengumpulkan tipe kembalian dan satu lagi untuk mengumpulkan tipe setiap parameter.