Java - 数字的魔法

2019/05/27 Java

Java 的包装类中有许多奇特的性质, 本文对其进行了一些研究

目录

整数边界

在 java 中数值被分为拥有确定值的整数和不一定拥有确定值的浮点数, 整数分为 Byte Short Integer 和 Long 类型, 这几种类型都有其确定范围:

类型 范围 DEC
Byte -27 ~ 27-1 -128 ~ 127
Short -215 ~ 215-1 -32768 ~ 32767
Integer -231 ~ 231-1 -2147483648 ~ 2147483647
Long -263 ~ 263-1 -9223372036854775808 ~ 9223372036854775807

由于整数存在边界, 且最小值比最大值距离 0 更远, 因此会出现下列奇特的现象:

  • 一个负数等于其绝对值

  • 一个数比它自身加一还大

如:

int a = Integer.MAX_VALUE;
System.out.println(a > a + 1);
int b = Integer.MIN_VALUE;
System.out.println(b < 0);
System.out.println(b == Math.abs(b));

OUTPUT:
true
true
true

这是由于整数的边界问题, 由于计算机运算模式为模运算, 因此最大值加一将回到最小值, 因此整型最大值 2147483647 + 1 = -2147483648, 因此, 会出现 a > a + 1 的现象
同样, 对 -2147483648 取绝对值为 2147483648, 超出整型最大值, 因此回到最小值 -2147478648 本身, 因此出现了 a = a + 1 的现象

无穷

Java 的浮点类型包装类(Double Float)中, 有以下全局常量:

常量名 常量值 意义
POSITIVE_INFINITY Infinity 无穷大
NEGATIVE_INFINITY -Infinity 无穷小
System.out.println(Double.POSITIVE_INFINITY);
System.out.println(Double.NEGATIVE_INFINITY);

OUTPUT:
Infinity
-Infinity

对于这些常量进行一些操作, 会产生有趣的现象:

double a = Double.POSITIVE_INFINITY;
System.out.println(a == a + 10);
System.out.println(a == a - 10);
System.out.println(a == a * 10);
System.out.println(a == a / 10);
System.out.println(a == a * 0);
System.out.println(a == a / 0);

OUTPUT:
true
true
true
true
false
true

对于无穷大与一个非零数的加减乘除操作, 结果依旧是无穷大, 这一点与现实相符, 因此也出现像 i + 10 == i 的奇特现象
但无穷大除以零等于其本身这一点倒是值得考究

无穷的定义

打开 Double 类的源码, 发现无穷常量有如下定义:

public static final double POSITIVE_INFINITY = 1.0 / 0.0;

Java 中竟然允许一个数除以 0 ? 或许很多人会感到意外, 因为有人曾遇到过 java.lang.ArithmeticException, 这是因为除以 0 而带来的算术异常, 那为什么这里可以除以 0 ?

这是因为对于浮点类型, Java 遵循了 IEEE 754 标准, 有如下规定:

对有限操作数的操作给出精确的无限结果, 默认返回 ±Infinity

因此, 在 java 中, 对于一个正浮点数, 其除以 0 的结果, 永远是 infinity, 对于负浮点数, 其除 0 结果永远是 -infinity, 而上文中, 正无穷大本身就是一个正数, 因此其除以零, 结果依旧是 infinity, 因此会有 i / 0 == i 的奇特现象发生.
而对于整型, 每一个整型变量必须拥有确切的值, 因此, 整型并没有无穷大的概念, 那除以 0 也就找不到结果了, 因此整型进行除零运算将抛出异常

此外, 除了除以 0 会发生无穷的结果, 取对数和取幂方同样可以返回无穷:

System.out.println(Math.log(0));
System.out.println(Math.log(Double.POSITIVE_INFINITY));
System.out.println(Math.pow(2, Double.NEGATIVE_INFINITY));
System.out.println(Math.pow(2, Double.POSITIVE_INFINITY));
System.out.println(Math.pow(Double.POSITIVE_INFINITY, 2));

OUTPUT:
-Infinity
Infinity
0.0
Infinity
Infinity

那为什么上文的 infinity * 0 != infinity 呢?

我们看看 infinity * 0 的结果是多少:

System.out.println(a * 0);

OUTPUT:
NaN

NaN 是什么?

NaN

NaN 是 Not a Number 的意思, 代表这并非一个数字

同样, 浮点包装类中定义了这样一个静态全局常量:

public static final double NaN = 0.0d / 0.0;

同样是由于 java 遵循 IEEE 754 规定:

EEE 754 指定一个特殊值, 称为 “不是数字” (NaN), 该值将由于某些 “无效” 操作 (如 0/0、X0 或 sqrt(−1) 而返回。一般情况下, 涉及 NaN 的操作都将返回 NaN, 或发出 “无效操作” 的异常

因此, 在 java 中进行如下操作, 都将返回 NaN:

System.out.println(Math.sqrt(-1));
System.out.println(0.0/0);
System.out.println(Double.NaN * 10);

OUTPUT:
NaN
NaN
NaN

真是因为这样, 上文的 infinity * 0 等于 一个正数 / 0.0 * 0 等于 0 / 0.0, 即 NaN

而需要注意的一点是, NaN与任何浮点数(包括自身)的比较结果都为假, 这是 NaN 的特性, 即当一个浮点变量 i 的值为 NaN 的时候, 系统会认为 i == i 结果为 false:

double a = Double.NaN;
double b = 0.00/0;
System.out.println(a == a);
System.out.println(a != a);
System.out.println(b == Double.NaN);
System.out.println(Double.NaN == Double.NaN);
System.out.println(Double.NaN != Double.NaN);


OUTPUT:
false
true
false
false
true

由此会产生一个令人疑惑的现象: a != a 是对的, 但这是 NaN 独有的特性, 因此我们不能通过 x == NaN 来判断 x 是否为 NaN, 而需要使用 isNaN(x) 方法来判断该值是否为 NaN, 而 isNaN() 方法是如何实现的呢:

public static boolean isNaN(double v) {
    return (v != v);
}

嘿嘿, 本质上依旧是利用其特性进行判断. 因此我们可以直接用 x != x 判断其是否为 NaN

缓存区

java 整数包装类都存在着一个称为缓存区的静态内部类:

包装类 缓存区 范围
Byte ByteCache -128 ~ 127
Short ShortCache -128 ~ 127
Integer IntegerCache -128 ~ 127
Long LongCache -128 ~ 127

所有类缓存区实现都是一样的, 通过一个静态常数组保存数据, 只不过将内部类中所有名称和数组类型改为自身相应的类型:

private static class ShortCache {
    private ShortCache(){}

    static final Short cache[] = new Short[-(-128) + 127 + 1];

    static {
        for(int i = 0; i < cache.length; i++)
            cache[i] = new Short((short)(i - 128));
    }
}

正因为缓存区的存在, 会发生以下奇怪的现象:

Byte aByte = 110, bByte = 110, cByte = -128, dByte = -128, eByte = -128;
System.out.println(aByte == bByte);
System.out.println(cByte == dByte);
System.out.println(cByte == eByte);
Short aShort = 110, bShort = 110, cShort = 128, dShort = 128;
System.out.println(aShort == bShort);
System.out.println(cShort == dShort);
Integer aInteger = 1, bInteger = 1, cInteger = 128, dInteger = 128;
System.out.println(aInteger == bInteger);
System.out.println(cInteger == dInteger);

OUTPUT:
true
true
true
true
false
true
false

那这和缓存区有什么关系呢? 我们先探究一下在对新声名的包装类对象使用等于号时发生了什么:

当为新声名对象直接赋值时, 将调用该包装类的 valueOf() 静态方法, 同样以 Short 为例, 其他整数包装类都是一致的:

public static Short valueOf(short s) {
    final int offset = 128;
    int sAsInt = s;
    if (sAsInt >= -128 && sAsInt <= 127) { // must cache
        return ShortCache.cache[sAsInt + offset];
    }
    return new Short(s);
}

若是直接赋值的值介于缓存区大小 (-128 ~ 127) 之间, 将不会产生该类的实例化对象, 而是返回其全局缓存区中静态数组的相应值, 此时, 若下一个新声名对象与处于缓存区的对象的值相同, 二者都指向了静态数组的同个元素, 因此二者是相等的, 所以返回 true, 而 128 刚好超出缓存区最大范围, 因此直接赋值 128 会直接返回实例化的新对象, 而不是返回缓存区的元素

因为 Byte 类型的大小与缓存区一致, 因此若通过直接赋值的方式是无法做到完全独立的实例化新 Byte 对象的, 要使用构造函数实例化该类:

Byte fByte = new Byte((byte)110);
Byte gByte = new Byte((byte)110);
System.out.println(fByte == gByte);
System.out.println(fByte.equals(gByte));

OUTPUT:
false
true

Tips: 包装类重写的 equals() 方法, 只要包装类对象值相等而地址不相等, 同样返回 true, 而 == 号通过判断二者的地址比较两个对象, 需要两个对象所指向的地址相同才会返回 true

此外, Character 和 Boolean 包装类同样存在着缓存区, 完整如下:

包装类 缓存区 范围
Byte ByteCache -128 ~ 127
Short ShortCache -128 ~ 127
Integer IntegerCache -128 ~ 127
Long LongCache -128 ~ 127
Boolean BooleanCache true, false
Character CharacterCache 0 ~ 127

0 ~ 127 覆盖了 ASCII 表全部, 因此一个好的习惯是, 创建一个包装类对象, 不要采用直接赋值的方式或使用 valueOf() 方法, 采用 new 的方式创建对象最为直接妥当

正在加载今日诗词....

Access Statistics

Table of Contents