JavaRush /مدونة جافا /Random-AR /الانعكاس في جافا - أمثلة الاستخدام

الانعكاس في جافا - أمثلة الاستخدام

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

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

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

كيفية الحصول على معلومات حول معدّلات الفئة والحقول والأساليب والثوابت والمنشئات والفئات الفائقة

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

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

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

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