8 minute read

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



Image Description

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

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

תענו מהר - כמה זה 32 חלקי 3?

התשובה הנכונה, לפי המתמטיקה שלימדו אותנו בתיכון, היא $10\frac{2}{3}$ - עשר ושני שלישים. בואו נכניס את החישוב הזה ל-Java ונראה מה אנחנו מקבלים:

package learning_java.divmod;

public class JavaDivAndMod {
    public static void main(String[] args) {
        System.out.println(32 / 3);
    }
}

נריץ ונקבל… 10. לא 10 ושני שליש, לא $10.6666…$, גם לא משהו קרוב כמו $10.6667$ שאפשר לומר עליו שזה קירוב. למה?

בפוסט של משתנים הסברתי את הטיפוסים השונים של משתנים בסיסיים ב-Java והסברתי שלערכים ב-Java יש צורה. ל-32 ול-3 כמו שהם מופיעים כאן יש “צורה” של int - שמתאים לטיפוס של מספר שלם (יש כמה טיפוסים של שלמים, כמו שהזכרתי באותו פוסט, אבל פה מדובר ב-int, ולצורך הפוסט הזה נתמקד רק בטיפוס הזה). אמרנו ש-32.0 שהוא עם “צורה” של double (מספר עשרוני) אמנם שווה ל-32, אבל הם לא אותו דבר.

לפעמים במתמטיקה, אם נחלק מספר שלם במספר שלם, נקבל מספר שלם. למשל 9 חלקי 3, גם במתמטיקה וגם ב-Java, יהיה 3. אבל לפעמים אם נחלק מספר שלם במספר שלם נקבל מספר לא שלם, למשל 15 חלקי 2 שווה ל-7.5. כאן המפתחים של Java קיבלו החלטה - הם היו צריכים לקבוע האם int חלקי int יהיה int או double. מכיוון שJava מבוססת בעיקר על שפת C, ושפת C בחרה ש-int חלקי int יהיה int, הם הלכו עם הבחירה של שפת C, וגם על הצורה שהגדירו את זה ב-C. יש סיבות טובות לזה שכך מחלקים מספרים שלמים בשפת C (והן לא מעניינות אותנו). לא כל השפות עשו את זה - בפייתון למשל, אמנם למשתנים אין טיפוס, אבל לערכים שונים יש טיפוס, ושם 15/2 יהיה 7.5, וכן 10/2 יהיה 5.0. זו ההחלטה שקיבלו המפתחים של פייתון.

אוקיי, אז המפתחים של Java החליטו שכמו ב-C, חלוקה של int ב-int תיתן int, ואם שני מספרים שלמים מתחלקים זה בזה, למשל 9 ב-3, לא תופתעו לשמוע שב-Java (וב-C) התוצאה תהיה כמו במתמטיקה: במתמטיקה, $9\div 3 = 3$ וגם ב-Java, התוצאה של 9/3 תהיה 3. אבל מה אם הם לא? לגבי 32/10 ראינו שהתוצאה היא 10, וזה אולי קצת מוזר, כי 11 יותר קרוב לתוצאה המתמטית האמיתית של החישוב הזה מאשר 10. התשובה היא ש-Java, התוצאה של $m/n$ תהיה עיגול למטה של התוצאה המתמטית. אם היא לא שלם, היא תעוגל למטה, גם אם זה לא השלם הקרוב יותר אליה.

מספרים שליליים

כל זה נכון לגבי חלוקה של מספרים חיוביים. אם מחלקים מספר שלילי במספר שלילי, אז כמו במתמטיקה, המינוסים מצטמצמים ומתקבלת אותה תוצאה שהייתה מתקבלת אלמלא המינוסים. כלומר, $(-19)/(-10)$ יהיה כמו $19/10$, שזה 1. אם המונה שלילי והמכנה חיובי, או להפך, Java מחלקת את המספרים כאילו היו חיוביים ואז מחזירה את המינוס לתוצאה. כלומר: \((-19)/10 = 19/(-10) = -1\) שזה למען האמת לא עיגול למטה של התוצאה אלא עיגול למעלה (הרי התוצאה המתמטית היא $-1.9$, ואם היינו מעגלים אותו למטה היינו מקבלים $-2$). כמו מה שאמרנו לגבי קאסטינג מ-double שאינו שלם ל-int, יכול להיות שיותר נכון לומר “התוצאה מעוגלת לכיוון האפס”.

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

שארית

בנוסף לארבע פעולות החשבון, ב-Java יש פעולה נוספת שקשורה היטב לפעולת החלוקה, ובפרט לחלוקת שלמים. מדובר בפעולת השארית (קיצור ל”שארית חלוקה”) והיא משתמשת בסימן ה-%. כמו שאמרנו, בהינתן שני מספרים שלמים $a, b$ (כאשר $b\neq 0 $), לא בהכרח תוצאת המנה המתמטית המדויקת $\frac{a}{b}$ תהיה מספר שלם, ואם היא לא תהיה מספר שלם, אז ב-Java, $a/b$ יעוגל כלפי מטה. וכמו שלמדנו ביסודי, אם מספרים לא מתחלקים באופן שלם, יש שארית חלוקה:

למשל, אם 13 מתוך חברי מועדון הדיבייט של אוניברסיטת בן-גוריון בבאר שבע צריכים לצאת לתחרות בחיפה (מה? יש לי תחומי עניין מלבד רובוטיקה ותכנות), והם נוסעים ברכבים שבכל אחד 5 נוסעים, אז $13/5$ ב-Java יגיד לנו שהמשלחת תמלא שני רכבים, אבל אלו רק כמות הרכבים שהנבחרת תמלא, כלומר שיהיו בהם חמישה אנשים. אם נוציא רק שני רכבים, יהיו שלושה חברי מועדון שלא יגיעו לחיפה היום, ולא יזכו להשתתף בגמר העוסק בשאלה האם כדאי להרשות לחיפאים לצוד את חזירי הבר המטילים את אימתם על העיר (אני כל כך לא ממציא את הדוגמה הזו).

בכל מקרה, זו פעולת השארית - אם היינו מחלקים את $a$ ב-$b$ חלוקת שלמים, כמה היה “נשאר”. וכאמור, ב-Java מסומן בעזרת %. זה לא אומר שזה קשור באף צורה לאחוזים, ואין לקרוא את זה בתור “האחוזים של b ב-a” או שום דבר כזה - זה פשוט שהיו צריכים לבחור סימן לזה, אז על זה הם הלכו (גם כאן, Java לא באמת קיבלו את ההחלטה הזו בעצמם, אלא קיבלו החלטה לאמץ את מה שמקובל בשפת C; אני לא יודע לומר אם C בעצמם לקחו את זה ממקום אחר).

דוגמאות:

System.out.println(159 % 10); // 9
System.out.println(159 % 100); // 59
System.out.println(159 % 2); // 1
System.out.println(30 % 10); // 0

שימו לב למשל לכמה דברים:

  • $a \% 10$ ייתן את ספרת האחדות של $a$
  • $a\%2$ תמיד יהיה 0 או 1 (למעשה, לכל $n$ חיובי, $a \% n$ תמיד יקיים $0\leq a\% n < n$)
  • $a \% n$ יהיה 0 אם ורק אם $a$ מתחלק ב-$n$ (למעשה, זו ממש המשמעות של “מתחלק”)

נסו לשכנע את עצמכן שאם ב-Java, $a/b = q$ ו-$a \% b = r$, אז $a=qb+r$. אם תשכנעו את עצמכן בזה, תוכלו גם לשכנע את עצמכן בתכונה האחרונה מבין אלו שציינתי למעלה - ששארית החלוקה תהיה 0 אם ורק אם $a$ מתחלק ב-$b$.

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

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

חלוקה באפס

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

מה שאני כן הולך להגיד זה מה הולך עם זה ב-Java. אם נבקש מ-Java את התוצאה של x/y, כאשר x, y הם עם “צורה” של מספרים שלמים, ו-Java ניגשת לפעולה ורואה ש-$y=0$, מה קורה? התשובה היא - שגיאת זמן ריצה.

שגיאת זמן ריצה היא מה שקורה כש-Java מבצעת הוראה ומשהו משתבש. שגיאת זמן ריצה שונה משגיאת קומפילציה, שהיא מה שקורה כאשר הקוד לא הגיוני מלכתחילה והקומפיילר של Java (עוד על מה זה קומפיילר כאן) לא יכול להבין ממנו איך הוא אמור להפוך אותו לפקודות מכונה. שגיאת קומפילציה דומה לאיך שתגיבי אם הייתי מבקש “קחי את השמבגדשלחקהן הזה ותפלקחי אותו אליי” - את לא תתחיל לעשות את זה כי אין לך מושג מה אני רןוצה שתעשי. לעומת זאת, שגיאת זמן ריצה דומה למה שקורה אם הייתי מבקש “גשי לחדר השני ותביאי לי את העותק שלי של ‘1984’ שמונח על השולחן”, וברגע שהיית נכנסת לחדר השני, היית רואה שאין ספר על השולחן - ההוראות היו הגיונית, ובזמן ביצוע משהו השתבש.

כאשר Java נתקלת בשגיאת זמן ריצה, היא מכריזה על זה, עוצרת את התוכנית, ולא ממשיכה. נסו למשל להריץ את התוכנית הבאה:

package learning_java.divmod;

public class DivByZeroExample {
    public static void main(String[] args) {
        int twelve = 12;
        int zero = 0;
        
        System.out.println("Computing 12 / 0...");
        System.out.println(twelve / zero);
        System.out.println("That's the answer");
    }
}

בפלט של התוכנית לא יופיע That's the answer. מה שיתקבל הוא שנראה את הפלט Computing 12 / 0, ואז נראה טקסט שמודיע לנו שהייתה שגיאת זמן ריצה של חלוקה באפס. ואחריה, כלום, כי Java לא המשיכה לבצע את התוכנית אחרי השגיאה.

כל זה נכון גם לגבי אופרטור השארית - מבחינת Java, $5/0$ הוא לא הגיוני וכך גם $5\% 0%$.

לגבי מספרים עשרוניים (double)

כל הדיון בפוסט הזה עסק בחלוקה של int ב-int. חלוקה של double ב-double היא double וערכו הוא כמו שהיינו מצפים במתמטיקה, למשל: \(10.0/2.0 = 5.0 \\ 10.0 / 4.0 = 2.5 \\ 10.5 / 0.3 = 35.0 \\ 2.5 / 1.2 = 2.0833333333333335\) (מספרים לא “עגולים” מספיק יצאו לא מדויקים בגלל מספר ספרות מוגבל, זה נכון לגבי double באופן כללי - נסו למשל לראות כמה יוצא $0.1 + 0.2$)

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

ומה עם חלוקה באפס במקרה של double? ל-double יש ערכים מיוחדים בשם אינסוף, מינוס אינסוף ו-NaN שמתקבלים במקרים האלה ואני לא רוצה להרחיב עליהם כרגע.

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

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

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

Comments