אולי נתקלת במושג "השתקפות" בחיים הרגילים. מילה זו מתייחסת בדרך כלל לתהליך של לימוד עצמי. בתכנות, יש לזה משמעות דומה - זהו מנגנון לניתוח נתונים על תוכנית, ואפילו שינוי מבנה והתנהגות של תוכנית, בזמן שהתוכנית פועלת.
הדבר החשוב כאן הוא שאנו עושים זאת בזמן ריצה, לא בזמן הידור. אבל למה לבחון את הקוד בזמן ריצה? אחרי הכל, אתה כבר יכול לקרוא את הקוד :/ יש סיבה לכך שרעיון השתקפות אולי לא ברור מיד: עד לנקודה זו תמיד ידעת עם אילו כיתות אתה עובד. לדוגמה, תוכל לכתוב
למען הקיצור, השמטנו קוד טיפול חריג מתאים, אשר יתפוס יותר מקום מהדוגמה עצמה. בתוכנית אמיתית, כמובן, אתה צריך לטפל במצבים הכוללים שמות שהוזנו לא נכון וכו'. בנאי ברירת המחדל הוא די פשוט, כך שכפי שאתה יכול לראות, קל להשתמש בו כדי ליצור מופע של המחלקה :) באמצעות

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:
- זהה/קבע את המחלקה של אובייקט.
- קבל מידע על משנה מחלקות, שדות, שיטות, קבועים, בנאים ומחלקות-על.
- גלה אילו שיטות שייכות לממשק(ים) מיושם.
- צור מופע של מחלקה ששם המחלקה שלה אינו ידוע עד להפעלת התוכנית.
- קבל והגדר את הערך של שדה מופע לפי שם.
- קרא לשיטת מופע בשם.
כיצד לזהות/לקבוע את המחלקה של אובייקט
בואו נתחיל עם היסודות. נקודת הכניסה למנוע השתקפות 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
המחלקה. כאן אנו קוראים שם של מחלקה שאת האובייקט שלה ניצור מהמסוף. התוכנה מזהה את שם המחלקה שהאובייקט שלה אמור להיווצר. 
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
שדה! זו בעיה: אנחנו לא יכולים להגיע לשדה, כי יש לו את ה- private
modifier, וה-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 נותן לנו. במצבים קשים ובלתי צפויים (כמו דוגמאות שלנו עם כיתה מספרייה סגורה), זה באמת יכול לעזור לנו מאוד. אבל, כמו בכל כוח גדול, זה מביא אחריות גדולה. החסרונות של השתקפות מתוארים בחלק מיוחד
באתר אורקל. ישנם שלושה חסרונות עיקריים:
-
הביצועים גרועים יותר. לשיטות הנקראות באמצעות רפלקציה יש ביצועים גרועים יותר משיטות הנקראות בדרך הרגילה.
-
יש מגבלות אבטחה. מנגנון ההשתקפות מאפשר לנו לשנות התנהגות של תוכנית בזמן ריצה. אבל במקום העבודה שלך, כשאתה עובד על פרויקט אמיתי, אתה עלול להתמודד עם מגבלות שאינן מאפשרות זאת.
-
סיכון לחשיפה של מידע פנימי. חשוב להבין שהשתקפות היא הפרה ישירה של עקרון האנקפסולציה: היא מאפשרת לנו לגשת לשדות פרטיים, שיטות וכו'. אני לא חושב שצריך להזכיר שיש לנקוט בהפרה ישירה ובוטה של עקרונות OOP רק במקרים הקיצוניים ביותר, כאשר אין דרכים אחרות לפתור בעיה מסיבות שאינן בשליטתך.
GO TO FULL VERSION