JavaRush /مدونة جافا /Random-AR /جهاز الأعداد الحقيقية

جهاز الأعداد الحقيقية

نشرت في المجموعة
مرحبًا! في محاضرة اليوم سنتحدث عن الأعداد في لغة جافا، وبالتحديد عن الأعداد الحقيقية. جهاز الأعداد الحقيقية - 1لا تُصب بالذعر! :) لن يكون هناك أي صعوبات رياضية في المحاضرة. سنتحدث عن الأعداد الحقيقية حصريًا من وجهة نظرنا "المبرمجة". إذًا، ما هي "الأرقام الحقيقية"؟ الأعداد الحقيقية هي الأعداد التي تحتوي على جزء كسري (والذي يمكن أن يكون صفرًا). يمكن أن تكون إيجابية أو سلبية. فيما يلي بعض الأمثلة: 15 56.22 0.0 1242342343445246 -232336.11 كيف يعمل العدد الحقيقي؟ بسيط للغاية: يتكون من جزء صحيح وجزء كسري وعلامة. بالنسبة للأرقام الموجبة، لا تتم الإشارة إلى العلامة بشكل صريح عادةً، ولكن يتم الإشارة إليها بالنسبة للأرقام السالبة. لقد درسنا سابقًا بالتفصيل العمليات التي يمكن إجراؤها على الأرقام في Java. وكان من بينها العديد من العمليات الرياضية القياسية - الجمع والطرح، وما إلى ذلك. وكانت هناك أيضًا بعض العمليات الجديدة بالنسبة لك: على سبيل المثال، باقي القسمة. ولكن كيف يعمل العمل مع الأرقام داخل الكمبيوتر؟ بأي شكل يتم تخزينها في الذاكرة؟

تخزين الأعداد الحقيقية في الذاكرة

أعتقد أنه لن يكون اكتشافًا بالنسبة لك أن الأرقام يمكن أن تكون كبيرة وصغيرة :) يمكن مقارنتها مع بعضها البعض. على سبيل المثال، الرقم 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. وكلمة " بت " نفسها تأتي من " الرقم الثنائي " الإنجليزي - وهو رقم ثنائي. أعتقد أنك ربما سمعت عن وجود نظام الأعداد الثنائية في الرياضيات. يمكن تمثيل أي رقم عشري نعرفه كمجموعة من الآحاد والأصفار. على سبيل المثال، الرقم 584.32 في النظام الثنائي سيبدو كما يلي: 100100100001010001111 . كل واحد وصفر في هذا الرقم هو بت منفصل. الآن يجب أن تكون أكثر وضوحًا بشأن الفرق بين أنواع البيانات. على سبيل المثال، إذا قمنا بإنشاء عدد من النوع float، فسيكون لدينا 32 بت فقط تحت تصرفنا. عند إنشاء رقم، floatهذا هو بالضبط مقدار المساحة المخصصة له في ذاكرة الكمبيوتر. إذا أردنا إنشاء الرقم 123456789.65656565656565، فسيبدو بالشكل الثنائي: 11101011011110011010001010110101000000 . وهو يتألف من 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.11111111111111 7 مرات متتالية. يجب أن تكون النتيجة 0.777777777777777. لكننا أنشأنا رقمًا float. يقتصر حجمه على 32 بت، وكما قلنا سابقًا، فهو قادر على عرض رقم يصل إلى العلامة العشرية السابعة تقريبًا. لذلك في النهاية النتيجة التي سنحصل عليها في الكونسول ستكون مختلفة عما توقعناه:

0.7777778
يبدو أن الرقم "مقطوع". أنت تعرف بالفعل كيفية تخزين البيانات في الذاكرة - في شكل بتات، لذلك لا ينبغي أن يفاجئك هذا. من الواضح سبب حدوث ذلك: النتيجة 0.7777777777777777 ببساطة لم تتناسب مع الـ 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) أو حتى أقل. ولكن ماذا لو كانت النتيجة الكاملة لبرنامجك شديد الخطورة تعتمد على هذه المقارنة؟
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