JavaRush /وبلاگ جاوا /Random-FA /بازتاب در جاوا - مثال های استفاده

بازتاب در جاوا - مثال های استفاده

در گروه منتشر شد
ممکن است در زندگی روزمره با مفهوم "بازتاب" برخورد کرده باشید. معمولاً این کلمه به فرآیند مطالعه خود اشاره دارد. در برنامه نویسی معنای مشابهی دارد - مکانیزمی برای بررسی داده های یک برنامه و همچنین تغییر ساختار و رفتار برنامه در طول اجرای آن است. نکته مهم در اینجا این است که در زمان اجرا انجام می شود نه در زمان کامپایل. اما چرا کد را در زمان اجرا بررسی کنیم؟ شما قبلاً آن را می بینید:/ نمونه هایی از استفاده از Reflection - 1ایده انعکاس ممکن است فوراً به یک دلیل مشخص نباشد: تا این لحظه، شما همیشه کلاس هایی را که با آنها کار می کردید می دانستید. خوب، برای مثال، می توانید یک کلاس بنویسید 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 +
           '}';
}

}
شما همه چیز را در مورد آن می دانید، می بینید که چه زمینه ها و روش هایی دارد. مطمئناً می توانید یک سیستم ارثی با یک کلاس مشترک برای راحتی ایجاد کنید Animal، اگر به طور ناگهانی برنامه به کلاس های دیگر حیوانات نیاز داشت. پیش از این، ما حتی یک کلاس کلینیک دامپزشکی ایجاد کردیم که در آن می‌توانستید یک شی والد را رد کنید Animal، و این برنامه بسته به سگ یا گربه بودن حیوان را درمان می‌کرد. اگرچه این کارها خیلی ساده نیستند، برنامه در زمان کامپایل تمام اطلاعات مورد نیاز خود را در مورد کلاس ها یاد می گیرد. بنابراین، وقتی main()یک شی را در یک روش Catبه روش های کلاس کلینیک دامپزشکی منتقل می کنید، برنامه از قبل می داند که این یک گربه است، نه یک سگ. حالا بیایید تصور کنیم که با کار دیگری روبرو هستیم. هدف ما نوشتن یک تحلیلگر کد است. ما باید یک کلاس CodeAnalyzerبا یک متد واحد ایجاد کنیم - void analyzeClass(Object o). این روش باید:
  • تعیین کنید که شی به چه کلاسی به آن ارسال شده است و نام کلاس را در کنسول نمایش دهید.
  • نام تمام فیلدهای این کلاس از جمله خصوصی را تعیین کرده و در کنسول نمایش دهید.
  • نام تمام متدهای این کلاس از جمله خصوصی را تعیین کرده و در کنسول نمایش دهید.
چیزی شبیه به این خواهد بود:
public class CodeAnalyzer {

   public static void analyzeClass(Object o) {

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

}
حالا تفاوت این مشکل با بقیه مشکلاتی که قبلا حل کردید قابل مشاهده است. در این مورد، دشواری در این واقعیت نهفته است که نه شما و نه برنامه نمی‌دانید دقیقاً چه چیزی به روش منتقل می‌شود analyzeClass(). شما یک برنامه می نویسید، برنامه نویسان دیگر شروع به استفاده از آن می کنند، که می توانند هر چیزی را به این روش منتقل کنند - هر کلاس استاندارد جاوا یا هر کلاسی که نوشته اند. این کلاس می تواند هر تعداد متغیر و متد داشته باشد. به عبارت دیگر، در این مورد ما (و برنامه ما) هیچ ایده ای نداریم که با چه کلاس هایی کار خواهیم کرد. و با این حال، ما باید این مشکل را حل کنیم. و در اینجا کتابخانه استاندارد جاوا به کمک ما می آید - Java Reflection API. Reflection API یک ویژگی زبان قدرتمند است. اسناد رسمی Oracle بیان می کند که این مکانیسم فقط توسط برنامه نویسان با تجربه که به خوبی درک می کنند که چه کاری انجام می دهند توصیه می شود. به زودی خواهید فهمید که چرا به طور ناگهانی از قبل چنین هشدارهایی به ما داده می شود :) در اینجا لیستی از کارهایی که می توان با استفاده از Reflection API انجام داد آمده است:
  1. کلاس یک شی را پیدا کنید/تعیین کنید.
  2. اطلاعاتی در مورد اصلاح‌کننده‌های کلاس، فیلدها، متدها، ثابت‌ها، سازنده‌ها و سوپرکلاس‌ها دریافت کنید.
  3. دریابید که کدام متدها متعلق به اینترفیس/اینترفیس های پیاده سازی شده هستند.
  4. زمانی که نام کلاس تا زمانی که برنامه اجرا نشود، یک نمونه از کلاس ایجاد کنید.
  5. مقدار یک فیلد شی را با نام دریافت و تنظیم کنید.
  6. متد یک شی را با نام فراخوانی کنید.
لیست تاثیرگذار، ها؟ :) توجه کنید:مکانیسم انعکاس قادر است همه این کارها را "در حال پرواز" انجام دهد بدون توجه به اینکه کدام شیء کلاس را به تحلیلگر کد خود منتقل می کنیم! بیایید با مثال هایی به قابلیت های Reflection API بپردازیم.

نحوه پیدا کردن / تعیین کلاس یک شی

بیایید با اصول اولیه شروع کنیم. نقطه ورود به مکانیسم بازتاب جاوا Class. بله، واقعا خنده دار به نظر می رسد، اما بازتاب برای همین است :) با استفاده از یک کلاس Class، ابتدا کلاس هر شیء ارسال شده به متد خود را تعیین می کنیم. بیایید این را امتحان کنیم:
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));
   }
}
خروجی کنسول:

class learn.javarush.Cat
به دو چیز توجه کنید. اولاً ما عمداً کلاس را Catدر یک بسته جداگانه قرار می دهیم.حالا learn.javarush;می بینید که getClass()نام کامل کلاس را برمی گرداند. در مرحله دوم، ما متغیر خود را نامگذاری کردیم clazz. کمی عجیب به نظر می رسد. البته باید آن را “class” نامید، اما “class” یک کلمه رزرو شده در زبان جاوا است و کامپایلر اجازه نمی دهد که متغیرها به این صورت نامیده شوند. من باید از آن خارج می شدم :) خب، شروع بدی نیست! چه چیز دیگری در لیست احتمالات داشتیم؟

نحوه به دست آوردن اطلاعات در مورد اصلاح کننده های کلاس، فیلدها، متدها، ثابت ها، سازنده ها و سوپرکلاس ها

این در حال حاضر جالب تر است! در کلاس فعلی هیچ ثابت و کلاس والد نداریم. بیایید آنها را برای کامل بودن اضافه کنیم. بیایید ساده ترین کلاس والد را ایجاد کنیم Animal:
package learn.javarush;
public class Animal {

   private String name;
   private int age;
}
و اجازه دهید Catارث از Animalو یک ثابت به کلاس خود اضافه کنیم:
package learn.javarush;

public class Cat extends Animal {

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

   private String name;
   private int age;

   //...остальная часть класса
}
حالا ما یک مجموعه کامل داریم! بیایید امکانات بازتاب را امتحان کنیم :)
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));
   }
}
این چیزی است که ما در کنسول دریافت می کنیم:
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)]
ما اطلاعات بسیار دقیقی در مورد کلاس دریافت کردیم! و نه تنها در مورد عمومی، بلکه در مورد قسمت های خصوصی. توجه کنید: private-متغیرها نیز در لیست نمایش داده می شوند. در واقع، "تحلیل" کلاس را می توان در این مرحله کامل در نظر گرفت: اکنون، با استفاده از روش، analyzeClass()ما هر چیزی را که ممکن است یاد خواهیم گرفت. اما اینها همه امکاناتی نیست که ما هنگام کار با بازتاب داریم. بیایید خود را به مشاهده ساده محدود نکنیم و به سمت عمل فعال برویم! :)

چگونه می توان نمونه ای از کلاس ایجاد کرد اگر نام کلاس قبل از اجرای برنامه ناشناخته باشد

بیایید با سازنده پیش فرض شروع کنیم. هنوز در کلاس ما نیست Cat، پس بیایید آن را اضافه کنیم:
public Cat() {

}
در اینجا کد برای ایجاد یک شی Catبا استفاده از بازتاب (روش 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());
   }
}
وارد کنسول شوید:

learn.javarush.Cat
خروجی کنسول:

Cat{name='null', age=0}
این یک خطا نیست: مقادیر nameو ageدر کنسول نمایش داده می شوند زیرا ما خروجی آنها را در متد toString()کلاس برنامه ریزی کرده ایم Cat. در اینجا نام کلاسی که شیء آن را از کنسول ایجاد خواهیم کرد را می خوانیم. برنامه در حال اجرا نام کلاسی که شیء آن را ایجاد خواهد کرد را می آموزد. نمونه هایی از استفاده از Reflection - 3برای اختصار، کد را برای مدیریت صحیح استثنا حذف کرده ایم تا فضای بیشتری از خود مثال اشغال نکند. در یک برنامه واقعی، مطمئناً ارزش رسیدگی به موقعیت هایی را دارد که نام های نادرست و غیره وارد می شود. سازنده پیش فرض چیز نسبتاً ساده ای است، بنابراین ایجاد یک نمونه از یک کلاس با استفاده از آن، همانطور که می بینید، دشوار نیست :) و با استفاده از متد، newInstance()یک شی جدید از این کلاس ایجاد می کنیم. Catاگر سازنده کلاس پارامترها را به عنوان ورودی بگیرد، موضوع دیگری است . بیایید سازنده پیش فرض را از کلاس حذف کنیم و دوباره سعی کنیم کد خود را اجرا کنیم.

null
java.lang.InstantiationException: learn.javarush.Cat
  at java.lang.Class.newInstance(Class.java:427)
مشکلی پیش آمد! ما یک خطا دریافت کردیم زیرا متدی را برای ایجاد یک شی از طریق سازنده پیش فرض فراخوانی کردیم. اما اکنون ما چنین طراح نداریم. این بدان معنی است که وقتی روش کار می کند، newInstance()مکانیسم بازتاب از سازنده قدیمی ما با دو پارامتر استفاده می کند:
public Cat(String name, int age) {
   this.name = name;
   this.age = age;
}
اما ما هیچ کاری با پارامترها انجام ندادیم، انگار که آنها را کاملاً فراموش کرده ایم! برای ارسال آنها به سازنده با استفاده از انعکاس، باید کمی آن را تغییر دهید:
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());
   }
}
خروجی کنسول:

Cat{name='Barsik', age=6}
بیایید نگاهی دقیق تر به آنچه در برنامه ما می افتد بیندازیم. ما آرایه ای از اشیاء ایجاد کرده ایم Class.
Class[] catClassParams = {String.class, int.class};
آنها با پارامترهای سازنده ما مطابقت دارند (ما فقط پارامترها Stringو int) را داریم. آنها را به متد منتقل می کنیم clazz.getConstructor()و به سازنده مورد نیاز دسترسی پیدا می کنیم. پس از این، تنها چیزی که باقی می‌ماند این است که متد را newInstance()با پارامترهای لازم فراخوانی کنیم و فراموش نکنیم که به صراحت شی را به کلاس مورد نیاز خود ارسال کنیم - Cat.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Barsik", 6);
در نتیجه، شی ما با موفقیت ایجاد می شود! خروجی کنسول:

Cat{name='Barsik', age=6}
بریم جلو :)

نحوه دریافت و تنظیم مقدار یک فیلد شی با نام

تصور کنید که از کلاسی استفاده می کنید که توسط برنامه نویس دیگری نوشته شده است. با این حال، شما فرصتی برای ویرایش آن ندارید. به عنوان مثال، یک کتابخانه کلاس آماده بسته بندی شده در یک JAR. می توانید کد کلاس را بخوانید، اما نمی توانید آن را تغییر دهید. برنامه نویسی که کلاس را در این کتابخانه ایجاد کرده است (بگذارید کلاس قدیمی ما باشد Cat) قبل از طراحی نهایی خواب کافی نداشت و گیرنده ها و تنظیم کننده ها را برای فیلد حذف کرد age. حالا این کلاس برای شما آمده است. این به طور کامل نیازهای شما را برآورده می کند، زیرا شما فقط به اشیاء در برنامه نیاز دارید Cat. اما شما با همان زمینه به آنها نیاز دارید age! این یک مشکل است: ما نمی توانیم به فیلد برسیم، زیرا یک اصلاح کننده دارد privateو دریافت کننده ها و تنظیم کننده ها توسط توسعه دهنده احتمالی این کلاس حذف شده اند:/ خوب، بازتاب می تواند در این شرایط به ما کمک کند! Catما به کد کلاس دسترسی داریم: حداقل می توانیم بفهمیم که چه فیلدهایی دارد و نام آنها چیست. با داشتن این اطلاعات، مشکل خود را حل می کنیم:
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());
   }
}
همانطور که در کامنت گفته شد، nameهمه چیز با این فیلد ساده است: توسعه دهندگان کلاس یک تنظیم کننده برای آن ارائه کردند. شما همچنین می دانید که چگونه از سازنده های پیش فرض اشیاء ایجاد کنید: یک روش برای این وجود دارد newInstance(). اما شما باید با زمینه دوم سرهم بندی کنید. بیایید بفهمیم اینجا چه خبر است :)
Field age = clazz.getDeclaredField("age");
در اینجا ما با استفاده از شیء خود Class clazzبه فیلد ageبا استفاده از getDeclaredField(). این توانایی را به ما می دهد که میدان سن را به عنوان یک شیء بدست آوریم Field age. اما این هنوز کافی نیست، زیرا privateبه فیلدها نمی توان به سادگی مقادیری را اختصاص داد. برای انجام این کار، باید فیلد را با استفاده از روش "در دسترس" قرار دهید setAccessible():
age.setAccessible(true);
برای فیلدهایی که این کار انجام می شود می توان مقادیری را به آنها اختصاص داد:
age.set(cat, 6);
همانطور که می بینید، یک نوع تنظیم کننده وارونه داریم: Field ageمقدار آن را به فیلد اختصاص می دهیم و همچنین شیئی را که این فیلد باید به آن اختصاص داده شود، به آن منتقل می کنیم. بیایید روش خود را اجرا کنیم main()و ببینیم:

Cat{name='Barsik', age=6}
عالی، ما همه کارها را انجام دادیم! :) ببینیم چه امکانات دیگری داریم...

نحوه فراخوانی متد یک شی با نام

بیایید کمی وضعیت را از مثال قبلی تغییر دهیم. فرض کنید توسعه‌دهنده کلاس Catبا فیلدها اشتباه کرده است - هر دو در دسترس هستند، گیرنده‌ها و تنظیم‌کننده‌ها برای آنها وجود دارد، همه چیز اوکی است. مشکل متفاوت است: او روشی را خصوصی ساخت که ما قطعاً به آن نیاز داریم:
private void sayMeow() {

   System.out.println("Meow!");
}
در نتیجه، ما اشیایی را Catدر برنامه خود ایجاد می کنیم، اما نمی توانیم متد آنها را فراخوانی کنیم sayMeow(). آیا گربه هایی خواهیم داشت که میو نمی کنند؟ خیلی عجیبه:/ چطور میتونم درستش کنم؟ یک بار دیگر، Reflection API به کمک می آید! ما نام روش مورد نیاز را می دانیم. بقیه مسائل مربوط به تکنیک است:
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();
   }
}
در اینجا ما تقریباً به همان روشی عمل می کنیم که در وضعیت دسترسی به یک زمینه خصوصی است. ابتدا متد مورد نیاز خود را دریافت می کنیم که در یک شی کلاس محصور شده است Method:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
با کمک، getDeclaredMethod()می‌توانید به روش‌های خصوصی «دسترسی پیدا کنید». سپس متد را قابل فراخوانی می کنیم:
sayMeow.setAccessible(true);
و در نهایت متد را روی شی مورد نظر فراخوانی می کنیم:
sayMeow.invoke(cat);
فراخوانی یک متد نیز شبیه یک "تماس معکوس" است: ما عادت داریم که یک شی را با استفاده از یک نقطه ( cat.sayMeow()) به روش مورد نیاز اشاره کنیم و هنگام کار با انعکاس، شیء را که باید از آن فراخوانی شود به متدی منتقل می کنیم. . چه چیزی در کنسول داریم؟

Meow!
همه چیز درست شد! :) حالا می بینید که مکانیسم بازتاب در جاوا چه امکانات گسترده ای به ما می دهد. در موقعیت‌های سخت و غیرمنتظره (مانند نمونه‌های کلاسی از یک کتابخانه بسته)، واقعاً می‌تواند به ما کمک زیادی کند. با این حال، مانند هر قدرت بزرگ، این نیز متضمن مسئولیت بزرگ است. در مورد مضرات بازتاب در بخش ویژه ای در وب سایت اوراکل نوشته شده است. سه عیب اصلی وجود دارد:
  1. بهره وری کاهش می یابد. روش هایی که با استفاده از بازتاب فراخوانی می شوند، نسبت به روش هایی که به طور معمول فراخوانی می شوند، عملکرد کمتری دارند.

  2. محدودیت های ایمنی وجود دارد. مکانیسم انعکاس به شما اجازه می دهد تا رفتار برنامه را در زمان اجرا تغییر دهید. اما در محیط کاری شما در یک پروژه واقعی ممکن است محدودیت هایی وجود داشته باشد که به شما اجازه این کار را ندهد.

  3. خطر افشای اطلاعات داخلی درک این نکته مهم است که استفاده از بازتاب مستقیماً اصل کپسولاسیون را نقض می کند: به ما امکان دسترسی به زمینه ها، روش ها و غیره خصوصی را می دهد. من فکر می کنم نیازی به توضیح نیست که نقض مستقیم و فاحش اصول OOP فقط در شدیدترین موارد باید متوسل شود، زمانی که هیچ راه دیگری برای حل مشکل به دلایل خارج از کنترل شما وجود ندارد.

از مکانیسم انعکاس عاقلانه و فقط در شرایطی که نمی توان از آن اجتناب کرد استفاده کنید و کاستی های آن را فراموش نکنید. این سخنرانی ما را به پایان می رساند! معلوم شد خیلی بزرگ است، اما امروز چیزهای جدید زیادی یاد گرفتید :)
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION