JavaRush /Blog Java /Random-MS /Refleksi dalam Java - Contoh Penggunaan

Refleksi dalam Java - Contoh Penggunaan

Diterbitkan dalam kumpulan
Anda mungkin pernah menemui konsep "refleksi" dalam kehidupan seharian. Biasanya perkataan ini merujuk kepada proses mengkaji diri sendiri. Dalam pengaturcaraan, ia mempunyai makna yang sama - ia adalah mekanisme untuk memeriksa data tentang program, serta mengubah struktur dan tingkah laku program semasa pelaksanaannya. Perkara penting di sini ialah ia dilakukan pada masa larian, bukan pada masa penyusunan. Tetapi mengapa memeriksa kod semasa runtime? Anda sudah melihatnya :/ Contoh penggunaan Refleksi - 1Idea refleksi mungkin tidak jelas dengan serta-merta atas satu sebab: sehingga saat ini, anda sentiasa mengetahui kelas yang anda sedang bekerja. Nah, sebagai contoh, anda boleh menulis kelas Cat:
package learn.javarush;

public class Cat {

   private String name;
   private int age;

   public Cat(String name, int age) {
       this.name = name;
       this.age = age;
   }

   public void sayMeow() {

       System.out.println("Meow!");
   }

   public void jump() {

       System.out.println("Jump!");
   }

   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
   }

   public int getAge() {
       return age;
   }

   public void setAge(int age) {
       this.age = age;
   }

@Override
public String toString() {
   return "Cat{" +
           "name='" + name + '\'' +
           ", age=" + age +
           '}';
}

}
Anda tahu segala-galanya mengenainya, anda melihat bidang dan kaedah yang ada. Sudah tentu anda boleh mencipta sistem warisan dengan kelas biasa untuk kemudahan Animal, jika tiba-tiba program memerlukan kelas haiwan lain. Sebelum ini, kami juga telah mencipta kelas klinik veterinar di mana anda boleh lulus objek induk Animal, dan program ini akan merawat haiwan itu bergantung pada sama ada ia anjing atau kucing. Walaupun tugasan ini tidak begitu mudah, program ini mempelajari semua maklumat yang diperlukan tentang kelas pada masa penyusunan. Oleh itu, apabila anda main()lulus objek dalam kaedah Catkepada kaedah kelas klinik veterinar, program itu sudah tahu bahawa ini adalah kucing, bukan anjing. Sekarang mari kita bayangkan bahawa kita berhadapan dengan tugas lain. Matlamat kami adalah untuk menulis penganalisis kod. Kita perlu mencipta kelas CodeAnalyzerdengan satu kaedah - void analyzeClass(Object o). Kaedah ini hendaklah:
  • tentukan kelas apa objek itu dihantar kepadanya dan paparkan nama kelas dalam konsol;
  • tentukan nama semua medan kelas ini, termasuk yang peribadi, dan paparkannya dalam konsol;
  • tentukan nama semua kaedah kelas ini, termasuk kaedah persendirian, dan paparkannya dalam konsol.
