JavaRush /בלוג Java /Random-HE /המכשיר של המספרים האמיתיים

המכשיר של המספרים האמיתיים

פורסם בקבוצה
שלום! בהרצאה של היום נדבר על מספרים בג'אווה, ובאופן ספציפי על מספרים ממשיים. מכשיר המספרים האמיתיים - 1לא להיבהל! :) לא יהיו קשיים מתמטיים בהרצאה. נדבר על מספרים אמיתיים אך ורק מנקודת המבט ה"מתכנת" שלנו. אז מה הם "מספרים אמיתיים"? מספרים ממשיים הם מספרים שיש להם חלק חלקי (שיכול להיות אפס). הם יכולים להיות חיוביים או שליליים. הנה כמה דוגמאות: 15 56.22 0.0 1242342343445246 -232336.11 איך עובד מספר ממשי? די פשוט: הוא מורכב מחלק שלם, חלק שבר וסימן. עבור מספרים חיוביים הסימן בדרך כלל אינו מצוין במפורש, אך עבור מספרים שליליים הוא מצוין. בעבר, בדקנו בפירוט אילו פעולות על מספרים ניתן לבצע בג'אווה. ביניהם היו הרבה פעולות מתמטיות סטנדרטיות - חיבור, חיסור וכו'. היו גם כמה חדשות עבורך: למשל, שאר החלוקה. אבל איך בדיוק עובדת עבודה עם מספרים בתוך מחשב? באיזו צורה הם מאוחסנים בזיכרון?

אחסון מספרים אמיתיים בזיכרון

אני חושב שזה לא יהיה תגלית עבורך שמספרים יכולים להיות גדולים וקטנים :) אפשר להשוות אותם אחד עם השני. לדוגמה, המספר 100 קטן מהמספר 423324. האם זה משפיע על פעולת המחשב והתוכנית שלנו? האמת שכן . כל מספר מיוצג ב-Java על ידי טווח ספציפי של ערכים :
סוּג גודל זיכרון (סיביות) טווח ערכים
byte 8 ביט -128 עד 127
short 16 ביט -32768 עד 32767
char 16 ביט מספר שלם ללא סימן המייצג תו UTF-16 (אותיות ומספרים)
int 32 ביטים מ-2147483648 ל-2147483647
long 64 ביטים מ-9223372036854775808 ל-9223372036854775807
float 32 ביטים מ-2 -149 עד (2-2 -23 )*2 127
double 64 ביטים מ-2 -1074 עד (2-2 -52 )*2 1023
היום נדבר על שני הסוגים האחרונים - floatו double. שניהם מבצעים את אותה משימה - מייצגים מספרים שברים. הם גם נקראים לעתים קרובות מאוד " מספרי נקודה צפה" . זכור את המונח הזה לעתיד :) לדוגמה, המספר 2.3333 או 134.1212121212. די מוזר. הרי מסתבר שאין הבדל בין שני הסוגים הללו, מאחר שהם מבצעים את אותה משימה? אבל יש הבדל. שימו לב לעמודה "גודל בזיכרון" בטבלה למעלה. כל המספרים (ולא רק מספרים - כל המידע באופן כללי) נשמרים בזיכרון המחשב בצורה של ביטים. מעט הוא יחידת המידע הקטנה ביותר. זה די פשוט. כל סיביות שווה ל-0 או ל-1. והמילה " bit " עצמה באה מהאנגלית " ספרה בינארית " - מספר בינארי. אני חושב שבטח שמעתם על קיומה של מערכת המספרים הבינארית במתמטיקה. כל מספר עשרוני שאנו מכירים יכול להיות מיוצג כקבוצה של אחדים ואפסים. לדוגמה, המספר 584.32 בבינארי ייראה כך: 100100100001010001111 . כל אחד ואפס במספר זה הוא ביט נפרד. כעת אתה אמור להיות ברור יותר לגבי ההבדל בין סוגי הנתונים. לדוגמה, אם ניצור מספר סוגים float, יש לנו רק 32 סיביות לרשותנו. כשיוצרים מספר, floatזה בדיוק כמה מקום יוקצה לו בזיכרון המחשב. אם נרצה ליצור את המספר 123456789.65656565656565, בבינארי זה ייראה כך: 111010110111100111010001010110101000000 . הוא מורכב מ-38 אחדות ואפסים, כלומר, יש צורך ב-38 סיביות כדי לאחסן אותו בזיכרון. floatהמספר הזה פשוט לא "יתאים" לסוג ! לכן, ניתן לייצג את המספר 123456789 כסוג double. לאחסן אותו מוקצים עד 64 ביטים: זה מתאים לנו! כמובן שגם טווח הערכים יתאים. מטעמי נוחות, אתה יכול לחשוב על מספר כקופסה קטנה עם תאים. אם יש מספיק תאים לאחסון כל ביט, אז סוג הנתונים נבחר נכון :) מכשיר המספרים האמיתיים - 2כמובן, כמויות שונות של זיכרון שהוקצה משפיעות גם על המספר עצמו. שימו לב שלסוגים floatיש doubleטווחי ערכים שונים. מה זה אומר בפועל? מספר doubleיכול לבטא דיוק גדול יותר ממספר float. למספרי נקודה צפה של 32 סיביות (ב-Java זה בדיוק הסוג float) יש דיוק של כ-24 סיביות, כלומר בערך 7 מקומות עשרוניים. ולמספרים של 64 סיביות (ב-Java זה הסוג double) יש דיוק של כ-53 סיביות, כלומר כ-16 מקומות עשרוניים. הנה דוגמה שמדגימה היטב את ההבדל הזה:
public class Main {

   public static void main(String[] args)  {

       float f = 0.0f;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}
מה אנחנו צריכים לקבל כאן כתוצאה מכך? נראה שהכל די פשוט. יש לנו את המספר 0.0, ואנו מוסיפים לו 0.1111111111111111 7 פעמים ברציפות. התוצאה צריכה להיות 0.77777777777777777. אבל יצרנו מספר float. גודלו מוגבל ל-32 סיביות, וכפי שאמרנו קודם, הוא מסוגל להציג מספר עד בערך ה-7 העשרוני. לכן, בסופו של דבר, התוצאה שנקבל בקונסולה תהיה שונה ממה שציפינו:

0.7777778
נראה היה שהמספר "נחתך". אתה כבר יודע איך נתונים מאוחסנים בזיכרון - בצורה של ביטים, אז זה לא אמור להפתיע אותך. ברור למה זה קרה: התוצאה 0.77777777777777777 פשוט לא התאימה ל-32 הסיביות שהוקצו לנו, אז היא נקטעה כדי להשתלב במשתנה הטיפוס float:) אנחנו יכולים לשנות את סוג המשתנה ל- doubleבדוגמה שלנו, ולאחר מכן את הסופי התוצאה לא תקוצר:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 7; i++) {
           f += 0.1111111111111111;
       }

       System.out.println(f);
   }
}

0.7777777777777779
יש כבר 16 מקומות עשרוניים, התוצאה "מתאימה" ל-64 סיביות. אגב, אולי שמתם לב שבשני המקרים התוצאות לא היו לגמרי נכונות? החישוב נעשה בשגיאות קלות. על הסיבות לכך נדבר להלן :) עכשיו בואו נגיד כמה מילים על איך אפשר להשוות מספרים זה עם זה.

השוואה של מספרים ממשיים

חלקית כבר נגענו בנושא הזה בהרצאה האחרונה, כשדיברנו על פעולות השוואה. לא ננתח מחדש פעולות כגון >, <, >=. <=הבה נסתכל במקום זאת על דוגמה מעניינת יותר:
public class Main {

   public static void main(String[] args)  {

       double f = 0.0;
       for (int i=1; i <= 10; i++) {
           f += 0.1;
       }

       System.out.println(f);
   }
}
איזה מספר לדעתך יוצג על המסך? התשובה ההגיונית תהיה התשובה: המספר 1. אנחנו מתחילים לספור מהמספר 0.0 ומוסיפים לו ברציפות 0.1 עשר פעמים ברציפות. הכל נראה נכון, זה צריך להיות אחד. נסה להריץ את הקוד הזה, והתשובה תפתיע אותך מאוד :) פלט מסוף:

0.9999999999999999
אבל מדוע אירעה שגיאה בדוגמה כל כך פשוטה? O_o כאן אפילו תלמיד כיתה ה' יכול לענות נכון בקלות, אבל תוכנית Java הניבה תוצאה לא מדויקת. "לא מדויק" היא מילה טובה יותר כאן מאשר "לא נכון". עדיין קיבלנו מספר קרוב מאוד לאחד, ולא רק איזה ערך אקראי :) הוא שונה מהנכון ממש במילימטר. אבל למה? אולי זו רק טעות חד פעמית. אולי המחשב קרס? בואו ננסה לכתוב דוגמה נוספת.
public class Main {

   public static void main(String[] args)  {

       //add 0.1 to zero eleven times in a row
       double f1 = 0.0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiply 0.1 by 11
       double f2 = 0.1 * 11;

       //should be the same - 1.1 in both cases
       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       // Let's check!
       if (f1 == f2)
           System.out.println("f1 and f2 are equal!");
       else
           System.out.println("f1 and f2 are not equal!");
   }
}
פלט מסוף:

f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
אז ברור שזה לא עניין של תקלות מחשב :) מה קורה? שגיאות כמו אלה קשורות לאופן שבו מספרים מיוצגים בצורה בינארית בזיכרון המחשב. העובדה היא שבמערכת הבינארית אי אפשר לייצג במדויק את המספר 0.1 . אגב, גם לשיטה העשרונית יש בעיה דומה: אי אפשר לייצג שברים נכון (ובמקום ⅓ נקבל 0.333333333333333..., גם זו לא לגמרי התוצאה הנכונה). זה נראה כמו פעוט: עם חישובים כאלה, ההבדל יכול להיות חלק מאה אלף (0.00001) או אפילו פחות. אבל מה אם כל התוצאה של תוכנית Very Serious שלך תלויה בהשוואה הזו?
if (f1 == f2)
   System.out.println("Rocket flies into space");
else
   System.out.println("The launch is canceled, everyone goes home");
ברור שציפינו ששני המספרים יהיו שווים, אבל בגלל עיצוב הזיכרון הפנימי, ביטלנו את שיגור הרקטה. מכשיר המספרים האמיתיים - 3אם כן, עלינו להחליט כיצד להשוות בין שני מספרי נקודה צפה כך שתוצאת ההשוואה תהיה יותר... אמממ... צפויה. אז, כבר למדנו כלל מס' 1 בעת השוואת מספרים ממשיים: לעולם אל תשתמש ==במספרי נקודה צפה בעת השוואת מספרים ממשיים. אוקיי, אני חושב שזה מספיק דוגמאות רעות :) בואו נסתכל על דוגמה טובה!
public class Main {

   public static void main(String[] args)  {

       final double threshold = 0.0001;

       //add 0.1 to zero eleven times in a row
       double f1 = .0;
       for (int i = 1; i <= 11; i++) {
           f1 += .1;
       }

       // Multiply 0.1 by 11
       double f2 = .1 * 11;

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       if (Math.abs(f1 - f2) < threshold)
           System.out.println("f1 and f2 are equal");
       else
           System.out.println("f1 and f2 are not equal");
   }
}
כאן אנחנו בעצם עושים את אותו הדבר, אבל משנים את הדרך שבה אנחנו משווים את המספרים. יש לנו מספר "סף" מיוחד - 0.0001, עשרת אלפים אחת. יכול להיות שזה שונה. זה תלוי במידת ההשוואה המדויקת שאתה צריך במקרה מסוים. אתה יכול לעשות את זה גדול או קטן יותר. באמצעות השיטה, Math.abs()נקבל את המודולוס של מספר. המודולוס הוא הערך של מספר ללא קשר לסימן. לדוגמה, המספרים -5 ו-5 יהיו בעלי אותו מודולוס והם יהיו שווים ל-5. נחסר את המספר השני מהראשון, ואם התוצאה המתקבלת, ללא קשר לסימן, קטנה מהסף שהגדרנו, אז המספרים שלנו שווים. בכל מקרה, הם שווים למידת הדיוק שקבענו באמצעות "מספר הסף" שלנו , כלומר, לכל הפחות הם שווים עד לעשרת אלפים אחת. שיטת השוואה זו תחסוך ממך את ההתנהגות הבלתי צפויה שראינו במקרה של ==. דרך טובה נוספת להשוות מספרים אמיתיים היא להשתמש במחלקה מיוחדת BigDecimal. מחלקה זו נוצרה במיוחד כדי לאחסן מספרים גדולים מאוד עם חלק חלקי. שלא כמו doubleו float, בעת שימוש BigDecimalבחיבור, חיסור ופעולות מתמטיות אחרות מתבצעות לא באמצעות אופרטורים ( +-וכו'), אלא באמצעות שיטות. כך זה ייראה במקרה שלנו:
import java.math.BigDecimal;

public class Main {

   public static void main(String[] args)  {

       /*Create two BigDecimal objects - zero and 0.1.
       We do the same thing as before - add 0.1 to zero 11 times in a row
       In the BigDecimal class, addition is done using the add () method */
       BigDecimal f1 = new BigDecimal(0.0);
       BigDecimal pointOne = new BigDecimal(0.1);
       for (int i = 1; i <= 11; i++) {
           f1 = f1.add(pointOne);
       }

       /*Nothing has changed here either: create two BigDecimal objects
       and multiply 0.1 by 11
       In the BigDecimal class, multiplication is done using the multiply() method*/
       BigDecimal f2 = new BigDecimal(0.1);
       BigDecimal eleven = new BigDecimal(11);
       f2 = f2.multiply(eleven);

       System.out.println("f1 = " + f1);
       System.out.println("f2 = " + f2);

       /*Another feature of BigDecimal is that number objects need to be compared with each other
       using the special compareTo() method*/
       if (f1.compareTo(f2) == 0)
           System.out.println("f1 and f2 are equal");
       else
           System.out.println("f1 and f2 are not equal");
   }
}
איזה סוג של פלט קונסולה נקבל?

f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
קיבלנו בדיוק את התוצאה שציפינו לה. ושימו לב עד כמה היו המספרים שלנו מדויקים, וכמה מקומות עשרוניים מתאימים להם! הרבה יותר מאשר ב floatואפילו ב double! זכרו את השיעור BigDecimalלעתיד, בהחלט תצטרכו אותו :) פיו! ההרצאה הייתה די ארוכה, אבל עשית את זה: כל הכבוד! :) נתראה בשיעור הבא, מתכנת עתידי!
הערות
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION