#9 Java double 溢出

2023-02-23

遇到一个这样的问题,总数是 t,由 a,b,c 三部分组成。计算 a 的比例,结果算出来一个负数。
原来是 a * 100 这一步溢出了。

import java.math.BigDecimal;
import java.math.RoundingMode;

public class Main {
    public static void main(String[] args) {
        double a = (double) (22890851 * 100) / 26487012;
        System.out.println(a);
        BigDecimal b = new BigDecimal(a);
        System.out.println(b);
        // 警告: [deprecation] BigDecimal中的ROUND_HALF_UP已过时
        // [deprecation] BigDecimal中的setScale(int,int)已过时
        // ROUND_HALF_UP is int
        BigDecimal c = b.setScale(2, RoundingMode.HALF_UP);
        System.out.println(c);
        double d = c.doubleValue();
        System.out.println(d);

        BigDecimal result = new BigDecimal(22890951)
                    .multiply(new BigDecimal(100))
                    .divide(new BigDecimal(26487012), 2, RoundingMode.HALF_UP);
        System.out.println(result);

        System.out.println("HELLO WORLD");
    }
}

OUTPUT

86.42
-75.73078443125257
-75.7307844312525730856577865779399871826171875
-75.73
-75.73
HELLO WORLD

#7 转载:云原生时代,Java 的危与机

2021-12-03

今天,25 岁的 Java 仍然是最具有统治力的编程语言,长期占据编程语言排行榜的首位,拥有一千二百万的庞大开发者群体,全世界有四百五十亿部物理设备使用着 Java 技术,同时,在云端数据中心的虚拟化环境里,还运行着超过两百五十亿个 Java 虚拟机的进程实例 (数据来自Oracle的WebCast)。
以上这些数据是 Java 过去 25 年巨大成就的功勋佐证,更是 Java 技术体系维持自己“天下第一”编程语言的坚实壁垒。Java 与其他语言竞争,底气从来不在于语法、类库有多么先进好用,而是来自它庞大的用户群和极其成熟的软件生态,这在朝夕之间难以撼动。然而,这个现在看起来仍然坚不可摧的 Java 帝国,其统治地位的稳固程度不仅没有高枕无忧,反而说是危机四伏也不为过。目前已经有了可预见的、足以威胁动摇其根基的潜在可能性正在酝酿,并随云原生时代而降临。

#6 不简单的绝对值

2021-09-17

这篇文章讲到了绝对值计算的问题:One does not simply calculate the absolute value

IEEE 754

三个特殊值:

  1. 如果指数是0并且尾数的小数部分是0,这个数 ±0(和符号位相关)
  2. 如果指数 = 2^e - 1 并且尾数的小数部分是 0,这个数是 ±∞(同样和符号位相关)
  3. 如果指数 = 2^e - 1 并且尾数的小数部分非 0,这个数表示为非数(NaN)。

abs 的实现

class Test {
    public static double abs(double value) {
        if (value < 0) {
            return -value;
        }
        return value;
    }
    public static void main(String[] args) {
        double x = -0.0;
        if (1 / abs(x) < 0) {
            System.out.println("oops");
        }
    }
}

if 中加上条件:value == -0.0 是行不通的,因为 +0.0 == -0.0,可以使用 JDK 中的 Double.compare:

public static double abs(double value) {
    if (value < 0 || Double.compare(value, -0.0) == 0) {
        return -value;
    }
    return value;
}

这样确实有效,不过效率上可能会受到影响,abs 的复杂性就上了一个台阶。

JDK 17 中的实现

java/lang/Double.java

public static int compare(double d1, double d2) {
    if (d1 < d2)
        return -1;           // Neither val is NaN, thisVal is smaller
    if (d1 > d2)
        return 1;            // Neither val is NaN, thisVal is larger

    // Cannot use doubleToRawLongBits because of possibility of NaNs.
    long thisBits    = Double.doubleToLongBits(d1);
    long anotherBits = Double.doubleToLongBits(d2);

    return (thisBits == anotherBits ?  0 : // Values are equal
            (thisBits < anotherBits ? -1 : // (-0.0, 0.0) or (!NaN, NaN)
             1));                          // (0.0, -0.0) or (NaN, !NaN)
}

重新实现

参考 JDK 中的实现,重写 abs

private static final long MINUS_ZERO_LONG_BITS = Double.doubleToLongBits(-0.0);
public static double abs(double value) {
    if (value < 0 || Double.doubleToLongBits(value) == MINUS_ZERO_LONG_BITS) {
        return -value;
    }
    return value;
}

新的问题:NaN 的处理,处理方法:把 doubleToLongBits 改成 doubleToRawLongBits

private static final long MINUS_ZERO_LONG_BITS = Double.doubleToRawLongBits(-0.0);
public static double abs(double value) {
    if (value < 0 || Double.doubleToRawLongBits(value) == MINUS_ZERO_LONG_BITS) {
        return -value;
    }
    return value;
}

JVM 的 JIT 会替换这次调用为底层的 CPU 寄存器操作,效率非常可观。

PS:如果可以省去这个分支的判断逻辑,JVM 可以给我们更好的性能优化?

  1. 中间涉及 CPU 分支预测(branch predictor),如果预测错误,可能会付出相对昂贵的代码。

    We know that branches are bad. If the CPU branch predictor guesses incorrectly, they can be very expensive.

  2. 有传言说,这个调用(doubleToRawLongBits)会导致浮点数寄存器转换到通用集成器。

    Although there are rumors saying that this call may still lead to a transfer from a floating-point register to a general-purpose register. Still it's very fast.

进一步优化

采用 0 减负数等于正数,并且 0 - -0 = 0 的规则:

public static double abs(double value) {
    if (value <= 0) {
        return 0.0 - value;
    }
    return value;
}

这就是长期以来(直到最新的 Java 17),JDK 使用的方法(return (a <= 0.0D) ? 0.0D - a : a;)。

参考:JDK 17 中的的实现:java/lang/Math.java

再进一步

有人提出了意见,认为目前官方的实现 too slow(6506405: Math.abs(float) is slow #4711)。

这就是 jdk-18+6 中引入的新方案(java/lang/Math.java#L1600~L1604):

public static double abs(double a) {
    return Double.longBitsToDouble(Double.doubleToRawLongBits(a) & DoubleConsts.MAG_BIT_MASK);
}

DoubleConsts.MAG_BIT_MASK 就是 0x7fffffffffffffffL, 0 + 63 个 1。

原理就是,通过位运算,清除符号位(使之为 0)。

参考资料与拓展阅读

#3 Java 现状

2020-11-11

谷歌 Java 趋势

Oracle Java SE Support Roadmap

Release GA Date Premier Support Until Extended Support Until
7 (LTS) July 2011 July 2019 July 2022
8 (LTS) March 2014 March 2022 December 2030
9 September 2017 March 2018 -
10 March 2018 September 2018 -
11 (LTS) September 2018 September 2023 September 2026
12 March 2019 September 2019 -
13 September 2019 March 2020 -
14 March 2020 September 2020 -
15 September 2020 March 2021 -
16 March 2021 September 2021 -
17 (LTS) September 2021 September 2026 September 2029
18 March 2022 September 2022 -
19 September 2022 March 2023 -
20 March 2023 September 2023 -
21 (LTS) September 2023 September 2028 September 2031

PS:Java 9 开始引入了新的模块机制,标准库结构。

PS: 2021 年 Java 17 发布时,Oracle 宣布以后每两年一个 LTS 版本,也就是说下一个 LTS 版本是 21 而非 23。

参考资料与拓展阅读

#2 sdkman 的使用

2017-04-17
# 安装
curl -s "https://get.sdkman.io" | bash

# 查看当前版本
sdk version

# 更新到最新版本
sdk update

# 列出可用 Java 版本
sdk list java

# 安装指定 Java 版本
sdk install java 11.0.16-albba

# 切换到另一个已安装 Java 版本
sdk use java 8.8.9-albba

除了管理 Java 版本,还可以用来管理其他开发工具,比如 Gradle,Maven,Scale 等。