阅读 1258

你真的会向上取整吗?小心Math.ceil的精度丢失

背景

在处理一个分页工作时,需要做一个向上取整的操作,类似这样:

// 总数
int totalSize = 799;
// 页大小
int pageSize = 200;
// 计算页数需要向上取整
// 这里的向上取整如何处理?
int pageCount = (double)totalSize / pageSize;
复制代码

这个向上取整计算其实很常用,但是似乎没有一个比较标准的处理方式。

常见的处理方式

方案一:检查余数

检查余数是否为0,这是我常用的写法。

// ...
int pageCount = totalSize / pageSize;
if (totalSize % pageSize > 0) {
	pageCount++;
}
复制代码

这种方式很笨,同时还有点啰嗦。

**方案二(不推荐!):Math.ceil(double x) **

也许看完第一个方案已经有人在说了,官方Math工具就提供了向上取整的方法,为什么不用呢?

// ...
int pageCount = (int)Math.ceil((double)totalSize / pageSize);
复制代码

最大的问题:Math.ceil的返回类型是double

且不说这个场景是两个整型运算,结果也是整型,中间就要经过两次类型转换操作,看起来不够流畅;我们还应该考虑到在类型转换中可能存在的精度丢失问题。

精度丢失可能仅仅在很极端的场景下出现,可一旦出现就是非常难以排查的隐式bug。举个例子:

  1. 假设某两个数的ceil计算结果原本是2.0,但由于精度问题,ceil结果其实是1.9999999999999999999999999
  2. 在结果转为int型数据时,发生了精度丢失,计算结果由2.0转换为1,相当于少了一页
  3. 在分页处理过程中,缺失了最后一页的数据
  4. 在复现过程中,仅有极个别case中会出现这种bug,且从代码逻辑来看其实也没什么问题

再举个例子,假设使用double类型的pageCount,如下一段代码就有可能出现ArrayOutOfBoundsException:

// ...
double pageCount = 2d;    // 可能实际存储的是2.000000000000001,请意会
for (int page = 0; page < pageCount; page++) {
	// Array或List的遍历操作
}
复制代码

在任何可能出现与浮点型数据进行比较,或浮点型与整型的类型转换都必须非常注意。

方案三(推荐):增加一点小处理

int pageCount = (totalSize + pageSize - 1) / pageSize;
复制代码

这个计算方式在一行代码内解决,没有类型转换或精度丢失,处理上是进位思想,只要理解了就非常自然。

唯一的缺点是如果值过大可能产生溢出,但足够满足绝大多数场景。

补充

为什么Math.ceil返回的是浮点数?

是的,返回浮点整数虽然符合ceil的语义,但并不符合我们平常的直观理解,如果能返回整型数该多好?

但是这个方法的入参就是double类型,而无论是int类型或是long类型的表示范围都远远小于double类型的范围,如果使用整型就有可能表示不了,因而造成这一奇怪的现实。

表示范围详见参考资料

便捷的List切分方式

写到一半时想了想,实际上我本次的处理场景是将一个整体任务的List数据水平切分为多个子任务List,再进行分发处理。

如果是针对List的切分场景,其实非常推荐使用Google的guava包来处理:

// 直接分割,非常方便
List<List<Object>> subTaskLists = Lists.partition(taskList, pageSize);
复制代码

Lists.partition的实现方式:固定步长,多次取subList。

参考资料

使用Math.ceil将Java四舍五入为int - 问答 - 云+社区 - 腾讯云

java中short、int、long、float、double取值范围 - qfikh的博客 - CSDN博客

本文搬自我的博客,欢迎参观!