שאלה לגבי טיפוסים ג'נריים בשפת ג'אווה

שאלה לגבי טיפוסים ג'נריים בשפת ג'אווה

קוד:
Public List<String> funcA()
{
   return new ArrayList<Object>();
}
אמנם אני יודע שהשאלה שלי היא בנושא "יחסית" מתקדם בג'אווה אבל אשמח לקבל תשובה עבורה (סיעפתי קצת את השאלה בתקווה שזה בסדר).
אנסה להסביר את השאלה כמה שיותר טוב ולפרט אותה בכמה דוגמאות.
1.קראתי שכאשר מכריזים ובונים אובייקט ג'נרי אז במידה ולא ציינתי את סוג האובייקט בצורה מפורשת בתוך סוגריים משולשות, למעשה מוצב שם טיפוס על "Object". מצד שני רשום שמקרה כזה פוגע ב- Safe Type". נתבונן בדוגמא הבאה:
קוד:
Public List<String> funcA()
{
   return new ArrayList<Object>();
}
הפונקציה שלמעלה לא מתקמפלת. לעומת זאת הפונקציה הבאה כן מתקמפלת:
קוד:
Public List<String> funcA()
{
   return new ArrayList();
}
האם אין פה סתירה? הרי נאמר שבמקרה ולא מוכרז שום טיפוס אז "מוצב" שם טיפוס Object
ואז המצב בפונקציה השנייה צריך להיות זהה למצב שבפונקציה הראשונה ולא להתקמפל. האם אני טועה?

2.בנוסף, "הצצתי" במימוש של ג'אווה למחלקה HashMap
ראיתי שם את המימוש הבא:
קוד:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);}
מה זה אומר סוגריים משולשות ריקות? איך זה מסתדר עם העובדה שהפונקציה מכריזה שהיא מחזירה <Node<K,V?
על זה בכלל לא מצאתי שום התייחסות.

3.עוד משהו אחד שקשור לטיפוסים כללים שאשמח לקבל עבורו התייחסות הוא קטע קוד נוסף מהמחלקה HashMap:

קוד:
public final boolean contains(Object o) {
            if (!(o instanceof Map.Entry))
                return false;
            Map.Entry<?,?> e = (Map.Entry<?,?>) o;
            Object key = e.getKey();
            Node<K,V> candidate = getNode(hash(key), key);
            return candidate != null && candidate.equals(e);
        }

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

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

selalerer

New member
כדאי למקד את השאלה. נראה שאתה מערבב כמה נושאים ביחד

קשה לדעת ככה על מה לענות
 
הכל סביב טיפוסים כללים והמונח erasure

לא רואה פה עירבוב...
אם מישהו יקרא יגיד מה בדיוק לא ברור אז אשמח לערוך
תודה
 

BravoMan

Active member
כמה תשובות:

נתחיל מהסוף:
סימן שאלה הוא wildcard - שאומר "אתה יכול לשים כאן כל טיפוס שתרצה".

יש תיעוד לזה גם באתר של Oracle עצמם, וגם תשובה די ברורה ב-SO.

הסיבה שאתה לא צריך לכתוב אחרי זה extends, היא כדי לחסוך לך הקלדה.
Java יוצא מנקודת הנחה שאם לא כתבת, אז אתה מתכוון "כל דבר שיורש מ-Object".

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

במקרה שלך, היות ו-new נמצא ב-return, הקומפיילר יבדוק איזה טיפוס return מצפה לקבל (כיצד מוכרזת המתודה), ולפי זה ימלא את הפרמטר.

הדרך הנכונה לכתוב את זה תהיה:
return new ArrayList<>();
הסיבה שמותר לך לרשום סוגריים משולשים ריקות, היא שכבר כתבת מה אמור להיות בסוגריים בכותרת הפונקציה.

זה עובד גם כשמגדירים משתנים או שדות של אובייקט:
ArrayList<String> var = new ArrayList<>();
אין סיבה לכתוב String פעמים באותה שורה. הקומפיילר מספיק חכם להבין מה אתה רוצה.

שים לב שאם תשמיט את הסוגריים לגמרי, תקבל אזהרה מהקומפיילר:
קוד:
warning: [unchecked] unchecked conversion
   		return new ArrayList();
   		       ^
  required: List<String>
  found:    ArrayList
הסינטקס הזה לא בדיוק תקני, אבל הוא עדיין יעבוד.
אם אתה רוצה לדעת מתי הוא יפול, תצטרך לחפור בקרביים של מימוש VM.

ולבסוף לגבי שאלה 2, ראה התחלה של תשובה 1
 
לגבי מה שאמרתי שג'אווה מחזירה רשימה של Object

רשום שזה מצב של Raw Type
שנוצר כדי לתמוך במימוש שהיה בעבר, בו לא היו קיימים מחלקות ג'נריים.
לפי מה שאני מבין בהיעדר סוגריים משולשות, אז המצב שקול למצב שלתוך T מוצב טיפוס Object, ואז אנחנו מקבלים List<Object>
אשמח אם תתקן אותי שוב כי אני אפילו לא יכול לתאר לך כמה חומר קראתי ויצאתי רק יותר מבולבל (שלא להגיד מתוסכל)
 

BravoMan

Active member
בכנות,אני לא עד כדי כך בקי ב-iternals של Java

אז עשיתי כמה בדיקות קטנות, ונראה שהלכה למעשה, כל ה-Jenerics קיימים רק ב-compile time.
&nbsp
כלומר, לא משנה מה תכתוב בסוגריים משולשים, אחרי שתקמפל, יהיה לך בכל מקרה אוסף של reference לאובייקטים שלא אכפת לו כלל מהטיפוס שלהם.
&nbsp
הדבר היחיד שהצבה בסוגריים עושה, זה לבדוק בזמן קומפילציה שאתה לא מנסה להפעיל על הרפרנס מתודה שלא קיימת, או להציב אותו במשתנה מטיפוס ספציפי שלא מתאים למה שהגדרת.
&nbsp
סה"כ זה הגיוני כשמכירים את Java - מתחת למכסה המנוע, יש לה רק רפרנסים (לכל מה שלא טיפוס פרימיטיב), ולא ממש אכפת לה אם מנסים בכוח להפעיל מתודה לא רלוונטית על רפרנס.
היא תזרוק חריגה וזהו.
&nbsp
בקיצור - הכיתוב המקוצר, ללא סוגריים, כנראה אכן קיים לצורך תאימות לאחור כמו שקראת, ולכן, לפחות בגרסאות אחרונות של Java הוא גם מסרב להתקמפל בלי פרמטר ספציפי לקומפיילר.
-Xlint:unchecked
&nbsp
אבל הוא למעשה עושה את אותו הדבר כמו סוגריים משולשים ריקים (שמכונים גם "יהלום").
 
אחלה תודה.. אבל אולי תוכל בכל זאת לעזור לי מהמסקנות

האלה, להסיק מה בעצם קרה בסעיף מספר 1, שקוד מסויים כן התקמפל והשני לא?
אני פשוט לא מוצא הסברים..
עדיין על זה כל ערב :)
 

BravoMan

Active member
נראה לי די פשוט:

הקומפיילר בודק מה אתה מנסה לעשות:
כשאתה אומר לו במפורש להחזיר ArrayList שמחזיק Object מפונקציה שמצפה ל-ArrayList שמחזיק String הוא רואה שאתה עושה טעות, ומסרב לשתף איתך פעולה.
&nbsp
לכן הקוד לא מתקמפל.
&nbsp
כשאתה אומר לו להחזיר ArrayList בלי לציין מה הוא מחזיק, הקומפיילר עדיין בוכה שזה מסוכן, אבל משתף איתך פעולה מתוף נקודת הנחה שתזכור מה התכוונת לדחוף לשם.
 

nocgod

New member
למרות שאני לא כותב בג'אווה באופן מקצועי היום... אנסה לענות

קוד:
Public List<String> funcA()
{
   return new ArrayList<Object>();
}

הקטע קוד הראשון שציינת לא מתקמפל מסיבה ברורה, מאחר והרשימה בפנים היא ArrayList of object הרי שיכלת להכניס לשם כל דבר, לא רק string. על כן casting של הדבר הזה ל List of String מסוכן ועל כן אסור.

לגבי המימוש של newNode ב HashMap יש הסבר פשוט וקליל בתיעוד של java עצמה - המבנה נקרא The diamond (יהלום) וקיים בג'אווה מאז ג'אווה 7. מה שהוא אומר זה כזה דבר: אם הקומיילר בזמן קומפילציה מצליח להבין מה הטיפוס צריך להיות שם - הוא יסתדר גם בלעדייך.

קוד:
The Diamond
In Java SE 7 and later, you can replace the type arguments required to invoke the constructor of a generic class with an empty set of type arguments (<>) as long as the compiler can determine, or infer, the type arguments from the context. This pair of angle brackets, <>, is informally called the diamond. For example, you can create an instance of Box<Integer> with the following statement:

Box<Integer> integerBox = new Box<>();
For more information on diamond notation and type inference, see Type Inference.

בקשר לסימני שאלה, כמו שכבר ענו לך, מדובר ב wild card. המטרה של המנגנון הזה ב generics של java הוא להגיד שלמימוש לגמרי לא אכפת מהטיפוס במקרה הזה וניתן להתייחס אליו בתור object.

