הקלד תלות ב- Java, חלק 1

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

הורד הורד את המקור קבל את קוד המקור למאמר זה, "סוג תלות בג'אווה, חלק 1". נוצר עבור JavaWorld על ידי ד"ר אנדראס סולימוסי.

מושגים וטרמינולוגיה

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

תְאִימוּת

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

אנדראס סולימוסי

אנו אומרים ששני סוגים תואמים ב- Java אם ניתן להעביר נתונים בין משתנים מהסוגים. העברת נתונים אפשרית אם המהדר מקבל זאת, והוא נעשה באמצעות הקצאה או העברת פרמטר. כדוגמה, shortהוא תואם intכיוון שהמשימה intVariable = shortVariable;אפשרית. אך booleanאינו תואם intכיוון שהמשימה intVariable = booleanVariable;אינה אפשרית; המהדר לא יקבל זאת.

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

מה שחשוב הוא שתאימות בין סוגי הפניות אפשרית רק בתוך היררכיית סוגים. כל סוגי Objectהמחלקות תואמים , למשל, מכיוון שכל המחלקות יורשות במשתמע מ Object. Integerאינו תואם Float, עם זאת, משום Floatשהוא אינו מעמד-על של Integer. Integerהוא תואם Number, כי Numberהיא על (מופשט) של Integer. מכיוון שהם ממוקמים באותה היררכיית סוג, המהדר מקבל את ההקצאה numberReference = integerReference;.

אנו מדברים על תאימות מרומזת או מפורשת , תלוי אם יש לסמן תאימות במפורש או לא. לדוגמא, קצר תואם באופן משתמעint (כפי שמוצג לעיל) אך לא להפך: המטלה shortVariable = intVariable;אינה אפשרית. עם זאת, קצר תואם באופן מפורשint , מכיוון שהמשימה shortVariable = (short)intVariable;אפשרית. כאן עלינו לסמן תאימות באמצעות ליהוק , המכונה גם המרת סוג.

באופן דומה, בין סוגי הפניות: integerReference = numberReference;אינו מקובל, רק integerReference = (Integer) numberReference;יתקבל. לכן, Integerהוא תואם באופן מרומזNumber אך Numberהוא תואם רק במפורשInteger .

תלות

סוג עשוי להיות תלוי בסוגים אחרים. לדוגמה, סוג המערך int[]תלוי בסוג הפרימיטיבי int. באופן דומה, הסוג הגנרי ArrayListתלוי בסוג Customer. השיטות יכולות להיות תלויות סוג, תלוי בסוג הפרמטרים שלהן. למשל, השיטה void increment(Integer i); תלוי בסוג Integer. שיטות מסוימות (כמו סוגים גנריים מסוימים) תלויות ביותר מסוג אחד - כגון שיטות בעלות יותר מפרמטר אחד.

משתנות וסתירות

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

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

אנדראס סולימוסי

התאימות של to מרמזת על התאימות של ) to ). הסוג התלוי נקרא משתנה ; או ליתר דיוק, ) משתנה ).T1T2A(T1A(T2A(T)A(T1A(T2

לדוגמה נוספת: בגלל המשימה numberArray = integerArray;היא אפשרית (ב- Java, לפחות), סוגי המערך Integer[]ואת Number[]הם קו-וריאנטית. אז, נוכל לומר כי Integer[]הוא במשתמע קו-וריאנטית כדי Number[]. ובעוד ההפך הוא לא נכון - את המשימה integerArray = numberArray;אינה אפשרית - המשימה עם הליהוק סוג ( integerArray = (Integer[])numberArray;) הוא אפשרי; לפיכך, אנו אומרים, Number[]הוא במפורש קו-וריאנטית כדי Integer[].

לסיכום: Integerתואם באופן מרומז Number, ולכן Integer[]משתנה באופן משתמע Number[], והוא Number[]משתנה באופן מפורש Integer[]. איור 3 ממחיש.

אנדראס סולימוסי

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

סתירה

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

סוג תלוי כגון A(T)נקרא contravariant אם התאימות של to מרמזת על התאימות של ) to ). איור 4 ממחיש.T1T2A(T2A(T1

אנדראס סולימוסי

מרכיב שפה (סוג או שיטה) A(T)תלוי Tהוא קו-וריאנטית אם התאימות של אל מעיד על התאימות של ) אל ). אם התאימות של to מרמזת על התאימות של ) to ), הסוג הוא סותר . אם התאימות של בין לא מרמזת על תאימות כלשהי בין ) ל- ), אז היא משתנה .T1T2A(T1A(T2T1T2A(T2A(T1A(T)T1T2A(T1A(T2A(T)

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

אלמנטים תלויי סוג: שיטות וסוגים

בג'אווה, שיטות, סוגי מערכים וסוגים גנריים (פרמטריים) הם האלמנטים התלויים בסוג. השיטות תלויות בסוג הפרמטרים שלהן. סוג מערך,, T[]תלוי בסוגי האלמנטים שלו T,. סוג כללי Gתלוי בפרמטר הסוג שלו T,. איור 5 ממחיש.

אנדראס סולימוסי

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

תאימות סוג מרומזת ומפורשת

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

 variableOfTypeT2 = variableOfTypeT1; // implicit compatible variableOfTypeT2 = (T2)variableOfTypeT1; // explicit compatible 

לדוגמה, intתואם באופן משתמע ותואם longבמפורש ל short:

 int intVariable = 5; long longVariable = intVariable; // implicit compatible short shortVariable = (short)intVariable; // explicit compatible 

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

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

פרמטרים של שיטה

אנו אומרים, שיטה קוראת פרמטרי קלט וכותבת פרמטרי פלט. פרמטרים מסוגים פרימיטיביים הם תמיד פרמטרים של קלט. ערך החזר של פונקציה הוא תמיד פרמטר פלט. פרמטרים של סוגי התייחסות יכולים להיות שניהם: אם השיטה משנה את ההתייחסות (או פרמטר פרימיטיבי), השינוי נשאר בתוך השיטה (כלומר הוא לא נראה מחוץ לשיטה לאחר השיחה - זה מכונה קריאה לפי ערך ) . אם השיטה משנה את האובייקט המופנה, עם זאת, השינוי נותר לאחר שהוחזר מהשיטה - זה נקרא call by reference .

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

 referenceOfSuperType = referenceOfSubType; // implicit compatible referenceOfSubType = (SubType)referenceOfSuperType; // explicit compatible 

מהדר Java מאפשר בדרך כלל תאימות מרומזת למשימה רק אם אין סכנה לאובדן מידע בזמן הריצה בין הסוגים השונים. (עם זאת, שים לב שהכלל הזה אינו תקף לאיבוד דיוק, כמו למשל בהקצאה מ- intto float.) לדוגמה, intהוא תואם באופן משתמע longמכיוון longשמשתנה מחזיק בכל intערך. לעומת זאת, shortמשתנה אינו מחזיק intבערכים כלשהם ; לפיכך, רק תאימות מפורשת מותרת בין אלמנטים אלה.

אנדראס סולימוסי

שים לב שהתאימות הגלומה באיור 6 מניחה שהקשר הוא חולף : shortתואם ל- long.

בדומה למה שאתה רואה באיור 6, תמיד ניתן להקצות הפניה של תת int- סוג הפניה של סוג -על. זכור שאותה מטלה בכיוון השני יכולה לזרוק a ClassCastException, אולם מהדר Java מאפשר זאת רק עם יציקת סוג.

משתנות וסתירות לסוגי מערכים

בג'אווה, סוגים מסוימים של מערכים הם משתנים ו / או מנוגדים. במקרה של משתנות, המשמעות Tהיא שאם זה תואם U, אז T[]זה גם תואם ל U[]. במקרה של סתירה זה אומר שזה U[]תואם ל T[]. מערכים מסוגים פרימיטיביים משתנים ב- Java:

 longArray = intArray; // type error shortArray = (short[])intArray; // type error 

מערכים של התייחסות לסוגים הם קו-וריאנטית במשתמע ו במפורש contravariant , אולם:

 SuperType[] superArray; SubType[] subArray; ... superArray = subArray; // implicit covariant subArray = (SubType[])superArray; // explicit contravariant 
אנדראס סולימוסי

איור 7. משתנות מרומזת למערכים

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

 superArray[1] = new SuperType(); // throws ArrayStoreException 

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

דוגמה לשונות משתנה

בדוגמה פשוטה, הפניה למערך היא מהסוג Object[]אך אובייקט המערך והאלמנטים הם מסוגים שונים:

 Object[] objectArray; // array reference objectArray = new String[3]; // array object; compatible assignment objectArray[0] = new Integer(5); // throws ArrayStoreException 

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

אנדראס סולימוסי

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

הבדלים וכרטיסי בר בסוגים גנריים

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

 Generic superGeneric; Generic subGeneric; subGeneric = (Generic)superGeneric; // type error superGeneric = (Generic)subGeneric; // type error 

שגיאות הסוג מתעוררות למרות subGeneric.getClass() == superGeneric.getClass(). הבעיה היא שהשיטה getClass()קובעת את הסוג הגולמי - זו הסיבה שפרמטר סוג לא שייך לחתימה של שיטה. לפיכך, שתי הצהרות השיטה

 void method(Generic p); void method(Generic p); 

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