แปลง float เป็นสองเท่าโดยไม่สูญเสียความแม่นยำ


97

ฉันมีลูกลอยดั้งเดิมและฉันต้องการเป็นคู่ดั้งเดิม เพียงแค่การลอยตัวเป็นสองเท่าทำให้ฉันมีความแม่นยำเป็นพิเศษ ตัวอย่างเช่น:

float temp = 14009.35F;
System.out.println(Float.toString(temp)); // Prints 14009.35
System.out.println(Double.toString((double)temp)); // Prints 14009.349609375

อย่างไรก็ตามหากแทนที่จะทำการแคสต์ฉันจะส่งออก float เป็นสตริงและแยกวิเคราะห์สตริงเป็นคู่ฉันจะได้สิ่งที่ต้องการ

System.out.println(Double.toString(Double.parseDouble(Float.toString(temp))));
// Prints 14009.35

มีวิธีที่ดีกว่าการไปที่ String และย้อนกลับหรือไม่?

คำตอบ:


125

มันไม่ใช่ว่าคุณจริงได้รับความแม่นยำเป็นพิเศษ - มันเป็นเรื่องที่ลอยไม่ต้องเป็นตัวแทนของหมายเลขที่คุณกำลังเล็งสำหรับเดิม คู่เป็นตัวแทนของการลอยตัวเดิมอย่างถูกต้อง toStringกำลังแสดงข้อมูล "พิเศษ" ซึ่งมีอยู่แล้ว

ตัวอย่างเช่น (และตัวเลขเหล่านี้ไม่ถูกต้องฉันแค่สร้างขึ้น) สมมติว่าคุณมี:

float f = 0.1F;
double d = f;

จากนั้นค่าfอาจเท่ากับ 0.100000234523 dจะมีค่าเท่ากันทุกประการ แต่เมื่อคุณแปลงเป็นสตริงมันจะ "เชื่อ" ว่ามันถูกต้องเป็นความแม่นยำที่สูงขึ้นดังนั้นจะไม่ปัดเศษทิ้งตั้งแต่เนิ่นๆและคุณจะเห็น "หลักพิเศษ" ซึ่งมีอยู่แล้ว ที่นั่น แต่ซ่อนตัวจากคุณ

เมื่อคุณแปลงเป็นสตริงและย้อนกลับคุณจะได้ค่าสองเท่าซึ่งใกล้เคียงกับค่าสตริงมากกว่าค่าโฟลตเดิม แต่จะดีก็ต่อเมื่อคุณเชื่อจริงๆว่าค่าสตริงเป็นสิ่งที่คุณต้องการจริงๆ

แน่ใจหรือว่า float / double เป็นประเภทที่เหมาะสมที่จะใช้แทนBigDecimal? หากคุณกำลังพยายามใช้ตัวเลขที่มีค่าทศนิยมที่แม่นยำ (เช่นเงิน) BigDecimalIMO เป็นประเภทที่เหมาะสมกว่า


40

ฉันพบว่าการแปลงเป็นการแสดงไบนารีนั้นง่ายกว่าที่จะเข้าใจปัญหานี้

float f = 0.27f;
double d2 = (double) f;
double d3 = 0.27d;

System.out.println(Integer.toBinaryString(Float.floatToRawIntBits(f)));
System.out.println(Long.toBinaryString(Double.doubleToRawLongBits(d2)));
System.out.println(Long.toBinaryString(Double.doubleToRawLongBits(d3)));

คุณสามารถเห็นการลอยตัวถูกขยายเป็นสองเท่าโดยการเพิ่ม 0s ต่อท้าย แต่การแทนค่าสองเท่าของ 0.27 นั้น 'ถูกต้องกว่า' ดังนั้นปัญหา

   111110100010100011110101110001
11111111010001010001111010111000100000000000000000000000000000
11111111010001010001111010111000010100011110101110000101001000

25

เพราะนี่คือสัญญาของFloat.toString(float),ที่กล่าวว่าในส่วน:

ต้องพิมพ์กี่หลักสำหรับส่วนที่เป็นเศษส่วน […]? ต้องมีอย่างน้อยหนึ่งหลักในการแสดงส่วนที่เป็นเศษส่วนและนอกเหนือจากนั้นให้มากแต่ต้องมีจำนวน หลักมากขึ้นเท่านั้นเพื่อแยกความแตกต่างของค่าอาร์กิวเมนต์จากค่าที่อยู่ติดกันของประเภท float นั่นคือสมมติว่า x เป็นค่าทางคณิตศาสตร์ที่แน่นอนที่แสดงโดยการแทนค่าทศนิยมที่สร้างโดยวิธีนี้สำหรับอาร์กิวเมนต์ที่ไม่ใช่ศูนย์ จำกัด f จากนั้น f ต้องเป็นค่าลอยที่ใกล้ x ที่สุด หรือถ้าค่า float สองค่าใกล้เคียงกับ x เท่า ๆ กันดังนั้น f จะต้องเป็นค่าใดค่าหนึ่งและค่านัยสำคัญน้อยที่สุดของ f ต้องเป็น 0


13

ฉันพบปัญหานี้ในวันนี้และไม่สามารถใช้ refactor กับ BigDecimal ได้เนื่องจากโครงการมีขนาดใหญ่มาก อย่างไรก็ตามฉันพบวิธีแก้ปัญหาโดยใช้ไฟล์

Float result = new Float(5623.23)
Double doubleResult = new FloatingDecimal(result.floatValue()).doubleValue()

และใช้งานได้

โปรดสังเกตว่าการเรียก result.doubleValue () ส่งกลับ 5623.22998046875

แต่การเรียก doubleResult.doubleValue () กลับ 5623.23 อย่างถูกต้อง

แต่ฉันไม่แน่ใจว่ามันเป็นวิธีที่ถูกต้องหรือไม่


1
ทำงานให้ฉัน ยังเป็นไปอย่างรวดเร็ว ไม่แน่ใจว่าเหตุใดจึงไม่ถูกทำเครื่องหมายว่าเป็นคำตอบ
Sameer

นี่เป็นวิธีที่ฉันใช้และคุณไม่จำเป็นต้องใช้ส่วน Float ใหม่ ... เพียงแค่สร้าง FloatingDecimal ด้วยแบบดั้งเดิม มันจะถูกบรรจุกล่องโดยอัตโนมัติ ... และทำงานได้เร็วด้วย ... มีอยู่ข้อเสียเปรียบอย่างหนึ่งคือ FloatingDecimal ไม่เปลี่ยนรูปดังนั้นคุณต้องสร้างหนึ่งสำหรับทุก ๆ ลอย ... ลองนึกภาพรหัสการคำนวณของ O (1e10)! !! แน่นอน BigDecimal มีข้อเสียเปรียบเหมือนกัน ...
Mostafa Zeinali

6
ข้อเสียเปรียบของโซลูชันนี้คือ sun.misc.FloatingDecimal เป็นคลาสภายในของ JVM และลายเซ็นของตัวสร้างได้รับการเปลี่ยนแปลงใน Java 1.8 ไม่มีใครควรใช้คลาสภายในในชีวิตจริง
Igor Bljahhin

8

ฉันพบวิธีแก้ไขปัญหาต่อไปนี้:

public static Double getFloatAsDouble(Float fValue) {
    return Double.valueOf(fValue.toString());
}

หากคุณใช้floatและdoubleแทนFloatและDouble ให้ใช้ดังต่อไปนี้:

public static double getFloatAsDouble(float value) {
    return Double.valueOf(Float.valueOf(value).toString()).doubleValue();
}

7

ใช้BigDecimalแทนfloat/ double. มีตัวเลขจำนวนมากที่ไม่สามารถแสดงเป็นจุดลอยตัวไบนารี (ตัวอย่างเช่น0.1) ดังนั้นคุณต้องปัดเศษผลลัพธ์ให้มีความแม่นยำที่ทราบหรือใช้BigDecimalเสมอ

ดูข้อมูลเพิ่มเติมได้ที่http://en.wikipedia.org/wiki/Floating_point


สมมติว่าคุณมีราคาซื้อขายแบบลอยตัว 10 ล้านในแคชของคุณคุณยังคงพิจารณาใช้ BigDecimal หรือไม่และสร้างอินสแตนซ์ที่ไม่ซ้ำกันให้พูดว่าวัตถุ ~ 6m (เพื่อลดฟลายเวท / ไม่เปลี่ยนรูป) .. หรือมีมากกว่านั้น?
Sendi_t

1
@Sendi_t โปรดอย่าใช้ความคิดเห็นเพื่อถามคำถามที่ซับซ้อน :-)
Aaron Digulla

ขอบคุณที่มองหา! .. เราใช้โฟลตในขณะนี้ตามที่ตกลงกันเมื่อสิบปีก่อน - ฉันพยายามที่จะเห็นมุมมองของคุณเกี่ยวกับสถานการณ์นี้ในการติดตาม - ขอบคุณ!
Sendi_t

@Sendi_t เข้าใจผิด. ฉันไม่สามารถตอบคำถามของคุณด้วยอักขระ 512 ตัว กรุณาถามคำถามที่เหมาะสมและส่งลิงค์มาให้ฉัน
Aaron Digulla

1
@GKFX ไม่มี "หนึ่งขนาดที่เหมาะกับทุกคน" ในการจัดการทศนิยมบนคอมพิวเตอร์ แต่เนื่องจากคนส่วนใหญ่ไม่เข้าใจฉันจึงชี้ให้พวกเขาทราบBigDecimalเพราะนั่นจะทำให้เกิดข้อผิดพลาดส่วนใหญ่ หากสิ่งต่างๆช้าเกินไปพวกเขาจำเป็นต้องเรียนรู้เพิ่มเติมและค้นหาวิธีที่จะปรับแก้ปัญหาให้เหมาะสม การเพิ่มประสิทธิภาพก่อนวัยอันควรเป็นรากเหง้าของความชั่วร้ายทั้งหมด - DE Knuth
Aaron Digulla

1

โดยธรรมชาติแล้วการลอยนั้นไม่ชัดเจนและมักจะมี "ประเด็น" ในการปัดเศษ หากความแม่นยำเป็นสิ่งสำคัญคุณอาจพิจารณาปรับโครงสร้างแอปพลิเคชันของคุณใหม่เพื่อใช้ทศนิยมหรือ BigDecimal

ใช่การลอยตัวจะเร็วกว่าทศนิยมในเชิงคำนวณเนื่องจากการสนับสนุนบนโปรเซสเซอร์ อย่างไรก็ตามคุณต้องการความรวดเร็วหรือแม่นยำ?


1
เลขคณิตทศนิยมไม่แน่นอนเช่นกัน (เช่น 1/3 * 3 == 0.9999999999999999999999999999) แน่นอนว่าดีกว่าสำหรับการแสดงจำนวนทศนิยมที่แน่นอนเช่นเงิน แต่สำหรับการวัดทางกายภาพจะไม่มีข้อได้เปรียบ
dan04

2
แต่ 1 == 0.99999999999999999999999999 :)
Emmanuel Bourg

0

สำหรับข้อมูลนี้อยู่ภายใต้ Item 48 - หลีกเลี่ยงการลอยตัวและเพิ่มเป็นสองเท่าเมื่อต้องการค่าที่แน่นอนของ Effective Java 2nd edition โดย Joshua Bloch หนังสือเล่มนี้อัดแน่นไปด้วยสาระดีๆและคุ้มค่าแก่การดูแน่นอน



0

วิธีง่ายๆที่ใช้งานได้ดีคือการแยกวิเคราะห์คู่จากการแสดงสตริงของการลอย:

double val = Double.valueOf(String.valueOf(yourFloat));

ไม่ค่อยมีประสิทธิภาพ แต่ใช้งานได้!

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.