同样是加1,为什么byte的三种写法结果不同?
旧的“数据类型与变量”笔记从字面量、类型分类和变量定义开始,但还没有走到真正容易出错的地方。与其补一张背诵表,不如先看三个很短的片段:
byte b = 126; b = b + 1; // 编译失败 byte b = 126; b += 1; // 得到127 byte b = 127; b += 1; // 得到-128,不抛异常为什么明明能算出127,第一种写法却过不了编译?为什么第二种允许,第三种又不帮我们检查越界?这三个现象分别涉及表达式类型、赋值上下文和窄化转换。
本文用Corretto JDK 17.0.12编译和运行,附录是完整程序;编译失败的片段各自放进完整类,不混进正常程序。
1. 结果在范围内,不等于表达式类型就是byte
普通变量b参加b + 1运算时,发生数值提升,表达式结果类型是int。再把它赋给byte,需要窄化。
public class BadSum { public static void main(String[] args) { byte b = 126; b = b + 1; } }这段编译失败。编译器不会因为我们目测这一刻的b是126,就为一般变量表达式自动做范围证明。对比以下常量:
byte b = 126 + 1; // 可以:常量表达式的值127可表示 byte b = 127 + 1; // 不可以:128超出范围所以“右边是int就永远不能赋给byte”也不准确。赋值上下文允许部分可表示的整型常量表达式隐式窄化。JLS 17类型转换与赋值上下文。
2. +=允许的不是“安全加法”,而是隐含转换
对于这里的简单变量,b += 1的作用可以理解成b = (byte)(b + 1):先按提升后的类型计算,再转换回左边的类型。它能编译,不承诺转换后保留原来的数学数值。
当b是127,int结果为128;窄化到有符号8位byte后得到-128。图表示转换路径,不是JVM对象内存布局。
更精确地说,复合赋值的左边只求值一次。若左边包含数组下标自增,不能机械改写成两个i++。完整程序也检查了这一点。JLS复合赋值。
业务里要限制取值,应该先在较宽类型里计算并检查范围,而不是见到+=能编译就认为数据安全:
int next = b + 1; 先检查 next 是否在允许范围内;检查通过才转换回 byte。这里的业务范围可能比byte范围更窄。例如音量只允许0到100,仅防止byte溢出还不够。
3. final也不等于编译期常量
final int n = 126; byte b = n + 1;可以编译,因为这里n是常量变量,其初始化来自常量表达式。
但以下完整程序不能编译。即使函数这次总返回126,方法调用也不是这里所需的常量表达式:
public class BadFinal { static int initial() { return 126; } public static void main(String[] args) { final int n = initial(); byte b = n + 1; } }需要区分“赋值后不再改变”与“编译阶段符合常量表达式规则”。同理,128本身也不会因为是字面量就获得豁免:
public class BadRange { public static void main(String[] args) { byte b = 128; } }4. 赋值能接受127,为什么传参不行?
public class BadArgument { static void take(byte value) { } public static void main(String[] args) { take(127); } }它也编译失败。127是int字面量,调用上下文并不照搬赋值上下文的常量窄化规则。先用byte b = 127; take(b);则可以。这是规则所在上下文不同,不是数值突然变了。JLS调用上下文。
5. 怎样验证,而不是只贴几行“预期”?
| 验证对象 | 检查方式 | 期望 |
|---|---|---|
| 普通变量相加后赋回byte | 单独编译BadSum | 类型不兼容 |
| final由方法调用初始化 | 单独编译BadFinal | 类型不兼容 |
| 128赋给byte | 单独编译BadRange | 类型不兼容 |
| int常量直接传byte参数 | 单独编译BadArgument | 类型不兼容 |
| 正常程序 | 编译后运行并核对输出 | 五项检查通过 |
| 错误的范围判断 | 把127加1仍期待127 | 断言失败 |
本地验证保存了各次javac诊断,不把“进程失败”直接当成预期成立:找不到JDK、文件名不匹配或语法写坏都不是我们要验证的失败原因。测试也对照成功程序和具体类型转换诊断。
下面是正常程序的实际输出,失败片段不会产生对应运行输出:
promoted=127 compound=127 narrowed=-128 constant=127 index=1 value=127 CHECKS_PASSED=56. 回头看旧笔记
字面量是源码里的写法,类型是表达式与值的规则,变量则承载值。不要把“字符串常量、空常量”这种入门分类直接写成基本类型清单;String不是基本类型,null也不是第九种基本类型。Java输出方法是System.out.println,不是System.Out.println。
这次不试图一篇讲完全部Java类型。读者真正需要带走的是:判断一个转换先问表达式是什么类型,再问处于赋值、调用还是复合赋值上下文,最后检查窄化会不会丢失需要的值。
附录:完整可运行程序
将以下类作为ByteConversionLab.java,用JDK 17执行javac ByteConversionLab.java,再执行java ByteConversionLab。它使用显式断言方法,不依赖-ea。
public class ByteConversionLab { private static int checks; private static void check(boolean ok, String message) { if (!ok) throw new AssertionError(message); checks++; } public static void main(String[] args) { byte first = 126; int promoted = first + 1; check(promoted == 127, "promoted sum"); System.out.println("promoted=" + promoted); byte compound = 126; compound += 1; check(compound == 127, "compound within range"); System.out.println("compound=" + compound); byte edge = 127; edge += 1; check(edge == -128, "narrowed byte wraps"); System.out.println("narrowed=" + edge); final int n = 126; byte constant = n + 1; check(constant == 127, "constant assignment"); System.out.println("constant=" + constant); byte[] cells = {126, 10}; int index = 0; cells[index++] += 1; check(index == 1 && cells[0] == 127 && cells[1] == 10, "compound evaluates left side once"); System.out.println("index=" + index + " value=" + cells[0]); System.out.println("CHECKS_PASSED=" + checks); } }