乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • 学习路径
  • 第1章 Java概述与环境搭建

    • Java概述与环境搭建
    • Java语言概述
    • 解释型语言与编译型语言对比
    • JDK安装与配置
    • JDK、JRE、JVM 详解
    • HelloWorld程序详解
    • IDE 介绍
  • 第2章 标识符与基本数据类型

    • 章节导读
    • 变量概述
    • 常量概述
    • 基本类型与包装类
    • 字节型 byte
    • 短整型 short
    • 整型 int
    • 长整型 long
    • 单精度浮点型 float
    • 双精度浮点型 double
    • 字符型 char
    • 布尔型 boolean
    • 类型转换
  • 第3章 运算符与表达式

    • 章节导读
    • 算术运算符
    • 赋值运算符
    • 关系运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 运算符优先级
    • 表达式
  • 第4章 流程控制

    • 章节导读
    • 常见的程序运行流程
    • if-else 选择结构
    • switch 多分支选择
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
  • 第5章 数组

    • 章节导读
    • 一维数组
    • 多维数组
    • Arrays 工具类
  • 第6章 类与对象

    • 章节导读
    • 类与对象
    • 方法定义与调用
    • 构造方法
    • 封装
    • 访问修饰符
    • package 与 import
    • static 关键字
    • this 关键字
    • 参数传递 详解
    • 枚举
    • 成员内部类
    • 局部内部类
    • 静态内部类
    • 匿名内部类
  • 第7章 接口与继承

    • 章节导读
    • 继承
    • super 关键字
    • final 关键字
    • 多态
    • 向上转型与向下转型
    • 抽象类
    • 接口
    • 抽象类与接口对比
  • 第8章 注解

    • 章节导读
    • 注解基础
    • 元注解详解
    • 自定义注解
  • 第9章 常用类

    • 章节导读:Java 常用类
    • Object 类:万类之祖
    • 包装类:基本类型的对象化
    • String:不可变的字符串
    • StringBuffer:线程安全的可变字符串
    • StringBuilder:可变的字符串构建器
    • Math:数学运算工具类
    • Random:伪随机数生成器
    • 大数值运算 详解
    • 日期时间API 详解
  • 第10章 异常机制

    • 章节导读
    • 异常体系与分类
    • try-catch-finally
    • try-with-resources
    • throws 与 throw
    • 自定义异常
  • 第11章 泛型

    • 章节导读
    • 泛型基础
    • 通配符与PECS原则
    • 类型擦除
  • 第12章 集合框架

    • 章节导读
    • 集合框架概述
    • ArrayList
    • LinkedList
    • HashMap 详解
    • LinkedHashMap 详解
    • TreeMap 详解
    • HashSet
    • TreeSet 详解
    • TreeSet 与 Comparable
    • Collections 工具类详解
  • 第13章 IO流

    • 章节导读
    • IO流概述
    • 字节流
    • 字符流
    • 缓冲流
    • 转换流 详解
    • 序列化 详解
    • NIO与Files 详解
    • NIO与Files工具类
  • 第14章 多线程与并发

    • 第十六章 多线程与并发 —— 章节导读
    • 线程基础详解
    • synchronized 详解
    • Lock 与显式锁详解
    • volatile 详解
    • wait 与 notify 详解
    • ThreadLocal详解
    • 原子类详解
    • 并发工具类详解
    • 线程池详解
  • 第15章 反射

    • 章节导读
    • 反射概述与 Class 对象
    • Constructor 与对象创建
    • Field与Method详解
    • 反射应用详解
  • 第16章 JDK8新特性

    • 章节导读
    • Lambda 表达式
    • Stream API 基础
    • Stream API 高级详解
    • Optional 详解
    • 新日期时间API详解
  • 第17章 JDK9-11新特性

    • 章节导读
    • 模块化系统 — Project Jigsaw(JDK 9)
    • var 局部变量类型推断(JDK 10)
    • 集合工厂方法与增强(JDK 9 / 10 / 11)
    • 接口增强:private 方法(JDK 9)
    • Stream API 增强(JDK 9)
    • Optional 增强(JDK 9 / 10 / 11)
    • String 新增方法(JDK 11)
    • HTTP Client 与 Files 增强(JDK 11)
    • 直接运行 Java 源文件 — JEP 330(JDK 11)
  • 第18章 JDK12-17新特性

    • 章节导读
    • Switch 表达式(JDK 12 预览 / JDK 14 正式)
    • 文本块 Text Blocks(JDK 13 预览 / JDK 15 正式)
    • Records 记录类(JDK 14 预览 / JDK 16 正式)
    • 密封类 Sealed Classes(JDK 15 预览 / JDK 17 正式)
    • instanceof 模式匹配(JDK 14 预览 / JDK 16 正式)
    • Switch 模式匹配 — Pattern Matching for switch(JDK 17 预览 / JDK 21 正式)
    • Helpful NPE 与 String 增强(JDK 12 / JDK 14 / JDK 15)
    • Stream 增强(JDK 12 / JDK 16)
    • 日期时间增强 — Day Period 支持(JDK 16)
  • 第19章 JDK18-21新特性

    • 章节导读
    • 虚拟线程(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 序列集合(JDK 21 正式)
    • Switch 模式匹配(JDK 17 预览 / JDK 18 第二预览 / JDK 20 第四预览 / JDK 21 正式)
    • Record 模式匹配(JDK 19 预览 / JDK 20 第二预览 / JDK 21 正式)
    • 未命名模式与变量(JDK 21 预览 / JDK 22 正式)
  • 第20章 JDK 22-25 新特性

    • 章节导读
    • 字符串模板(JDK 22 预览 / JDK 23 第二预览 / JDK 24 第三预览)
    • Stream Gatherers(JDK 22 预览 / JDK 24 第二预览)
    • 隐式声明类与实例方法(JDK 23 预览 / JDK 24 第二预览)
    • 原始类型模式匹配(JDK 24 预览)
  • 附录

    • Java 核心知识点
    • Java SE 专业术语
    • Java特性索引(JDK 8 → 25)

"小崔在飞翔科技财务系统里写工资计算模块,8888.88 * 12 + 5000 结果总是不对。白歌看了一眼:'整数除法的坑,5 / 2 在Java里不是2.5,是2。'大翔补充道:'还有 i++ 和 ++i 的区别——你以为只是'先用后加'和'先加后用',但背后的字节码完全不同,多线程下更是天壤之别。'"

算术运算符

概述

算术运算符是程序中最基础的操作符,负责数值的加减乘除、取模和自增自减。Java SE 8 提供7种算术运算符,涵盖常规四则运算与快捷自操作。

操作符行为表

操作符含义示例结果(假设 int a = 10, b = 3)
+加法a + b13
-减法a - b7
*乘法a * b30
/除法(整数除:截断)a / b3(不是3.333...)
%取模(取余数)a % b1
++自增(前/后)++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 同步块。

上一页
章节导读
下一页
赋值运算符