"小崔在飞翔科技财务系统里写工资计算模块,
8888.88 * 12 + 5000结果总是不对。白歌看了一眼:'整数除法的坑,5 / 2在Java里不是2.5,是2。'大翔补充道:'还有i++和++i的区别——你以为只是'先用后加'和'先加后用',但背后的字节码完全不同,多线程下更是天壤之别。'"
算术运算符
概述
算术运算符是程序中最基础的操作符,负责数值的加减乘除、取模和自增自减。Java SE 8 提供7种算术运算符,涵盖常规四则运算与快捷自操作。
操作符行为表
| 操作符 | 含义 | 示例 | 结果(假设 int a = 10, b = 3) |
|---|---|---|---|
+ | 加法 | a + b | 13 |
- | 减法 | a - b | 7 |
* | 乘法 | a * b | 30 |
/ | 除法(整数除:截断) | a / b | 3(不是3.333...) |
% | 取模(取余数) | a % b | 1 |
++ | 自增(前/后) | ++a 或 a++ | 11(但表达式值不同) |
-- | 自减(前/后) | --a 或 a-- | 9(但表达式值不同) |
注意:Java中不存在幂运算符
**(某些语言有)。求幂需使用Math.pow(base, exponent)。
深度原理分析
一、整数除法的截断机制
Java中整数与整数相除,结果依然是整数——小数部分被直接丢弃(截断,而非四舍五入):
int result = 5 / 2; // 结果是 2,不是 2.5
int negResult = -5 / 2; // 结果是 -2(向零方向截断)
原理:JVM的 idiv 指令对整数除法执行向零舍入(truncation toward zero)。这与数学中的向下取整不同——对于负数,-5 / 2 = -2(不是 -3)。
要获得精确浮点结果,必须将至少一个操作数转为浮点类型:
double result = 5.0 / 2; // 2.5
double result2 = (double) 5 / 2; // 2.5
二、取模运算的符号规则
Java中 % 的结果符号与被除数(左操作数)相同,与除数无关:
System.out.println(10 % 3); // 1
System.out.println(10 % -3); // 1 (被除数为正,结果为正)
System.out.println(-10 % 3); // -1 (被除数为负,结果为负)
System.out.println(-10 % -3); // -1
公式:a % b = a - (a / b) * b,其中 a / b 是向零截断的整数除法。
三、自增/自减的字节码深度分析(核心)
i++ 和 ++i 的区别绝不仅仅是"先用后加"vs"先加后用"——它们在JVM字节码层面的指令顺序完全不同。
// 代码 A:后置自增
int i = 0;
int j = i++; // j = 0, i = 1
// 代码 B:前置自增
int m = 0;
int n = ++m; // n = 1, m = 1
代码A的字节码(javap -c 反编译):
0: iconst_0 // 将常量 0 压入操作数栈
1: istore_1 // 弹出栈顶存入局部变量表 slot1 (i=0)
2: iload_1 // 将 i 的值(0)压入操作数栈 —— 先取当前值
3: iinc 1, 1 // 将局部变量表 slot1 的值自增1 (i=1) —— 变量自增
6: istore_2 // 弹出栈顶(旧值0)存入 slot2 (j=0)
代码B的字节码:
0: iconst_0 // 将常量 0 压入操作数栈
1: istore_1 // 弹出栈顶存入局部变量表 slot1 (m=0)
2: iinc 1, 1 // 将局部变量表 slot1 的值自增1 (m=1) —— 先自增
5: iload_1 // 将 m 的新值(1)压入操作数栈 —— 再取新值
6: istore_2 // 弹出栈顶(新值1)存入 slot2 (n=1)
关键差异:iinc 指令和 iload 指令的执行顺序颠倒,导致了表达式值的不同:
记忆口诀:
i++是"先用后加"(返回值是旧值),++i是"先加后用"(返回值是新值)。但底层原因是字节码指令顺序的差异,与操作数栈的压栈时机有关。
四、自增自减的表达式副作用陷阱
当自增/自减出现在复杂表达式中时,因为副作用发生时机不同,结果可能出乎意料:
int a = 1;
int b = a++ + a++ + a++;
// 分析:
// a++ 返回1,然后a变成2
// a++ 返回2,然后a变成3
// a++ 返回3,然后a变成4
// b = 1 + 2 + 3 = 6, a = 4
在单条语句中对同一变量多次自增属于未定义行为——Java虽然定义了求值顺序,但这属于应当避免的坏代码。
完整示例一:飞翔科技员工工资计算
场景
飞翔科技技术部小崔需要计算部门员工的月薪、年薪和年终奖基数。公司月薪基数为 8888.88 元,技术部按1.5倍系数发放,年终奖为年薪的20%。
/**
* 飞翔科技工资计算器
* 演示算术运算符的完整用法
*/
public class SalaryCalculator {
public static void main(String[] args) {
// --- 数据准备 ---
float baseSalary = 8888.88f; // 飞翔科技基础月薪
float techCoefficient = 1.5f; // 技术部系数
int staffCount = 7; // 技术部人数
float yearBonus = 5000.0f; // 年度固定奖金
// --- 加法:月薪 * 系数 ---
float monthlyPay = baseSalary * techCoefficient;
System.out.println("===== 飞翔科技技术部工资明细 =====");
System.out.println("基础月薪: ¥" + baseSalary);
System.out.println("技术系数: " + techCoefficient);
System.out.println("实际月薪: ¥" + monthlyPay);
// --- 乘法:年薪 = 月薪 * 12 ---
float annualSalary = monthlyPay * 12;
System.out.println("年薪: ¥" + annualSalary);
// --- 加法:年薪 + 固定奖金 ---
float totalAnnualPay = annualSalary + yearBonus;
System.out.println("年薪+奖金: ¥" + totalAnnualPay);
// --- 取模:部门聚餐AA制(总金额 % 人数)---
float dinnerTotal = 1688.88f;
float perPerson = dinnerTotal / staffCount;
float remainder = dinnerTotal % staffCount; // 取模可用于float
System.out.println("\n===== 部门聚餐AA =====");
System.out.println("聚餐总额: ¥" + dinnerTotal);
System.out.println("人均: ¥" + String.format("%.2f", perPerson));
System.out.println("取模余数: ¥" + remainder + "(float也可取模)");
// --- 自增:工龄增长 ---
int workYears = 3;
System.out.println("\n===== 工龄统计 =====");
System.out.println("当前工龄: " + workYears + " 年");
System.out.println("(后置++)明年工龄: " + (workYears++) + "(表达式值为旧值)");
System.out.println("实际上已变为: " + workYears + " 年");
// 前置++
System.out.println("(前置++)后年工龄: " + (++workYears) + "(表达式值为新值)");
// --- 除法:整数除 vs 浮点除 ---
System.out.println("\n===== 除法精度对比 =====");
int bonusPool = 100000; // 年终奖池(元)
System.out.println("整数除 (100000/7): " + (bonusPool / staffCount) + " 元(截断,丢失小数)");
System.out.println("浮点除 (100000.0/7): " + (bonusPool / (float) staffCount) + " 元(保留小数)");
}
}
运行输出:
===== 飞翔科技技术部工资明细 =====
基础月薪: ¥8888.88
技术系数: 1.5
实际月薪: ¥13333.32
年薪: ¥159999.84
年薪+奖金: ¥164999.84
===== 部门聚餐AA =====
聚餐总额: ¥1688.88
人均: ¥241.27
取模余数: ¥2.8798828(float也可取模)
===== 工龄统计 =====
当前工龄: 3 年
(后置++)明年工龄: 3(表达式值为旧值)
实际上已变为: 4 年
(前置++)后年工龄: 5(表达式值为新值)
===== 除法精度对比 =====
整数除 (100000/7): 14285 元(截断,丢失小数)
浮点除 (100000.0/7): 14285.714 元(保留小数)
完整示例二:飞翔商城商品价格计算
场景
大翔要求开发飞翔商城(feixiang.net)的促销模块——满减折扣、积分抵扣,黄俪负责实现核心计算逻辑。
/**
* 飞翔商城促销价格计算
* 演示 /、%、++、-- 的应用
*/
public class ProductPricing {
public static void main(String[] args) {
float originalPrice = 188.88f; // 飞翔商城商品原价
int quantity = 5; // 购买数量
int discountPercent = 15; // 折扣百分比
// --- 乘法 + 除法:折扣价 ---
float discountAmount = originalPrice * discountPercent / 100;
float discountedPrice = originalPrice - discountAmount;
System.out.println("===== 飞翔商城 feixiang.net =====");
System.out.println("商品原价: ¥" + originalPrice);
System.out.println("折扣(" + discountPercent + "%): -¥" + discountAmount);
System.out.println("折后单价: ¥" + discountedPrice);
// --- 乘法:总价 ---
float totalPrice = discountedPrice * quantity;
System.out.println("购买数量: " + quantity);
System.out.println("折后总价: ¥" + totalPrice);
// --- 取模:库存批次号 ---
int stockCount = 47; // 库存总量
int batchSize = 10; // 每批10件
int fullBatches = stockCount / batchSize; // 完整批次
int remaining = stockCount % batchSize; // 零散数量
System.out.println("\n===== 库存管理 =====");
System.out.println("库存总量: " + stockCount + " 件");
System.out.println("完整批次: " + fullBatches + " 批(每批" + batchSize + "件)");
System.out.println("零散数量: " + remaining + " 件");
// --- 自减:库存扣减 ---
int currentStock = stockCount;
System.out.println("\n===== 下单扣库存 =====");
System.out.println("下单前库存: " + currentStock);
System.out.println("第1次扣减(后置--): " + (currentStock--) + "(返回旧库存,但实际已减1)");
System.out.println("第2次扣减(前置--): " + (--currentStock) + "(先减再返回)");
System.out.println("剩余库存: " + currentStock);
// --- 整数除法的精度陷阱再现 ---
System.out.println("\n===== 常见陷阱 =====");
System.out.println("5 / 2 = " + (5 / 2) + "(整数除,截断为2!)");
System.out.println("5.0 / 2 = " + (5.0 / 2) + "(浮点除,正确)");
System.out.println("(double)5/2 = " + ((double) 5 / 2) + "(强制转换,正确)");
}
}
运行输出:
===== 飞翔商城 feixiang.net =====
商品原价: ¥188.88
折扣(15%): -¥28.332
折后单价: ¥160.54799
购买数量: 5
折后总价: ¥802.7399
===== 库存管理 =====
库存总量: 47 件
完整批次: 4 批(每批10件)
零散数量: 7 件
===== 下单扣库存 =====
下单前库存: 47
第1次扣减(后置--): 47(返回旧库存,但实际已减1)
第2次扣减(前置--): 45(先减再返回)
剩余库存: 45
===== 常见陷阱 =====
5 / 2 = 2(整数除,截断为2!)
5.0 / 2 = 2.5(浮点除,正确)
(double)5/2 = 2.5(强制转换,正确)
易错场景
反例一:整数除法精度丢失
小崔写了一个平均工资计算:
// ❌ 错误:整数除法导致精度丢失
int totalSalary = 100000; // 总工资(元)
int employeeCount = 7; // 员工数
double avgSalary = totalSalary / employeeCount; // 14285.0,而非 14285.714...
System.out.println("人均工资: " + avgSalary);
// 输出:人均工资: 14285.0 —— 小数部分完全丢失!
原因:totalSalary / employeeCount 两个都是 int,先执行整数除法(结果=14285),再隐式转换为 double(14285.0),精度已经丢失。
纠正:
// ✅ 正确:至少一个操作数为浮点类型
double avgSalary = (double) totalSalary / employeeCount; // 14285.714285714286
// 或
double avgSalary = totalSalary * 1.0 / employeeCount;
反例二:i++ 在复杂表达式中的困惑
// ❌ 错误:在同一表达式中多次修改同一变量
int x = 1;
int y = x++ + ++x + x++;
// y = ? x = ? —— 这是绕口令级别的代码!
System.out.println("y=" + y + ", x=" + x);
// 输出:y=7, x=4
// 分析:
// x++: 返回1, x→2
// ++x: x→3, 返回3
// x++: 返回3, x→4
// y = 1 + 3 + 3 = 7
纠正:
// ✅ 正确:每行只做一件事,避免副作用叠加
int x = 1;
int y = x; // y = 1
x++; // x = 2
y += x; // y = 1 + 2 = 3
x++; // x = 3
y += x++; // y = 3 + 3 = 6,然后 x = 4
反例三:浮点数取模精度问题
// ⚠️ 注意:float取模结果可能不精确
float dinner = 1688.88f;
int people = 7;
float remainder = dinner % people;
System.out.println(remainder); // 输出 2.8798828,而非你期望的精确值
原因:float 类型在二进制中无法精确表示很多十进制小数,取模运算放大了这种不精确性。对精度敏感的场景(如金额),使用 BigDecimal。
面试考点
Q1:i++ 和 ++i 在字节码层面有什么区别?哪个效率更高?
字节码区别:
i++(后置)的字节码顺序是iload → iinc → istore,先将旧值压栈再自增;++i(前置)的顺序是iinc → iload → istore,先自增再将新值压栈。两者的核心区别在于"压入操作数栈的值是自增前还是自增后"。效率:在C++中,对于迭代器等对象类型,
++i比i++高效(因为i++需要保存一份临时副本)。但在Java中,对于基本类型int,两者编译后的字节码指令数完全相同(都是3条指令),效率无差别;对于包装类型,差异也微乎其微。但从代码可读性角度,for循环中建议使用++i(语义更清晰)。
Q2:Java中 5 / 2 等于多少?如何得到 2.5?
5 / 2等于2(整数除法,向零截断)。要得到2.5,需要至少一个操作数为浮点类型:
5.0 / 2(字面量加.0)(double) 5 / 2(强制类型转换)5D / 2(使用double字面量后缀)
Q3:取模运算 % 的结果符号由谁决定?
结果符号与被除数(左操作数)相同,与除数无关。
-10 % 3 = -1,10 % -3 = 1。内部公式为a % b = a - (a / b) * b,其中a / b是向零截断的整数除法。
Q4:++ 操作是线程安全的吗?
不是。
i++在字节码层面由多条指令组成(iload → iinc → istore),不是原子操作。在多线程环境下,如果多个线程同时对同一个变量执行i++,会产生竞态条件(race condition),导致计数不准确。解决方案:使用AtomicInteger.incrementAndGet()或synchronized同步块。