JavaRush /จาวาบล็อก /Random-TH /อุปกรณ์ของจำนวนจริง

อุปกรณ์ของจำนวนจริง

เผยแพร่ในกลุ่ม
สวัสดี! ในการบรรยายวันนี้ เราจะพูดถึงตัวเลขในภาษา Java โดยเฉพาะเกี่ยวกับจำนวนจริง อุปกรณ์ของจำนวนจริง - 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 และคำว่า " bit " นั้นมาจากภาษาอังกฤษ " binary digit " ซึ่งเป็นเลขฐานสอง ฉันคิดว่าคุณคงเคยได้ยินเกี่ยวกับการมีอยู่ของระบบเลขฐานสองในวิชาคณิตศาสตร์ เลขทศนิยมใดๆ ที่เราคุ้นเคยสามารถแสดงเป็นชุดของหนึ่งและศูนย์ได้ ตัวอย่างเช่น หมายเลข 584.32 ในไบนารี่จะมีลักษณะดังนี้: 100100100001010001111 แต่ละตัวและศูนย์ในจำนวนนี้เป็นบิตที่แยกจากกัน ตอนนี้คุณควรมีความชัดเจนมากขึ้นเกี่ยวกับความแตกต่างระหว่างประเภทข้อมูล ตัวอย่างเช่น ถ้าเราสร้าง type ขึ้นมาจำนวนหนึ่ง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.1111111111111111 เข้าไป 7 ครั้งติดต่อกัน ผลลัพธ์ควรเป็น 0.7777777777777777 แต่เราสร้างตัวเลขขึ้นfloatมา ขนาดของมันจำกัดอยู่ที่ 32 บิต และดังที่เราได้กล่าวไว้ก่อนหน้านี้ มันสามารถแสดงตัวเลขได้มากถึงประมาณทศนิยมตำแหน่งที่ 7 ดังนั้นท้ายที่สุดแล้ว ผลลัพธ์ที่เราได้รับในคอนโซลจะแตกต่างจากที่เราคาดไว้:

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 แม้แต่เด็กป.5 ก็สามารถตอบถูกได้อย่างง่ายดาย แต่โปรแกรม Java กลับให้ผลลัพธ์ที่ไม่ถูกต้อง “ไม่ถูกต้อง” เป็นคำที่ดีกว่าที่นี่มากกว่า “ไม่ถูกต้อง” เรายังมีตัวเลขที่ใกล้เคียงกับ 1 มาก และไม่ใช่แค่ค่าสุ่ม :) มันแตกต่างจากค่าที่ถูกต้องในหน่วยมิลลิเมตร แต่ทำไม? บางทีนี่อาจเป็นเพียงความผิดพลาดเพียงครั้งเดียว บางทีคอมพิวเตอร์อาจพัง? ลองเขียนอีกตัวอย่างหนึ่ง
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.33333333333333... ซึ่งยังไม่ใช่ผลลัพธ์ที่ถูกต้องนัก) ดูเหมือนจะเป็นเรื่องเล็ก: ด้วยการคำนวณเช่นนี้ความแตกต่างอาจเป็นหนึ่งแสนส่วน (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