ArithmeticException是Java中最常见的运行时异常之一,它继承自RuntimeException,典型的触发场景就是整数除以零。很多初学者第一次遇到这个异常时往往会感到困惑,明明代码编译通过了,为什么运行的时候还是崩了。这是因为编译器只负责语法检查,无法预知程序运行时变量的实际取值。想要彻底避免这类问题,需要从异常的产生机制、防御性编码以及异常捕获三个层面入手。

ArithmeticException产生的根本原因
在Java的数值体系里,整数运算和浮点运算对除零的处理方式完全不同。当int或long类型的除数为零时,JVM会直接抛出ArithmeticException,因为数学上整数除零没有定义结果,硬件层面也没有对应的表示方式。而浮点数遵循IEEE 754标准,除以零会得到Infinity或者NaN这样的特殊值,不会抛异常。看下面的代码示例:
public class DivideDemo {
public static void main(String[] args) {
int a = 10;
int b = 0;
// 整数除零,抛出ArithmeticException: / by zero
System.out.println(a / b);
double x = 10.0;
double y = 0.0;
// 浮点除零,输出Infinity,不抛异常
System.out.println(x / y);
}
}除了除零之外,取模运算同样会触发这个异常。对零取模在数学上同样无意义,JVM的处理方式与除零一致。还有一种容易被忽略的场景是BigInteger的除法运算,当调用divide方法且除数为ZERO时,也会抛出ArithmeticException,错误信息同样提示除以零。此外,某些超出精度要求的操作也会抛出这个异常,比如BigDecimal在除法运算结果无法精确表示且没有指定舍入模式时,同样会抛出ArithmeticException,这种情况在金额计算中尤其常见。
import java.math.BigDecimal;
import java.math.BigInteger;
public class OtherCases {
public static void main(String[] args) {
BigInteger big = new BigInteger("100");
// BigInteger除以零也会抛ArithmeticException
System.out.println(big.divide(BigInteger.ZERO));
BigDecimal d1 = new BigDecimal("10");
BigDecimal d2 = new BigDecimal("3");
// 10除以3是无限循环小数,未指定精度会抛异常
System.out.println(d1.divide(d2));
}
}运算前的条件判断是首选方案
避免ArithmeticException最直接有效的办法,就是在执行除法运算之前先判断除数是否合法。这种防御性检查成本极低,一次比较操作几乎不影响性能,却能从根源上杜绝异常的发生。实际项目中,除数往往来自外部输入、数据库字段或者上游服务的返回值,开发阶段很难保证这些值一定不为零,主动校验是必要的。
public class SafeDivide {
public static int safeDivide(int dividend, int divisor) {
if (divisor == 0) {
// 除数为零时返回默认值或抛出业务异常
throw new IllegalArgumentException("除数不能为零");
}
return dividend / divisor;
}
public static void main(String[] args) {
try {
System.out.println(safeDivide(100, 5));
System.out.println(safeDivide(100, 0));
} catch (IllegalArgumentException e) {
System.out.println("参数校验失败: " + e.getMessage());
}
}
}对于BigDecimal的精度问题,解决方案是在调用divide方法时显式指定精度和舍入模式。金额计算中推荐使用RoundingMode.HALF_UP也就是四舍五入,并明确指定保留的小数位数。这样既避免了异常,也保证了计算结果符合业务预期。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class BigDecimalDemo {
public static void main(String[] args) {
BigDecimal d1 = new BigDecimal("10");
BigDecimal d2 = new BigDecimal("3");
// 指定保留两位小数,四舍五入
BigDecimal result = d1.divide(d2, 2, RoundingMode.HALF_UP);
System.out.println(result); // 输出3.33
}
}值得注意的是,使用new BigDecimal(0.1)这种传入double的构造方式会带来精度陷阱,因为double本身就无法精确表示0.1,构造出来的BigDecimal会带有很长的尾巴。正确做法是使用字符串构造或者BigDecimal.valueOf方法,从源头上保证数值的精确性。
异常捕获与业务场景的结合
并不是所有场景都能在运算前完成校验,比如除数来自复杂的异步计算链条,校验点和运算点分离,这时捕获异常就成了兜底手段。捕获ArithmeticException时要注意,try块的范围应该尽量小,只包裹可能抛异常的那一行运算代码,避免掩盖其他真正的程序缺陷。
public class CatchDemo {
public static void main(String[] args) {
int[] data = {100, 200, 0, 300};
int total = 500;
for (int divisor : data) {
try {
// 只包裹可能出错的运算行
int avg = total / divisor;
System.out.println("平均值: " + avg);
} catch (ArithmeticException e) {
// 除数为零时的降级处理
System.out.println("除数为" + divisor + ",跳过该组计算");
}
}
}
}关于异常捕获和条件判断该如何选择,业界的共识是:能预判且属于正常业务分支的情况用条件判断,即所谓EAFP与LBYL之争中的LBYL思路;而异常本身发生概率极低且属于意外情况时,用try-catch兜底更合适。频繁抛出和捕获异常的性能开销远大于一次简单的if判断,在循环或高频调用的代码路径中尤其要避免用异常控制流程。
实际项目中的综合防御策略
在一个真实的业务系统里,数值计算往往散落在多个层次,比如订单金额分摊、库存均分、统计报表等场景。单一手段难以覆盖所有情况,建议建立分层防御体系。首先是入口层校验,所有外部传入的数值参数在进入业务逻辑前统一校验,非法值直接拒绝。其次是计算层封装,把除法这类危险运算封装成工具方法,统一处理零值和精度问题,避免每个开发人员各自为战。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class MathUtils {
/**
* 安全除法:除数为零时返回默认值
*/
public static BigDecimal divide(BigDecimal a, BigDecimal b, BigDecimal defaultVal) {
if (a == null || b == null || BigDecimal.ZERO.compareTo(b) == 0) {
return defaultVal;
}
return a.divide(b, 4, RoundingMode.HALF_UP);
}
public static void main(String[] args) {
// 典型场景:计算占比,分母为零时返回零
BigDecimal ratio = MathUtils.divide(
new BigDecimal("30"), new BigDecimal("0"), BigDecimal.ZERO);
System.out.println("占比: " + ratio);
}
}最后是测试层面的保障,针对数值计算相关的代码,单元测试必须覆盖除数为零、极端大数、负数取模等边界情况。数值类型的溢出问题也值得关注,Math.addExact和Math.multiplyExact这类方法在溢出时会抛出ArithmeticException,这其实是一种把隐患提前暴露的设计思路。配合长时间的线上监控,通过日志统计异常发生的位置和频率,可以持续发现系统中的薄弱环节。把校验、封装、测试和监控结合起来,ArithmeticException就不再是线上事故的常客了。
Java ArithmeticException 异常处理修改时间:2026-09-13 14:11:26