"黄俪在写飞翔商城的促销逻辑时发现了一行精妙的代码:
discount = isVip ? 0.15f : 0.05f;。'这就是三元运算符的魅力——一行顶四行if-else。'小崔凑过来看:'那total += price里的+=有没有坑?'白歌推了推眼镜:'有坑。+=自带隐式类型转换,byte b = 1; b += 128;编译通过但结果很诡异——因为+=会做强制截断。'"
赋值运算符
概述
赋值运算符将右侧的值存入左侧的变量。Java SE 8 提供了基本赋值符 = 和12种复合赋值运算符(+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=、>>>=)。赋值操作本身是一个表达式,它的值是赋值后左侧变量的值,因此Java支持链式赋值。
操作符行为表
| 操作符 | 含义 | 等价展开 | 示例(假设 int a = 10) | 运算后a的值 |
|---|---|---|---|---|
= | 基本赋值 | a = b | a = 5 | 5 |
+= | 加后赋值 | a = a + b | a += 3 | 13 |
-= | 减后赋值 | a = a - b | a -= 3 | 7 |
*= | 乘后赋值 | a = a * b | a *= 2 | 20 |
/= | 除后赋值 | a = a / b | a /= 3 | 3 |
%= | 取模后赋值 | a = a % b | a %= 3 | 1 |
&= | 按位与后赋值 | a = a & b | a &= 3 (1010&0011) | 2 |
|= | 按位或后赋值 | a = a | b | a |= 3 (1010|0011) | 11 |
^= | 按位异或后赋值 | a = a ^ b | a ^= 3 (1010^0011) | 9 |
<<= | 左移后赋值 | a = a << b | a <<= 1 | 20 |
>>= | 带符号右移后赋值 | a = a >> b | a >>= 1 | 5 |
>>>= | 无符号右移后赋值 | a = a >>> b | a >>>= 1 | 5(正数同>>) |
深度原理分析
一、复合赋值运算符的隐式类型转换(核心陷阱)
复合赋值运算符(如 +=)与普通展开式(a = a + b)有一个关键区别:复合赋值会执行一个隐式的强制类型转换。
byte b = 10;
// 以下两行看似等价,实则不同:
b = b + 1; // ❌ 编译错误!b + 1 是 int,不能赋值给 byte
b += 1; // ✅ 编译通过!等价于 b = (byte)(b + 1)
原理:JLS(Java语言规范)规定,复合赋值表达式 E1 op= E2 等价于 E1 = (T)((E1) op (E2)),其中 T 是 E1 的类型。即复合赋值自带一次强制类型转换。
这个隐式转换在边界情况下非常危险:
byte b = 100;
b += 100; // 正常:b = (byte)(100 + 100) = (byte)200 = -56(溢出截断!)
System.out.println(b); // -56 —— 你以为200,实际上byte溢出了
二、赋值表达式的值——"赋值即表达式"
在Java中,赋值操作本身是一个表达式,它的值是赋值后左侧变量的值。这允许链式赋值:
int a, b, c;
a = b = c = 100; // 从右向左结合
// c = 100 → 表达式值为100
// b = 100 → 表达式值为100
// a = 100
// 也允许在表达式中间赋值(但应避免滥用):
int x = 5;
int y = (x = 10) * 2; // x变成10,表达式的值是10,y = 20
完整示例一:飞翔科技工资核算系统
场景
小崔负责飞翔科技的工资核算模块,需要用复合赋值累加各项津贴。
/**
* 飞翔科技工资核算 —— 复合赋值运算符
*/
public class PayrollCalculator {
public static void main(String[] args) {
System.out.println("===== 飞翔科技工资核算 feixiang.net =====\n");
float baseSalary = 8888.88f; // 基础工资
float techAllowance = 1500.00f; // 技术津贴
float transportAllowance = 300.00f; // 交通补贴
float mealAllowance = 500.00f; // 餐补
float insurance = 800.00f; // 五险一金扣除
// --- 复合赋值运算符:累加各项 ---
float totalSalary = 0f;
totalSalary += baseSalary; // 等价 totalSalary = totalSalary + baseSalary
System.out.println("基础工资: ¥" + baseSalary);
totalSalary += techAllowance; // += 技术津贴
System.out.println("技术津贴: ¥" + techAllowance);
totalSalary += transportAllowance;
System.out.println("交通补贴: ¥" + transportAllowance);
totalSalary += mealAllowance;
System.out.println("餐补: ¥" + mealAllowance);
totalSalary -= insurance; // -= 扣除五险一金
System.out.println("五险一金: -¥" + insurance);
System.out.println("----------------------------");
System.out.println("应发工资: ¥" + totalSalary);
// --- 链式赋值:初始化多个计数器 ---
int monthCount, quarterCount, yearCount;
monthCount = quarterCount = yearCount = 0; // 全部清零
System.out.println("\n===== 考勤统计 =====");
monthCount += 22; // 本月出勤22天
quarterCount += monthCount; // 本季度累加
System.out.println("本月出勤: " + monthCount + " 天");
System.out.println("季度累计: " + quarterCount + " 天");
}
}
运行输出:
===== 飞翔科技工资核算 feixiang.net =====
基础工资: ¥8888.88
技术津贴: ¥1500.0
交通补贴: ¥300.0
餐补: ¥500.0
五险一金: -¥800.0
----------------------------
应发工资: ¥10388.88
===== 考勤统计 =====
本月出勤: 22 天
季度累计: 22 天
完整示例二:飞翔商城促销折扣系统
场景
黄俪为飞翔商城(feixiang.net)开发促销模块,演示复合赋值运算符的各种用法和陷阱。
/**
* 飞翔商城促销折扣系统
* 演示复合赋值运算符
*/
public class PromoDiscount {
public static void main(String[] args) {
System.out.println("===== 飞翔商城 feixiang.net 促销系统 =====\n");
float productPrice = 188.88f; // 商品原价
int quantity = 3; // 购买数量
boolean isVip = true; // 是否VIP会员
boolean isDoubleEleven = false; // 是否双十一
// --- 复合赋值:计算折后价 ---
float finalPrice = productPrice;
// 双十一全场8折
if (isDoubleEleven) {
finalPrice *= 0.8f; // 等价 finalPrice = finalPrice * 0.8
}
// VIP额外95折
if (isVip) {
finalPrice *= 0.95f; // *= 自动强制转换为float
}
// 满减:满500减50
float totalPrice = finalPrice * quantity;
if (totalPrice >= 500) {
totalPrice -= 50; // -= 满减
}
System.out.println("商品原价: ¥" + productPrice);
System.out.println("购买数量: " + quantity);
System.out.println("VIP折扣后单价: ¥" + finalPrice);
System.out.println("总价(已含满减): ¥" + totalPrice);
// --- 赋值表达式的值:在日志中嵌入赋值 ---
System.out.println("\n===== 库存变动 =====");
int stock = 100;
System.out.println("当前库存: " + stock);
// 赋值表达式的值就是赋值后的值,可以嵌入到打印中
System.out.println("下单后库存: " + (stock -= quantity));
System.out.println("退货后库存: " + (stock += 1));
// --- 复合赋值 += 的隐式转换陷阱演示 ---
System.out.println("\n===== byte 的 += 陷阱 =====");
byte b = 100;
System.out.println("原始: b = " + b);
b += 28; // 编译通过,但溢出
System.out.println("b += 28 → " + b + "(期望128,实际溢出了!)");
System.out.println("原因: byte范围-128~127,(byte)128 = -128");
// 对比:b = b + 28 会直接编译失败
// byte c = 100;
// c = c + 28; // ❌ 编译错误:int不能赋给byte
}
}
运行输出:
===== 飞翔商城 feixiang.net 促销系统 =====
商品原价: ¥188.88
购买数量: 3
VIP折扣后单价: ¥179.436
总价(已含满减): ¥488.308
===== 库存变动 =====
当前库存: 100
下单后库存: 97
退货后库存: 98
===== byte 的 += 陷阱 =====
原始: b = 100
b += 28 → -128(期望128,实际溢出了!)
原因: byte范围-128~127,(byte)128 = -128
易错场景
反例一:+= 的隐式转换导致溢出
// ❌ 危险:复合赋值会静默截断
byte value = 100;
value += 200; // 编译通过!但 value 变成 44(300 - 256 = 44,实际更复杂)
System.out.println(value); // 44 —— 与预期完全不符
纠正:
// ✅ 正确:对byte/short使用复合赋值时,先升级为int计算,再判断范围
int temp = 100;
temp += 200; // 300,安全
if (temp >= Byte.MIN_VALUE && temp <= Byte.MAX_VALUE) {
byte value = (byte) temp;
} else {
System.out.println("溢出!值 " + temp + " 超出 byte 范围");
}
面试考点
Q1:a += b 和 a = a + b 有什么区别?
(1)隐式类型转换:
a += b等价于a = (type of a)(a + b),复合赋值自动执行强制类型转换,而a = a + b不会。因此byte b = 1; b += 128;编译通过(但溢出),而b = b + 128;编译失败(int不能赋给byte)。(2)求值次数:在
a += b中,a只被求值一次;在a = a + b中,a被求值两次。对于简单变量无差别,但对于array[index++] += 1这种,差异显著——index++只执行一次。
Q2:赋值语句本身有返回值吗?Java支持链式赋值吗?
有。赋值表达式
a = b的值就是赋值后a的值。因此Java支持链式赋值:a = b = c = 0;,从右向左执行。这也意味着可以在更大表达式中嵌入赋值:int y = (x = 10) * 2;(不推荐,降低可读性)。
Q3:== 和 = 混淆的典型bug及如何防范?
经典bug:
if (flag = true)错把赋值当比较。在Java中,if条件要求boolean类型,所以如果flag是boolean,这个错误代码恰好编译通过(因为赋值表达式的值也是boolean)。防范措施:(1)使用IDE警告(IntelliJ IDEA会提示"Assignment to itself");(2)将常量放在左边:if (true == flag),如果误写成if (true = flag)会编译失败;(3)对于boolean变量直接写if (flag)而非if (flag == true)。