Ia akan kelihatan seperti ini:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

       //Вывести название класса, к которому принадлежит an object o
       //Вывести названия всех переменных этого класса
       //Вывести названия всех методов этого класса
   }

}
Kini perbezaan antara masalah ini dan masalah lain yang anda selesaikan sebelum ini dapat dilihat. Dalam kes ini, kesukarannya terletak pada hakikat bahawa anda mahupun program tidak tahu apa sebenarnya yang akan dihantar kepada kaedah analyzeClass(). Anda menulis program, pengaturcara lain akan mula menggunakannya, yang boleh menghantar apa-apa ke dalam kaedah ini - mana-mana kelas Java standard atau mana-mana kelas yang telah mereka tulis. Kelas ini boleh mempunyai sebarang bilangan pembolehubah dan kaedah. Dalam erti kata lain, dalam kes ini kami (dan program kami) tidak tahu kelas apa yang akan kami bekerjasama. Namun, kita mesti menyelesaikan masalah ini. Dan di sini perpustakaan Java standard datang untuk membantu kami - Java Reflection API. Reflection API ialah ciri bahasa yang berkuasa. Dokumentasi Oracle rasmi menyatakan bahawa mekanisme ini disyorkan untuk digunakan hanya oleh pengaturcara berpengalaman yang memahami dengan baik apa yang mereka lakukan. Anda akan segera memahami mengapa kami tiba-tiba diberi amaran sedemikian terlebih dahulu :) Berikut ialah senarai perkara yang boleh dilakukan menggunakan API Refleksi:
  1. Mengetahui/menentukan kelas sesuatu objek.
  2. Dapatkan maklumat tentang pengubahsuai kelas, medan, kaedah, pemalar, pembina dan superclass.
  3. Ketahui kaedah yang tergolong dalam antara muka/antara muka yang dilaksanakan.
  4. Buat contoh kelas apabila nama kelas tidak diketahui sehingga program dilaksanakan.
  5. Dapatkan dan tetapkan nilai medan objek mengikut nama.
  6. Panggil kaedah objek dengan nama.
Senarai yang mengagumkan, ya? :) Beri perhatian:Mekanisme pantulan dapat melakukan semua ini "dengan cepat" tanpa mengira objek kelas mana yang kami berikan kepada penganalisis kod kami! Mari kita lihat keupayaan API Refleksi dengan contoh.

Bagaimana untuk mengetahui / menentukan kelas sesuatu objek

Mari kita mulakan dengan asas. Titik masuk ke mekanisme refleksi Java ialah Class. Ya, ia kelihatan benar-benar lucu, tetapi itulah maksud pantulan :) Menggunakan kelas Class, kita pertama sekali menentukan kelas mana-mana objek yang dihantar kepada kaedah kita. Mari cuba ini:
import learn.javarush.Cat;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println(clazz);
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Output konsol:

class learn.javarush.Cat
Perhatikan dua perkara. Pertama, kami sengaja meletakkan kelas Catdalam pakej yang berasingan. learn.javarush;Kini anda boleh melihat bahawa ia getClass()mengembalikan nama penuh kelas. Kedua, kami menamakan pembolehubah kami clazz. Nampak pelik sikit. Sudah tentu, ia harus dipanggil "kelas", tetapi "kelas" ialah perkataan terpelihara dalam bahasa Java, dan pengkompil tidak akan membenarkan pembolehubah dipanggil sedemikian. Saya terpaksa keluar darinya :) Nah, bukan permulaan yang buruk! Apa lagi yang kami ada dalam senarai kemungkinan?

Bagaimana untuk mendapatkan maklumat tentang pengubahsuai kelas, medan, kaedah, pemalar, pembina dan superclass

Ini sudah lebih menarik! Dalam kelas semasa kita tidak mempunyai pemalar dan tiada kelas induk. Mari tambahkannya untuk kesempurnaan. Mari buat kelas induk yang paling mudah Animal:
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
Dan mari tambah Catwarisan daripada Animaldan satu pemalar pada kelas kita:
package learn.javarush;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Семейство кошачьих";

   private String name;
   private int age;

   //...остальная часть класса
}
Kini kami mempunyai set lengkap! Mari cuba kemungkinan muhasabah :)
import learn.javarush.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Name класса: " + clazz);
       System.out.println("Поля класса: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Родительский класс: " + clazz.getSuperclass());
       System.out.println("Методы класса: " +  Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Конструкторы класса: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Barsik", 6));
   }
}
Inilah yang kami dapat dalam konsol:
Name класса: class learn.javarush.Cat
Поля класса: [private static final java.lang.String learn.javarush.Cat.ANIMAL_FAMILY, private java.lang.String learn.javarush.Cat.name, private int learn.javarush.Cat.age]
Родительский класс: class learn.javarush.Animal
Методы класса: [public java.lang.String learn.javarush.Cat.getName(), public void learn.javarush.Cat.setName(java.lang.String), public void learn.javarush.Cat.sayMeow(), public void learn.javarush.Cat.setAge(int), public void learn.javarush.Cat.jump(), public int learn.javarush.Cat.getAge()]
Конструкторы класса: [public learn.javarush.Cat(java.lang.String,int)]
Kami menerima begitu banyak maklumat terperinci tentang kelas! Dan bukan sahaja tentang orang awam, tetapi juga tentang alat sulit. Beri perhatian: private-pembolehubah juga dipaparkan dalam senarai. Sebenarnya, "analisis" kelas boleh dianggap lengkap pada ketika ini: sekarang, menggunakan kaedah itu, analyzeClass()kita akan mempelajari semua yang mungkin. Tetapi ini bukan semua kemungkinan yang kita ada apabila bekerja dengan refleksi. Jangan menghadkan diri kita kepada pemerhatian mudah dan teruskan kepada tindakan aktif! :)

Bagaimana untuk mencipta contoh kelas jika nama kelas tidak diketahui sebelum program dilaksanakan

Mari kita mulakan dengan pembina lalai. Ia belum ada dalam kelas kita lagi Cat, jadi mari tambahkannya:
public Cat() {

}
Inilah rupa kod untuk mencipta objek Catmenggunakan refleksi (method createCat()):
import learn.javarush.Cat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

   public static Cat createCat() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException {

       BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
       String className = reader.readLine();

       Class clazz = Class.forName(className);
       Cat cat = (Cat) clazz.newInstance();

       return cat;
   }

public static Object createObject() throws Exception {

   BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
   String className = reader.readLine();

   Class clazz = Class.forName(className);
   Object result = clazz.newInstance();

   return result;
}

   public static void main(String[] args) throws IOException, IllegalAccessException, ClassNotFoundException, InstantiationException {
       System.out.println(createCat());
   }
}
Masuk ke dalam konsol:

learn.javarush.Cat
Output konsol:

Cat{name='null', age=0}
Ini bukan ralat: nilai namedan agedipaparkan dalam konsol kerana kami memprogramkan outputnya dalam kaedah toString()kelas Cat. Di sini kita membaca nama kelas yang objeknya akan kita buat daripada konsol. Program berjalan mempelajari nama kelas yang objeknya akan dicipta. Contoh penggunaan Refleksi - 3Demi ringkasnya, kami telah meninggalkan kod untuk pengendalian pengecualian yang betul supaya ia tidak mengambil lebih banyak ruang daripada contoh itu sendiri. Dalam program sebenar, sudah tentu, ia pasti bernilai mengendalikan situasi di mana nama yang salah dimasukkan, dsb. Pembina lalai adalah perkara yang agak mudah, jadi mencipta contoh kelas yang menggunakannya, seperti yang anda lihat, tidak sukar :) Dan menggunakan kaedah itu, newInstance()kami mencipta objek baharu kelas ini. Perkara lain jika pembina kelas Catmengambil parameter sebagai input. Mari keluarkan pembina lalai daripada kelas dan cuba jalankan kod kami sekali lagi.

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
Sesuatu telah berlaku! Kami menerima ralat kerana kami memanggil kaedah untuk mencipta objek melalui pembina lalai. Tetapi sekarang kami tidak mempunyai pereka seperti itu. Ini bermakna apabila kaedah berfungsi, newInstance()mekanisme pantulan akan menggunakan pembina lama kami dengan dua parameter:
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
Tetapi kami tidak melakukan apa-apa dengan parameter, seolah-olah kami telah melupakannya sepenuhnya! Untuk menyampaikannya kepada pembina menggunakan refleksi, anda perlu mengubahnya sedikit:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.javarush.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Output konsol:

Cat{name='Barsik', age=6}
Mari kita lihat dengan lebih dekat apa yang berlaku dalam program kami. Kami telah mencipta pelbagai objek Class.
Class[] catClassParams = {String.class, int.class};
Ia sepadan dengan parameter pembina kami (kami hanya mempunyai parameter Stringdan int). Kami menyampaikannya kepada kaedah clazz.getConstructor()dan mendapatkan akses kepada pembina yang diperlukan. Selepas ini, semua yang tinggal ialah memanggil kaedah newInstance()dengan parameter yang diperlukan dan jangan lupa untuk secara eksplisit menghantar objek ke kelas yang kita perlukan - Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
Akibatnya, objek kami akan berjaya dibuat! Output konsol:

Cat{name='Barsik', age=6}
Jom teruskan :)

Bagaimana untuk mendapatkan dan menetapkan nilai medan objek mengikut nama

Bayangkan anda menggunakan kelas yang ditulis oleh pengaturcara lain. Walau bagaimanapun, anda tidak mempunyai peluang untuk mengeditnya. Contohnya, perpustakaan kelas siap pakai yang dibungkus dalam JAR. Anda boleh membaca kod kelas, tetapi anda tidak boleh mengubahnya. Pengaturcara yang mencipta kelas dalam perpustakaan ini (biarlah ia kelas lama kami Cat) tidak mendapat cukup tidur sebelum reka bentuk akhir dan mengalih keluar getter dan setter untuk medan age. Sekarang kelas ini telah datang kepada anda. Ia memenuhi keperluan anda sepenuhnya, kerana anda hanya memerlukan objek dalam program Cat. Tetapi anda memerlukannya dengan bidang yang sama age! Ini adalah masalah: kami tidak dapat mencapai medan, kerana ia mempunyai pengubah suai private, dan pengambil dan penetap telah dialih keluar oleh bakal pembangun kelas ini :/ Nah, refleksi boleh membantu kami dalam situasi ini juga! CatKami mempunyai akses kepada kod kelas : sekurang-kurangnya kami dapat mengetahui medan apa yang ada padanya dan apa namanya. Berbekalkan maklumat ini, kami menyelesaikan masalah kami:
import learn.javarush.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;
       try {
           clazz = Class.forName("learn.javarush.Cat");
           cat = (Cat) clazz.newInstance();

           //с полем name нам повезло - для него в классе есть setter
           cat.setName("Barsik");

           Field age = clazz.getDeclaredField("age");

           age.setAccessible(true);

           age.set(cat, 6);

       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InstantiationException e) {
           e.printStackTrace();
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchFieldException e) {
           e.printStackTrace();
       }

       return cat;
   }

   public static void main(String[] args) {
       System.out.println(createCat());
   }
}
Seperti yang dinyatakan dalam ulasan, namesemuanya mudah dengan medan: pembangun kelas menyediakan penetap untuknya. Anda juga sudah tahu cara membuat objek daripada pembina lalai: terdapat kaedah untuk ini newInstance(). Tetapi anda perlu bermain-main dengan bidang kedua. Mari kita fikirkan apa yang berlaku di sini :)
Field age = clazz.getDeclaredField("age");
Di sini kami, menggunakan objek kami Class clazz, mengakses medan agemenggunakan getDeclaredField(). Ia memberi kita keupayaan untuk mendapatkan medan umur sebagai objek Field age. Tetapi ini belum mencukupi, kerana privatemedan tidak boleh diberikan nilai semata-mata. Untuk melakukan ini, anda perlu membuat medan "tersedia" menggunakan kaedah setAccessible():
age.setAccessible(true);
Medan yang mana ini dilakukan boleh diberikan nilai:
age.set(cat, 6);
Seperti yang anda boleh lihat, kami mempunyai sejenis penetap yang terbalik: kami menetapkan medan Field agenilainya, dan juga menyampaikan objek yang medan ini harus diberikan. Mari jalankan kaedah kami main()dan lihat:

Cat{name='Barsik', age=6}
Hebat, kami melakukan semuanya! :) Jom tengok apa lagi kemungkinan yang kita ada...

Bagaimana untuk memanggil kaedah objek dengan nama

Mari kita ubah sedikit keadaan dari contoh sebelumnya. Katakan pembangun kelas Catmembuat kesilapan dengan medan - kedua-duanya tersedia, terdapat pengambil dan penetap untuknya, semuanya ok. Masalahnya berbeza: dia menjadikan kaedah persendirian yang kami perlukan:
private void sayMeow() {

   System.out.println("Meow!");
}
Akibatnya, kami akan mencipta objek Catdalam program kami, tetapi tidak akan dapat memanggil kaedah mereka sayMeow(). Adakah kita akan mempunyai kucing yang tidak mengeong? Agak pelik :/ Bagaimana saya boleh membetulkannya? Sekali lagi, Reflection API datang untuk menyelamatkan! Kami tahu nama kaedah yang diperlukan. Selebihnya adalah soal teknik:
import learn.javarush.Cat;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {

   public static void invokeSayMeowMethod()  {

       Class clazz = null;
       Cat cat = null;
       try {

           cat = new Cat("Barsik", 6);

           clazz = Class.forName(Cat.class.getName());

           Method sayMeow = clazz.getDeclaredMethod("sayMeow");

           sayMeow.setAccessible(true);

           sayMeow.invoke(cat);

       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (NoSuchMethodException e) {
           e.printStackTrace();
       } catch (IllegalAccessException e) {
           e.printStackTrace();
       } catch (InvocationTargetException e) {
           e.printStackTrace();
       }
   }

   public static void main(String[] args) {
       invokeSayMeowMethod();
   }
}
Di sini kita bertindak dengan cara yang sama seperti dalam situasi dengan akses kepada medan peribadi. Mula-mula kita mendapat kaedah yang kita perlukan, yang terkandung dalam objek kelas Method:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Dengan bantuan, getDeclaredMethod()anda boleh "menjangkau" kaedah peribadi. Seterusnya kami membuat kaedah boleh dipanggil:
sayMeow.setAccessible(true);
Dan akhirnya, kami memanggil kaedah pada objek yang dikehendaki:
sayMeow.invoke(cat);
Memanggil kaedah juga kelihatan seperti "panggilan terbalik": kita digunakan untuk menunjuk objek ke kaedah yang diperlukan menggunakan titik ( cat.sayMeow()), dan apabila bekerja dengan pantulan, kita meneruskan ke kaedah objek yang mana ia perlu dipanggil . Apa yang kita ada dalam konsol?

Meow!
Semuanya berjaya! :) Sekarang anda melihat kemungkinan besar yang diberikan oleh mekanisme pantulan dalam Java kepada kami. Dalam situasi yang sukar dan tidak dijangka (seperti dalam contoh dengan kelas dari perpustakaan tertutup), ia benar-benar boleh membantu kita. Walau bagaimanapun, seperti mana-mana kuasa besar, ia juga membayangkan tanggungjawab yang besar. Kelemahan refleksi ditulis dalam bahagian khas di laman web Oracle. Terdapat tiga kelemahan utama:
  1. Produktiviti berkurangan. Kaedah yang dipanggil menggunakan refleksi mempunyai prestasi yang lebih rendah daripada kaedah yang dipanggil biasa.

  2. Terdapat sekatan keselamatan. Mekanisme refleksi membolehkan anda mengubah tingkah laku program semasa masa jalan. Tetapi dalam persekitaran kerja anda pada projek sebenar mungkin terdapat sekatan yang tidak membenarkan anda melakukan ini.

  3. Risiko pendedahan maklumat dalaman. Adalah penting untuk memahami bahawa menggunakan refleksi secara langsung melanggar prinsip enkapsulasi: ia membolehkan kami mengakses medan peribadi, kaedah, dll. Saya rasa tidak perlu menjelaskan bahawa pelanggaran langsung dan kasar prinsip OOP harus dilakukan hanya dalam kes yang paling ekstrem, apabila tiada cara lain untuk menyelesaikan masalah atas sebab di luar kawalan anda.

Gunakan mekanisme refleksi dengan bijak dan hanya dalam situasi di mana ia tidak dapat dielakkan, dan jangan lupa tentang kekurangannya. Ini mengakhiri kuliah kami! Ia ternyata agak besar, tetapi hari ini anda belajar banyak perkara baru :)
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION