BigDecimal(double)和BigDecimal(String)有什么区别?

典型回答 有区别,而且区别很大。 因为double是不精确的,所以使用一个不精确的数字来创建BigDecimal,得到的数字也是不精确的。如0.1这个数字,double只能表示他的近似值。 所以,当我们使用new BigDecimal(0.1)创建一个BigDecimal 的时候,其实创建出来的值并不是正好等于0.1的。 而是0.1000000000000000055511151231257827021181583404541015625。这是因为double自身表示的只是一个近似值。 而对于BigDecimal(String) ,当我们使用new BigDecimal(“0.1”)创建一个BigDecimal 的时候,其实创建出来的值正好就是等于0.1的。 那么他的标度也就是1 扩展知识 在《阿里巴巴Java开发手册》中有一条建议,或者说是要求: BigDecimal如何精确计数? 如果大家看过BigDecimal的源码,其实可以发现,实际上一个BigDecimal是通过一个"无标度值"和一个"标度"来表示一个数的。 无标度值(Unscaled Value):这是一个整数,表示BigDecimal的实际数值。 标度(Scale):这是一个整数,表示小数点后的位数。 BigDecimal的实际数值计算公式为:unscaledValue × 10^(-scale)。 假设有一个BigDecimal表示的数值是123.45,那么无标度值(Unscaled Value)是12345。标度(Scale)是2。因为123.45 = 12345 × 10^(-2)。 涉及到的字段就是这几个: 1 2 3 4 5 public class BigDecimal extends Number implements Comparable<BigDecimal> { private final BigInteger intVal; private final int scale; private final transient long intCompact; } 关于无标度值的压缩机制大家了解即可,不是本文的重点,大家只需要知道BigDecimal主要是通过一个无标度值和标度来表示的就行了。 那么标度到底是什么呢? 除了scale这个字段,在BigDecimal中还提供了scale()方法,用来返回这个BigDecimal的标度。 1 2 3 4 5 6 7 8 9 10 11 12 13 /** * Returns the <i>scale</i> of this {@code BigDecimal}. If zero * or positive, the scale is the number of digits to the right of * the decimal point. If negative, the unscaled value of the * number is multiplied by ten to the power of the negation of the * scale. For example, a scale of {@code -3} means the unscaled * value is multiplied by 1000. * * @return the scale of this {@code BigDecimal}. */ public int scale() { return scale; } 那么,scale到底表示的是什么,其实上面的注释已经说的很清楚了。 ...

March 22, 2026 · 2 min · santu

Integer a=1000,Integer b=1000,==是什么结果,如果是100呢?

典型回答 这个问题其实考察的是java中包装类的缓存机制。先说答案。 1 2 3 4 5 6 7 Integer a = 1000; Integer b = 1000; System.out.println(a == b); // false Integer c = 100; Integer d = 100; System.out.println(c == d); // true Java 中的包装类缓存机制,主要是指 Java 对部分包装类(如 Integer, Byte, Short, Long, Character, Boolean)在一定范围内的数值使用了缓存池,以提升性能、节省内存。 Java 对以下包装类中的部分数值进行了缓存: 包装类 缓存范围 Integer [-128, 127] Short [-128, 127] Byte [-128, 127] Long [-128, 127] Character [0, 127] Boolean true, false 注意:Float、Double不支持缓存。我猜测可能是小数太多了,没办法穷举。 ...

March 22, 2026 · 1 min · santu

Java是值传递还是引用传递?