המונח erasure שהעלת הוא מונח מעניין שמדבר ספציפית על צורת המימוש של generics בג'אווה. ספציפית גם את זה אפשר לקרוא בתיעוד. בקצרה, יש כמה דרכים לממש generics.
כאשר מדובר בשיטה שבה מימוש generics בג'אווה מדובר בעצם ב compile time generics, במצב הזה כבר בזמן הקומפילציה הקומפיילר מעיף לעזאזל את כל ה <T> ומחליף אותם ב cast על ה boundaries, העדר boudaries הוא מחליף את זה ב cast ל object. כך יוצא, בעצם, שבזמן ריצה, אין מידע על הטיפוסים ה"גנריים" שיש לך ביד, כלומר
ArrayList<String>
ArrayList<Integer>
מבחינת ה runtime הם זהים, כלומר שתיהם ArrayList בעצם, וההבדל היחיד הוא שה compiler בזמן הקומפילציה הוסיף cast ל String או ל Integer בכל המקומות הנחוצים על מנת שהקוד יעבוד.
שיטת המימוש הזאת מביאה איתה הרבה מגבלות (לדוגמא אי אפשר לייצר ArryList of int אלא רק של Integer) ומקומות מעניינים.
תנסה לממש לבד DI Container, זה דרך מעניינת ללמוד על java generics.

כאנקדוטה קטנה על אותו הנושא: ב #C החליטו לממש runtime generics (לא השם של השיטה - לא מצאתי שם אז הנפצתי), בגדול מה שזה אומר שנוצרים classes מיוחדים עבור כל טיפוס וטיפוס, כלומר
List<string>
List<int>
הם בעצם 2 טיפוסים שונים לגמרי כמו שניתן לראות בדוגמא
קוד:
public class C {
    public void M() {
    DemoClass<string> s = new DemoClass<string>();
    DemoClass<int> i = new DemoClass<int>();
    }
}
בקוד הIL (ככה נקראת השפת ביניים של #C - ה bytecode של java)
קוד:
DemoClass`1<int32>
DemoClass`1<string>
 
התייחסות לתגובה

קודם כל תודה :)
מצטער של- BravoMan לא מסרתי תודה בתגובה הקודמת. מגיע לו ומקווה שהוא יראה את זה פה :)
שנית, אשמח לקבל התייחסות נוספת לכמה מהדברים שעלו לי בראש בזכות תגובתך:
1. אני מבין את הבעיה שאתה מתאר. אבל למה הפונקציה השנייה שציינתי כן התקמפלה? מה בעצם מוכרז שם כשרושמים:
return new ArrayList()
ללא סוגריים... ניסיתי לשאול גם את BravoMan בתגובה שלי, האם לא מדובר בהחזרה של raw type ששקול להחזרה של רשימה של טיפוסים מסוג Object?
2. במקרה של הדוגמא שהראתי, "ההסקה" ב-:
קוד:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
        return new Node<>(hash, key, value, next);}

מתבצעת מ-
Node<K,V>

?

3. בדוגמא הנחמדה שהראית, מה רצית להראות (האם אני מבין נכון): א.שבסי שארפ אפשר להכניס בתור Type Argument גם טיפוסים פרימיטייבים? ב. שעבור כל הצבה של Type Argumnet השפה ממש יוצרת class משלו?
 

nocgod

New member
שוב - אני לא כותב בג'אווה ביום-יום שלי

הייתי לוקח את התשובות שלי בערבון מוגבל.
1. אני מניח שבגלל שבג'אווה אין בזמן ריצה את הטיפוס הזה
ArrayList<T>
אלא מדובר ב ArrayList שפשוט עושים בכל מקום פעולות cast כדי להמיר/לוודא את האלמנט שאתה מעביר, אזי כשאתה עושה פשוט ArrayList אז אתה מקבל התנהגות דיפולטיבית של ArrayList דהיינו
ArrayList<Object>

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

3. כן אנקדוטקה קטנה על איך שפות ש"מאוד דומות" (אני לא מאמין לקשקוש הדימיון) מתחת לפני השטח מאוד שונות. בעצם ב #C וב java בחרו גישות שונות לחלוטין למימוש של generics (וכנראה של עוד לא מעט דברים). דומה דומה אך מאוד שונה.
אבל אם לדייק - כן, בגלל הצורה השונה מאוד שבה מימשו את מנגנון ה generics בשפת #C (בתחלס זה בכל שפות CLR) אתה בעצם יכול להשתמש בקלות בטיפוסים פרימיטיביים בתור הפרמטר הגנרי שלך, מבלי לעשות boxing.
כן, בעצם עבור כל generic class מיוצר מימוש ייחודי, שלפעמים כולל אופטימיזציות שונות לחלוטין בהתבסס ב type constraints שאתה מגדיר, ככה הקומפיילר יכול למחוק קטעים שלמים של קוד שלא רלוונטיים בשום צורה עבור הטיפוס הקונקרטי איתו השתמשת.
נושא מעניין, מבלבל ועמוק. :)
 
למעלה