8 minute read

⚠ שימו לב - העמקה



Image Description

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

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

הנקודה הבאה ב-Java שאני רוצה לדבר עליה היא קאסטינג (casting). בפוסט הקודם (לינק) דיברתי על משתנים ב-Java. תזכורת מהירה לנקודות הרלוונטיות לפוסט הזה:

  • ב-Java, למשתנים יש טיפוסים, שהם סוג הערכים שיכולים להיות בהם. אני חושב עליהם בתור “צורות”
  • מערכת הטיפוסים מוודאת שאנחנו משתמשים במשתנים בצורה נכונה
  • דוגמאות לכמה טיפוסים בסייסים ב-Java הם: int למספרים שלמים, long שגם הוא למספרים שלמים אבל תומך בטווח יותר גדול של מספרים, double למספרים עשרוניים (כמו -3.14 אבל גם כמו 2212.0 שהוא אמנם נראה כמו מספר שלם אבל הוא “בצורה” של מספר עשרוני), ו-char לתווים יחידים של טקסט. יש טבלה (לינק) מפורטת יותר בפוסט על משתנים.

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

קאסטינג מ-int ל-double

אז נגיד שיש לי משתנה int - לצורך העניין נגיד ששמו myInt. מה שלא יהיה הערך בו כרגע, בין אם זה 0 או 4661 או מינוס 31415, זה בטוח מספר שלם (זה מה שמבטיח משתנה int), ולכן זה גם מספר עשרוני. כלומר, מתמטית, אין שום הבדל בין 37 ל-37.0 - זה אותו מספר בדיוק. אז אם יש לי משתנה myDouble מסוג double, הגיוני שאוכל להכניס אליו את מה שיש בתוך myInt, מה שזה לא יהיה כרגע, כי כל מספר שלם הוא גם מספר עשרוני. ואכן, הקוד הבא ב-Java מתקמפל, כלומר, מבחינת Java, הוא תקין והגיוני:

int myInt = 14142;
double myDouble = myInt;

ומה יהיה הערך של myDouble אחרי ההשמה הזו? התשובה פשוטה - $14142.0$.

קאסטינג מ-double ל-int

אבל כיוון ש-14142.0 הוא בתכלס מספר שלם, שרק יש לו את הצורה של מספר עשרוני, הגיוני שנוכל לעשות גם את הכיוון ההפוך -

int myInt = 14142;
double myDouble = myInt;
int backToInt = myDouble;

אבל לא - הקוד הזה לא מתקמפל - מבחינת Java, השורה האחרונה בו לא הגיונית. אני ואתם יודעים שכאשר Java תגיע לשורה הזו, myDouble יכיל את 14142.0 ועבורנו זה יהיה הגיוני ש-backToInt פשוט יקבל את הערך 14142.0, אבל כל מה ש-Java יודעת הוא ש-myDouble מכיל ערך עשרוני כלשהו, שיכול גם להיות 3.5. אז מה עושים? ובכן, הביטו בשינוי הבא בקוד:

int myInt = 14142;
double myDouble = myInt;
int backToInt = (int) myDouble;

הוספתי את המילה int בסוגריים לפני הערך שאותו רוצה להכניס ל-backToInt. המשמעות של זה היא “ל-myDouble אמנם אין ‘צורה’ של int, אבל ‘תמעך’ אותו לצורה של int ואת זה תכניס ל-backToInt”. הפעולה הזו, של העברה בין צורות, היא קאסטינג (casting).

קאסטינג מפורש מול מרומז

בואו נחזור לקוד הראשון, שבו הכנסנו ערך של int לתוך משתנה מסוג double:

int myInt = 14142;
double myDouble = myInt;

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

int myInt = 14142;
double myDouble = (double) myInt;

שפת Java מקלה עלינו בכך שאת הקאסטינג מ-int ל-double היא מבינה גם במרומז, אבל מבחינתה, בין אם כתבנו את ההמרה במפורש (כמו הקוד האחרון) או שלא במפורש (כמו הקוד שמעליו), זה אותו קאסטינג. אם אנחנו כותבים בפירוש את הקאסטינג, כלומר כן כוללים בקוד את הסוגריים עם שם הטיפוס שאליו ממירים לפני הערך שאותו ממירים, זה נקרא קאסטינג מפורש (explicit casting), ואם לא נכתוב אותו במפורש, זה נקרא קאסטינג מרומז (implicit casting).

אז למה Java לא מאפשרת לנו קאסטינג במרומז גם בכיוון השני? כלומר, למה בקוד הזה -

double someDouble = 1.0;
int someInt = (int) someDouble;

אם אסיר את ה-(int), תתקבל שגיאת קומפילציה? למה Java, כאשר ממירים מ-double ל-int, מחייבת אותנו לעשות קאסטינג במפורש? את התשובה התחלתי קודם - כי אמנם אני ואתם יודעים שבמקרה הזה, כאשר נגיע לשורה הזו, someDouble יכיל 1.0 שהוא מספר שלם ב”צורה” של עשרוני, אבל הקומפיילר של Java לא יודע את זה. כל מה שהקומפיילר רואה הוא שלביטוי myDouble יש צורה של double ולכן מבחינתו יכול להיות שיהיה שם בכלל 3.01 או 8.9 או 4.5. לכן, כשקבעו איך Java תעבוד ומה ייחשב לחוקי ולא חוקי בה, קבעו שבהמרה של double ל-int יהיה חובה לעשות אותה מפורשת. ולמה דווקא בכיוון הזה? כי זה הכיוון שאינו ברור מאליו. אמרנו שברור שכל מספר שלם יהפוך למספר העשרוני ששווה לו, אבל לא לכל מספר עשרוני יש מספר שלם ששווה לו בדיוק. ואם כבר -

קאסטינג מ-double ל-int כאשר המספר לא שלם

int a = (int) 12.0;
int b = (int) 3.01;
int c = (int) 4.5;
int d = (int) 9.9999;

מה יהיו הערכים של a, b, c ו-d? התשובה: a, באופן לא מפתיע (שכבר דיברנו עליו), יהיה 12. גם לא מפתיע ש-b יהיה 3. במשתנה c יהיה 4 - כלומר 4.5 עוגל למטה - ובמשתנה d יהיה… 9. כן, 9.9999 הרבה יותר קרוב ל-10 מאשר ל-9, אבל ב-Java, כל מספר עשרוני שאינו שלם בדיוק, יעוגל למטה אם נעשה לו קאסטינג ל-int.

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

int e = (int) -0.1; // 0
int f = (int) -0.9; // 0
int g = (int) -8.5; // -8
int h = (int) -9.9; // -9

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

קאסטינג בין int ל-long

אם כבר הזכרנו את long, מה איתו? קודם כל, כמו שהצורה של int ב-Java שונה מזו של double, גם long זו צורה בפני עצמה. קודם כל, אם אנחנו רוצים לכתוב מספר שלם בתור long ב-Java, נסיים את המספר ב-L (גם l קטנה היא חוקית, אבל לדעתי, ולא רק לדעתי, עדיף L גדולה), למשל:

long five = 5L;

אם לא נסמן L, הפקודה הזו עדיין תהיה חוקית:

long five = 5;

אבל ל-5 כאן יש “צורה” של int כי הוא בלי ה-L ולכן שוב יש לנו כאן קאסטינג מרומז. כלומר, Java מתייחסת לקוד הזה בתור:

long five = (long) 5;

ואז, בתוך המשתנה five יהיה 5L, כלומר 5 בתור long.

אז בין int ל-long יש המרה מרומזת, והסיבה לכך היא שברור איך כל int יהפוך ל-long: הוא פשוט יהיה אותו ערך מספרי, אבל ב”צורה” של long.

וכמו שאפשר לנחש, בכיוון השני ההמרה חייבת להיות מפורשת:

long asLong = 5L;
int asInt = (int) asLong;

אילו היינו מוחקים את ה-(int), התוכנית לא הייתה מתקמפלת. זה כי לא כל long יכול “להיכנס” ל-int.

המספר החיובי הגדול ביותר שאפשר להכניס ב-int הוא $2^{31} - 1 = 2147483647$ (לא משנה עכשיו מה קורה כשעושים על זה פעולות חשבוניות כמו להוסיף 1). כלומר, 2147483647 “נכנס” ב-int, אבל 2147483648 לא. לעומת זאת, המספר הזה כן יכול להיכנס ב-long (ואז נחשוב עליו בתור 2147483648L). ומה אם נמיר את המספר הזה ל-int?

long tooLargeForInt = 2147483648L;
int asInt = (int) tooLargeForInt;
System.out.println(asInt);

הפלט של התוכנית הזו הוא $-2147483648$. הסיבה לכך חורגת ממטרת הפוסט הזה (ספוילר למי שמעוניין: הסיבה לכך היא גם התשובה לשאלה שדילגתי עליה למעלה, של מה קורה אם מוסיפים 1 למספר הכי גדול שאפשר להכניס ל-int). המסקנה שיש לי עבורכם מזה היא שיש להיזהר כשממירים מ-long ל-int. אם הגעתם למצב כזה בקוד, יכול להיות (אבל לא בהכרח) שדברים מסוימים שיהיו long היו יכולים וצריכים להיות int, או להפך.

עוד טיפוסים של מספרים

יש עוד טיפוסים של מספרים שלמים. ספציפית, יש עוד שני גדלים של מספרים שלמים: int הוא מספיק לרוב המטרות, ויש פעמים שצריך את long שתומך ביותר מספרים. אבל יש גם גדלים של מספרים יותר קטנים מ-int: הטיפוס short תומך במספרים בין $-32768$ ל-$32767$, ו-byte תומך במספרים בין $-127$ ל-$128$. למה זה טוב? זה בגלל הסיבה שיש בכלל טיפוסים שונים של מספרים שלמים - הטיפוסים השונים האלה תופסים גדלים שונים בזיכרון. יש אנשים שעבורם short ו-byte הם רלוונטיים, אבל עבורנו, int כמעט תמיד יהיה מתאים, וברוב שאר המצבים, נצטרך long.

הקאסטינג בין טיפוסי השלמים השונים הם כמו בין int ל-long: מהטיפוס ה”גדול” ל”קטן” חייבים להיות מפורשים, בכיוון השני זה אופציונלי. אבל שוב, זה לא ממש משנה. לשם השלמות אציין שיש גם עוד טיפוס של מספר עשרוני חוץ מ-double והוא נקרא float - הטיפוס double תומך במספרים גדולים יותר (וביותר ספרות אחרי הנקודה), ולכן היחס בין double ל-float הוא כמו היחס בין long ל-int. אבל גם float לא מאוד משנה לנו.

נקודות עיקריות

  • קאסטינג היא המרה בין טיפוסים
  • יש שתי אפשרויות לקאסטינג: מפורש (explicit) או מרומז (implicit)
  • התחביר של קאסטינג מפורש הוא סוגריים ובהם שם הטיפוס שאליו ממירים ואחריו הביטוי שאותו ממירים
  • קאסטינג מרומז הוא בדיוק מה שזה נשמע - הוא מרומז - התחביר שלו הוא שלא כותבים אותו בפירוש, ו-Java “מבינה” שהוא שם.
  • קאסטינג בין int ל-double או בין long ל-double יכול להתבצע גם במרומז
  • קאסטינג בין double ל-int או בין double ל-long חייב להיות מפורש, והוא מעגל את המספרים כלפי מטה אם הם חיוביים, ומעגל אותם כלפי מעלה אם הם שליליים (כלומר, זה לאו דווקא יעגל למספר השלם הכי קרוב).
  • יש עוד טיפוסי שלמים ויש גם קאסטינג ביניהם (אבל לרוב זה לא מעניין אותנו)
  • יש גם עוד טיפוס של מספרים עשרוניים חוץ מ-double ויש גם קאסטינג ביניהם (וגם זה לרוב לא מעניין אותנו)

Comments