شاید شما در زندگی عادی با مفهوم «بازتاب» مواجه شده باشید. این کلمه معمولاً به فرآیند مطالعه خود اشاره دارد. در برنامه نویسی، معنای مشابهی دارد - مکانیزمی است برای تجزیه و تحلیل داده های یک برنامه، و حتی تغییر ساختار و رفتار یک برنامه، در حالی که برنامه در حال اجرا است.
نکته مهم در اینجا این است که ما این کار را در زمان اجرا انجام می دهیم، نه در زمان کامپایل. اما چرا کد را در زمان اجرا بررسی کنیم؟ پس از همه، شما قبلاً می توانید کد را بخوانید:/ دلیلی وجود دارد که ایده بازتاب ممکن است فوراً واضح نباشد: تا این مرحله، همیشه می دانستید که با چه کلاس هایی کار می کنید. به عنوان مثال، می توانید یک
برای اختصار، کد مدیریت استثنای مناسب را حذف کردیم، که فضای بیشتری را نسبت به خود مثال اشغال میکرد. در یک برنامه واقعی، البته، شما باید موقعیتهایی را که شامل نامهای اشتباه وارد شده و غیره است، مدیریت کنید. سازنده پیشفرض بسیار ساده است، بنابراین همانطور که میبینید، استفاده از آن برای ایجاد نمونهای از کلاس آسان است :) با استفاده از

Cat
کلاس بنویسید:
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 +
'}';
}
}
شما همه چیز را در مورد آن می دانید و می توانید زمینه ها و روش هایی را که دارد مشاهده کنید. فرض کنید شما به طور ناگهانی نیاز دارید تا کلاس های حیوانات دیگر را به برنامه معرفی کنید. احتمالاً می توانید یک ساختار ارث بری کلاس با یک Animal
کلاس والد برای راحتی ایجاد کنید. پیش از این، ما حتی یک کلاس به نمایندگی از یک کلینیک دامپزشکی ایجاد کردیم که میتوانستیم یک Animal
شی (نمونهای از کلاس والدین) را به آن منتقل کنیم و برنامه بر اساس سگ یا گربه بودن حیوان، رفتار مناسبی با حیوان داشت. اگرچه اینها ساده ترین کارها نیستند، برنامه قادر است تمام اطلاعات لازم در مورد کلاس ها را در زمان کامپایل بیاموزد. Cat
بر این اساس، هنگامی که یک شی را به روش های کلاس کلینیک دامپزشکی در روش منتقل می کنید main()
، برنامه از قبل می داند که یک گربه است، نه یک سگ. حالا بیایید تصور کنیم که با یک کار متفاوت روبرو هستیم. هدف ما نوشتن یک تحلیلگر کد است. ما باید یک CodeAnalyzer
کلاس با یک متد ایجاد کنیم: void analyzeObject(Object o)
. این روش باید:
- کلاس شیء ارسال شده به آن را تعیین کنید و نام کلاس را در کنسول نمایش دهید.
- نام تمام فیلدهای کلاس پاس شده از جمله خصوصی را تعیین کنید و آنها را در کنسول نمایش دهید.
- نام تمام متدهای کلاس پاس شده از جمله خصوصی را تعیین کرده و در کنسول نمایش دهید.
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
}
}
اکنون می توانیم به وضوح ببینیم که این کار چه تفاوتی با سایر کارهایی که قبلاً حل کرده اید دارد. با هدف فعلی ما، مشکل در این واقعیت است که نه ما و نه برنامه نمی دانیم دقیقاً چه چیزی به analyzeClass()
روش منتقل می شود. اگر چنین برنامه ای بنویسید، برنامه نویسان دیگر شروع به استفاده از آن خواهند کرد و ممکن است هر چیزی را به این روش منتقل کنند - هر کلاس استاندارد جاوا یا هر کلاس دیگری که بنویسند. کلاس پاس می تواند هر تعداد متغیر و متد داشته باشد. به عبارت دیگر، ما (و برنامه ما) هیچ ایده ای نداریم که با چه کلاس هایی کار خواهیم کرد. اما با این حال، ما باید این کار را کامل کنیم. و اینجاست که Java Reflection API استاندارد به کمک ما می آید. Reflection API یک ابزار قدرتمند این زبان است. اسناد رسمی Oracle توصیه می کند که این مکانیسم فقط باید توسط برنامه نویسان با تجربه ای که می دانند چه کاری انجام می دهند استفاده شود. به زودی متوجه خواهید شد که چرا ما از قبل به این نوع هشدار می دهیم :) در اینجا لیستی از کارهایی است که می توانید با Reflection API انجام دهید:
- شناسایی/تعیین کلاس یک شی.
- اطلاعاتی در مورد اصلاح کننده های کلاس، فیلدها، متدها، ثابت ها، سازنده ها و سوپرکلاس ها دریافت کنید.
- دریابید که کدام متدها به یک رابط(های) پیاده سازی شده تعلق دارند.
- یک نمونه از کلاسی ایجاد کنید که نام کلاس آن تا زمانی که برنامه اجرا نشود مشخص نیست.
- مقدار یک فیلد نمونه را با نام دریافت و تنظیم کنید.
- یک متد نمونه را با نام فراخوانی کنید.
نحوه شناسایی/تعیین کلاس یک شی
بیایید با اصول اولیه شروع کنیم. نقطه ورود به موتور بازتاب جاواClass
کلاس است. بله، واقعا خنده دار به نظر می رسد، اما بازتاب این است :) با استفاده از Class
کلاس، ابتدا کلاس هر شیء ارسال شده به متد خود را تعیین می کنیم. بیایید سعی کنیم این کار را انجام دهیم:
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));
}
}
خروجی کنسول:
class learn.codegym.Cat
به دو چیز توجه کنید. ابتدا، ما عمداً Cat
کلاس را در یک learn.codegym
بسته جداگانه قرار می دهیم. اکنون می توانید ببینید که getClass()
متد نام کامل کلاس را برمی گرداند. دوم، ما متغیر خود را نامگذاری کردیم clazz
. که کمی عجیب به نظر می رسد. منطقی است که آن را "کلاس" بنامیم، اما "کلاس" یک کلمه رزرو شده در جاوا است. کامپایلر اجازه نمی دهد که متغیرها به این شکل نامیده شوند. ما مجبور شدیم به نحوی از آن دور شویم :) برای شروع بد نیست! چه چیز دیگری در آن لیست از قابلیت ها داشتیم؟
نحوه به دست آوردن اطلاعات در مورد اصلاح کننده های کلاس، فیلدها، متدها، ثابت ها، سازنده ها و سوپرکلاس ها.
حالا همه چیز جالب تر می شود! در کلاس فعلی، هیچ ثابت یا کلاس والد نداریم. بیایید آنها را اضافه کنیم تا یک تصویر کامل ایجاد کنیم. ساده ترینAnimal
کلاس والد را ایجاد کنید:
package learn.codegym;
public class Animal {
private String name;
private int age;
}
Cat
و کلاس خود را به ارث می بریم Animal
و یک ثابت اضافه می کنیم:
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
}
حالا ما تصویر کامل را داریم! بیایید ببینیم بازتاب چه توانایی هایی دارد :)
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));
}
}
در اینجا چیزی است که ما در کنسول می بینیم:
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)]
به تمام آن اطلاعات دقیق کلاسی که توانستیم به دست آوریم نگاه کنید! و نه فقط اطلاعات عمومی، بلکه اطلاعات خصوصی! توجه داشته باشید: private
متغیرها نیز در لیست نمایش داده می شوند. "تحلیل" ما از کلاس را می توان اساساً کامل در نظر گرفت: ما از این analyzeObject()
روش برای یادگیری هر چیزی که می توانیم استفاده می کنیم. اما این همه کاری نیست که ما می توانیم با بازتاب انجام دهیم. ما محدود به مشاهده ساده نیستیم - ما به اقدام خواهیم پرداخت! :)
نحوه ایجاد نمونه ای از کلاسی که نام کلاس آن تا زمانی که برنامه اجرا نشود مشخص نیست.
بیایید با سازنده پیش فرض شروع کنیم. کلاس ماCat
هنوز یکی ندارد، پس بیایید آن را اضافه کنیم:
public Cat() {
}
در اینجا کد ایجاد یک Cat
شی با استفاده از بازتاب ( createCat()
روش) آمده است:
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());
}
}
ورودی کنسول:
learn.codegym.Cat
خروجی کنسول:
Cat{name='null', age=0}
این یک خطا نیست: مقادیر name
و age
در کنسول نمایش داده می شوند زیرا ما کدی را برای خروجی آنها در toString()
متد کلاس نوشته ایم Cat
. در اینجا نام کلاسی را می خوانیم که شیء آن را از کنسول می سازیم. برنامه نام کلاسی را که قرار است شیء آن ایجاد شود را تشخیص می دهد. 
newInstance()
متد ، یک شی جدید از این کلاس ایجاد می کنیم. Cat
اگر سازنده آرگومان ها را به عنوان ورودی بگیرد، موضوع دیگری است . بیایید سازنده پیش فرض کلاس را حذف کنیم و دوباره سعی کنیم کد خود را اجرا کنیم.
null
java.lang.InstantiationException: learn.codegym.Cat
at java.lang.Class.newInstance(Class.java:427)
مشکلی پیش آمد! ما یک خطا دریافت کردیم زیرا متدی را برای ایجاد یک شی با استفاده از سازنده پیش فرض فراخوانی کردیم. اما ما الان چنین سازنده ای نداریم. بنابراین هنگامی که newInstance()
متد اجرا می شود، مکانیسم بازتاب از سازنده قدیمی ما با دو پارامتر استفاده می کند:
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
اما ما هیچ کاری با پارامترها انجام ندادیم، انگار که آنها را کاملاً فراموش کرده ایم! استفاده از بازتاب برای انتقال آرگومان ها به سازنده نیاز به کمی «خلاقیت» دارد:
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());
}
}
خروجی کنسول:
Cat{name='Fluffy', age=6}
بیایید نگاهی دقیق تر به آنچه در برنامه ما می افتد بیندازیم. ما یک آرایه از Class
اشیاء ایجاد کردیم.
Class[] catClassParams = {String.class, int.class};
آنها با پارامترهای سازنده ما (که فقط دارای String
و int
پارامترها هستند) مطابقت دارند. آنها را به متد منتقل می کنیم clazz.getConstructor()
و به سازنده مورد نظر دسترسی پیدا می کنیم. پس از آن، تنها کاری که باید انجام دهیم این است که newInstance()
متد را با آرگومان های لازم فراخوانی کنیم و فراموش نکنیم که به طور صریح شی را به نوع دلخواه بریزیم: Cat
.
cat = (Cat) clazz.getConstructor(catClassParams).newInstance("Fluffy", 6);
اکنون شیء ما با موفقیت ایجاد شد! خروجی کنسول:
Cat{name='Fluffy', age=6}
حرکت درست در امتداد :)
نحوه دریافت و تنظیم مقدار یک فیلد نمونه بر اساس نام.
تصور کنید که از کلاسی استفاده می کنید که توسط برنامه نویس دیگری نوشته شده است. علاوه بر این، شما توانایی ویرایش آن را ندارید. به عنوان مثال، یک کتابخانه کلاس آماده بسته بندی شده در یک JAR. شما می توانید کد کلاس ها را بخوانید، اما نمی توانید آن را تغییر دهید. فرض کنید برنامه نویسی که یکی از کلاس های این کتابخانه را ایجاد کرده است (بگذاریدCat
کلاس قدیمی ما باشد)، که شب قبل از نهایی شدن طرح نتوانسته خواب کافی داشته باشد، گیرنده و تنظیم کننده را برای فیلد حذف کرده است age
. حالا این کلاس برای شما آمده است. تمام نیازهای شما را برآورده می کند، زیرا شما فقط به Cat
اشیاء در برنامه خود نیاز دارید. اما برای داشتن یک میدان به آنها نیاز دارید age
! این یک مشکل است: ما نمی توانیم به فیلد برسیم، زیرا اصلاح کننده را دارد private
و دریافت کننده و تنظیم کننده توسط توسعه دهنده محروم از خواب که کلاس را ایجاد کرده است حذف شده است:/ خوب، بازتاب می تواند در این شرایط به ما کمک کند! ما به کد کلاس دسترسی داریم Cat
، بنابراین حداقل می توانیم بفهمیم که چه فیلدهایی دارد و نام آنها چیست. با استفاده از این اطلاعات، می توانیم مشکل خود را حل کنیم:
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());
}
}
همانطور که در نظرات گفته شد، همه چیز در مورد فیلد name
ساده است، زیرا توسعه دهندگان کلاس یک تنظیم کننده ارائه کردند. شما قبلاً می دانید که چگونه از سازنده های پیش فرض اشیاء ایجاد کنید: ما newInstance()
برای این کار داریم. اما ما باید در زمینه دوم کمی سرهم بندی کنیم. بیایید بفهمیم اینجا چه خبر است :)
Field age = clazz.getDeclaredField("age");
در اینجا با استفاده از Class clazz
شیء خود، age
از طریق getDeclaredField()
متد به فیلد دسترسی پیدا می کنیم. این به ما امکان می دهد که فیلد سن را به عنوان یک Field age
شیء دریافت کنیم. اما این کافی نیست، زیرا نمی توانیم به سادگی مقادیری را به private
فیلدها اختصاص دهیم. برای انجام این کار، باید با استفاده از setAccessible()
روش، فیلد را در دسترس قرار دهیم:
age.setAccessible(true);
هنگامی که این کار را به یک فیلد انجام می دهیم، می توانیم یک مقدار را تعیین کنیم:
age.set(cat, 6);
همانطور که می بینید، Field age
آبجکت ما یک نوع تنظیم کننده داخلی به بیرون دارد که یک مقدار int و شیئی که قرار است فیلد آن اختصاص داده شود را به آن ارسال می کنیم. ما main()
روش خود را اجرا می کنیم و می بینیم:
Cat{name='Fluffy', age=6}
عالی! ما آن را انجام دادیم! :) ببینیم دیگه چیکار می تونیم بکنیم...
چگونه یک متد نمونه را با نام فراخوانی کنیم.
بیایید وضعیت را در مثال قبلی کمی تغییر دهیم. بیایید بگوییم کهCat
توسعه دهنده کلاس با دریافت کننده ها و تنظیم کننده ها اشتباه نکرده است. از این نظر همه چیز درست است. اکنون مشکل متفاوت است: روشی وجود دارد که ما قطعاً به آن نیاز داریم، اما توسعه دهنده آن را خصوصی کرده است:
private void sayMeow() {
System.out.println("Meow!");
}
این بدان معناست که اگر در برنامه خود اشیایی ایجاد کنیم Cat
، نمی توانیم sayMeow()
متد را روی آنها فراخوانی کنیم. ما گربه هایی خواهیم داشت که میو نمی کنند؟ عجیب است :/ چگونه این را برطرف کنیم؟ یک بار دیگر، Reflection API به ما کمک می کند! ما نام روش مورد نیاز خود را می دانیم. هر چیز دیگری یک جنبه فنی است:
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();
}
}
در اینجا ما تقریباً همان کاری را انجام می دهیم که هنگام دسترسی به یک فیلد خصوصی انجام دادیم. ابتدا روش مورد نیاز خود را بدست می آوریم. در یک شی محصور شده است Method
:
Method sayMeow = clazz.getDeclaredMethod("sayMeow");
این getDeclaredMethod()
روش به ما امکان می دهد به روش های خصوصی برسیم. در مرحله بعد، متد را قابل فراخوانی می کنیم:
sayMeow.setAccessible(true);
و در نهایت متد را روی شی مورد نظر فراخوانی می کنیم:
sayMeow.invoke(cat);
در اینجا، فراخوانی متد ما شبیه یک "بازخوانی" است: ما عادت داریم از یک نقطه برای نشان دادن یک شی به روش مورد نظر استفاده کنیم ( cat.sayMeow()
)، اما هنگام کار با انعکاس، به روشی که میخواهیم آن را فراخوانی کنیم، منتقل میکنیم. آن روش روی کنسول ما چیست؟
Meow!
همه چیز کار کرد! :) اکنون می توانید امکانات گسترده ای که مکانیسم بازتاب جاوا به ما می دهد را ببینید. در شرایط سخت و غیرمنتظره (مانند مثال های ما با یک کلاس از یک کتابخانه بسته)، واقعاً می تواند به ما کمک زیادی کند. اما، مانند هر قدرت بزرگ، مسئولیت بزرگی به همراه دارد. معایب انعکاس در بخش ویژه ای
در وب سایت اوراکل توضیح داده شده است. سه عیب اصلی وجود دارد:
-
عملکرد بدتر است. روش هایی که با استفاده از بازتاب نامیده می شوند، عملکرد بدتری نسبت به روش هایی دارند که به روش معمولی نامیده می شوند.
-
محدودیت های امنیتی وجود دارد. مکانیسم انعکاس به ما امکان می دهد رفتار برنامه را در زمان اجرا تغییر دهیم. اما در محل کار خود، هنگام کار بر روی یک پروژه واقعی، ممکن است با محدودیت هایی روبرو شوید که این اجازه را نمی دهد.
-
خطر قرار گرفتن در معرض اطلاعات داخلی درک این نکته مهم است که بازتاب نقض مستقیم اصل کپسولهسازی است: به ما امکان میدهد به زمینهها، روشها و غیره خصوصی دسترسی پیدا کنیم. فکر نمیکنم لازم باشد اشاره کنم که باید به نقض مستقیم و آشکار اصول OOP متوسل شد. فقط در شدیدترین موارد، زمانی که هیچ راه دیگری برای حل مشکل به دلایلی خارج از کنترل شما وجود ندارد.
GO TO FULL VERSION