CodeGym /مدونة جافا /Random-AR /أمثلة على الانعكاس
John Squirrels
مستوى
San Francisco

أمثلة على الانعكاس

نشرت في المجموعة
ربما واجهت مفهوم "التأمل" في الحياة العادية. تشير هذه الكلمة عادةً إلى عملية دراسة الذات. في البرمجة، لها معنى مماثل - إنها آلية لتحليل البيانات حول البرنامج، وحتى تغيير هيكل البرنامج وسلوكه أثناء تشغيل البرنامج. أمثلة على الانعكاس - 1 المهم هنا هو أننا نقوم بذلك في وقت التشغيل، وليس في وقت الترجمة. ولكن لماذا فحص التعليمات البرمجية في وقت التشغيل؟ بعد كل شيء، يمكنك بالفعل قراءة الكود :/ هناك سبب لعدم وضوح فكرة الانعكاس على الفور: حتى هذه اللحظة، كنت تعرف دائمًا الفئات التي كنت تعمل معها. على سبيل المثال، يمكنك كتابة 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 قياسية أو أي فئة أخرى يكتبونها. يمكن أن تحتوي الفئة التي تم تمريرها على أي عدد من المتغيرات والأساليب. بمعنى آخر، ليس لدينا (وبرنامجنا) أي فكرة عن الفئات التي سنعمل معها. ولكن لا يزال يتعين علينا إكمال هذه المهمة. وهذا هو المكان الذي تأتي فيه واجهة برمجة تطبيقات Java Reflection القياسية لمساعدتنا. تعد واجهة برمجة تطبيقات Reflection أداة قوية للغة. توصي وثائق Oracle الرسمية بعدم استخدام هذه الآلية إلا من قبل المبرمجين ذوي الخبرة الذين يعرفون ما يفعلونه. ستفهم قريبًا سبب تقديمنا لهذا النوع من التحذير مسبقًا :) إليك قائمة بالأشياء التي يمكنك القيام بها باستخدام Reflection API:
  1. تحديد/تحديد فئة الكائن.
  2. احصل على معلومات حول معدّلات الفئة والحقول والأساليب والثوابت والمنشئات والفئات الفائقة.
  3. تعرف على الطرق التي تنتمي إلى الواجهة (الواجهات) المطبقة.
  4. قم بإنشاء مثيل لفئة لا يُعرف اسم الفئة الخاصة بها حتى يتم تنفيذ البرنامج.
  5. احصل على قيمة حقل المثيل وقم بتعيينها بالاسم.
  6. استدعاء أسلوب المثيل بالاسم.
قائمة مثيرة للإعجاب، هاه؟ :) ملحوظة:يمكن لآلية الانعكاس أن تفعل كل هذه الأشياء "بسرعة"، بغض النظر عن نوع الكائن الذي نمرره إلى محلل التعليمات البرمجية الخاص بنا! دعنا نستكشف إمكانيات Reflection API من خلال النظر في بعض الأمثلة.

كيفية تحديد/تحديد فئة الكائن

هيا لنبدأ مع الأساسيات. نقطة الدخول إلى محرك انعكاس Java هي 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. نقرأ هنا اسم الفئة التي سنقوم بإنشاء كائنها من وحدة التحكم. يتعرف البرنامج على اسم الفئة التي سيتم إنشاء كائنها. أمثلة على الانعكاس - 3من أجل الإيجاز، قمنا بحذف التعليمات البرمجية المناسبة لمعالجة الاستثناءات، والتي قد تشغل مساحة أكبر من المثال نفسه. في البرنامج الحقيقي، بالطبع، يجب عليك التعامل مع المواقف التي تتضمن أسماء تم إدخالها بشكل غير صحيح، وما إلى ذلك. المُنشئ الافتراضي بسيط جدًا، لذا كما ترون، من السهل استخدامه لإنشاء مثيل للفئة :) باستخدام 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، وقد تم حذف getter وsetter بواسطة المطور المحروم من النوم الذي أنشأ الفصل :/ حسنًا، يمكن أن يساعدنا التفكير في هذا الموقف! لدينا إمكانية الوصول إلى رمز الفصل الدراسي 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! نحن نعرف اسم الطريقة التي نحتاجها. كل شيء آخر هو تقنية:

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!
كل شيء يعمل! :) يمكنك الآن رؤية الإمكانيات الهائلة التي توفرها لنا آلية الانعكاس في Java. في المواقف الصعبة وغير المتوقعة (مثل الأمثلة التي قدمناها مع فصل دراسي من مكتبة مغلقة)، يمكن أن يساعدنا ذلك كثيرًا. ولكن، كما هو الحال مع أي قوة عظمى، فإنها تحمل مسؤولية كبيرة. تم توضيح عيوب الانعكاس في قسم خاص على موقع Oracle الإلكتروني. هناك ثلاثة عيوب رئيسية:
  1. الأداء أسوأ. الطرق التي تسمى باستخدام الانعكاس لها أداء أسوأ من الطرق التي تسمى بالطريقة العادية.

  2. هناك قيود أمنية. تتيح لنا آلية الانعكاس تغيير سلوك البرنامج في وقت التشغيل. لكن في مكان عملك، عند العمل على مشروع حقيقي، قد تواجه قيودًا لا تسمح بذلك.

  3. خطر التعرض للمعلومات الداخلية. من المهم أن نفهم أن الانعكاس هو انتهاك مباشر لمبدأ التغليف: فهو يتيح لنا الوصول إلى الحقول الخاصة والأساليب وما إلى ذلك. لا أعتقد أنني بحاجة إلى الإشارة إلى أنه ينبغي اللجوء إلى انتهاك مباشر وصارخ لمبادئ OOP فقط في الحالات القصوى، عندما لا تكون هناك طرق أخرى لحل مشكلة ما لأسباب خارجة عن إرادتك.

استخدم التفكير بحكمة وفقط في المواقف التي لا يمكن تجنبها، ولا تنسى عيوبه. وبهذا يكون درسنا قد انتهى. اتضح أنها طويلة جدًا، لكنك تعلمت الكثير اليوم :)
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION