CodeGym /בלוג Java /Random-HE /דוגמאות של השתקפות
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 או כל מחלקה אחרת שהם כותבים. המחלקה שעברה יכולה לכלול כל מספר של משתנים ושיטות. במילים אחרות, לנו (ולתוכנית שלנו) אין מושג עם אילו שיעורים נעבוד. אבל עדיין, אנחנו צריכים להשלים את המשימה הזו. וכאן בא לעזרתנו ה-API הסטנדרטי של Java Reflection. ה-Reflection API הוא כלי רב עוצמה של השפה. התיעוד הרשמי של אורקל ממליץ להשתמש במנגנון זה רק על ידי מתכנתים מנוסים שיודעים מה הם עושים. בקרוב תבינו למה אנחנו נותנים אזהרה כזו מראש :) הנה רשימה של דברים שתוכל לעשות עם 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הכיתה הישנה שלנו), שלא הצליח לישון מספיק בלילה שלפני סיום העיצוב, הסיר את ה-getter וה-seter לשטח age. עכשיו השיעור הזה הגיע אליכם. זה עונה על כל הצרכים שלך, מכיוון שאתה רק צריך Catאובייקטים בתוכנית שלך. אבל אתה צריך שיהיה להם ageשדה! זו בעיה: אנחנו לא יכולים להגיע לשדה, כי יש לו את ה- privatemodifier, וה-getter וה-seter נמחקו על ידי המפתח חסר השינה שיצר את המחלקה :/ ובכן, השתקפות יכולה לעזור לנו במצב הזה! יש לנו גישה לקוד של 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);
כאן, קריאת השיטה שלנו נראית כמו "callback": אנחנו רגילים להשתמש בנקודה כדי להצביע על אובייקט על השיטה הרצויה ( cat.sayMeow()), אבל כשעובדים עם השתקפות, אנחנו מעבירים לשיטה את האובייקט שעליו אנחנו רוצים לקרוא השיטה הזו. מה יש בקונסולה שלנו?

Meow!
הכל עבד! :) עכשיו אתה יכול לראות את האפשרויות העצומות שמנגנון ההשתקפות של Java נותן לנו. במצבים קשים ובלתי צפויים (כמו דוגמאות שלנו עם כיתה מספרייה סגורה), זה באמת יכול לעזור לנו מאוד. אבל, כמו בכל כוח גדול, זה מביא אחריות גדולה. החסרונות של השתקפות מתוארים בחלק מיוחד באתר אורקל. ישנם שלושה חסרונות עיקריים:
  1. הביצועים גרועים יותר. לשיטות הנקראות באמצעות רפלקציה יש ביצועים גרועים יותר משיטות הנקראות בדרך הרגילה.

  2. יש מגבלות אבטחה. מנגנון ההשתקפות מאפשר לנו לשנות התנהגות של תוכנית בזמן ריצה. אבל במקום העבודה שלך, כשאתה עובד על פרויקט אמיתי, אתה עלול להתמודד עם מגבלות שאינן מאפשרות זאת.

  3. סיכון לחשיפה של מידע פנימי. חשוב להבין שהשתקפות היא הפרה ישירה של עקרון האנקפסולציה: היא מאפשרת לנו לגשת לשדות פרטיים, שיטות וכו'. אני לא חושב שצריך להזכיר שיש לנקוט בהפרה ישירה ובוטה של ​​עקרונות OOP רק במקרים הקיצוניים ביותר, כאשר אין דרכים אחרות לפתור בעיה מסיבות שאינן בשליטתך.

השתמשו ברפלקציה בחוכמה ורק במצבים בהם לא ניתן להימנע ממנה, ואל תשכחו את חסרונותיה. בכך, השיעור שלנו הגיע לסיומו. התברר שזה די ארוך, אבל למדת הרבה היום :)
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION