Java基础(001):关于 short i = 1; i += 1;

注:如未特别说明,Java语言规范 jls 均基于JDK8,使用环境是 eclipse4.5 + win10 + JDK 8

  本篇的知识点,主要是涉及到 Java 中一些比较常见的默认窄化处理(Java编译器自动添加的),这里将从一个问题开始,据说这也是一道常见的笔试题/面试题:

  • 为什么 short i = 1; i += 1; 可以正确编译运行而 short i = 1; i = i + 1; 会出现编译错误?
  • 其他说法:都放在一起编译会出现有什么结果,哪一行报错?为什么?

笔者注:其实这其中会涉及到一些编译优化和底层的知识,限于知识面,本篇不涉及,如有需要,可自行搜索。

  本文的目录结构如下:

1、结论

  关于开篇提出的问题,这里先直接给出结论:

  • Java语言规范规定基础数据类型运算默认使用32位精度的int类型[13]
  • 只要是对基本类型做窄化处理的,例如 long -> int -> short -> char,都需要做强制转换,有些是Java编译器默认添加的,有的则是代码中显式做强制转换的。
  • short i = 1; i += 1; 可以正确编译运行是因为Java编译器自己添加了强制窄化处理,即对于任何的T a; X b; a += b;等价于T a; X b; a = (T) (a + b); Java编译器会默认做这个显式强制转换(尽管有时候会出现精度问题,例如 b 是 float 、 double 类型,强烈建议不要有这样的操作)。前面的i += 1其实就等价于i = (int) (i + 1),即便将数字1换成是double类型的1.0D也是如此。
  • short i = 1; i = i + 1;编译不通过的原因就很明显了:无论是代码中,还是Java编译器,都没有做强制转换,int 类型直接赋给 short ,因此编译出错
  • 对于常量(数字常量、常量表达式、final常量等),Java编译器同样也可以做默认的强制类型转换,只要常量在对应的数据范围内即可

2、详解

  接下来讲详细分析为什么 short i = 1; i += 1; 可以正确编译而 short i = 1; i = i + 1; 则会编译失败。先列一下搜出来的一些令人眼前一亮(or 困惑)的代码[16](来源 Java语言规范 jls 和 stackoverflow ,详细链接见参考):

public static void main(String[] args) {
    // 注:short ∈ [-32768, 32767]
    {
        /*
         * 1、对于 +=, -=, *=, /=, Java编译器默认会添加强制类型转换,
         * 即 T a; X b; a += b; 等价于 T a; X b; a = (T) (a + b);
         */

        // 0是int类型的常量,且在short范围内,被Java编译器默认强制转换的
        short i = 0;
        i += 1;     // 等价于 i = (short) (i + 1);
        System.out.println("[xin01] i=" + i); // 输出结果: 1
        i = (short) (i + 1);
        System.out.println("[xin02] i=" + i); // 输出结果: 2
        /*
         * 下面这2行都会有编译报错提示:
         * Exception in thread "main" java.lang.Error: Unresolved compilation problem:
         *     Type mismatch: cannot convert from int to short
         * [注]错误: 不兼容的类型: 从int转换到short可能会有损失
         * Eclipse 也会有提示: Type mismatch: cannot convert from int to short
         */
        // i = i + 1;
        // i = 32768;

        i = 0;
        i += 32768; // 等价于 i = (short) (i + 32768); 下同
        System.out.println("[xin03] i=" + i); // 输出结果: -32768
        i += -32768;
        System.out.println("[xin04] i=" + i); // 输出结果: 0

        i = 0;
        long j = 32768;
        i += j;
        System.out.println("[xin05] i=" + i); // 输出结果: -32768

        i = 0;
        float f = 1.23F;
        i += f;
        System.out.println("[xin06] i=" + i); // (小数位截断)输出结果: 1

        i = 0;
        double d = 4.56D;
        i += d;
        System.out.println("[xin07] i=" + i); // (小数位截断)输出结果: 4
        
        i = 10;
        i *= 3.14D;
        System.out.println("[xin08] i=" + i); // 输出结果: 31
        
        i = 100;
        i /= 2.5D;
        System.out.println("[xin09] i=" + i); // 输出结果: 40
    }

    {
        /*
         * 2、常量表达式和编译器优化: 常量折叠
         */
        // 2 * 16383 = 32766
        // (-2) * 16384 = -32768
        // 都在 short 范围内,常量表达式在编译优化后直接用对应的常量结果,然后编译器做强制转换
        short i = 2 * 16383;        // 等价于 short i = (short) (2 * 16383);
        short j = (-2) * 16384;
        // 2 * 16384 = 32768,超过 short 范围,编译器不会做转换
        // Type mismatch: cannot convert from int to short
        // short k = 2 * 16384;
        
        // 常量表达式在编译优化后直接用对应的常量结果,然后编译器做强制转换
        short cThirty = 3 * 10;
        short three = 3;
        short ten = 10;
        // Type mismatch: cannot convert from int to short
        // short thirty = three * ten;

        final short fTthree = 3;
        final short fTen = 10;
        // 常量表达式在编译优化后直接用对应的常量结果,然后编译器做强制转换
        short fThirty = fTthree * fTen;
        
        final short a = 16384;
        final short b = 16383;
        // 常量表达式在编译优化后直接用对应的常量结果,然后编译器做强制转换
        short c = a + b;
    }
}

    接下来根据代码罗列的两部分分别进行说明:

2.1、对于 +=, -=, *=, /=, Java编译器默认会添加强制类型转换,即 T a; X b; a += b; 等价于 T a; X b; a = (T) (a + b);

  这个结论其实来自于 Java语言规范 jls 对复合赋值运算符(Compound Assignment Operators)的相关描述,具体参考[1]15.26.2. Compound Assignment Operators https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.26.2,并且还举了相关的例子,如下:


   A compound assignment expression of the form E1 op= E2 is equivalent to E1 = (T) ((E1) op (E2)), where T is the type of E1, except that E1 is evaluated only once.

  For example, the following code is correct:

short x = 3;
x += 4.6;

  and results in x having the value 7 because it is equivalent to:

short x = 3;
x = (short)(x + 4.6);

   stackoverflow 上的相关讨论可以参考[2]Why don't Java's +=, -=, *=, /= compound assignment operators require casting? https://stackoverflow.com/questions/8710619/why-dont-javas-compound-assignment-operators-require-casting

笔者注:

  • 实际上,直接加上强制类型转换的写法,也是大家都熟悉且理解起来最清晰的方式,可以避免可能潜在的类型不匹配时出现的精度损失问题,使用的时候需要注意。当然,笔者认为这些方式都没有好坏之分,正确地使用即可。
  • Java从语言规范层面对此做了限制。有兴趣的还可以通过 class文件 和 javap -c 反汇编对所使用的字节码作进一步的研究。

  知道了Java语言相关的规范约定,我们就可以看出,与之对应的是以下这种出现编译错误的写法(报错提示:Type mismatch: cannot convert from int to short):

short i = 1;
// i + 1 是 int 类型,需要强制向下类型转换
i = i + 1;

  

2.2、常量表达式和编译器优化: 常量折叠

  需要注意的是,前面的示例short x = 3;中的3其实默认是 int 类型,但是却可以赋值给short类型的x。这里涉及到到的其实是 常量表达式

  Java语言规范[3]5.2. Assignment Conversion https://docs.oracle.com/javase/specs/jls/se8/html/jls-5.html#jls-5.2中提到:


  In addition, if the expression is a constant expression (§15.28) of type byte, short, char, or int:

  • A narrowing primitive conversion may be used if the type of the variable is byte, short, or char, and the value of the constant expression is representable in the type of the variable.
  • A narrowing primitive conversion followed by a boxing conversion may be used if the type of the variable is:
    • Byte and the value of the constant expression is representable in the type byte.
    • Short and the value of the constant expression is representable in the type short.
    • Character and the value of the constant expression is representable in the type char.

   对于常量表达式,其结果是可以自动做窄化处理的,只要是在对应的数据类型范围内,Java编译器就进行做默认强制类型转换。

  关于常量表达式,Java语言规范中[4]15.2. Forms of Expressions https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.2 中提到了有一类可以在编译期就确定值的表达式:常量表达式


  Some expressions have a value that can be determined at compile time. These are constant expressions (§15.28).


   具体说明参考[5]15.28. Constant Expressions https://docs.oracle.com/javase/specs/jls/se8/html/jls-15.html#jls-15.28, 如下都是相关的常量表达式例子:

true
(short)(1*2*3*4*5*6)
Integer.MAX_VALUE / 2
2.0 * Math.PI
"The integer " + Long.MAX_VALUE + " is mighty big."

   另外还有关于final变量的说明[7]4.12.4. final Variables https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.12.4


  A constant variable is a final variable of primitive type or type String that is initialized with a constant expression (§15.28).


   通过常量表达式赋值的final变量都是常量,这种也是编译期可以确认最终值,会通过编译优化直接赋予最终值,而且可以直接依靠编译器做窄化处理。

  相关讨论参考 [8]Java char to byte casting https://stackoverflow.com/questions/30346587/java-char-to-byte-casting

  对于这一块,其实对应的是一个比较基础的编译器优化:常量折叠(Constant Folding),有兴趣的可以自行搜索。stackoverflow 上由相关的讨论,参考[9]、[10]、[11]

笔者注:

  • 尽管常量表达式最终都被编译器优化为直接值,但是为了清晰,提高可读性、可维护性,代码中没必要对常量直接换算,例如一天 24 * 60 * 60 秒,其实可以分别用可读性更强的final常量来表示。
  • Bloch大神的 Java Puzzlers 中也有相关的一些说明,有兴趣的可以去看看

3、参考

原文地址:https://www.cnblogs.com/wpbxin/p/14618755.html