הצפה ותחתון בג'אווה

1. הקדמה

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

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

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

2. הצפה ותחתון

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

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

בואו נסתכל על דוגמה בה אנו מנסים להקצות את הערך 1010001 עם 1000 אפסים) למשתנה מסוג int אוֹ לְהַכפִּיל. הערך גדול מדי עבור int אוֹ לְהַכפִּיל משתנה ב- Java, ויהיה הצפה.

כדוגמה שנייה, נניח שאנחנו מנסים להקצות את הערך 10-1000 (שקרוב מאוד ל- 0) למשתנה מסוג לְהַכפִּיל. ערך זה קטן מדי עבור a לְהַכפִּיל משתנה ב- Java, ותהיה תת-זרימה.

בואו נראה מה קורה בג'אווה במקרים אלה ביתר פירוט.

3. סוגי נתונים שלמים

סוגי הנתונים השלמים ב- Java הם בתים (8 סיביות), קצר (16 ביטים), int (32 ביטים), ו- ארוך (64 ביטים).

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

מספר שלם מסוג int ב- Java יכול להיות שלילי או חיובי, מה שאומר שעם 32 הביטים שלו, אנחנו יכולים להקצות ערכים בין -231 (-2147483648) ו 231-1 (2147483647).

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

3.1. דוגמא

מה יקרה אם נגדיר משתנה M מהסוג int ונסה להקצות ערך גדול מדי (למשל 21474836478 = MAX_VALUE + 1)?

תוצאה אפשרית של מטלה זו היא שהערך של M לא יוגדר או שתהיה שגיאה.

שתיהן תוצאות תקפות; עם זאת, ב- Java, הערך של M יהיה -2147483648 (הערך המינימלי). מצד שני, אם ננסה להקצות ערך של -2147483649 (= MIN_VALUE - 1), M יהיה 2147483647 (הערך המרבי). התנהגות זו נקראת מספר שלם-עוטף.

בואו ניקח בחשבון את קטע הקוד הבא כדי להמחיש התנהגות זו טוב יותר:

ערך int = Integer.MAX_VALUE-1; עבור (int i = 0; i <4; i ++, value ++) {System.out.println (value); }

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

2147483646 2147483647 -2147483648 -2147483647 

4. טיפול בתהליך זרימה והצפה של סוגי נתונים שלמים

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

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

4.1. השתמש בסוג נתונים אחר

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

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

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

בואו נראה איך לשכתב את הדוגמה שלנו לעיל ביג-שלם:

BigInteger largeValue = BigInteger חדש (Integer.MAX_VALUE + ""); עבור (int i = 0; i <4; i ++) {System.out.println (largeValue); largeValue = largeValue.add (BigInteger.ONE); }

נראה את הפלט הבא:

2147483647 2147483648 2147483649 2147483650

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

4.2. זרוק חריג

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

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

ערך int = Integer.MAX_VALUE-1; עבור (int i = 0; i <4; i ++) {System.out.println (value); value = Math.addExact (value, 1); }

השיטה הסטטית addExact () מבצע תוספת רגילה, אך משליך חריג אם הפעולה מביאה לגלישה או זרימה:

2147483646 2147483647 חריג בחוט "ראשי" java.lang.ArithmeticException: גלישה שלמה ב- java.lang.Math.addExact (Math.java:790) ב- baeldung.underoverflow.OverUnderflow.main (OverUnderflow.java:115)

בנוסף ל addExact (), ה מתמטיקה החבילה ב- Java 8 מספקת שיטות מדויקות המתאימות לכל פעולות החשבון. עיין בתיעוד Java לקבלת רשימה של כל השיטות הללו.

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

להמרה מ ארוך ל int:

intic public int toIntExact (ארוך א)

ולגיור מ ביג-שלם ל int אוֹ ארוך:

BigInteger largeValue = BigInteger.TEN; longValue ארוך = largeValue.longValueExact (); int intValue = largeValue.intValueExact ();

4.3. לפני Java 8

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

ציבורי סטטי ציבורי int addExact (int x, int y) {int r = x + y; אם (((x ^ r) & (y ^ r)) <0) {זרוק אריתמטיקה חדשה ("int overflow"); } להחזיר r; }

5. סוגי נתונים שאינם שלמים

הסוגים שאינם שלמים לָצוּף ו לְהַכפִּיל לא מתנהגים באותה צורה כמו סוגי הנתונים השלמים כשמדובר בפעולות חשבון.

הבדל אחד הוא שפעולות חשבון על מספרים של נקודות צפות יכולות לגרום ל- a NaN. יש לנו מאמר ייעודי על NaN בג'אווה, לכן לא נבחן את הנושא במאמר זה. יתר על כן, אין שיטות חשבון מדויקות כגון הוסף מדויק אוֹ להכפיל מדויק עבור סוגים שאינם שלמים ב- מתמטיקה חֲבִילָה.

Java עוקבת אחר תקן IEEE לחשבון נקודה צפה (IEEE 754) עבורו לָצוּף ו לְהַכפִּיל סוגי מידע. תקן זה הוא הבסיס לאופן שבו ג'אווה מטפלת בתהליכי יתר ותחתון של מספרי נקודות צפות.

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

5.1. הצפה

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

assertTrue (Double.MAX_VALUE + 1 == Double.MIN_VALUE);

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

assertTrue (Double.MAX_VALUE + 1 == Double.MAX_VALUE);

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

אם נגדיל את ערך המשתנה שלנו כך שנגדיל את אחד הסיביות המשמעותיות של המשתנה, למשתנה יהיה הערך אינסוף:

assertTrue (Double.MAX_VALUE * 2 == Double.POSITIVE_INFINITY);

ו NEGATIVE_INFINITY לערכים שליליים:

assertTrue (Double.MAX_VALUE * -2 == Double.NEGATIVE_INFINITY);

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

5.2. תת-זרימה

ישנם שני קבועים המוגדרים לערכים המינימליים של a לְהַכפִּיל ערך: MIN_VALUE (4.9e-324) ו- MIN_NORMAL (2.2250738585072014E-308).

תקן IEEE לחשבון נקודה צפה (IEEE 754) מסביר את הפרטים להבדל בין אלה בפירוט רב יותר.

בואו נתמקד מדוע בכלל צריך ערך מינימלי למספרי נקודות צפות.

א לְהַכפִּיל הערך לא יכול להיות קטן באופן שרירותי מכיוון שיש לנו רק מספר מוגבל של ביטים לייצוג הערך.

הפרק על סוגי, ערכים ומשתנים במפרט השפה Java SE מתאר כיצד מיוצגים סוגי נקודות צפות. המעריך המינימלי לייצוג בינארי של a לְהַכפִּיל ניתן כ -1074. כלומר הערך החיובי הקטן ביותר שיכול להיות לכפול הוא Math.pow (2, -1074), שהוא שווה ל- 4.9e-324.

כתוצאה מכך, הדיוק של א לְהַכפִּיל ב- Java אינו תומך בערכים בין 0 ל- 4.9e-324, או בין -4.9e-324 ו 0 לערכים שליליים.

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

עבור (int i = 1073; i <= 1076; i ++) {System.out.println ("2 ^" + i + "=" + Math.pow (2, -i)); }

עם פלט:

2 ^ 1073 = 1.0E-323 2 ^ 1074 = 4.9E-324 2 ^ 1075 = 0.0 2 ^ 1076 = 0.0 

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

באופן דומה, עבור ערכים שליליים, תת זרם יביא לערך של -0.0 (אפס שלילי).

6. איתור זרימה ושיטפון של סוגי נתוני נקודה צפה

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

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

כוח כפול סטטי ציבורי מדויק (בסיס כפול, מעריך כפול) {if (base == 0.0) {return 0.0; } תוצאה כפולה = Math.pow (בסיס, מעריך); אם (תוצאה == Double.POSITIVE_INFINITY) {לזרוק ArithmeticException ("הצפה כפולה וכתוצאה מכך POSITIVE_INFINITY"); } אחרת אם (תוצאה == Double.NEGATIVE_INFINITY) {לזרוק ArithmeticException חדש ("הצפה כפולה וכתוצאה מכך NEGATIVE_INFINITY"); } אחר אם (Double.compare (-0.0f, תוצאה) == 0) {לזרוק ArithmeticException ("גלישה כפולה וכתוצאה מכך אפס שלילי"); } אחר אם (Double.compare (+ 0.0f, תוצאה) == 0) {זרוק ArithmeticException חדש ("הצפה כפולה וכתוצאה מכך אפס חיובי"); } להחזיר תוצאה; }

בשיטה זו עלינו להשתמש בשיטה Double.compare (). מפעילי ההשוואה הרגילים (< ו >) אין להבחין בין אפס חיובי לשלילי.

7. חיובי ושלילי אֶפֶס

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

בואו נגדיר כמה משתנים להדגמה:

כפול a = + 0f; כפול b = -0f;

כי חיובי ושלילי 0 נחשבים שווים:

assertTrue (a == b);

ואילו אינסוף חיובי ושלילי נחשבים שונים:

assertTrue (1 / a == Double.POSITIVE_INFINITY); assertTrue (1 / b == Double.NEGATIVE_INFINITY);

עם זאת, הקביעה הבאה נכונה:

assertTrue (1 / a! = 1 / b);

מה שנראה כסתירה לקביעה הראשונה שלנו.

8. מסקנה

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

ראינו גם כיצד נוכל לזהות זרימת יתר ותחתית במהלך ביצוע התוכנית.

כרגיל, קוד המקור השלם זמין ב- Github.


$config[zx-auto] not found$config[zx-overlay] not found