原创

IntStream.rangeClosed 处理整数上界,别手写 end 加一

想遍历从start到end的所有整数,于是写 IntStream.range(start, end + 1)。普通样本运行正确,end取Integer.MAX_VALUE时,结果却突然是空流。错误在进入range以前已经发生:end加一回绕成最小负数。

如果需求就是包含右端点,rangeClosed直接表达这件事,不需要调用方自己构造一个int无法表示的上界。便利的不是少写两个字符,而是让边界与所选类型一致。

import java.util.Arrays;
import java.util.stream.*;
public class InclusiveRange {
    static long checkSpan(int start, int end, long maxItems) {
        if (start > end) throw new IllegalArgumentException("reversed bounds");
        long count = (long)end - start + 1;
        if (count > maxItems) throw new IllegalArgumentException("range exceeds budget");
        return count;
    }
    public static void main(String[] args) {
        int start = Integer.MAX_VALUE - 2, end = Integer.MAX_VALUE;
        System.out.println("overflowed_end=" + (end + 1));
        System.out.println("bad_count=" + IntStream.range(start, end + 1).count());
        System.out.println("closed=" + Arrays.toString(IntStream.rangeClosed(start, end).toArray()));
        System.out.println("long_half_open=" + Arrays.toString(LongStream.range(start, (long)end + 1).toArray()));
        System.out.println("validated_span=" + checkSpan(start, end, 100));
        try { checkSpan(Integer.MIN_VALUE, Integer.MAX_VALUE, 100); throw new AssertionError(); }
        catch (IllegalArgumentException expected) { System.out.println("large_span=REJECTED"); }
        System.out.println("single=" + IntStream.rangeClosed(end, end).count());
        System.out.println("reversed=" + IntStream.rangeClosed(2, 1).count());
    }
}

在JDK 17把代码保存为 InclusiveRange.java,运行 java InclusiveRange.java:

overflowed_end=-2147483648
bad_count=0
closed=[2147483645, 2147483646, 2147483647]
long_half_open=[2147483645, 2147483646, 2147483647]
validated_span=3
large_span=REJECTED
single=1
reversed=0

错在加法,不在 Stream 忽略元素

样本从最大值前两个数开始,正确结果应该有三项。bad分支的上界却变成Integer.MIN_VALUE,小于start,range按自己的契约返回空结果。这个方法没有能力知道调用方原来想表达什么。

普通int加法不报告溢出,调试时如果只打印终止结果0,容易把问题误归到过滤或管线惰性。把 end + 1 单独打印为整数,就能看见真正的变化。使用Math.addExact可以把溢出变成异常,但对合法的闭区间需求而言,直接选rangeClosed更贴近语义。

同样的问题也存在于手写for循环。for (int i=start; i<=end; i++)在end为最大值时,i经过最大值再加一会回绕,而不是自然结束。若必须写循环,要显式处理终点退出或使用更宽的索引类型,不能因为条件用了小于等于就认为已经涵盖上界。

扩成 long 应该发生在运算之前

样本还用LongStream.range构造半开区间。重点是先把end提升到long,再加1;若先执行int的end加一,随后再强转,错误结果已经确定,扩大类型也救不回来。

同理,区间长度可以写成 (long) end - start + 1,不能先用int完成相减再赋给long。start与end跨越正负两端时,合法结果数量可能超过int。Stream.count本身返回long,业务代码却可能在前面用int把范围计算坏了。

逆向区间还应按接口定义处理。rangeClosed的起点大于终点时是空流,不是自动倒序。如果需求允许倒序,就要选择相应生成方式并明确步长;不要让一个空结果掩盖输入次序错误。

API 能表达,不代表规模就值得执行

整个int值域的闭区间有四十多亿个数。接口能正确表示,不代表应该collect成List或数组。计数、汇总与逐项I/O成本不同,预算仍要由业务决定。

本例只取上界附近三项,避免为了验证一个溢出问题生成巨大集合。测试应围绕最小值、最大值、单点、逆序和空范围组织;不必用大负载才能证明边界正确。若将区间交给批处理,还要提前限制跨度,避免合法输入制造不可承受的工作量。

修复时也要核对已有调用的区间口径。把所有range机械替换成rangeClosed,会让原本半开区间多执行一项。应先确认end代表最后一个合法值,还是第一个不包含值,再选择API。含不含终点属于调用协议,整数溢出只是暴露这项协议没有被类型正确表达的一个入口。

批处理入口先计算跨度,再生成任务

假设一个本地重放工具接受起止编号,最多允许一次处理100项。它不仅要正确包含最大编号,还要拒绝从最小int到最大int的巨大范围。代码里的checkSpan先检查次序,再以long计算数量,最后核对预算,调用方只有通过以后才开始生成或分派任务。

这样不会为了检查规模先toArray,也不会把一个已经溢出的int计数拿来判断。最大值附近三项成功,整个int范围明确拒绝,两份样本同时说明“边界合法”与“工作量合规”是不同条件。maxItems本身也应由配置输入规则保证为合理正数,不能让调用者随意放大预算。

如果业务允许分批处理大范围,应返回或消费惰性的批次描述,仍用更宽类型计算剩余量。不要用(end-start)/batchSize先得到一个可能错误的int,再在外层补long。空范围、逆序范围是否允许,也要在入口固定;本文的生成API允许逆序为空,但重放入口选择直接拒绝,把配置错误尽早暴露。

当编号来自外部字符串时,还要在解析阶段检查是否确实可表示为int。截断成int以后再做这份校验,已经无法恢复原始意图。范围协议应从输入解析一直保留到执行循环,任何一层的隐式缩窄都可能把看似安全的边界重新打破。

正文到此结束
评论插件初始化中...
Loading...
本文目录