היסודות של Java Generics

1. הקדמה

Java Generics הוצגו ב- JDK 5.0 במטרה להפחית באגים ולהוסיף שכבה נוספת של הפשטה על פני סוגים.

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

2. הצורך בגנריות

בואו נדמיין תרחיש בו אנו רוצים ליצור רשימה בג'אווה לאחסון מספר שלם; אנחנו יכולים להתפתות לכתוב:

רשימת רשימות = LinkedList חדש (); list.add (מספר שלם חדש (1)); מספר שלם i = list.iterator (). הבא (); 

באופן מפתיע, המהדר יתלונן על השורה האחרונה. הוא לא יודע איזה סוג נתונים מוחזר. המהדר ידרוש ליהוק מפורש:

מספר שלם i = (מספר שלם) list.iterator.next ();

אין חוזה שיכול להבטיח שסוג ההחזרה של הרשימה הוא מספר שלם. הרשימה המוגדרת יכולה להכיל כל אובייקט. אנחנו רק יודעים שאנחנו מאחזרים רשימה על ידי בדיקת ההקשר. כאשר מסתכלים על סוגים, זה יכול רק להבטיח שמדובר ב- לְהִתְנַגֵד, ולכן דורש צוות שחקנים מפורש בכדי להבטיח שהסוג בטוח.

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

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

בואו ונשתנה את השורה הראשונה של קטע הקוד הקודם ל:

רשימת רשימה = LinkedList חדש ();

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

בתוכניות קטנות זה עשוי להיראות כמו תוספת טריוויאלית, אולם בתוכניות גדולות יותר זה יכול להוסיף חסינות משמעותית ולהקל על קריאת התוכנית.

3. שיטות גנריות

שיטות גנריות הן שיטות שנכתבות בהצהרת שיטה אחת וניתן לקרוא להן עם טיעונים מסוגים שונים. המהדר יבטיח את נכונותו של הסוג שבו משתמשים. אלה כמה מאפיינים של שיטות גנריות:

  • לשיטות כלליות יש פרמטר סוג (מפעיל היהלום הסוגר את הסוג) לפני סוג ההחזרה של הצהרת השיטה
  • ניתן לתחום את פרמטרי הסוג (גבולות מוסברים בהמשך המאמר)
  • בשיטות כלליות יכולות להיות פרמטרים מסוגים שונים המופרדים באמצעות פסיקים בחתימת השיטה
  • גוף השיטה לשיטה גנרית הוא בדיוק כמו שיטה רגילה

דוגמה להגדרת שיטה כללית להמרת מערך לרשימה:

רשימה ציבורית מ- ArrayToList (T [] a) {return Arrays.stream (a) .collect (Collectors.toList ()); }

בדוגמה הקודמת, ה- בחתימת השיטה משתמע שהשיטה תעסוק בסוג גנרי ט. זה נחוץ גם אם השיטה מחזירה בטל.

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

רשימה סטטית ציבורית מ- ArrayToList (T [] a, פונקציה mapperFunction) {return Arrays.stream (a) .map (mapperFunction) .collect (Collectors.toList ()); }

אנו מעבירים פונקציה הממירה מערך עם רכיבי הסוג ט לרשימה עם אלמנטים מסוג ז. דוגמה תהיה להמיר מספר שלם שלה חוּט יִצוּג:

@Test הציבור בטל givenArrayOfIntegers_thanListOfStringReturnedOK () {Integer [] intArray = {1, 2, 3, 4, 5}; List stringList = Generics.fromArrayToList (intArray, Object :: toString); assertThat (stringList, hasItems ("1", "2", "3", "4", "5")); }

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

3.1. גנריות מוגבלות

כאמור, ניתן לתחום את פרמטרי הסוג. מוגבל פירושו "מוגבלת", אנו יכולים להגביל סוגים שניתן לקבל בשיטה.

לדוגמא, אנו יכולים לציין כי שיטה מקבלת סוג ואת כל מחלקות המשנה שלו (גבול עליון) או סוג של כל מחלקות העל שלה (גבול תחתון).

כדי להכריז על סוג גבולות עליון אנו משתמשים במילת המפתח מרחיב אחרי הסוג ואחריו הגבול העליון שאנחנו רוצים להשתמש בו. לדוגמה:

רשימה ציבורית מ- ArrayToList (T [] א) {...} 

מילת המפתח מרחיב משמש כאן פירושו שהסוג ט מרחיב את הגבול העליון במקרה של מחלקה או מיישם גבול עליון במקרה של ממשק.

3.2. גבולות מרובים

סוג יכול לכלול מספר גבולות עליונים כדלקמן:

אם אחד מהסוגים המורחבים ב ט הוא כיתה (כלומר מספר), יש לשים אותו במקום הראשון ברשימת הגבולות. אחרת, זה יגרום לשגיאת זמן הידור.

4. שימוש בתווים כלליים עם גנריות

תווים כלליים מיוצגים על ידי סימן השאלה בג'אווה "?”והם משמשים להתייחס לסוג לא ידוע. תווים כלליים שימושיים במיוחד בעת שימוש בגנריות ויכולים לשמש כסוג פרמטר אך ראשית, יש חָשׁוּב הערה לשקול.

ידוע ש לְהִתְנַגֵד הוא סוג העל של כל שיעורי Java, עם זאת, אוסף של לְהִתְנַגֵד אינו סוג העל של אוסף כלשהו.

לדוגמא, א רשימה אינו סוג העל של רשימה והקצאת משתנה מסוג רשימה למשתנה מסוג רשימה יגרום לשגיאת מהדר. זה כדי למנוע סכסוכים אפשריים שיכולים לקרות אם נוסיף לאותו אוסף טיפוסים הטרוגניים.

אותו הכלל חל על כל אוסף מסוג וסוגי המשנה שלו. שקול דוגמה זו:

צבע ריק ריק סטטי ציבורי AllBuildings (רשימת בניינים) {בניינים.לכל (בניין :: צבע); }

אם נדמיין תת-סוג של בִּניָן, למשל, א בַּיִת, איננו יכולים להשתמש בשיטה זו עם רשימה של בַּיִת, למרות ש בַּיִת הוא תת-סוג של בִּניָן. אם עלינו להשתמש בשיטה זו עם סוג Building וכל תת-הסוגים שלה, אז התו הכללי התוחם יכול לעשות את הקסם:

צבע ריק סטטי ציבורי AllBuildings (רשימת בניינים) {...} 

כעת, שיטה זו תעבוד עם סוג בִּניָן וכל תתי-הסוגים שלו. זה נקרא תו כללי מוגבל עליון שבו סוג בִּניָן הוא הגבול העליון.

ניתן לציין גם תווים כלליים עם גבול תחתון, כאשר הסוג הלא ידוע צריך להיות סוג-על מהסוג שצוין. ניתן לציין גבולות תחתונים באמצעות סוּפֶּר מילת מפתח ואחריה הסוג הספציפי, למשל, פירושו סוג לא ידוע שהוא מעמד-על של ט (= T וכל הוריו).

5. הקלד מחיקה

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

מחיקת סוג מסירה את כל פרמטרי הסוג ומחליפה אותו בגבולותיהם או באמצעותם לְהִתְנַגֵד אם פרמטר הסוג אינו מוגבל. לפיכך, קוד הביס לאחר האוסף מכיל רק מחלקות, ממשקים ושיטות נורמליות ובכך מבטיח שלא יוצרו סוגים חדשים. יציקה נכונה מוחלת גם על לְהִתְנַגֵד הקלד בזמן הידור.

זו דוגמה למחיקת סוגים:

רשימה ציבורית genericMethod (רשימת רשימה) {return list.stream (). collect (Collectors.toList ()); } 

עם מחיקת סוג, הסוג הבלתי מוגבל ט מוחלף ב לְהִתְנַגֵד כדלהלן:

// להמחשה רשימה ציבורית withErasure (רשימת רשימה) {return list.stream (). collect (Collectors.toList ()); } // אשר בפועל מביא לרשימה ציבורית withErasure (רשימת רשימה) {return list.stream (). collect (Collectors.toList ()); } 

אם הסוג מוגבל, הסוג יוחלף בכריכה בזמן הקומפילציה:

חלל ציבורי genericMethod (T t) {...} 

ישתנה לאחר ההידור:

public void genericMethod (בניין t) {...}

6. גנריות וסוגי נתונים פרימיטיביים

מגבלה של הגנריות בג'אווה היא שפרמטר הסוג לא יכול להיות סוג פרימיטיבי.

לדוגמה, הדברים הבאים אינם מתקבצים:

רשימת רשימה = ArrayList חדש (); list.add (17);

כדי להבין מדוע סוגי נתונים פרימיטיביים אינם עובדים, בואו נזכור זאת גנריות הן תכונת זמן הידורכלומר פרמטר הסוג נמחק וכל הסוגים הגנריים מיושמים כסוג לְהִתְנַגֵד.

כדוגמה, בואו נסתכל על ה- לְהוֹסִיף שיטת רשימה:

רשימת רשימה = ArrayList חדש (); list.add (17);

החתימה של לְהוֹסִיף השיטה היא:

תוספת בוליאנית (E e);

ויורכב ל:

תוספת בוליאנית (אובייקט e);

לכן, פרמטרים מסוג חייבים להיות ניתנים להמרה ל לְהִתְנַגֵד. מאחר וסוגים פרימיטיביים אינם מתרחבים לְהִתְנַגֵד, איננו יכולים להשתמש בהם כפרמטרים מסוג.

עם זאת, ג'אווה מספקת סוגי ארגזים לפרימיטיביים, יחד עם תיבות אוטומטיות ואי-איגרוף כדי לפרוק אותם:

שלם a = 17; int b = a; 

לכן, אם אנו רוצים ליצור רשימה שיכולה להכיל מספרים שלמים, נוכל להשתמש בעטיפה:

רשימת רשימה = ArrayList חדש (); list.add (17); int first = list.get (0); 

הקוד המהולל יהיה שווה ערך ל:

רשימת רשימה = ArrayList חדש (); list.add (Integer.valueOf (17)); int ראשון = ((Integer) list.get (0)). intValue (); 

גרסאות עתידיות של Java עשויות לאפשר סוגי נתונים פרימיטיביים עבור גנריות. פרויקט ואלהאלה שואף לשפר את אופן הטיפול בגנריות. הרעיון הוא ליישם התמחות גנרית כמתואר ב- JEP 218.

7. מסקנה

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

קוד המקור המלווה את המאמר זמין באתר GitHub.