Java数值类型提升机制(三目条件运算符与空指针问题)

Posted 小杨Vita

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java数值类型提升机制(三目条件运算符与空指针问题)相关的知识,希望对你有一定的参考价值。

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

问题

以下几段代码为什么是这样的运行结果:

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可能会有损失

数值提升

数字类型提升机制被用于算术运算符上,通常使用场景为:

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

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

一元数值提升

某些运算符将一元数值提升用在了单操作数运算中,其必定能得到一个数字类型的值,规则如下:

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

以上自动拆箱均可能出现NPE情况。

一元数值提升还用在以下情境的表达式中(提升为int):

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

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

例:

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

二元数值提升

二元运算符的操作数皆可转化为数字类型时,那么将采用如下二元数值提升规则:

  • 如果任一操作数为引用类型,那么对其进行自动拆箱(可能触发NPE)。
  • 以下情况会出现向上转型(拓宽类型转换):
    • 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 两个操作数中有一个的类型为原始类型,而另一个为它的装箱类型,那么条件表达式的最终类型就是该原始类型。(此时可能触发 自动拆箱
  • else if 其中一个操作数是编译期null类型(即写死的null),另一个为引用类型,那么条件表达式的类型就是该引用类型。
  • else if 两个操作数都可转化为数字类型,那么分为以下情况:
    • if 其中一个类型为byteByte,另一个类型为shortShort,那么条件表达式类型为short。(自动拆箱+只升到short)
    • else if 其中一个类型为t(t为byteshortchar中一种),另一个类型为int常量表达式,且在t可表示的范围内,那么最终类型t。(不类型提升)
    • else if 其中一个类型为T(T为ByteShortCharacter中一种),另一个类型为 int常量表达式,且在T的拆箱类型的范围内,那么条件表达式类型为T的拆箱类型。(自动拆箱+不类型提升)
    • else 对两个操作数使用上述的二元数值提升机制算法 计算出的类型即为最终类型(只套用算法,并没有真的去转换类型)(可能出现自动拆箱)。

以上操作仅用于编译器判断条件表达式的最终类型x,当最终选择的那个结果操作数(第二个表达式的或第三个表达式的)与x不符时会进行自动拆箱类型提升操作,注意此时可能触发NPE,也就是对null进行拆箱时出现空指针。

总结一下,运算中的类型提升通常都是将低于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类型,向低精度转化时需要显示强制转换才行。

以上是关于Java数值类型提升机制(三目条件运算符与空指针问题)的主要内容,如果未能解决你的问题,请参考以下文章

Java学习-----01.Java基础

关于java 三目运算符

java(

JAVA三目运算符Char类型的输出结果

Java 三目运算符?:简单使用

java啥叫“三目条件运算符”?