Java数值类型提高机制

Java语法特性隐藏在了代码中的每一个角落,最多见的就是自动拆装箱和类型提高了。这些特性在带来编码便利性的同时也在代码中藏下了些不易察觉定时炸弹,好比对null拆箱时引起的空指针异常NPE。本文就将JLS中关于数值提高的机制译述出来,便于更深入地理解代码后面的东西。web

问题

如下几段代码为何是这样的运行结果:数组

Object k = true ? null : 1;
System.out.println(k);

// 输出:
null
Integer a = null;
Object k = true ? a : 1;
System.out.println(k);

// 运行时报NPE错误
byte a = 2;
byte k = true ? a : 128;
System.out.println(k);

// 编译时报错。 不兼容的类型: 从int转换到byte可能会有损失

数值提高

数字类型提高机制被用于算术运算符上,一般使用场景为:svg

  • 同一类型转换
    虽然并没有什么做用,但有时可使代码更清晰。
  • 拓宽原始类型转换
    指byte、short、int、long、float、double由低向高转换。
  • 自动拆箱转换
    基础类型引用类的拆箱方法,如r.intValue()

数值提高用于将算术运算中的操做数转化为一个相同的类型以便于运算,具体分为两种状况:一元数值提高和二元数值提高。oop

一元数值提高

某些运算符将一元数值提高用在了单操做数运算中,其一定能获得一个数字类型的值,规则以下:编码

  • if 操做数是编译时类型ByteShortCharacterInteger,那么它会先拆箱为对应的原始类型,而后拓宽为int类型
  • else if 操做数为编译时类型LongFloatDouble,那么就直接拆箱为对应的原始类型。
  • else if 操做数是编译时类型byteshortcharint,那么就拓宽为int类型
  • else 保持原样。

一元数值提高还用在如下情境的表达式中(提高为int):指针

  • 数组建立表达式的维度
  • 数组索引表达式的索引
  • 正号运算符(+)的操做数
  • 负号运算符(-)的操做数
  • 按位补运算符(~)的操做数
  • 移位运算符(>>, >>>, << )的每个操做数。注意移位运算并不会使两边的操做数提高到相同类型,如 A << B 中若B为long类型,A并不会被提高到long

注意:自增和自减单目运算符一样也会进行类型提高,但运算后会自动进行强制类型转换,如
byte a = 127; a++; // a在运算后为int类型,转为byte截断后变成-128
等价于
byte a = (byte)128;code

例:xml

class Test {
    public static void main(String[] args) {
        byte b = 2;
        int a[] = new int[b];  // 维度表达式提高
        char c = '\u0001';
        a[c] = 1;              // 索引表达式提高
        a[0] = -c;             // 负号 提高
        System.out.println("a: " + a[0] + "," + a[1]);
        b = -1;
        int i = ~b;            // 按位补提高
        System.out.println("~0x" + Integer.toHexString(b)
                           + "==0x" + Integer.toHexString(i));
        i = b << 4L;           // 移位提高(左操做数)
        System.out.println("0x" + Integer.toHexString(b)
                           + "<<4L==0x" + Integer.toHexString(i));
    }
}

输出:
a: -1,1
~0xffffffff==0x0
0xffffffff<<4L==0xfffffff0

二元数值提高

二元运算符的操做数皆可转化为数字类型时,那么将采用以下二元数值提高规则:索引

  • 若是任一操做数为引用类型,那么对其进行自动拆箱。
  • 拓宽类型转换被应用于如下状况:
    • if 某一操做数为double类型,那么另外一个也转为double
    • else if 某一操做数为float类型,那么另外一个也转为float
    • else if 某一操做数为long类型,那么另外一个也转为long
    • else 两个操做数都转为int

二元数值提高应用于如下运算符上:编译器

  • 乘法运算符: * 、 / 、%
  • 针对数字类型的加减运算符: + 、 -
  • 数值比较运算符:< 、<= 、> 、>=
  • 数值相等比较运算符: == 、 !=
  • 整数按位运算符: & 、^ 、|
  • 某些状况下的条件运算符 ? : 中,后面将详解

注意:混合赋值运算符一样也会自动进行强制类型转换,如
byte a = 127; a += 1; // a在运算后为int类型,转为byte截断后变成-128

例:

class Test {
    public static void main(String[] args) {
        int i    = 0;
        float f  = 1.0f;
        double d = 2.0;
        // int*float 先是被提高为 float*float,而后
        // float==double 被提高为 double==double:
        if (i * f == d) System.out.println("oops");
		
        // char&byte 被提高为 int&int:
        byte b = 0x1f;
        char c = 'G';
        int control = c & b;
        System.out.println(Integer.toHexString(control));
		
        // 此处 int:float 被提高为 float:float:
        f = (b==0) ? i : 4.0f;
        System.out.println(1.0/f);
    }
}

输出:
7
0.25

补充:条件运算符(? :)中的类型提高

三元运算符的数值提高机制较为复杂,这儿详细介绍分析一下。

条件运算符? : 介绍

条件运算符? : 在语法上是右结合的(从右到左结合)。所以, a?b:c?d:e?f:g 等价于 a?b:(c?d:(e?f:g))

条件运算符由三个表达式构成,第一个表达式的值类型必须为 booleanBoolean,不然将产生编译时错误。

当第二个或第三个表达式为void方法时,也将抛出编译时错误。

条件运算符的类型

条件表达式最终产生的类型取决于下述状况:

  • if 第二个操做数和第三个操做数有相同的类型(能够都为null),那么它就是条件表达式的类型。
  • else if 两个操做数中有一个的类型为原始类型T,而另外一个为T的装箱类型,那么条件表达式的类型就是T。
  • else if 其中一个操做数是编译时null类型,另外一个为引用类型,那么条件表达式的类型就是该引用类型。
  • else if 两个操做数均可转化为数字类型,那么分为如下状况:
    • if 其中一个类型为byteByte,另外一个类型为shortShort,那么条件表达式类型为short。
    • else if 其中一个类型为T,T为byteshortchar,另外一个类型为int类型的常量表达式,且可用T来表达(在T可表示的范围内),那么条件表达式类型为T。
    • else if 其中一个类型为T,T为ByteShortCharacter,另外一个类型为 int类型的常量表达式,且可用U来表达(U为T的拆箱类型),那么条件表达式类型为U。
    • else 对两个操做数使用二元数值提高机制(并无真的去转换类型),获得的相同数值类型就是条件表达式的类型。

以上操做仅用于编译器判断条件表达式的最终类型T,只有在最终选择的操做数(第二个表达式的或第三个表达式的)与T不符时才会进行自动拆箱/类型提高操做

总结一下,运算中的类型提高一般都是将低于int位数的类型提高为int,高于int的拆箱后保持不变,两边操做数位数不一样则升为高精度的那一个类型。

解题

最后咱们分析下开头的几个问题:

Object k = true ? null : 1;
System.out.println(k);

// 输出:
null

注意上面的null为编译时类型,1此时会自动装箱为Integer类型,此时按照上述规则条件表达式的类型为Integer类型。由于条件表达式的结果为操做数null,因此k的实际类型为Integer,值为null

Integer a = null;
Object k = true ? a : 1;
System.out.println(k);

// 运行时报NPE错误

上面条件表达式中的a在编译时被识别为Integer类型,而非null类型,按照上述规则条件表达式的最终类型为int类型。由于条件表达式的结果操做数a与最终类型不符,因此此时将对a进行自动拆箱操做(a.intValue()),因为a运行时为null,所以将报NPE错误。

byte a = 2;
byte k = true ? a : 128;
System.out.println(k);

// 编译时报错。 不兼容的类型: 从int转换到byte可能会有损失

因为128超出了byte的范围,所以返回值为inta将转化为int返回,而接收方kbyte类型,向低精度转化时须要显示强制转换才行。