典型回答 编程语言中需要进行方法间的参数传递,这个传递的策略叫做求值策略。 在程序设计中,求值策略有很多种,比较常见的就是值传递和引用传递。 值传递和引用传递最大的区别是传递的过程中有没有**复制**出一个副本来,如果是传递副本,那就是值传递,否则就是引用传递。 Java对象的传递,是通过复制的方式把引用关系传递了,因为有**复制**的过程,所以是值传递,只不过对于Java对象的传递,传递的内容是对象的引用。 扩展知识 Java的求值策略 我们说当进行方法调用的时候,需要把实际参数传递给形式参数,那么传递的过程中到底传递的是什么东西呢? 这其实是程序设计中求值策略(Evaluation strategies)的概念。 在计算机科学中,求值策略是确定编程语言中表达式的求值的一组(通常确定性的)规则。求值策略定义何时和以何种顺序求值给函数的实际参数、什么时候把它们代换入函数、和代换以何种形式发生。 求值策略分为两大基本类,基于如何处理给函数的实际参数,分为严格的和非严格的。 严格求值 在“严格求值”中,函数调用过程中,给函数的实际参数总是在应用这个函数之前求值。多数现存编程语言对函数都使用严格求值。所以,我们本文只关注严格求值。 在严格求值中有几个关键的求值策略是我们比较关心的,那就是传值调用(Call by value)、传引用调用(Call by reference)以及传共享对象调用(Call by sharing)。 传值调用(值传递) 在传值调用中,实际参数先被求值,然后其值通过复制,被传递给被调函数的形式参数。因为形式参数拿到的只是一个”局部拷贝”,所以如果在被调函数中改变了形式参数的值,并不会改变实际参数的值。 传引用调用(应用传递) 在传引用调用中,传递给函数的是它的实际参数的隐式引用而不是实参的拷贝。因为传递的是引用,所以,如果在被调函数中改变了形式参数的值,改变对于调用者来说是可见的。 传共享对象调用(共享对象传递) 传共享对象调用中,先获取到实际参数的地址,然后将其复制,并把该地址的拷贝传递给被调函数的形式参数。因为参数的地址都指向同一个对象,所以我们称也之为”传共享对象”,所以,如果在被调函数中改变了形式参数的值,调用者是可以看到这种变化的。 不知道大家有没有发现,其实传共享对象调用和传值调用的过程几乎是一样的,都是进行”求值”、”拷贝”、”传递”。你品,你细品。 但是,传共享对象调用和内传引用调用的结果又是一样的,都是在被调函数中如果改变参数的内容,那么这种改变也会对调用者有影响。你再品,你再细品。 那么,共享对象传递和值传递以及引用传递之间到底有很么关系呢? 对于这个问题,我们应该关注过程,而不是结果,因为传共享对象调用的过程和传值调用的过程是一样的,而且都有一步关键的操作,那就是”复制”,所以,通常我们认为传共享对象调用是传值调用的特例。 前面我们介绍过了传值调用、传引用调用以及传值调用的特例传共享对象调用,那么,Java中是采用的哪种求值策略呢? 很多人说Java中的基本数据类型是值传递的,这个基本没有什么可以讨论的,普遍都是这样认为的。 但是,有很多人却误认为Java中的对象传递是引用传递。之所以会有这个误区,主要是因为Java中的变量和对象之间是有引用关系的。Java语言中是通过对象的引用来操纵对象的。所以,很多人会认为对象的传递是引用的传递。 而且很多人还可以举出以下的代码示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main(String[] args) { Test pt = new Test(); User hollis = new User(); hollis.setName("Hollis"); hollis.setGender("Male"); pt.pass(hollis); System.out.println("print in main , user is " + hollis); } public void pass(User user) { user.setName("hollischuang"); System.out.println("print in pass , user is " + user); } 输出结果: ...

March 22, 2026 · 2 min · santu

SimpleDateFormat是线程安全的吗?使用时应该注意什么?

典型回答 在日常开发中,我们经常会用到时间,我们有很多办法在Java代码中获取时间。但是不同的方法获取到的时间的格式都不尽相同,这时候就需要一种格式化工具,把时间显示成我们需要的格式。 最常用的方法就是使用SimpleDateFormat类。这是一个看上去功能比较简单的类,但是,一旦使用不当也有可能导致很大的问题。 在阿里巴巴Java开发手册中,有如下明确规定: 也就是说SimpleDateFormat是非线程安全的,所以在多线程场景中,不能使用SimpleDateFormat作为共享变量。 因为SimpleDateFormat中的format方法在执行过程中,会使用一个成员变量calendar来保存时间。 如果我们在声明SimpleDateFormat的时候,使用的是static定义的。那么这个SimpleDateFormat就是一个共享变量,随之,SimpleDateFormat中的calendar也就可以被多个线程访问到。 假设线程1刚刚执行完calendar.setTime把时间设置成2018-11-11,还没等执行完,线程2又执行了calendar.setTime把时间改成了2018-12-12。这时候线程1继续往下执行,拿到的calendar.getTime得到的时间就是线程2改过之后的。 想要保证线程安全,要么就是不要把SDF设置成成员变量,只设置成局部变量就行了,要不然就是加锁避免并发,或者使用JDK 1.8中的DateTimeFormatter 扩展知识 SimpleDateFormat用法 SimpleDateFormat是Java提供的一个格式化和解析日期的工具类。它允许进行格式化(日期 -> 文本)、解析(文本 -> 日期)和规范化。SimpleDateFormat 使得可以选择任何用户定义的日期-时间格式的模式。 在Java中,可以使用SimpleDateFormat的format方法,将一个Date类型转化成String类型,并且可以指定输出格式。 1 2 3 4 5 // Date转String Date data = new Date(); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dataStr = sdf.format(data); System.out.println(dataStr); 以上代码,转换的结果是:2018-11-25 13:00:00,日期和时间格式由"日期和时间模式"字符串指定。如果你想要转换成其他格式,只要指定不同的时间模式就行了。 在Java中,可以使用SimpleDateFormat的parse方法,将一个String类型转化成Date类型。 1 2 // String转Data System.out.println(sdf.parse(dataStr)); 日期和时间模式表达方法 在使用SimpleDateFormat的时候,需要通过字母来描述时间元素,并组装成想要的日期和时间模式。常用的时间元素和字母的对应表如下: 模式字母通常是重复的,其数量确定其精确表示。如下表是常用的输出格式的表示方法。 输出不同时区的时间 时区是地球上的区域使用同一个时间定义。以前,人们通过观察太阳的位置(时角)决定时间,这就使得不同经度的地方的时间有所不同(地方时)。1863年,首次使用时区的概念。时区通过设立一个区域的标准时间部分地解决了这个问题。 世界各个国家位于地球不同位置上,因此不同国家,特别是东西跨度大的国家日出、日落时间必定有所偏差。这些偏差就是所谓的时差。 现今全球共分为24个时区。由于使用上常常1个国家,或1个省份同时跨着2个或更多时区,为了照顾到行政上的方便,常将1个国家或1个省份划在一起。所以时区并不严格按南北直线来划分,而是按自然条件来划分。例如,中国幅员宽广,差不多跨5个时区,但为了使用方便简单,实际上在只用东八时区的标准时即北京时间为准。 由于不同的时区的时间是不一样的,甚至同一个国家的不同城市时间都可能不一样,所以,在Java中想要获取时间的时候,要重点关注一下时区问题。 默认情况下,如果不指明,在创建日期的时候,会使用当前计算机所在的时区作为默认时区,这也是为什么我们通过只要使用new Date()就可以获取中国的当前时间的原因。 那么,如何在Java代码中获取不同时区的时间呢?SimpleDateFormat可以实现这个功能。 1 2 3 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); sdf.setTimeZone(TimeZone.getTimeZone("America/Los_Angeles")); System.out.println(sdf.format(Calendar.getInstance().getTime())); 以上代码,转换的结果是: 2018-11-24 21:00:00 。既中国的时间是11月25日的13点,而美国洛杉矶时间比中国北京时间慢了16个小时(这还和冬夏令时有关系,就不详细展开了)。 ...

March 22, 2026 · 3 min · santu

String str=new String(_hollis_)创建了几个对象?

典型回答 创建的对象数应该是1个或者2个。 首先要清楚什么是对象? Java是一种面向对象的语言,而Java对象在JVM中的存储也是有一定的结构的,在HotSpot虚拟机中,存储的形式就是oop-klass model,即Java对象模型。我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了两部分信息,对象头以及元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息。元数据其实维护的是指针,指向的是对象所属的类的instanceKlass。 这才叫对象。其他的,一概都不叫对象。 那么不管怎么样,一次new的过程,都会在堆上创建一个对象,那么就是起码有一个对象了。至于另外一个对象,到底有没有要看具体情况了。 另外这一个对象就是常量池中的字符串常量,这个字符串其实是类编译阶段就进到Class常量池的,然后在运行期,字符串常量在第一次被调用(准确的说是ldc指令)的时候,进行解析并在字符串池中创建对应的String实例的。 ✅什么是Class常量池,和运行时常量池关系是什么? ✅字符串常量是什么时候进入到字符串常量池的? 在运行时常量池中,也并不是会立刻被解析成对象,而是会先以JVM_CONSTANT_UnresolveString_info的形式驻留在常量池。在后面,该引用第一次被LDC指令执行到的时候,就尝试在堆上创建字符串对象,并将对象的引用驻留在字符串常量池中。 通过看上面的过程,你也能发现,这个过程的触发条件是我们没办法决定的,问题的题干中也没提到。有可能执行这段代码的时候是第一次LDC指令执行,也许在前面就执行过了。 所以,如果是第一次执行,那么就是会同时创建两个对象。一个字符串常量引用指向的对象,一个我们new出来的对象。 如果不是第一次执行,那么就只会创建我们自己new出来的对象。 至于有人说什么在字符串池内还有在栈上还有一个引用对象,你听听这说法,引用就是引用。别往对象上面扯。 扩展知识 字面量和运行时常量池 JVM为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。为了减少在JVM中创建的字符串的数量,字符串类维护了一个字符串常量池。 在JVM运行时区域的方法区中,有一块区域是运行时常量池,主要用来存储编译期生成的各种字面量和符号引用。 了解Class文件结构或者做过Java代码的反编译的朋友可能都知道,在java代码被javac编译之后,文件结构中是包含一部分Constant pool的。比如以下代码: 1 2 3 public static void main(String[] args) { String s = "Hollis"; } 经过编译后,常量池内容如下: 1 2 3 4 5 6 7 8 9 10 11 Constant pool: #1 = Methodref #4.#20 // java/lang/Object."<init>":()V #2 = String #21 // Hollis #3 = Class #22 // StringDemo #4 = Class #23 // java/lang/Object ... #16 = Utf8 s .. #21 = Utf8 Hollis #22 = Utf8 StringDemo #23 = Utf8 java/lang/Object 上面的Class文件中的常量池中,比较重要的几个内容: ...

March 22, 2026 · 2 min · santu

String为什么设计成不可变的?

为什么要把String设计成不可变的呢?有什么好处呢? 这个问题,困扰过很多人,甚至有人直接问过Java的创始人James Gosling。 在一次采访中James Gosling被问到什么时候应该使用不可变变量,他给出的回答是: I would use an immutable whenever I can. 那么,他给出这个答案背后的原因是什么呢?是基于哪些思考的呢? 其实,主要是从缓存、安全性、线程安全和性能等角度出发的。 缓存 字符串是使用最广泛的数据结构。大量的字符串的创建是非常耗费资源的,所以,Java提供了对字符串的缓存功能,可以大大的节省堆空间。 JVM中专门开辟了一部分空间来存储Java字符串,那就是字符串池。 通过字符串池,两个内容相同的字符串变量,可以从池中指向同一个字符串对象,从而节省了关键的内存资源。 1 2 String s = "abcd"; String s2 = s; 对于这个例子,s和s2都表示"abcd",所以他们会指向字符串池中的同一个字符串对象: 但是,之所以可以这么做,主要是因为字符串的不变性。试想一下,如果字符串是可变的,我们一旦修改了s的内容,那必然导致s2的内容也被动的改变了,这显然不是我们想看到的。 安全性 字符串在Java应用程序中广泛用于存储敏感信息,如用户名、密码、连接url、网络连接等。JVM类加载器在加载类的时也广泛地使用它。 因此,保护String类对于提升整个应用程序的安全性至关重要。 当我们在程序中传递一个字符串的时候,如果这个字符串的内容是不可变的,那么我们就可以相信这个字符串中的内容。 但是,如果是可变的,那么这个字符串内容就可能随时都被修改。那么这个字符串内容就完全不可信了。这样整个系统就没有安全性可言了。 线程安全 不可变会自动使字符串成为线程安全的,因为当从多个线程访问它们时,它们不会被更改。 因此,一般来说,不可变对象可以在同时运行的多个线程之间共享。它们也是线程安全的,因为如果线程更改了值,那么将在字符串池中创建一个新的字符串,而不是修改相同的值。因此,字符串对于多线程来说是安全的。 hashcode缓存 由于字符串对象被广泛地用作数据结构,它们也被广泛地用于哈希实现,如HashMap、HashTable、HashSet等。在对这些散列实现进行操作时,经常调用hashCode()方法。 不可变性保证了字符串的值不会改变。因此,hashCode()方法在String类中被重写,以方便缓存,这样在第一次hashCode()调用期间计算和缓存散列,并从那时起返回相同的值。 在String类中,有以下代码: 1 private int hash;//this is used to cache hash code. 性能 前面提到了的字符串池、hashcode缓存等,都是提升性能的体现。 因为字符串不可变,所以可以用字符串池缓存,可以大大节省堆内存。而且还可以提前对hashcode进行缓存,更加高效 由于字符串是应用最广泛的数据结构,提高字符串的性能对提高整个应用程序的总体性能有相当大的影响。

March 22, 2026 · 1 min · santu

String是如何实现不可变的?

典型回答 我们都知道String是不可变的,但是它是怎么实现的呢? 先来看一段String的源码(JDK 1.8): 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[]; /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -6849794470754667710L; public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); } public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true); } } 以上代码,其实就包含了String不可变的主要实现了。 ...

March 22, 2026 · 1 min · santu

为什么Java不支持多继承?

典型回答 因为如果要实现多继承,就会像C++中一样,存在菱形继承的问题,C++为了解决菱形继承问题,又引入了虚继承。因为支持多继承,引入了菱形继承问题,又因为要解决菱形继承问题,引入了虚继承。而经过分析,人们发现我们其实真正想要使用多继承的情况并不多。所以,在 Java 中,不允许“多继承”,即一个类不允许继承多个父类。 除了菱形的问题,支持多继承复杂度也会增加。一个类继承了多个父类,可能会继承大量的属性和方法,导致类的接口变得庞大、难以理解和维护。此外,在修改一个父类时,可能会影响到多个子类,增加了代码的耦合度。 在Java 8以前,接口中是不能有方法的实现的。所以一个类同时实现多个接口的话,也不会出现C++中的歧义问题。因为所有方法都没有方法体,真正的实现还是在子类中的。但是,Java 8中支持了默认函数(default method ),即接口中可以定义一个有方法体的方法了。 而又因为Java支持同时实现多个接口,这就相当于通过implements就可以从多个接口中继承到多个方法了,但是,Java8中为了避免菱形继承的问题,在实现的多个接口中如果有相同方法,就会要求该类必须重写这个方法。 扩展知识 菱形继承问题 Java的创始人James Gosling曾经回答过,他表示: “Java之所以不支持一个类继承多个类,主要是因为在设计之初我们听取了来自C++和Objective-C等阵营的人的意见。因为多继承会产生很多歧义问题。” Gosling老人家提到的歧义问题,其实是C++因为支持多继承之后带来的菱形继承问题。 假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。 这时候,因为D同时继承了B和C,并且B和C又同时继承了A,那么,D中就会因为多重继承,继承到两份来自A中的属性和方法。 这时候,在使用D的时候,如果想要调用一个定义在A中的方法时,就会出现歧义。 因为这样的继承关系的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。 而C++为了解决菱形继承问题,又引入了虚继承。 因为支持多继承,引入了菱形继承问题,又因为要解决菱形继承问题,引入了虚继承。而经过分析,人们发现我们其实真正想要使用多继承的情况并不多。 所以,在 Java 中,不允许“声明多继承”,即一个类不允许继承多个父类。但是 Java 允许“实现多继承”,即一个类可以实现多个接口,一个接口也可以继承多个父接口。由于接口只允许有方法声明而不允许有方法实现(Java 8之前),这就避免了 C++ 中多继承的歧义问题。 Java 8中的多继承 Java不支持多继承,但是是支持多实现的,也就是说,同一个类可以同时实现多个接口。 我们知道,在Java 8以前,接口中是不能有方法的实现的。所以一个类同时实现多个接口的话,也不会出现C++中的歧义问题。因为所有方法都没有方法体,真正的实现还是在子类中的。 那么问题来了。 Java 8中支持了默认函数(default method ),即接口中可以定义一个有方法体的方法了。 1 2 3 4 5 6 public interface Pet { public default void eat(){ System.out.println("Pet Is Eating"); } } 而又因为Java支持同时实现多个接口,这就相当于通过implements就可以从多个接口中继承到多个方法了,这不就是变相支持了多继承么。 那么,Java是怎么解决菱形继承问题的呢?我们再定义一个哺乳动物接口,也定义一个eat方法。 1 2 3 4 5 6 public interface Mammal { public default void eat(){ System.out.println("Mammal Is Eating"); } } 然后定义一个Cat,让他分别实现两个接口: ...

March 22, 2026 · 1 min · santu

为什么不能用BigDecimal的equals方法做等值比较?

典型回答 因为BigDecimal的equals方法和compareTo并不一样,equals方法会比较两部分内容,分别是值(value)和标度(scale),而对于0.1和0.10这两个数字,他们的值虽然一样,但是精度是不一样的,所以在使用equals比较的时候会返回false。 扩展知识 BigDecimal,相信对于很多人来说都不陌生,很多人都知道他的用法,这是一种java.math包中提供的一种可以用来进行精确运算的类型。 很多人都知道,在进行金额表示、金额计算等场景,不能使用double、float等类型,而是要使用对精度支持的更好的BigDecimal。 所以,很多支付、电商、金融等业务中,BigDecimal的使用非常频繁。而且不得不说这是一个非常好用的类,其内部自带了很多方法,如加,减,乘,除等运算方法都是可以直接调用的。 除了需要用BigDecimal表示数字和进行数字运算以外,代码中还经常需要对于数字进行相等判断。 关于这个知识点,在最新版的《阿里巴巴Java开发手册》中也有说明: 这背后的思考是什么呢? BigDecimal的比较 我在之前的CodeReview中,看到过以下这样的低级错误: 1 2 3 if(bigDecimal == bigDecimal1){ // 两个数相等 } 这种错误,相信聪明的读者一眼就可以看出问题,因为BigDecimal是对象,所以不能用**==**来判断两个数字的值是否相等。 以上这种问题,在有一定的经验之后,还是可以避免的,但是聪明的读者,看一下以下这行代码,你觉得他有问题吗: 1 2 3 if(bigDecimal.equals(bigDecimal1)){ // 两个数相等 } 可以明确的告诉大家,以上这种写法,可能得到的结果和你预想的不一样! 先来做个实验,运行以下代码: 1 2 3 4 5 6 7 8 9 10 11 12 13 BigDecimal bigDecimal = new BigDecimal(1); BigDecimal bigDecimal1 = new BigDecimal(1); System.out.println(bigDecimal.equals(bigDecimal1)); BigDecimal bigDecimal2 = new BigDecimal(1); BigDecimal bigDecimal3 = new BigDecimal(1.0); System.out.println(bigDecimal2.equals(bigDecimal3)); BigDecimal bigDecimal4 = new BigDecimal("1"); BigDecimal bigDecimal5 = new BigDecimal("1.0"); System.out.println(bigDecimal4.equals(bigDecimal5)); 以上代码,输出结果为: ...

March 22, 2026 · 2 min · santu

为什么不能用浮点数表示金额?

典型回答 因为不是所有的小数都能用二进制表示(扩展知识中介绍为啥不能表示),所以,为了解决这个问题,IEEE提出了一种使用近似值表示小数的方式,并且引入了精度的概念。这就是我们所熟知的浮点数。 比如0.1+0.2 != 0.3,而是等于0.30000000000000004 (甚至有一个网站就叫做 https://0.30000000000000004.com/ ,就是来解释这个现象的) 所以,浮点数只是近似值,并不是精确值,所以不能用来表示金额。否则会有精度丢失。 扩展知识 十进制转二进制 首先我们看一下,如何把十进制整数转换成二进制整数? 十进制整数转换为二进制整数采用"除2取余,逆序排列"法。 具体做法是: 用2整除十进制整数,可以得到一个商和余数; 再用2去除商,又会得到一个商和余数,如此进行,直到商为小于1时为止 然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。 如,我们想要把127转换成二进制,做法如下: 那么,十进制小数转换成二进制小数,又该如何计算呢? 十进制小数转换成二进制小数采用"乘2取整,顺序排列"法。 具体做法是: 用2乘十进制小数,可以得到积 将积的整数部分取出,再用2乘余下的小数部分,又得到一个积 再将积的整数部分取出,如此进行,直到积中的小数部分为零,此时0或1为二进制的最后一位。或者达到所要求的精度为止。 所以,十进制的0.625对应的二进制就是0.101。 不是所有数都能用二进制表示 我们知道了如何将一个十进制小数转换成二进制,那么是不是计算就可以直接用二进制表示小数了呢? 前面我们的例子中0.625是一个特列,那么还是用同样的算法,请计算下0.1对应的二进制是多少? 我们发现,0.1的二进制表示中出现了无限循环的情况,也就是(0.1)10 = (0.000110011001100…)2 这种情况,计算机就没办法用二进制精确的表示0.1了。 也就是说,对于像0.1这种数字,我们是没办法将他转换成一个确定的二进制数的。 IEEE 754 为了解决部分小数无法使用二进制精确表示的问题,于是就有了IEEE 754规范。 IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。 浮点数和小数并不是完全一样的,计算机中小数的表示法,其实有定点和浮点两种。因为在位数相同的情况下,定点数的表示范围要比浮点数小。所以在计算机科学中,使用浮点数来表示实数的近似值。 IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现)。 其中最常用的就是32位单精度浮点数和64位双精度浮点数。 IEEE并没有解决小数无法精确表示的问题,只是提出了一种使用近似值表示小数的方式,并且引入了精度的概念。 浮点数是一串0和1构成的位序列(bit sequence),从逻辑上用三元组{S,E,M}表示一个数N,如下图所示: S(sign)表示N的符号位。对应值s满足:n>0时,s=0; n≤0时,s=1。 E(exponent)表示N的指数位,位于S和M之间的若干位。对应值e值也可正可负。 M(mantissa)表示N的尾数位,恰好,它位于N末尾。M也叫有效数字位(significand)、系数位(coefficient), 甚至被称作"小数"。 则浮点数N的实际值n由下方的式子表示: 上面这个公式看起来很复杂,其中符号位和尾数位还比较容易理解,但是这个指数位就不是那么容易理解了。 其实,大家也不用太过于纠结这个公式,大家只需要知道对于单精度浮点数,最多只能用32位字符表示一个数字,双精度浮点数最多只能用64位来表示一个数字。 而对于那些无限循环的二进制数来说,计算机采用浮点数的方式保留了一定的有效数字,那么这个值只能是近似值,不可能是真实值。 至于一个数对应的IEEE 754浮点数应该如何计算,不是本文的重点,这里就不再赘述了,过程还是比较复杂的,需要进行对阶、尾数求和、规格化、舍入以及溢出判断等。 但是这些其实不需要了解的太详细,我们只需要知道,小数在计算机中的表示是近似数,并不是真实值。根据精度不同,近似程度也有所不同。 如0.1这个小数,他对应的在双精度浮点数的二进制为:0.00011001100110011001100110011001100110011001100110011001 。 0.2这个小数0.00110011001100110011001100110011001100110011001100110011 。 所以两者相加: 转换成10进制之后得到:0.30000000000000004! 避免精度丢失 在Java中,使用float表示单精度浮点数,double表示双精度浮点数,表示的都是近似值。 ...

March 22, 2026 · 1 min · santu

留言给博主