JavaRush /בלוג Java /Random-HE /Reflection API. הִשׁתַקְפוּת. הצד האפל של ג'אווה
Oleksandr Klymenko
רָמָה
Харків

Reflection API. הִשׁתַקְפוּת. הצד האפל של ג'אווה

פורסם בקבוצה
שלום, פדאוואן הצעיר. במאמר זה אספר לכם על ה-Force, שבכוחו משתמשים מתכנתי ג'אווה רק במצב חסר סיכוי לכאורה. אז, הצד האפל של ג'אווה הוא -Reflection API
Reflection API.  הִשׁתַקְפוּת.  הצד האפל של ג'אווה - 1
השתקפות ב-Java מתבצעת באמצעות Java Reflection API. מה זה ההשתקפות הזו? ישנה הגדרה קצרה ומדויקת שפופולרית גם באינטרנט. רפלקציה (מתוך Late Latin reflexio - חוזר אחורה) הוא מנגנון ללימוד נתונים על תוכנית במהלך ביצועה. Reflection מאפשר לך לבחון מידע על שדות, שיטות ובני מחלקות. מנגנון ההשתקפות עצמו מאפשר לעבד סוגים שחסרים במהלך ההידור, אך מופיעים במהלך הפעלת התוכנית. השתקפות ונוכחות של מודל קוהרנטי מבחינה לוגית לדיווח על שגיאות מאפשרת ליצור קוד דינמי נכון. במילים אחרות, ההבנה כיצד פועלת השתקפות בג'אווה פותחת בפניך מספר הזדמנויות מדהימות. אתה יכול ממש ללהטט בין שיעורים ומרכיביהם.
Reflection API.  הִשׁתַקְפוּת.  הצד האפל של ג'אווה - 2
להלן רשימה בסיסית של מה שהשתקפות מאפשרת:
  • גלה/קבע את המחלקה של אובייקט;
  • קבל מידע על משנה מחלקות, שדות, שיטות, קבועים, בנאים ומחלקות-על;
  • גלה אילו שיטות שייכות לממשק/ממשקים המיושמים;
  • צור מופע של מחלקה, ושם המחלקה אינו ידוע עד להפעלת התוכנית;
  • קבל והגדר את הערך של שדה אובייקט לפי שם;
  • קרא למתודה של אובייקט בשם.
השתקפות משמשת כמעט בכל טכנולוגיות Java המודרניות. קשה לדמיין אם ג'אווה כפלטפורמה הייתה יכולה להשיג אימוץ כה עצום ללא השתקפות. סביר להניח שלא הצלחתי. הכרתם את הרעיון התיאורטי הכללי של השתקפות, עכשיו בואו נרד ליישום המעשי שלו! לא נלמד את כל השיטות של ה-Reflection API, אלא רק את מה שנתקל בו בפועל. מכיוון שמנגנון השתקפות כולל עבודה עם כיתות, תהיה לנו מחלקה פשוטה - MyClass:
public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
כפי שאנו יכולים לראות, זהו המעמד הנפוץ ביותר. הקונסטרוקטור עם הפרמטרים מוזכר מסיבה כלשהי, נחזור לזה מאוחר יותר. אם הסתכלת מקרוב על תוכן השיעור, כנראה שראית את היעדר getter'a עבור ה- name. השדה עצמו nameמסומן במשנה גישה private; לא נוכל לגשת אליו מחוץ למחלקה עצמה; =>לא נוכל לקבל את הערך שלו. "אז מה הבעיה? - אתה אומר. "הוסף getterאו שנה את משנה הגישה." ואתה תצדק, אבל מה אם MyClassזה בספריית ar מהודרת או במודול סגור אחר ללא גישת עריכה, ובפועל זה קורה לעתים קרובות ביותר. ואיזה מתכנת לא קשוב פשוט שכח לכתוב getter. הגיע הזמן להיזכר בהשתקפות! בואו ננסה להגיע privateלשדה nameהכיתה MyClass:
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //no getter =(
   System.out.println(number + name);//output 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name);//output 0default
}
בואו נבין מה קרה כאן עכשיו. יש שיעור נפלא בג'אווה Class. הוא מייצג מחלקות וממשקים ביישום Java בר הפעלה. לא ניגע בקשר בין Classלבין ClassLoader. זה לא נושא המאמר. לאחר מכן, כדי לקבל את השדות של המחלקה הזו, צריך לקרוא ל- method getFields(), שיטה זו תחזיר לנו את כל השדות הזמינים של המחלקה. זה לא מתאים לנו, מכיוון שהתחום שלנו הוא private, אז אנחנו משתמשים בשיטה getDeclaredFields().שיטה זו גם מחזירה מערך של שדות מחלקה, אבל עכשיו גם privateוגם protected. במצב שלנו, אנחנו יודעים את שם התחום שמעניין אותנו, ונוכל להשתמש בשיטה getDeclaredField(String), איפה Stringשם השדה הרצוי. הערה: getFields()ואל getDeclaredFields()תחזירו את השדות של כיתת ההורים! מעולה, קיבלנו חפץ Field עם קישור ל- name. כי השדה לא היה публичным(ציבורי), יש לתת גישה לעבוד איתו. השיטה setAccessible(true)מאפשרת לנו להמשיך לעבוד. עכשיו השדה nameלגמרי בשליטתנו! אתה יכול לקבל את הערך שלו על ידי קריאה get(Object)לאובייקט Field, שבו Objectהוא מופע של המחלקה שלנו MyClass. אנו מטילים אותו Stringומשייכים אותו למשתנה שלנו name. במקרה שאין לנו פתאום setter'a, נוכל להשתמש בשיטה כדי להגדיר ערך חדש לשדה השם set:
field.set(myClass, (String) "new value");
מזל טוב! זה עתה שלטת במנגנון ההשתקפות הבסיסי והצלחת לגשת לתחום private! שימו לב לחסימה try/catchולסוגי החריגים המטופלים. ה-IDE עצמו יציין את נוכחותם החובה, אך שמם מבהיר מדוע הם כאן. לך על זה! כפי שאולי שמתם לב, שלנו MyClassכבר יש שיטה להצגת מידע על נתוני הכיתה:
private void printData(){
       System.out.println(number + name);
   }
אבל המתכנת הזה השאיר מורשת גם כאן. השיטה נמצאת תחת משנה הגישה private, והיינו צריכים לכתוב את קוד הפלט בעצמנו בכל פעם. זה לא בסדר, איפה ההשתקפות שלנו?... בואו נכתוב את הפונקציה הבאה:
public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
כאן ההליך הוא בערך כמו בקבלת שדה - אנחנו מקבלים את השיטה הרצויה לפי השם ונותנים לה גישה. וכדי לקרוא לאובייקט Methodשאנו משתמשים בו invoke(Оbject, Args), היכן Оbjectהוא גם מופע של המחלקה MyClass. Args- טיעוני שיטה - שלנו אין כאלה. כעת אנו משתמשים בפונקציה כדי להציג מידע printData:
public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // outout 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// output 0new value
}
היי, עכשיו יש לנו גישה לשיטה הפרטית של הכיתה. אבל מה אם לשיטה עדיין יש טיעונים, ומדוע יש בנאי מוער? לכל דבר יש את הזמן שלו. מההגדרה בהתחלה ברור שהשתקפות מאפשרת ליצור מופעים של מחלקה במצב runtime(בזמן שהתוכנית פועלת)! אנחנו יכולים ליצור אובייקט של מחלקה לפי השם המלא של אותה מחלקה. שם הכיתה המלא הוא שם הכיתה, בהינתן הנתיב אליה ב- package.
Reflection API.  הִשׁתַקְפוּת.  הצד האפל של ג'אווה - 3
בהיררכיה שלי, packageהשם המלא MyClassיהיה " reflection.MyClass". אתה יכול גם לגלות את שם הכיתה בצורה פשוטה (הוא יחזיר את שם הכיתה כמחרוזת):
MyClass.class.getName()
בואו ניצור מופע של המחלקה באמצעות השתקפות:
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
בזמן הפעלת יישום ה-Java, לא כל המחלקות נטענות ל-JVM. אם הקוד שלך לא מתייחס למחלקה MyClass, אז מי שאחראי על טעינת מחלקות ל-JVM, כלומר ClassLoader, לעולם לא יטען אותו שם. לכן, עלינו להכריח ClassLoaderאותו לטעון ולקבל תיאור של המחלקה שלנו בצורה של משתנה מסוג Class. עבור משימה זו, יש שיטה forName(String), היכן Stringהוא שם המחלקה שאנו דורשים את התיאור שלה. לאחר קבלת Сlass, קריאת השיטה newInstance()תחזור Object, אשר תיווצר על פי אותו תיאור. נותר להביא את החפץ הזה לכיתה שלנו MyClass. מגניב! זה היה קשה, אבל אני מקווה שזה מובן. עכשיו אנחנו יכולים ליצור מופע של מחלקה ממש משורה אחת! למרבה הצער, השיטה המתוארת תעבוד רק עם בנאי ברירת המחדל (ללא פרמטרים). איך קוראים למתודות עם ארגומנטים ובנאים עם פרמטרים? הגיע הזמן לבטל את ההערות של הבנאי שלנו. כצפוי, newInstance()הוא לא מוצא את בנאי ברירת המחדל ואינו עובד יותר. הבה נשכתב את היצירה של מופע מחלקה:
public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
כדי להשיג בנאי מחלקה, קרא למתודה מתיאור המחלקה getConstructors(), וכדי לקבל פרמטרים של בנאי, קרא getParameterTypes():
Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
כך נקבל את כל הבנאים ואת כל הפרמטרים אליהם. בדוגמה שלי, יש קריאה לבנאי ספציפי עם פרמטרים ספציפיים שכבר ידועים. וכדי לקרוא לבנאי זה, אנו משתמשים בשיטה newInstance, שבה אנו מציינים את הערכים של הפרמטרים הללו. אותו דבר יקרה invokeלשיטות שיחות. נשאלת השאלה: היכן קריאה רפלקטיבית של בנאים יכולה להיות שימושית? טכנולוגיות Java מודרניות, כאמור בהתחלה, אינן יכולות להסתדר בלי Reflection API. לדוגמה, DI (Dependency Injection), שבו הערות בשילוב עם השתקפות של שיטות ובנאים יוצרים את ספריית Dagger, הפופולרית בפיתוח אנדרואיד. לאחר קריאת מאמר זה, אתה יכול לראות את עצמך בביטחון מואר במנגנוני ה-Reflection API. לא בכדי השתקפות נקראת הצד האפל של ג'אווה. זה שובר לחלוטין את פרדיגמת OOP. ב-java, אנקפסולציה משמשת להסתרה ולהגביל את הגישה של חלק ממרכיבי התוכנית לאחרים. בשימוש ב-private modifier אנו מתכוונים שהגישה לשדה זה תהיה רק ​​בתוך המחלקה שבה קיים שדה זה, על בסיס זה אנו בונים את הארכיטקטורה הנוספת של התוכנית. במאמר זה ראינו כיצד אתה יכול להשתמש בהשתקפות כדי להגיע לכל מקום. דוגמה טובה בצורת פתרון אדריכלי היא תבנית העיצוב הגנרטיבית - Singleton. הרעיון המרכזי שלו הוא שלאורך כל פעולת התוכנית, לכיתה המיישמת תבנית זו צריך להיות רק עותק אחד. זה נעשה על ידי הגדרת ברירת המחדל של שינוי הגישה לפרטי עבור הבנאי. וזה יהיה רע מאוד אם איזה מתכנת עם השתקפות משלו יוצר שיעורים כאלה. אגב, יש שאלה מאוד מעניינת ששמעתי לאחרונה מהעובד שלי: האם לכיתה המיישמת תבנית יכולים להיות Singletonיורשים? האם ייתכן שאפילו השתקפות חסרת אונים במקרה זה? כתבו את המשוב שלכם על המאמר ואת התשובה בתגובות, וגם שאלו את שאלותיכם! הכוח האמיתי של Reflection API מגיע בשילוב עם Runtime Annotations, שעליהם כנראה נדבר במאמר עתידי על הצד האפל של Java. תודה לך על תשומת הלב!
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION