Giữ lại độ chính xác với gấp đôi trong Java


Câu trả lời:


151

Như những người khác đã đề cập, có lẽ bạn sẽ muốn sử dụng BigDecimallớp, nếu bạn muốn có một đại diện chính xác là 11.4.

Bây giờ, một lời giải thích nhỏ về lý do tại sao điều này đang xảy ra:

Các kiểu nguyên thủy floatdoublenguyên thủy trong Java là các số dấu phẩy động , trong đó số được lưu trữ dưới dạng biểu diễn nhị phân của một phân số và số mũ.

Cụ thể hơn, giá trị dấu phẩy động có độ chính xác kép, chẳng hạn như doubleloại là giá trị 64 bit, trong đó:

  • 1 bit biểu thị dấu (dương hoặc âm).
  • 11 bit cho số mũ.
  • 52 bit cho các chữ số có nghĩa (phần phân số dưới dạng nhị phân).

Những phần này được kết hợp để tạo ra một doubleđại diện của một giá trị.

(Nguồn: Wikipedia: Độ chính xác kép )

Để biết mô tả chi tiết về cách xử lý các giá trị dấu phẩy động trong Java, hãy xem Phần 4.2.3: Các loại dấu phẩy động , Định dạng và Giá trị của Đặc tả ngôn ngữ Java.

Các byte, char, int, longloại được cố định điểm số, đó là representions chính xác của con số. Không giống như các số điểm cố định, một số số dấu phẩy động sẽ đôi khi (an toàn khi giả sử "hầu hết thời gian") không thể trả về một đại diện chính xác của một số. Đây là lý do tại sao bạn kết thúc với 11.399999999999kết quả là 5.6 + 5.8.

Khi yêu cầu một giá trị chính xác, chẳng hạn như 1.5 hoặc 150.1005, bạn sẽ muốn sử dụng một trong các loại điểm cố định, có thể biểu thị chính xác số.

Như đã đề cập nhiều lần rồi, Java có một BigDecimallớp sẽ xử lý số lượng rất lớn và số lượng rất nhỏ.

Từ tham chiếu API Java cho BigDecimallớp:

Số thập phân bất biến, chính xác tùy ý. Một BigDecimal bao gồm một giá trị nguyên không chính xác tùy ý và tỷ lệ số nguyên 32 bit. Nếu bằng 0 hoặc dương, thang đo là số chữ số ở bên phải dấu thập phân. Nếu âm, giá trị không được tính của số được nhân với mười với sức mạnh của phủ định của thang đo. Do đó, giá trị của số được đại diện bởi BigDecimal là (unscaledValue × 10 ^ -scale).

Đã có nhiều câu hỏi về Stack Overflow liên quan đến vấn đề số dấu phẩy động và độ chính xác của nó. Dưới đây là danh sách các câu hỏi liên quan có thể được quan tâm:

Nếu bạn thực sự muốn tìm hiểu chi tiết về các số dấu phẩy động, hãy xem những gì mọi nhà khoa học máy tính nên biết về số học dấu phẩy động .


3
Trong thực tế, thường có 53 bit đáng kể bởi vì 1 trước điểm "thập phân" được ngụ ý cho tất cả trừ các giá trị không chuẩn hóa, cho thêm một chút độ chính xác. ví dụ 3 được lưu dưới dạng (1.) 1000 ... x 2 ^ 1 trong khi 0,5 được lưu dưới dạng (1.) 0000 ... x 2 ^ -1 Khi giá trị không được chuẩn hóa (tất cả các bit số mũ đều bằng 0) có thể, và thông thường, sẽ có ít chữ số có nghĩa hơn, ví dụ 1 x 2 ^ -1030 được lưu trữ dưới dạng (0.) 00000001 x 2 ^ -1022, vì vậy bảy chữ số có nghĩa đã được hy sinh theo tỷ lệ.
Sarah Phillips

1
Cần lưu ý rằng trong khi BigDecimalchậm hơn nhiều so với doubletrong trường hợp này thì không cần thiết vì double có 15 vị trí thập phân chính xác, bạn chỉ cần làm tròn.
Peter Lawrey

2
@PeterLawrey Nó có 15 chữ số thập phân chính xác, nếu tất cả chúng đều nằm trước dấu thập phân. Bất cứ điều gì cũng có thể xảy ra sau dấu thập phân, vì tính không tương thích của phân số thập phân và nhị phân.
Hầu tước Lorne

@EJP Bạn nói đúng, nó có khoảng 15 chữ số chính xác. Nó có thể là 16 nhưng sẽ an toàn hơn khi cho rằng nó là 15 hoặc có lẽ 14.
Peter Lawrey

@PeterLawrey Sự điều chỉnh của EJP là do câu hỏi của tôi: stackoverflow.com/questions/36344758/. Bạn có thể vui lòng mở rộng về lý do tại sao nó không chính xác là 15 và tình huống có thể là 16 hoặc 14 không?
Shivam Sinha

103

Ví dụ, khi bạn nhập một số kép, 33.33333333333333giá trị bạn nhận được thực sự là giá trị chính xác kép có thể biểu thị gần nhất, chính xác là:

33.3333333333333285963817615993320941925048828125

Chia cho 100 cho:

0.333333333333333285963817615993320941925048828125

số này cũng không thể biểu diễn dưới dạng số có độ chính xác kép, do đó, một lần nữa, nó được làm tròn đến giá trị đại diện gần nhất, chính xác là:

0.3333333333333332593184650249895639717578887939453125

Khi bạn in giá trị này ra, nó sẽ được làm tròn một lần nữa thành 17 chữ số thập phân, đưa ra:

0.33333333333333326

114
Đối với bất kỳ ai đang đọc điều này trong tương lai và bối rối về lý do tại sao câu trả lời không liên quan gì đến câu hỏi: một số người điều hành đã quyết định hợp nhất câu hỏi mà tôi (và những người khác) đã trả lời với câu hỏi này, thay vì khác.
Stephen Canon

Làm thế nào để bạn biết giá trị gấp đôi chính xác?
Michael Yaworski

@mikeyaworski en.wikipedia.org/wiki/Double-precision_floating-point_format Xem các ví dụ chính xác kép
Jaydee

23

Nếu bạn chỉ muốn xử lý các giá trị dưới dạng phân số, bạn có thể tạo một lớp Phân số chứa trường tử số và mẫu số.

Viết các phương thức để cộng, trừ, nhân và chia cũng như phương thức toDouble. Bằng cách này bạn có thể tránh nổi trong quá trình tính toán.

EDIT: Thực hiện nhanh chóng,

public class Fraction {

private int numerator;
private int denominator;

public Fraction(int n, int d){
    numerator = n;
    denominator = d;
}

public double toDouble(){
    return ((double)numerator)/((double)denominator);
}


public static Fraction add(Fraction a, Fraction b){
    if(a.denominator != b.denominator){
        double aTop = b.denominator * a.numerator;
        double bTop = a.denominator * b.numerator;
        return new Fraction(aTop + bTop, a.denominator * b.denominator);
    }
    else{
        return new Fraction(a.numerator + b.numerator, a.denominator);
    }
}

public static Fraction divide(Fraction a, Fraction b){
    return new Fraction(a.numerator * b.denominator, a.denominator * b.numerator);
}

public static Fraction multiply(Fraction a, Fraction b){
    return new Fraction(a.numerator * b.numerator, a.denominator * b.denominator);
}

public static Fraction subtract(Fraction a, Fraction b){
    if(a.denominator != b.denominator){
        double aTop = b.denominator * a.numerator;
        double bTop = a.denominator * b.numerator;
        return new Fraction(aTop-bTop, a.denominator*b.denominator);
    }
    else{
        return new Fraction(a.numerator - b.numerator, a.denominator);
    }
}

}

1
Chắc chắn numeratordenominatornên được ints? Tại sao bạn muốn độ chính xác dấu phẩy động?
Samir Talwar

Đoán nó không thực sự cần thiết nhưng nó tránh truyền trong hàm toDouble để mã đọc tốt hơn.
Viral Shah

5
ViralShah: Nó cũng có khả năng đưa ra lỗi dấu phẩy động khi xử lý các phép toán. Cho rằng quan điểm của bài tập này là để tránh chính xác điều đó, có vẻ thận trọng để thay đổi nó.
Samir Talwar

Chỉnh sửa để sử dụng ints thay vì nhân đôi, vì những lý do được đề cập bởi Samir Talwar ở trên.
Viral Shah

3
Việc thực hiện phân số này có vấn đề vì nó không giảm chúng thành một hình thức đơn giản nhất. 2/3 * 1/2 cho 2/6 nơi bạn thực sự muốn câu trả lời là 1/3. Lý tưởng nhất trong hàm tạo bạn muốn tìm gcd của tử số và ước số và chia cả hai cho đó.
Salix alba

15

Quan sát rằng bạn có cùng một vấn đề nếu bạn đã sử dụng số học thập phân có độ chính xác giới hạn và muốn giải quyết với 1/3: 0.333333333 * 3 là 0.999999999, không phải 1.00000000.

Thật không may, 5,6, 5,8 và 11,4 chỉ không làm tròn số nhị phân, vì chúng liên quan đến số năm. Vì vậy, đại diện float của chúng không chính xác, cũng như 0.3333 không chính xác là 1/3.

Nếu tất cả các số bạn sử dụng là số thập phân không định kỳ và bạn muốn có kết quả chính xác, hãy sử dụng BigDecimal. Hoặc như những người khác đã nói, nếu các giá trị của bạn giống như tiền theo nghĩa là tất cả chúng là bội số của 0,01 hoặc 0,001 hoặc một cái gì đó, thì nhân mọi thứ với một công suất cố định là 10 và sử dụng int hoặc long (cộng và trừ tầm thường: coi chừng nhân).

Tuy nhiên, nếu bạn hài lòng với tính toán nhị phân, nhưng bạn chỉ muốn in mọi thứ ra ở định dạng hơi thân thiện hơn, hãy thử java.util.Formatterhoặc String.format. Trong chuỗi định dạng xác định độ chính xác nhỏ hơn độ chính xác đầy đủ của một đôi. Theo 10 con số quan trọng, giả sử, 11.399999999999 là 11.4, do đó, kết quả sẽ gần như chính xác và dễ đọc hơn trong trường hợp kết quả nhị phân rất gần với giá trị chỉ cần một vài số thập phân.

Độ chính xác để xác định phụ thuộc một chút vào số lượng toán bạn đã thực hiện với các số của bạn - nói chung bạn càng làm nhiều, sẽ càng có nhiều lỗi, nhưng một số thuật toán tích lũy nó nhanh hơn nhiều so với các số khác (chúng được gọi là "không ổn định" như trái ngược với "ổn định" đối với các lỗi làm tròn). Nếu tất cả những gì bạn đang làm là thêm một vài giá trị, thì tôi đoán rằng chỉ cần bỏ một vị trí chính xác thập phân sẽ sắp xếp mọi thứ. Thí nghiệm.


3
Không, không sử dụng gấp đôi với các giá trị tiền tệ! Bạn cần sự chính xác với tiền, thay vào đó hãy sử dụng BigDecimal. Nếu không, câu trả lời của bạn là tốt. Bất cứ điều gì bạn cần chính xác, hãy sử dụng BigDecimal, nếu độ chính xác không quan trọng, bạn có thể sử dụng float hoặc double.
MetroidFan2002

1
Câu hỏi không còn nêu hoặc ngụ ý rằng tiền có liên quan. Tôi đặc biệt nói rằng sử dụng BigDecimal hoặc số nguyên để kiếm tiền. Có vấn đề gì vậy?
Steve Jessop

1
Và tương đương với "không sử dụng gấp đôi tiền" là "không sử dụng BigDecimal hoặc gấp đôi cho phần ba". Nhưng đôi khi một vấn đề liên quan đến sự phân chia, trong trường hợp tất cả các cơ sở không chia hết cho tất cả các thừa số nguyên tố của tất cả các mẫu số đều xấu như nhau.
Steve Jessop

1
0,9999 = 1 nếu độ chính xác của bạn nhỏ hơn 4 chữ số có nghĩa
Brian Leahy

9

Bạn có thể muốn xem xét việc sử dụng lớp java.math.BigDecimal của java nếu bạn thực sự cần toán chính xác. Đây là một bài viết hay từ Oracle / Sun về trường hợp của BigDecimal . Mặc dù bạn không bao giờ có thể đại diện cho 1/3 như ai đó đã đề cập, bạn có thể có quyền quyết định chính xác mức độ chính xác mà bạn muốn kết quả đạt được. setScale () là bạn của bạn .. :)

Ok, bởi vì tôi có quá nhiều thời gian trên tay vào lúc này đây là một ví dụ mã liên quan đến câu hỏi của bạn:

import java.math.BigDecimal;
/**
 * Created by a wonderful programmer known as:
 * Vincent Stoessel
 * xaymaca@gmail.com
 * on Mar 17, 2010 at  11:05:16 PM
 */
public class BigUp {

    public static void main(String[] args) {
        BigDecimal first, second, result ;
        first = new BigDecimal("33.33333333333333")  ;
        second = new BigDecimal("100") ;
        result = first.divide(second);
        System.out.println("result is " + result);
       //will print : result is 0.3333333333333333


    }
}

và để kết nối ngôn ngữ yêu thích mới của tôi, Groovy, đây là một ví dụ gọn gàng hơn về cùng một điều:

import java.math.BigDecimal

def  first =   new BigDecimal("33.33333333333333")
def second = new BigDecimal("100")


println "result is " + first/second   // will print: result is 0.33333333333333

5

Khá chắc chắn rằng bạn có thể biến nó thành một ví dụ ba dòng. :)

Nếu bạn muốn độ chính xác chính xác, hãy sử dụng BigDecimal. Nếu không, bạn có thể sử dụng số nguyên nhân nhân với 10 ^ bất cứ độ chính xác nào bạn muốn.


5

Như những người khác đã lưu ý, không phải tất cả các giá trị thập phân có thể được biểu diễn dưới dạng nhị phân vì số thập phân dựa trên quyền hạn 10 và nhị phân dựa trên quyền hạn của hai.

Nếu độ chính xác quan trọng, hãy sử dụng BigDecimal, nhưng nếu bạn chỉ muốn đầu ra thân thiện:

System.out.printf("%.2f\n", total);

Sẽ cung cấp cho bạn:

11.40


5

Bạn không thể, vì 7.3 không có biểu diễn hữu hạn ở dạng nhị phân. Gần nhất bạn có thể nhận được là 2054767329987789/2 ** 48 = 7.3 + 1/737374883553280.

Hãy xem http://docs.python.org/tutorial/floatingpoint.html để được giải thích thêm. (Đó là trên trang web Python, nhưng Java và C ++ có cùng "vấn đề".)

Giải pháp phụ thuộc vào chính xác vấn đề của bạn là gì:

  • Nếu bạn không muốn nhìn thấy tất cả các chữ số nhiễu đó, thì hãy sửa định dạng chuỗi của bạn. Không hiển thị hơn 15 chữ số có nghĩa (hoặc 7 cho dấu phẩy).
  • Nếu đó là sự không chính xác của các số của bạn đang phá vỡ những thứ như câu lệnh "if", thì bạn nên viết if (abs (x - 7.3) <TOLERANCE) thay vì if (x == 7.3).
  • Nếu bạn đang làm việc với tiền, thì điều bạn có thể thực sự muốn là điểm cố định thập phân. Lưu trữ một số nguyên xu hoặc bất kể đơn vị tiền tệ nhỏ nhất của bạn là gì.
  • (RẤT KHÔNG GIỚI HẠN) Nếu bạn cần nhiều hơn 53 bit có ý nghĩa (15-16 chữ số có nghĩa), thì hãy sử dụng loại dấu phẩy động có độ chính xác cao, như BigDecimal.

7.3 có thể không có biểu diễn hữu hạn ở dạng nhị phân, nhưng tôi chắc chắn nhận được -7.3 khi tôi thử điều tương tự trong C ++
tên người dùng sai vào

2
Tên người dùng sai: Không, bạn không. Nó chỉ hiển thị theo cách đó. Sử dụng định dạng "% .17g" (hoặc tốt hơn là "% .51g") để xem câu trả lời thực sự.
dan04

4
private void getRound() {
    // this is very simple and interesting 
    double a = 5, b = 3, c;
    c = a / b;
    System.out.println(" round  val is " + c);

    //  round  val is  :  1.6666666666666667
    // if you want to only two precision point with double we 
            //  can use formate option in String 
           // which takes 2 parameters one is formte specifier which 
           // shows dicimal places another double value 
    String s = String.format("%.2f", c);
    double val = Double.parseDouble(s);
    System.out.println(" val is :" + val);
    // now out put will be : val is :1.67
}

3

Sử dụng java.math.BigDecimal

Nhân đôi là phân số nhị phân trong nội bộ, vì vậy đôi khi chúng không thể biểu thị phân số thập phân cho số thập phân chính xác.


1
-1 cho mù quáng giới thiệu BigDecimal. Nếu bạn không thực sự cần số học thập phân (nghĩa là, nếu bạn đang tính toán bằng tiền), thì BigDecimal không giúp bạn. Nó không giải quyết được tất cả các lỗi dấu phẩy động của bạn: Bạn vẫn phải xử lý 1/3 * 3 = 0.9999999999999999999999999999 và sqrt (2) ** 2 = 1.999999999999999999999999999. Hơn nữa, BigDecimal mang một hình phạt tốc độ rất lớn. Tồi tệ hơn, vì Java quá tải toán tử, bạn phải viết lại tất cả mã của mình.
dan04

2
@ dan04 - Nếu bạn tính toán bằng tiền tại sao lại sử dụng biểu diễn nổi biết lỗi cố hữu trong đó .... Vì không có phần xu nào bạn có thể sử dụng số thập phân và tính xu thay vì sử dụng số tiền gần đúng mà bạn có số tiền chính xác. Nếu bạn thực sự muốn phần trăm sử dụng aa dài và tính toán hàng ngàn xu. Hơn nữa OP không đề cập đến các số vô tỷ, tất cả những gì anh ta quan tâm là bổ sung. Hãy đọc bài viết một cách cẩn thận và hiểu vấn đề trước khi bạn trả lời, có thể giúp bạn bớt bối rối.
Newtopian

3
@Newtopian: Tôi không có gì phải xấu hổ. OP không đề cập đến tiền, cũng không có dấu hiệu nào cho thấy vấn đề của anh ta có bất kỳ số thập phân vốn có nào.
dan04

@ dan04 - Không OP không ... BẠN đã làm và mù quáng đưa ra ý kiến ​​bối cảnh cho những gì rất có thể là một câu trả lời hoàn toàn chấp nhận được với số lượng chi tiết kém được cung cấp
Newtopian

2

Nhân mọi thứ với 100 và lưu trữ trong một khoảng thời gian dài bằng xu.


2
@Draemon - nhìn vào bài đăng trước lần chỉnh sửa cuối cùng - tất cả những thứ "shoppingTotal" và "calcGST" và "calcPST" trông giống như tiền đối với tôi.
Paul Tomblin

2

Máy tính lưu trữ số nhị phân và thực sự không thể đại diện cho các số như 33.333333333 hoặc 100.0 chính xác. Đây là một trong những điều khó khăn khi sử dụng đồ đôi. Bạn sẽ phải làm tròn câu trả lời trước khi hiển thị cho người dùng. May mắn thay trong hầu hết các ứng dụng, dù sao bạn cũng không cần nhiều vị trí thập phân.


Tôi đang thực hiện một số tính toán tỷ lệ cược tôi muốn có độ chính xác cao nhất có thể. Nhưng tôi hiểu rằng có những hạn chế
Aly

2

Số dấu phẩy động khác với số thực ở chỗ đối với bất kỳ số dấu phẩy động cụ thể nào, sẽ có số dấu phẩy động cao hơn tiếp theo. Tương tự như số nguyên. Không có số nguyên giữa 1 và 2.

Không có cách nào để đại diện cho 1/3 như một cái phao. Có một cái phao bên dưới nó và có một cái phao bên trên nó, và có một khoảng cách nhất định giữa chúng. Và 1/3 là trong không gian đó.

Apfloat cho Java tuyên bố hoạt động với các số dấu phẩy động chính xác tùy ý, nhưng tôi chưa bao giờ sử dụng nó. Có lẽ đáng xem. http://www.apfloat.org/apfloat_java/

Một câu hỏi tương tự đã được hỏi ở đây trước khi thư viện độ chính xác cao của dấu phẩy động Java


1

Nhân đôi là xấp xỉ các số thập phân trong nguồn Java của bạn. Bạn đang thấy hậu quả của sự không khớp giữa giá trị kép (là giá trị được mã hóa nhị phân) và nguồn của bạn (được mã hóa thập phân).

Java đang tạo ra xấp xỉ nhị phân gần nhất. Bạn có thể sử dụng java.text.DecimalFormat để hiển thị giá trị thập phân trông đẹp hơn.


1

Sử dụng một BigDecimal. Nó thậm chí còn cho phép bạn chỉ định quy tắc làm tròn (như ROUND_HALF_EVEN, sẽ giảm thiểu lỗi thống kê bằng cách làm tròn đến hàng xóm chẵn nếu cả hai có cùng khoảng cách, tức là cả 1,5 và 2,5 đến 2).


1

Câu trả lời ngắn: Luôn sử dụng BigDecimal và đảm bảo rằng bạn đang sử dụng hàm tạo với đối số String , không phải đối số kép.

Quay lại ví dụ của bạn, đoạn mã sau sẽ in 11.4, như bạn muốn.

public class doublePrecision {
    public static void main(String[] args) {
      BigDecimal total = new BigDecimal("0");
      total = total.add(new BigDecimal("5.6"));
      total = total.add(new BigDecimal("5.8"));
      System.out.println(total);
    }
}

0

Hãy xem BigDecimal, nó xử lý các vấn đề liên quan đến số học dấu phẩy động như thế.

Cuộc gọi mới sẽ như thế này:

term[number].coefficient.add(co);

Sử dụng setScale () để đặt số chính xác của vị trí thập phân sẽ được sử dụng.


0

Tại sao không sử dụng phương thức round () từ lớp Math?

// The number of 0s determines how many digits you want after the floating point
// (here one digit)
total = (double)Math.round(total * 10) / 10;
System.out.println(total); // prints 11.4

0

Nếu bạn không có lựa chọn nào khác ngoài việc sử dụng các giá trị kép, có thể sử dụng mã dưới đây.

public static double sumDouble(double value1, double value2) {
    double sum = 0.0;
    String value1Str = Double.toString(value1);
    int decimalIndex = value1Str.indexOf(".");
    int value1Precision = 0;
    if (decimalIndex != -1) {
        value1Precision = (value1Str.length() - 1) - decimalIndex;
    }

    String value2Str = Double.toString(value2);
    decimalIndex = value2Str.indexOf(".");
    int value2Precision = 0;
    if (decimalIndex != -1) {
        value2Precision = (value2Str.length() - 1) - decimalIndex;
    }

    int maxPrecision = value1Precision > value2Precision ? value1Precision : value2Precision;
    sum = value1 + value2;
    String s = String.format("%." + maxPrecision + "f", sum);
    sum = Double.parseDouble(s);
    return sum;
}

-1

Đừng lãng phí hiệu ứng của bạn bằng BigDecimal. Trong 99.99999% trường hợp bạn không cần nó. loại kép java là nguồn gần đúng nhưng trong hầu hết các trường hợp, nó đủ chính xác. Lưu ý rằng bạn có một lỗi ở chữ số thứ 14. Điều này thực sự không đáng kể!

Để có đầu ra đẹp, hãy sử dụng:

System.out.printf("%.2f\n", total);

2
Tôi nghĩ rằng anh ấy lo lắng bởi đầu ra, không phải là độ chính xác số. và BigDecimal sẽ không giúp được gì nếu bạn ví dụ. chia cho ba. Nó thậm chí có thể làm mọi thứ tồi tệ hơn ...
Maciek D.

Bạn không bao giờ nên không bao giờ sử dụng dấu phẩy động để kiếm tiền. Tôi đã thấy việc làm lại lớn được thi hành trên một nhà thầu đã phá vỡ quy tắc này mặc dù được hướng dẫn như vậy.
Hầu tước Lorne
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.