3.1 表达式、赋值与求值顺序
前置知识:第 2 章变量、类型与转换
预计时间:25 分钟
练习环境:JDK 17+ 与 Bash;首个示例写入临时目录
完成标志:能区分表达式与语句,并用括号写出清楚的求值意图
表达式、值与语句
public class Checkout {
public static void main(String[] args) {
int apples = 5;
double price = 3.5;
double total = apples * price;
System.out.println(total);
}
}先建立临时目录;运行到 mkdir out 后,把上面的代码保存为当前目录中的 Checkout.java,再编译运行:
chapter3_workdir=$(mktemp -d)
readonly chapter3_workdir
cd "$chapter3_workdir"
mkdir out
javac -d out Checkout.java
java -cp out Checkout输出是 17.5。
下面的表达式有类型,正常求值后会得到值。另一些表达式不产生值,例如调用返回 void 的 println(...);求值也可能因为异常而突然结束。先看有值的例子:
| 代码 | 类型或作用 |
|---|---|
5 | int 字面量表达式 |
apples | 变量访问表达式,类型为 int |
apples * price | 数值表达式,经提升后类型为 double |
apples > 3 | 关系表达式,类型为 boolean |
total = apples * price | 赋值表达式,类型和值来自赋值后的左侧变量 |
double total = ...; 整体是局部变量声明语句,不是表达式。total = ...; 则是赋值表达式作为表达式语句使用,计算出的赋值结果被丢弃。Java 的“赋值本身也是表达式”解释了链式赋值为何可以编译:
int left;
int right;
left = right = 10;链式写法不适合所有场景;类型不同或意图不清时分行更好。
操作数按从左到右求值
Java 对操作数求值顺序有明确规定。先看:
public class EvaluationOrder {
static int mark(String label, int value) {
System.out.println(label);
return value;
}
public static void main(String[] args) {
int result = mark("left", 2) + mark("right", 3);
System.out.println(result);
}
}输出顺序是 left、right,结果为 5。优先级决定表达式如何分组,不会把右操作数提前到左操作数之前求值。
复杂表达式中混入赋值、++ 或方法副作用仍然难读。语言规则能告诉你结果,却不能让糟糕结构变清楚。把副作用拆成独立语句。
优先级、结合性与括号
int result = 10 + 3 * 2; // 16乘法优先级高,所以分组等价于 10 + (3 * 2)。
int difference = 20 - 5 - 3; // 12减法同级且左结合,因此等价于 (20 - 5) - 3。赋值通常右结合,这使 left = right = 10 等价于 left = (right = 10)。
括号应表达业务分组,而不是机械包住所有记号:
boolean eligible = isMember && (spent >= minimumSpend);
double finalPrice = eligible ? total * discountRate : total;不要依赖一张不完整的优先级口诀处理生产代码。涉及三种以上操作符,或读者需要停顿才能理解时,用命名中间变量和括号拆开。
字符串连接也是 +
如果任一相关操作数触发字符串连接,+ 会把值转成文本并连接。它仍按左结合分组:
System.out.println("total=" + 1 + 2); // total=12
System.out.println("total=" + (1 + 2)); // total=3日志或循环中大量连接可能产生额外对象,后面会学习参数化日志和 StringBuilder。本章先把括号写对。
复合赋值不只是缩写
int count = 10;
count += 3;对简单变量,它看起来像 count = count + 3。但复合赋值还包含隐式转换,并且左侧只求值一次:
byte level = 127;
level += 1;
System.out.println(level); // -128level = level + 1 不能直接编译,因为 level + 1 是 int。level += 1 会把结果隐式转换回 byte,所以也可能静默窄化。
对数组元素、字段或带副作用的左侧表达式,两种写法的求值次数也可能不同。不要把复合赋值简单替换成“读一次、算一次、写一次”以外的随意展开。
条件操作符 ?:
String result = score >= 60 ? "pass" : "retry";先求条件,只求两个分支中的一个。它适合选择一个短值,不适合塞入多层业务流程:
String label = value == null
? "missing"
: value.isBlank() ? "blank" : "present"; // 能编译,但阅读成本高改成命名方法或 if 通常更清楚。两个分支的类型还会影响整个条件表达式的类型,涉及 null、装箱或不同数值类型时不要凭直觉猜,交给编译器并写测试。
专业速查
| 问题 | 答案 |
|---|---|
| 表达式一定产生值吗 | 不一定;void 调用不产生值,求值也可能因异常突然结束 |
| 赋值是表达式吗 | 是;变量声明整体不是表达式 |
| 操作数顺序 | Java 通常按语法规定从左到右求值 |
| 优先级决定什么 | 语法分组;不等同于任意重排求值 |
| 复合赋值等同普通赋值吗 | 有隐式转换和左侧单次求值差异 |
| 三元何时合适 | 简短、无副作用的二选一值 |
离开这一页前
确认自己能区分声明语句与赋值表达式、优先级与求值顺序,并能预测字符串连接的左结合结果。然后清理首个示例:
cd
ls -ld -- "$chapter3_workdir"
rm -r -- "$chapter3_workdir"