JavaRush /בלוג Java /Random-HE /השתקפות בג'אווה - דוגמאות לשימוש

השתקפות בג'אווה - דוגמאות לשימוש

פורסם בקבוצה
ייתכן שנתקלת במושג "השתקפות" בחיי היומיום. בדרך כלל מילה זו מתייחסת לתהליך של לימוד עצמי. בתכנות יש לזה משמעות דומה - זהו מנגנון לבחינת נתונים על תכנית וכן לשינוי מבנה והתנהגות התכנית במהלך ביצועה. הדבר החשוב כאן הוא שזה נעשה בזמן ריצה, לא בזמן הידור. אבל למה לבחון את הקוד בזמן ריצה? אתה כבר רואה את זה :/ דוגמאות לשימוש ברפלקציה - 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 הסטנדרטית - ה-API של Java Reflection. ה-Reflection API הוא תכונת שפה רבת עוצמה. התיעוד הרשמי של אורקל קובע שמומלץ להשתמש במנגנון זה רק על ידי מתכנתים מנוסים שמבינים היטב מה הם עושים. בקרוב תבינו למה פתאום נותנים לנו אזהרות כאלה מראש :) הנה רשימה של מה שאפשר לעשות באמצעות ה-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) לא שינה מספיק לפני העיצוב הסופי והסיר את ה-Getters ו-seters לשטח age. עכשיו השיעור הזה הגיע אליכם. זה עונה באופן מלא על הצרכים שלך, כי אתה רק צריך אובייקטים בתוכנית Cat. אבל אתה צריך אותם עם אותו תחום age! זו בעיה: אנחנו לא יכולים להגיע לשדה, כי יש לו משתנה private, וה-Getters ו-seters הוסרו על ידי המפתח לעתיד של המחלקה הזו :/ ובכן, השתקפות יכולה לעזור לנו גם במצב הזה! 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