CodeGym /Blog Java /rawak /Contoh refleksi
John Squirrels
Tahap
San Francisco

Contoh refleksi

Diterbitkan dalam kumpulan
Mungkin anda pernah menemui konsep "refleksi" dalam kehidupan biasa. Perkataan ini biasanya merujuk kepada proses mengkaji diri sendiri. Dalam pengaturcaraan, ia mempunyai makna yang sama — ia adalah mekanisme untuk menganalisis data tentang program, dan juga mengubah struktur dan tingkah laku program, semasa program sedang berjalan. Contoh refleksi - 1 Yang penting di sini ialah kami melakukan ini pada masa jalan, bukan pada masa penyusunan. Tetapi mengapa memeriksa kod semasa runtime? Lagipun, anda sudah boleh membaca kod tersebut :/ Terdapat sebab mengapa idea refleksi mungkin tidak jelas dengan serta-merta: sehingga tahap ini, anda sentiasa tahu kelas yang anda bekerjasama. Sebagai contoh, anda boleh menulis Catkelas:

package learn.codegym;

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 tentangnya, dan anda boleh melihat medan dan kaedah yang ada padanya. Katakan anda tiba-tiba perlu memperkenalkan kelas haiwan lain kepada program ini. Anda mungkin boleh membuat struktur warisan kelas dengan Animalkelas induk untuk kemudahan. Terdahulu, kami juga telah mencipta kelas yang mewakili klinik veterinar, yang mana kami boleh lulus objek Animal(contoh kelas induk), dan program ini merawat haiwan itu dengan sewajarnya berdasarkan sama ada ia anjing atau kucing. Walaupun ini bukan tugas yang paling mudah, program ini dapat mempelajari semua maklumat yang diperlukan tentang kelas pada masa penyusunan. Sehubungan itu, apabila anda lulus Catobjek kepada kaedah kelas klinik veterinar dimain()kaedah, program sudah tahu bahawa ia adalah kucing, bukan anjing. Sekarang mari kita bayangkan bahawa kita menghadapi tugas yang berbeza. Matlamat kami adalah untuk menulis penganalisis kod. Kita perlu membuat CodeAnalyzerkelas dengan satu kaedah: void analyzeObject(Object o). Kaedah ini hendaklah:
  • tentukan kelas objek yang dihantar kepadanya dan paparkan nama kelas pada konsol;
  • tentukan nama semua medan kelas yang diluluskan, termasuk yang peribadi, dan paparkannya pada konsol;
  • tentukan nama semua kaedah kelas yang diluluskan, termasuk yang peribadi, dan paparkannya pada konsol.
Ia akan kelihatan seperti ini:

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
      
       // Print the name of the class of object o
       // Print the names of all variables of this class
       // Print the names of all methods of this class
   }
  
}
Kini kita dapat melihat dengan jelas bagaimana tugasan ini berbeza daripada tugasan lain yang telah anda selesaikan sebelum ini. Dengan objektif semasa kami, kesukarannya terletak pada fakta bahawa kami mahupun program tidak tahu apa sebenarnya yang akan diserahkan kepadaanalyzeClass()kaedah. Jika anda menulis program sedemikian, pengaturcara lain akan mula menggunakannya, dan mereka mungkin menghantar apa sahaja kepada kaedah ini — mana-mana kelas Java standard atau mana-mana kelas lain yang mereka tulis. Kelas yang diluluskan boleh mempunyai sebarang bilangan pembolehubah dan kaedah. Dalam erti kata lain, kami (dan program kami) tidak tahu kelas apa yang akan kami bekerjasama. Namun begitu, kami perlu menyiapkan tugasan ini. Dan di sinilah API Refleksi Java standard membantu kami. API Refleksi ialah alat bahasa yang berkuasa. Dokumentasi rasmi Oracle mengesyorkan bahawa mekanisme ini hanya boleh digunakan oleh pengaturcara berpengalaman yang tahu apa yang mereka lakukan. Anda tidak lama lagi akan memahami sebab kami memberikan amaran seperti ini lebih awal :) Berikut ialah senarai perkara yang boleh anda lakukan dengan API Refleksi:
  1. Mengenal pasti/menentukan kelas sesuatu objek.
  2. Dapatkan maklumat tentang pengubahsuai kelas, medan, kaedah, pemalar, pembina dan superclass.
  3. Ketahui kaedah yang tergolong dalam antara muka yang dilaksanakan.
  4. Buat contoh kelas yang nama kelasnya tidak diketahui sehingga program dilaksanakan.
  5. Dapatkan dan tetapkan nilai medan contoh mengikut nama.
  6. Panggil kaedah contoh dengan nama.
Senarai yang mengagumkan, ya? :) Catatan:mekanisme pantulan boleh melakukan semua perkara ini "dengan cepat", tanpa mengira jenis objek yang kami hantar kepada penganalisis kod kami! Mari kita terokai keupayaan Reflection API dengan melihat beberapa contoh.

Cara mengenal pasti/menentukan kelas sesuatu objek

Mari kita mulakan dengan asas. Titik masuk ke enjin pantulan Java ialah kelas Class. Ya, ia kelihatan sangat lucu, tetapi itulah pantulan :) Dengan menggunakan Classkelas, kami mula-mula menentukan kelas mana-mana objek yang dihantar kepada kaedah kami. Mari cuba lakukan ini:

import learn.codegym.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("Fluffy", 6));
   }
}
Output konsol:

class learn.codegym.Cat
Perhatikan dua perkara. Pertama, kami sengaja meletakkan Catkelas dalam learn.codegympakej yang berasingan. Kini anda boleh melihat bahawa getClass()kaedah mengembalikan nama penuh kelas. Kedua, kami menamakan pembolehubah kami clazz. Itu kelihatan pelik sedikit. Adalah masuk akal untuk memanggilnya "kelas", tetapi "kelas" ialah perkataan terpelihara di Jawa. Pengkompil tidak akan membenarkan pembolehubah dipanggil itu. Kami terpaksa mengatasinya entah bagaimana :) Tidak buruk untuk permulaan! Apa lagi yang kita ada dalam senarai keupayaan itu?

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

Sekarang perkara menjadi lebih menarik! Dalam kelas semasa, kami tidak mempunyai sebarang pemalar atau kelas induk. Mari tambah mereka untuk membuat gambar yang lengkap. Buat Animalkelas induk yang paling mudah:

package learn.codegym;
public class Animal {

   private String name;
   private int age;
}
Dan kami akan menjadikan Catkelas kami mewarisi Animaldan menambah satu pemalar:

package learn.codegym;

public class Cat extends Animal {

   private static final String ANIMAL_FAMILY = "Feline family";

   private String name;
   private int age;

   // ...the rest of the class
}
Kini kami mempunyai gambaran lengkap! Mari lihat apa yang mampu dilakukan oleh refleksi :)

import learn.codegym.Cat;

import java.util.Arrays;

public class CodeAnalyzer {

   public static void analyzeClass(Object o) {
       Class clazz = o.getClass();
       System.out.println("Class name: " + clazz);
       System.out.println("Class fields: " + Arrays.toString(clazz.getDeclaredFields()));
       System.out.println("Parent class: " + clazz.getSuperclass());
       System.out.println("Class methods: " + Arrays.toString(clazz.getDeclaredMethods()));
       System.out.println("Class constructors: " + Arrays.toString(clazz.getConstructors()));
   }

   public static void main(String[] args) {

       analyzeClass(new Cat("Fluffy", 6));
   }
}
Inilah yang kita lihat pada konsol:

Class name:  class learn.codegym.Cat 
Class fields: [private static final java.lang.String learn.codegym.Cat.ANIMAL_FAMILY, private java.lang.String learn.codegym.Cat.name, private int learn.codegym.Cat.age] 
Parent class: class learn.codegym.Animal 
Class methods: [public java.lang.String learn.codegym.Cat.getName(), public void learn.codegym.Cat.setName(java.lang.String), public void learn.codegym.Cat.sayMeow(), public void learn.codegym.Cat.setAge(int), public void learn.codegym.Cat.jump(), public int learn.codegym.Cat.getAge()] 
Class constructors: [public learn.codegym.Cat(java.lang.String, int)]
Lihat semua maklumat kelas terperinci yang kami dapat! Dan bukan hanya maklumat awam, tetapi juga maklumat peribadi! Catatan: privatepembolehubah juga dipaparkan dalam senarai. "Analisis" kelas kami boleh dianggap pada asasnya lengkap: kami menggunakan analyzeObject()kaedah untuk mempelajari semua yang kami boleh. Tetapi ini bukan semua yang boleh kita lakukan dengan refleksi. Kami tidak terhad kepada pemerhatian mudah — kami akan terus mengambil tindakan! :)

Bagaimana untuk mencipta contoh kelas yang nama kelasnya tidak diketahui sehingga program dilaksanakan.

Mari kita mulakan dengan pembina lalai. Kelas kami Catbelum mempunyai satu lagi, jadi mari tambahkannya:

public Cat() {
  
}
Inilah kod untuk mencipta Catobjek menggunakan refleksi ( createCat()kaedah):

import learn.codegym.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());
   }
}
Input konsol:

learn.codegym.Cat
Output konsol:

Cat{name='null', age=0}
Ini bukan ralat: nilai namedan agedipaparkan pada konsol kerana kami menulis kod untuk mengeluarkannya dalam toString()kaedah kelas Cat. Di sini kita membaca nama kelas yang objeknya akan kita buat daripada konsol. Program ini mengenali nama kelas yang objeknya hendak dibuat. Contoh refleksi - 3Untuk kepentingan ringkas, kami meninggalkan kod pengendalian pengecualian yang betul, yang akan mengambil lebih banyak ruang daripada contoh itu sendiri. Dalam program sebenar, sudah tentu, anda harus mengendalikan situasi yang melibatkan nama yang dimasukkan dengan salah, dsb. Pembina lalai adalah agak mudah, jadi seperti yang anda lihat, ia adalah mudah untuk menggunakannya untuk mencipta contoh kelas :) Menggunakan newInstance()kaedah , kami mencipta objek baharu kelas ini. Ia adalah perkara lain jikaCatpembina mengambil hujah sebagai input. Mari keluarkan pembina lalai kelas dan cuba jalankan kod kami sekali lagi.

null
java.lang.InstantiationException: learn.codegym.Cat 
at java.lang.Class.newInstance(Class.java:427)
Sesuatu telah berlaku! Kami mendapat ralat kerana kami memanggil kaedah untuk mencipta objek menggunakan pembina lalai. Tetapi kami tidak mempunyai pembina seperti itu sekarang. Jadi apabila newInstance()kaedah berjalan, mekanisme pantulan 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! Menggunakan refleksi untuk menyampaikan hujah kepada pembina memerlukan sedikit "kreativiti":

import learn.codegym.Cat;

import java.lang.reflect.InvocationTargetException;

public class Main {

   public static Cat createCat()  {

       Class clazz = null;
       Cat cat = null;

       try {
           clazz = Class.forName("learn.codegym.Cat");
           Class[] catClassParams = {String.class, int.class};
           cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 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='Fluffy', age=6}
Mari kita lihat dengan lebih dekat apa yang berlaku dalam program kami. Kami mencipta pelbagai Classobjek.

Class[] catClassParams = {String.class, int.class};
Ia sepadan dengan parameter pembina kami (yang hanya mempunyai Stringdan intparameter). Kami menyampaikannya kepada clazz.getConstructor()kaedah dan mendapat akses kepada pembina yang dikehendaki. Selepas itu, apa yang perlu kita lakukan ialah memanggil newInstance()kaedah dengan hujah yang diperlukan, dan jangan lupa untuk menghantar objek secara eksplisit ke jenis yang dikehendaki: Cat.

cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
Kini objek kami berjaya dibuat! Output konsol:

Cat{name='Fluffy', age=6}
Bergerak terus :)

Bagaimana untuk mendapatkan dan menetapkan nilai medan contoh mengikut nama.

Bayangkan anda menggunakan kelas yang ditulis oleh pengaturcara lain. Selain itu, anda tidak mempunyai keupayaan untuk mengeditnya. Contohnya, perpustakaan kelas siap pakai yang dibungkus dalam JAR. Anda boleh membaca kod kelas, tetapi anda tidak boleh mengubahnya. Katakan bahawa pengaturcara yang mencipta salah satu kelas dalam perpustakaan ini (biarlah ia Catkelas lama kami), gagal mendapat tidur yang cukup pada malam sebelum reka bentuk dimuktamadkan, mengeluarkan pengambil dan penetap untuk medan age. Sekarang kelas ini telah datang kepada anda. Ia memenuhi semua keperluan anda, kerana anda hanya memerlukan Catobjek dalam program anda. Tetapi anda memerlukan mereka untuk mempunyai agebidang! Ini adalah masalah: kami tidak boleh sampai ke lapangan, kerana ia mempunyaiprivatepengubah suai, dan pengambil dan penetap telah dipadamkan oleh pembangun yang kurang tidur yang mencipta kelas :/ Nah, refleksi boleh membantu kita dalam situasi ini! Kami mempunyai akses kepada kod untuk Catkelas, jadi kami sekurang-kurangnya dapat mengetahui medan yang ada padanya dan apa nama mereka. Berbekalkan maklumat ini, kami boleh menyelesaikan masalah kami:

import learn.codegym.Cat;

import java.lang.reflect.Field;

public class Main {

   public static Cat createCat()  {

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

           // We got lucky with the name field, since it has a setter
           cat.setName("Fluffy");

           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, segala-galanya dengan namemedan adalah mudah, kerana pembangun kelas menyediakan penetap. Anda sudah tahu cara membuat objek daripada pembina lalai: kami mempunyai newInstance()untuk ini. Tetapi kita perlu melakukan beberapa mengutak-atik bidang kedua. Mari kita fikirkan apa yang berlaku di sini :)

Field age = clazz.getDeclaredField("age");
Di sini, menggunakan Class clazzobjek kami, kami mengakses agemedan melalui getDeclaredField()kaedah. Ia membolehkan kita mendapatkan medan umur sebagai Field ageobjek. Tetapi ini tidak mencukupi, kerana kita tidak boleh hanya memberikan nilai kepada privatemedan. Untuk melakukan ini, kita perlu membuat medan boleh diakses dengan menggunakan setAccessible()kaedah:

age.setAccessible(true);
Sebaik sahaja kami melakukan ini pada medan, kami boleh menetapkan nilai:

age.set(cat, 6);
Seperti yang anda boleh lihat, objek kami Field agemempunyai sejenis penetap dalam-keluar yang kami luluskan nilai int dan objek yang medannya akan diberikan. Kami menjalankan main()kaedah kami dan lihat:

Cat{name='Fluffy', age=6}
Cemerlang! Kita berjaya! :) Jom tengok apa lagi yang boleh kita buat...

Bagaimana untuk memanggil kaedah contoh dengan nama.

Mari kita ubah sedikit keadaan dalam contoh sebelumnya. Katakan Catpembangun kelas tidak membuat kesilapan dengan getter dan setter. Semuanya baik-baik saja dalam hal itu. Sekarang masalahnya berbeza: terdapat kaedah yang pasti kami perlukan, tetapi pembangun menjadikannya peribadi:

private void sayMeow() {

   System.out.println("Meow!");
}
Ini bermakna jika kita mencipta Catobjek dalam program kita, maka kita tidak akan dapat memanggil sayMeow()kaedah padanya. Kami akan mempunyai kucing yang tidak mengeong? Itu ganjil :/ Bagaimana kita akan membetulkannya? Sekali lagi, Reflection API membantu kami! Kami tahu nama kaedah yang kami perlukan. Segala-galanya adalah teknikal:

import learn.codegym.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("Fluffy", 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 kami melakukan banyak perkara yang sama seperti yang kami lakukan semasa mengakses medan peribadi. Pertama, kami mendapat kaedah yang kami perlukan. Ia terkandung dalam Methodobjek:

Method sayMeow = clazz.getDeclaredMethod("sayMeow");
Kaedah ini getDeclaredMethod()membolehkan kita pergi ke kaedah peribadi. Seterusnya, kami membuat kaedah boleh dipanggil:

sayMeow.setAccessible(true);
Dan akhirnya, kami memanggil kaedah pada objek yang dikehendaki:

sayMeow.invoke(cat);
Di sini, panggilan kaedah kami kelihatan seperti "panggilan balik": kami terbiasa menggunakan noktah untuk menunjuk objek pada kaedah yang diingini ( cat.sayMeow()), tetapi apabila bekerja dengan refleksi, kami meneruskan ke kaedah objek yang ingin kami panggil. kaedah itu. Apa yang ada pada konsol kami?

Meow!
Semuanya berjaya! :) Kini anda boleh melihat kemungkinan besar yang diberikan oleh mekanisme pantulan Java kepada kami. Dalam situasi yang sukar dan tidak dijangka (seperti contoh kami dengan kelas daripada perpustakaan tertutup), ia benar-benar boleh membantu kami. Tetapi, seperti mana-mana kuasa besar, ia membawa tanggungjawab yang besar. Kelemahan refleksi diterangkan dalam bahagian khas di laman web Oracle. Terdapat tiga kelemahan utama:
  1. Prestasi lebih teruk. Kaedah yang dipanggil menggunakan refleksi mempunyai prestasi yang lebih teruk daripada kaedah yang dipanggil dengan cara biasa.

  2. Terdapat sekatan keselamatan. Mekanisme refleksi membolehkan kami mengubah tingkah laku program semasa masa jalan. Tetapi di tempat kerja anda, apabila bekerja pada projek sebenar, anda mungkin menghadapi batasan yang tidak membenarkan ini.

  3. Risiko pendedahan maklumat dalaman. Adalah penting untuk memahami refleksi adalah pelanggaran langsung terhadap prinsip pengkapsulan: ia membolehkan kita mengakses medan peribadi, kaedah, dll. Saya tidak fikir saya perlu menyebut bahawa pelanggaran langsung dan terang-terangan terhadap prinsip OOP harus diambil. kepada hanya dalam kes yang paling ekstrem, apabila tiada cara lain untuk menyelesaikan masalah atas sebab di luar kawalan anda.

Gunakan renungan dengan bijak dan hanya dalam situasi di mana ia tidak dapat dielakkan, dan jangan lupa tentang kekurangannya. Dengan ini, pelajaran kita telah berakhir. Ia ternyata agak panjang, tetapi anda belajar banyak hari ini :)
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION