JavaRush /Blog Java /Random-VI /Thiết bị số thực

Thiết bị số thực

Xuất bản trong nhóm
Xin chào! Trong bài giảng hôm nay chúng ta sẽ nói về các con số trong Java và đặc biệt là về số thực. Thiết bị của số thực - 1Không hoảng loạn! :) Sẽ không có khó khăn toán học trong bài giảng. Chúng ta sẽ chỉ nói về số thực theo quan điểm “lập trình viên” của chúng ta. Vậy “số thực” là gì? Số thực là số có phần phân số (có thể bằng 0). Chúng có thể tích cực hoặc tiêu cực. Dưới đây là một số ví dụ: 15 56,22 0,0 1242342343445246 -232336.11 Số thực hoạt động như thế nào? Khá đơn giản: nó bao gồm một phần nguyên, một phần phân số và một dấu. Đối với các số dương, dấu thường không được biểu thị rõ ràng, nhưng đối với các số âm thì nó được biểu thị. Trước đây, chúng ta đã xem xét chi tiết những thao tác nào trên số có thể được thực hiện trong Java. Trong số đó có nhiều phép toán tiêu chuẩn - cộng, trừ, v.v. Ngoài ra còn có một số phép tính mới dành cho bạn: ví dụ: phần dư của phép chia. Nhưng chính xác thì việc xử lý các con số diễn ra như thế nào trong máy tính? Chúng được lưu trữ trong bộ nhớ dưới dạng nào?

Lưu số thực vào bộ nhớ

Tôi nghĩ sẽ không phải là một khám phá đối với bạn khi các con số có thể lớn và nhỏ :) Chúng có thể được so sánh với nhau. Ví dụ: số 100 nhỏ hơn số 423324. Điều này có ảnh hưởng đến hoạt động của máy tính và chương trình của chúng ta không? Trên thực tế là . Mỗi số được biểu thị bằng Java theo một phạm vi giá trị cụ thể :
Kiểu Kích thước bộ nhớ (bit) Phạm vi giá trị
byte 8 bit -128 đến 127
short 16bit -32768 đến 32767
char 16bit số nguyên không dấu đại diện cho ký tự UTF-16 (chữ cái và số)
int 32 bit từ -2147483648 đến 2147483647
long 64 bit từ -9223372036854775808 đến 9223372036854775807
float 32 bit từ 2 -149 đến (2-2 -23 )*2 127
double 64 bit từ 2 -1074 đến (2-2 -52 )*2 1023
Hôm nay chúng ta sẽ nói về hai loại cuối cùng - floatdouble. Cả hai đều thực hiện cùng một nhiệm vụ - biểu thị số phân số. Chúng cũng thường được gọi là “ số dấu phẩy động” . Hãy nhớ thuật ngữ này cho tương lai :) Ví dụ: số 2.3333 hoặc 134.1212121212. Khá lạ. Rốt cuộc thì hóa ra không có sự khác biệt nào giữa hai loại này, vì chúng thực hiện cùng một nhiệm vụ? Nhưng có một sự khác biệt. Hãy chú ý đến cột “kích thước trong bộ nhớ” trong bảng trên. Tất cả các số (và không chỉ các số - tất cả thông tin nói chung) đều được lưu trữ trong bộ nhớ máy tính dưới dạng bit. Bit là đơn vị thông tin nhỏ nhất. Nó khá đơn giản. Bất kỳ bit nào cũng bằng 0 hoặc 1. Và bản thân từ “ bit ” có nguồn gốc từ “ chữ số nhị phân ” trong tiếng Anh - một số nhị phân. Tôi nghĩ bạn có thể đã nghe nói về sự tồn tại của hệ thống số nhị phân trong toán học. Bất kỳ số thập phân nào chúng ta quen thuộc đều có thể được biểu diễn dưới dạng tập hợp các số 1 và số 0. Ví dụ: số 584,32 ở dạng nhị phân sẽ trông như thế này: 100100100001010001111 . Mỗi một và số 0 trong số này là một bit riêng biệt. Bây giờ bạn nên rõ ràng hơn về sự khác biệt giữa các loại dữ liệu. Ví dụ: nếu chúng ta tạo một số loại float, chúng ta chỉ có 32 bit tùy ý sử dụng. Khi tạo một số, floatđây chính xác là bao nhiêu dung lượng sẽ được phân bổ cho số đó trong bộ nhớ máy tính. Nếu chúng ta muốn tạo số 123456789.65656565656565 thì ở dạng nhị phân nó sẽ trông như thế này: 11101011011110011010001010110101000000 . Nó bao gồm 38 số 1 và 0, nghĩa là cần 38 bit để lưu trữ nó trong bộ nhớ. floatCon số này đơn giản là không “phù hợp” với loại ! Do đó, số 123456789 có thể được biểu diễn dưới dạng loại double. Có tới 64 bit được phân bổ để lưu trữ nó: điều này phù hợp với chúng tôi! Tất nhiên, phạm vi giá trị cũng sẽ phù hợp. Để thuận tiện, bạn có thể coi số là một hộp nhỏ có các ô. Nếu có đủ ô để lưu trữ từng bit thì kiểu dữ liệu được chọn chính xác :) Thiết bị của số thực - 2Tất nhiên, lượng bộ nhớ được phân bổ khác nhau cũng ảnh hưởng đến chính con số đó. Xin lưu ý rằng các loại floatdoublephạm vi giá trị khác nhau. Điều này có ý nghĩa gì trong thực tế? Một số doublecó thể thể hiện độ chính xác cao hơn một số float. Các số dấu phẩy động 32 bit (trong Java đây chính xác là loại float) có độ chính xác xấp xỉ 24 bit, tức là khoảng 7 chữ số thập phân. Và các số 64 bit (trong Java đây là loại double) có độ chính xác xấp xỉ 53 bit, tức là khoảng 16 chữ số thập phân. Đây là một ví dụ thể hiện rõ sự khác biệt này:
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);
   }
}
Kết quả là chúng ta sẽ nhận được gì ở đây? Có vẻ như mọi thứ khá đơn giản. Chúng ta có số 0,0 và chúng ta thêm 0,11111111111111111 vào số đó 7 lần liên tiếp. Kết quả phải là 0,7777777777777777. Nhưng chúng tôi đã tạo ra một con số float. Kích thước của nó được giới hạn ở 32 bit và như chúng tôi đã nói trước đó, nó có khả năng hiển thị một số lên đến khoảng chữ số thập phân thứ 7. Do đó, cuối cùng, kết quả chúng ta nhận được trong bảng điều khiển sẽ khác với những gì chúng ta mong đợi:

0.7777778
Con số dường như bị “cắt”. Bạn đã biết cách dữ liệu được lưu trữ trong bộ nhớ - dưới dạng bit, vì vậy điều này không làm bạn ngạc nhiên. Rõ ràng tại sao điều này xảy ra: kết quả 0.7777777777777777 đơn giản là không vừa với 32 bit được phân bổ cho chúng tôi, vì vậy nó đã bị cắt bớt để vừa với một biến kiểu float:) Chúng ta có thể thay đổi loại biến thành doubletrong ví dụ của mình và sau đó là kết quả cuối cùng kết quả sẽ không bị cắt ngắn:
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
Đã có 16 chữ số thập phân, kết quả “khớp” thành 64 bit. Nhân tiện, có lẽ bạn nhận thấy rằng trong cả hai trường hợp, kết quả đều không hoàn toàn chính xác? Việc tính toán được thực hiện với những lỗi nhỏ. Chúng ta sẽ nói về lý do của điều này bên dưới :) Bây giờ hãy nói vài lời về cách bạn có thể so sánh các số với nhau.

So sánh số thực

Chúng ta đã đề cập một phần đến vấn đề này trong bài giảng trước, khi nói về các phép toán so sánh. Chúng tôi sẽ không phân tích lại các hoạt động như >, <, >=. <=Thay vào đó hãy xem một ví dụ thú vị hơn:
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);
   }
}
Bạn nghĩ số nào sẽ hiển thị trên màn hình? Câu trả lời hợp lý sẽ là câu trả lời: số 1. Chúng ta bắt đầu đếm từ số 0,0 và cộng 0,1 vào số đó mười lần liên tiếp. Mọi thứ dường như đều đúng, nó phải là một. Hãy thử chạy mã này và câu trả lời sẽ khiến bạn vô cùng ngạc nhiên :)

0.9999999999999999
Nhưng tại sao lại xảy ra lỗi trong một ví dụ đơn giản như vậy? O_o Ở đây ngay cả một học sinh lớp năm cũng có thể dễ dàng trả lời đúng, nhưng chương trình Java lại đưa ra kết quả không chính xác. “Không chính xác” ở đây hay hơn từ “không chính xác”. Chúng tôi vẫn nhận được một số rất gần với một chứ không chỉ là một số giá trị ngẫu nhiên :) Nó khác với số chính xác theo đúng nghĩa đen từng milimet. Nhưng tại sao? Có lẽ đây chỉ là một sai lầm một lần. Có lẽ máy tính bị hỏng? Hãy thử viết một ví dụ khác.
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!");
   }
}
Đầu ra của bảng điều khiển:

f1 = 1.0999999999999999
f2 = 1.1
f1 и f2 не равны!
Vì vậy, đây rõ ràng không phải là vấn đề trục trặc máy tính :) Chuyện gì đang xảy ra vậy? Những loại lỗi này liên quan đến cách biểu diễn các số ở dạng nhị phân trong bộ nhớ máy tính. Thực tế là trong hệ nhị phân không thể biểu diễn chính xác số 0,1 . Nhân tiện, hệ thập phân cũng có một vấn đề tương tự: không thể biểu diễn phân số một cách chính xác (và thay vì ⅓ chúng ta nhận được 0,33333333333333..., đây cũng không phải là kết quả hoàn toàn chính xác). Nó có vẻ như là một chuyện vặt: với những tính toán như vậy, sự khác biệt có thể là một phần trăm nghìn (0,00001) hoặc thậm chí ít hơn. Nhưng điều gì sẽ xảy ra nếu toàn bộ kết quả của Chương trình Rất Nghiêm túc của bạn phụ thuộc vào sự so sánh này?
if (f1 == f2)
   System.out.println("Rocket flies into space");
else
   System.out.println("The launch is canceled, everyone goes home");
Rõ ràng chúng tôi đã mong đợi hai con số sẽ bằng nhau, nhưng do thiết kế bộ nhớ trong nên chúng tôi đã hủy vụ phóng tên lửa. Thiết bị của số thực - 3Nếu vậy, chúng ta cần quyết định cách so sánh hai số có dấu phẩy động để kết quả so sánh chính xác hơn... ừm... có thể dự đoán được. Như vậy, chúng ta đã học được quy tắc số 1 khi so sánh số thực: không bao giờ sử dụng ==số có dấu phẩy động khi so sánh số thực. Ok, tôi nghĩ thế là đủ ví dụ tồi rồi :) Hãy xem một ví dụ điển hình!
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");
   }
}
Ở đây về cơ bản chúng ta đang làm điều tương tự nhưng thay đổi cách chúng ta so sánh các con số. Chúng tôi có một số “ngưỡng” đặc biệt - 0,0001, một phần mười nghìn. Nó có thể khác. Nó phụ thuộc vào mức độ so sánh chính xác mà bạn cần trong một trường hợp cụ thể. Bạn có thể làm cho nó lớn hơn hoặc nhỏ hơn. Sử dụng phương pháp này, Math.abs()chúng ta thu được mô đun của một số. Mô đun là giá trị của một số bất kể dấu. Ví dụ: các số -5 và 5 sẽ có cùng mô đun và bằng 5. Chúng ta trừ số thứ hai với số thứ nhất và nếu kết quả thu được, bất kể dấu nào, nhỏ hơn ngưỡng mà chúng ta đặt, thì số lượng của chúng tôi bằng nhau. Trong mọi trường hợp, chúng bằng với mức độ chính xác mà chúng tôi đã thiết lập bằng cách sử dụng “số ngưỡng” của chúng tôi , nghĩa là, chúng tối thiểu bằng một phần mười nghìn. Phương pháp so sánh này sẽ giúp bạn tránh khỏi hành vi không mong muốn mà chúng tôi đã thấy trong trường hợp ==. Một cách hay khác để so sánh số thực là sử dụng một lớp đặc biệt BigDecimal. Lớp này được tạo đặc biệt để lưu trữ số lượng rất lớn với phần phân số. Không giống như doublefloat, khi sử dụng BigDecimalphép cộng, phép trừ và các phép toán khác được thực hiện không sử dụng toán tử ( +-, v.v.) mà sử dụng các phương thức. Đây là những gì nó sẽ trông giống như trong trường hợp của chúng tôi:
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");
   }
}
Chúng ta sẽ nhận được loại đầu ra giao diện điều khiển nào?

f1 = 1.1000000000000000610622663543836097232997417449951171875
f2 = 1.1000000000000000610622663543836097232997417449951171875
f1 и f2 равны
Chúng tôi đã nhận được chính xác kết quả mà chúng tôi mong đợi. Và hãy chú ý đến độ chính xác của các con số của chúng tôi và có bao nhiêu chữ số thập phân phù hợp với chúng! Nhiều hơn cả trong floatvà thậm chí trong double! Hãy nhớ lớp học BigDecimalcho tương lai nhé, chắc chắn bạn sẽ cần nó :) Phù! Bài giảng khá dài nhưng bạn đã làm được: làm tốt lắm! :) Hẹn gặp lại bạn ở bài học tiếp theo, lập trình viên tương lai!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION