现象

假如有如下代码定义了一个方法 test(),它入参可以任何一个 int 类型的整数,那么它输出结果可能是什么?

public class Test {
public static void test(int a) {
System.out.println("The result of absolute value compare to zero is:" + (Math.abs(a) >= 0));
}
}

如果你的结论是 true,那么恭喜你,你掉入到绝对值不绝对的坑里面了。这个方法输出的结果有可能是 true,但是也有可能是 false。比如下面的调用代码将分别输出 true,true,true , false,如下图所示:

public static void main(String[] args) {
test(1);
test(-1);
test(Integer.MAX_VALUE);
test(Integer.MIN_VALUE);
}

在 Java 中,通过 Math.abs() 函数返回的值有的时候并不是这个数的绝对值。如下面的代码所示:

public static void main(String[] args) {
System.out.println(Math.abs(Integer.MIN_VALUE));
}

上面的代码输出的结果并不是 Integer.MIN_VALUE 的绝对值,输出的结果是它自己,如下图所示:

从输出可以看到因为 Math.abs(Integer.MIN_VALUE) 的结果还是 Integer.MIN_VALUE,因此它是小于 0 的,这个也解释了上面判断大于等于 0 结果有可能输出的是 false。

为什么 Math.abs(Integer.MIN_VALUE) 的结果还是 Integer.MIN_VALUE 而不是它的绝对值呢?

原理

int 类型可以表示的数的范围解释是比较好理解的。以 int 类型为例,它能够表示的范围是 2^31 到 2^31 - 1。即 -2147483648 到 2147483647,可以看到最小的负数是 -2147483648 。它的绝对值实际上应该是 2147483648,但是这个值已经超过了 int 类型能够表示的最大的数 2147483647 了。如果返回 2147483648,它是不能在一个 int 类型的数中表示的。如果我们直接把 2147483648 这个数赋值给一个 int 类型的变量,编译器也会提示 Integer number too large,如下图所示:

因此这里 Math.abs() 函数返回的结果并不能是 214748364,因为 int 类型根本表示不了这个数。

Math.abs() 方法做了什么操作呢?查看 Math.abs() 方法的源码,实现逻辑如下:

在方法中就是判断了一下这个数是否小于 0,如果小于 0 的话,就返回对这个数取反后的值。那这个取反操作具体做了什么事情呢?为什么对 Integer.MIN_VALUE 进行了求反操作返回的还是它自己?

要回答这些问题,那就得知道计算机底层是表示一个整数的方式以及 int 类型表示的数的范围是 2^31 到 2^31 - 1 的原因。

Java语言规范中对此做了描述,规范中说到 Java 语言中使用 「two's-complement representation」 来表示整数,因为 「two's-complement representation」 的值不是对称的,所以对 int 或者 long 类型的最小值的取反的结果还是它们自身,在这个场景是有「溢出」发生的。而对一个整数的取反操作相当于把它的所有比特位取反,然后再加上 1。如下图所示:

规范这里的提到的 「two's-complement」 就是我们常常说的「补码」,学过计算机组成原理相关课程的应该对这个词语比较耳熟。

补码就是将二进制位的最高位作为符号位,它的权重是 -2^(w -1) (这里的 w 为比特位的个数) ,如果它设置为 1 表示负数,如果设置为 0,表示非负数。如下图所示:





根据补码的定义来看,补码能够表示的最大的数是 2^(w -1) - 1,而它能够表示的最小的的数是 -2^(w -1) (这里的 w 为比特位的个数)。那么最小数的绝对值是比最大数的绝对值还要大 1 的。从上面的图也可以看出(上图中的比特位数为 4),数轴最左侧的刻度是 -8,而数轴最右侧的刻度是 7。

对于补码的取反操作是把每个比特位都取反,然后加上 1。为什么补码的取反要这样操作?从数学的角度上讲一个数 x 加上它的取反 -x 的结果应该是 0。从计算机的角度我们可以知道 x 加上 x 的每个比特位取反的结果是每个比特位都是 1,按照补码的表示方式就是 10 进制的 -1,然后再加上 1 那就是 0,这样的结果就和数学上是相符合的了。比如假设总的比特位数是 8,1 的补码是 0000_0001,取反之后就是 1111_1110,相加的结果是 1111_1111,即 -1,然后加上 1 就是 0000_0000,即 0。如下图所示:

补码的英文名字「 two's complement」 这个名字的由来是如果把一个数的补码和它取反的补码得到的二进制都看作是无符号数的话,它相加的结果就是 2^w (这里的 w 为比特位的个数)。如下图所示:

回过头来看 Java 中的 int 类型,它的最小值 Integer.MIN_VALUE 的补码表示形式就是 1000_0000_0000_0000_0000_0000_0000_0000,按照补码取反操作的规则,应该是把它的补码按位取反得到 0111_1111_1111_1111_1111_1111_1111_1111,然后加 1,得到的结果还是 1000_0000_0000_0000_0000_0000_0000_0000,即它自己。因此在 Math.abs() 函数中对 Integer.MIN_VALUE 取反后得到的值仍然是 Integer.MIN_VALUE

解决方法

对于 Integer.MIN_VALUE 的绝对值溢出现象的解决方法有以下几种:

一种是使用 Math.absExact() 方法,该方法在获取绝对值之前会判断是否超过了表示范围,如果超过了表示范围会抛出一个异常,如下图所示:

public class Test {
public static void main(String[] args) {
System.out.println(Math.absExact(Integer.MIN_VALUE));
}
}

实现原理其实就是在进入方法时判断了一下是否是 Integer.MIN_VALUE ,如果是就直接抛出异常了,如下图所示:

也可以转为 long 类型后再获取绝对值,因为 Integer.MIN_VALUE 的绝对值是可以用 long 类型来表示的,因此转为 long 类型来获取绝对值也是可以的,但是这个方法就解决不了 Long.MIN_VALUE 绝对值溢出现象。如下图所示:

public static void main(String[] args) {
int a = Integer.MIN_VALUE;
System.out.println(Math.abs((long) a) >= 0);
}

还有一种可以使用 Integer.MIN_VALUE 构造一个 BigInteger 对象,然后通过获取这个对象的绝对值来和 BigDecimal.ZERO 来比较,这种方式不仅可以解决 Integer.MIN_VALUE 的绝对值溢出问题,还可以解决 Long.MIN_VALUE 的绝对值溢出问题。如下面的代码:

public static void main(String[] args) {
BigInteger minInt = BigInteger.valueOf(Integer.MIN_VALUE);
System.out.println(minInt.abs().compareTo(BigInteger.ZERO) >= 0); BigInteger minLong = BigInteger.valueOf(Long.MIN_VALUE);
System.out.println(minLong.abs().compareTo(BigInteger.ZERO) >= 0);
}

参考

Unary Minus Operator

Sign–magnitude

Ones' complement

Two's_complement

二进制—原码、反码、补码

Computer Organization and Design

Computer Systems: A Programmer's Perspective

Java 中的这个绝对值有点不绝对啊!的更多相关文章

  1. java中取整数绝对值_Java之——位运算求整数绝对值通过下面的位运算可以得到一个整数的绝对值

    public int abs( int a ) {return (a + (a >> 31)) ^ (a >> 31) ;//前半部分-1或+0,后半部分取反 } a为正数的情 ...

  2. 在Java中,负数的绝对值竟然不一定是正数!!!

    绝对值是指一个数在数轴上所对应点到原点的距离,所以,在数学领域,正数的绝对值是这个数本身,负数的绝对值应该是他的相反数. 这几乎是每个人都知道的. 在Java中,想要获得有个数字的绝对值,可以使用ja ...

  3. Java中的数是用补码表示的检验

    一.基本介绍(关于下列五个定义来自http://www.cnblogs.com/zhangziqiu/archive/2011/03/30/ComputerCode.html#!comments,谢原 ...

  4. JAVA中分为基本数据类型及引用数据类型

    一.基本数据类型: byte:Java中最小的数据类型,在内存中占8位(bit),即1个字节,取值范围-128~127,默认值0 short:短整型,在内存中占16位,即2个字节,取值范围-32768 ...

  5. Java中的大数处理类BigInteger和BigDecimar浅析

    这两个类位于java.math包内,要使用它们必须在类前面引用该包:import java.math.BigInteger;和import java.math.BigDecimal; BigInteg ...

  6. java中的二进制

    (1)按位与运算 & 1 & 1 = 1, 0 & 1 = 0 51 & 5  即 0011  0011 & 0000  0101 =0000 0001 = 1 ...

  7. 位运算及在java中的应用整理

    计算机编码: 原码 符号位为0表示正数,为1表示负数: 其余各位等同于真值的绝对值. 如:0000 0000 0000 0010 =2,1000 0000 0000 0010 =-2 反码 符号位的用 ...

  8. Java中的二进制及基本的位运算

    Java中的二进制及基本的位运算 二进制是计算技术中广泛采用的一种数制.二进制数据是用0和1两个数码来表示的数.它的基数为2,进位规则是"逢二进一",借位规则是"借一当二 ...

  9. 浅谈TreeMap以及在java中的使用

    treemap结构是红黑树 1.先介绍一下平衡二叉树 其特点是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树.也就是说该二叉树的任何一个子节点,其左右子树的高度 ...

  10. Java基础学习(五)-- Java中常用的工具类、枚举、Java中的单例模式之详解

    Java中的常用类 1.Math : 位于java.lang包中 (1)Math.PI:返回一个最接近圆周率的 (2)Math.abs(-10):返回一个数的绝对值 (3)Math.cbrt(27): ...

随机推荐

  1. nestjs 登录和验证码结合验证 svgCaptcha 包 session 会话标识

    // ps: 现在用户验证使用 token jwt 了 代替了 session // session 是服务器为每个用户建立的唯一标识 以区分用户 会话标识 // session 是express中的 ...

  2. kotlin基础——>基本数据类型、控制流、返回和跳转

    1.对于数字的定义,支持java 8的下划线分割的方式 val a = 1_2_3 与 val a = 123 是相同的 2.如果要指定数字类型有两种方式 val a : Float = 1 或者 v ...

  3. Android复习(六)核心组件—>Activity 任何和返回栈、进程和应用生命周期、Parcelable和Bundle

    了解任务和返回堆栈 任务是用户在执行某项工作时与之互动的一系列 Activity 的集合.这些 Activity 按照每个 Activity 打开的顺序排列在一个返回堆栈中.例如,电子邮件应用可能有一 ...

  4. python实现链表(单链,双链)

    # code:utf-8 # createTime:2023.8.17 # -------------------------------------------------------------- ...

  5. 浅谈 K-D Tree 及其进阶应用

    前言 \(\text{K-D Tree (K-Dimension Tree)}\) 是一种可以有效处理高维信息的数据结构. 在一般信息学竞赛题目中 \(k = 2\),此时它又称 \(\text{2- ...

  6. HDFS 重要机制之 checkpoint

    核心概念 hdfs checkpoint 机制对于 namenode 元数据的保护至关重要, 是否正常完成检查点是评估 hdfs 集群健康度和风险的重要指标 editslog : 对 hdfs 操作的 ...

  7. 洛谷 P1328 [NOIP2014 提高组] 生活大爆炸版石头剪刀布

    题目大意 小A和小B,要进行 \(N\) 次猜拳,每次按照一定周期出拳,胜负情况如下: 求出小A和小B分别赢了几次. 思路 枚举 \(N\) 次猜拳,每次比较 \(a[powera]\) 与 \(b[ ...

  8. 实时数仓及olap可视化构建(基于mysql,将maxwell改成seatunnel可以快速达成异构数据源实时同步)

    1. OLAP可视化实现(需要提前整合版本) Linux121 Linux122 Linux123 jupyter spark python3+SuperSet3.0 hive ClinckHouse ...

  9. 机器学习框架推理流程简述(以一项部署在windows上的MNN框架大模型部署过程为例子)

    一.写在前面 公司正好有这个需求,故我这边简单接受进行模型的部署和demo程序的编写,顺便学习了解整个大模型的部署全流程.这篇博客会简单提到大模型部署的全流程,侧重点在推理这里.并且这篇博客也是结合之 ...

  10. docker新建自定义网桥,实现不同主机容器互联

    不同主机间的容器网络互联,网络上的所有教程都是通过open vswitch等虚拟网桥方式实现的,但是最近本人发现可以直接通过配置网桥实现网络的互联,而不用安装配置open vswitch.在这里分享一 ...