原创

partitioningBy 遇到空输入,为什么 Map 里仍有两个键

判断 partitions.isEmpty() 为false,于是程序宣布“本批有记录”,可输入其实一条都没有。若结果来自partitioningBy,这个判断从一开始就不成立:返回Map始终包含true和false两个分区。

这不是多出来两条假数据。键表示预先定义的两类,值才表示本次每类收到了什么。一个空列表可以属于已知分区,正如统计报表可以给一个没有观测的类别显示零。

import java.util.*;
import java.util.stream.Collectors;
public class EmptyPartitions {
    static void count(String name, List<Integer> input) {
        Map<Boolean,Long> parts = input.stream().collect(Collectors.partitioningBy(n -> n > 0, Collectors.counting()));
        System.out.println(name + "_buckets=" + parts.size());
        System.out.println(name + "_true=" + parts.get(true));
        System.out.println(name + "_false=" + parts.get(false));
        System.out.println(name + "_hasInput=" + (parts.get(true) + parts.get(false) > 0));
    }
    public static void main(String[] args) {
        count("empty", List.of()); count("positive", List.of(1, 2));
        Map<Boolean,Optional<Integer>> maxima = List.<Integer>of().stream()
                .collect(Collectors.partitioningBy(n -> n > 0, Collectors.maxBy(Comparator.naturalOrder())));
        System.out.println("empty_max_present=" + maxima.get(true).isPresent());
    }
}

使用JDK 17保存为 EmptyPartitions.java,执行 java EmptyPartitions.java:

empty_buckets=2
empty_true=0
empty_false=0
empty_hasInput=false
positive_buckets=2
positive_true=2
positive_false=0
positive_hasInput=true
empty_max_present=false

Map 的结构不适合承担输入存在性

第一组输入为空,分区数仍为2,但两边计数都是0,hasInput为false。第二组全部为正数,false分区仍然存在,只是计数为0。程序从具体值计算总记录数,才能回答是否有输入。

如果分区结果是列表,应检查两份列表的总规模;若后续收集只保留部分数据,比如每组只取最大值,则值本身已经不再提供完整数量,最好另外保留输入计数。不要因为当前下游刚好是List,就把结构大小与输入规模绑死在一个通用工具里。

这对批次状态也有影响。空批次、全部通过与全部拒绝是三种不同结果。Map有两个键既不代表批次已经处理过,也不代表两类都出现过。任务状态应来自实际执行与输入计数,不能从布尔分类容器的固定形状猜测。

把分区接到一个导入预检接口,会更容易看出空批次与“全部通过”的差别。每条Checked已经带有稳定源行标识,rejection为空表示通过,否则保留具体拒绝原因。分区负责组织判断结果,不在predicate里临时吞掉解析异常。

下面可以单独保存为 BatchPartitionReport.java 并运行。状态由输入与两个结果集合共同确定,公开返回的列表不允许调用方增删:

import java.util.*;
import java.util.stream.Collectors;
public class BatchPartitionReport {
    record Checked(String id, String rejection) {}
    enum State { EMPTY, ACCEPTED, REJECTED, PARTIAL }
    record Report(State state, List<Checked> accepted, List<Checked> rejected) {}
    static Report report(List<Checked> input) {
        Set<String> ids = new HashSet<>();
        for (Checked item : input) {
            if (item == null || item.id() == null || !ids.add(item.id()))
                throw new IllegalArgumentException("invalid or duplicate row identity");
        }
        Map<Boolean,List<Checked>> parts = input.stream()
                .collect(Collectors.partitioningBy(item -> item.rejection() == null));
        List<Checked> yes = List.copyOf(parts.get(true));
        List<Checked> no = List.copyOf(parts.get(false));
        State state = input.isEmpty() ? State.EMPTY : no.isEmpty() ? State.ACCEPTED
                : yes.isEmpty() ? State.REJECTED : State.PARTIAL;
        return new Report(state, yes, no);
    }
    public static void main(String[] args) {
        List<List<Checked>> samples = List.of(List.of(), List.of(new Checked("A",null)),
                List.of(new Checked("B","invalid code")),
                List.of(new Checked("A",null), new Checked("B","invalid code")));
        for (List<Checked> input : samples) {
            Report r = report(input);
            System.out.println(r.state() + ": accepted=" + r.accepted().size() + ", rejected=" + r.rejected().size());
        }
        try { report(List.of(new Checked("A",null),new Checked("A","duplicate"))); }
        catch (IllegalArgumentException e) { System.out.println("duplicate identity=rejected"); }
        if (report(List.of()).state() != State.EMPTY) throw new AssertionError("empty batch");
    }
}
EMPTY: accepted=0, rejected=0
ACCEPTED: accepted=1, rejected=0
REJECTED: accepted=0, rejected=1
PARTIAL: accepted=1, rejected=1
duplicate identity=rejected

四种状态分别对应没有输入、有输入且全通过、全拒绝和部分通过。空批次不会因为拒绝列表为空就被标成ACCEPTED;全部通过也不会因为false键依旧存在就被标成PARTIAL。状态规则直接写在代码里,可以逐条与产品含义对照。

示例拒绝重复行标识,而不是让同一个标识同时落入通过和拒绝列表,再交给前端猜哪条有效。如果业务允许同一商品在一个文件中出现多次,可以另选源文件行号作为标识;若标识代表对象本身,则还需要定义重复项合并或冲突规则。不能只因分区顺利完成,就以为结果已经适合逐行回写。

Checked也不是持久化成功记录。ACCEPTED只表示本次预检通过,后续写入仍可能遇到唯一冲突、状态变化或存储错误。接口应使用明确的“预检状态”,不能把它直接显示成“已经导入”。若程序根本没有开始读取文件,也不能用一个空分区结果来代替“尚未执行”。

请求被截断、文件解析失败与业务拒绝同样应分开。前两种情况下,调用者可能还不知道完整输入规模,不能把已读到的几条拿去分区,然后把剩余缺失记录算作零。可以返回批次级失败,或者在协议允许部分预检时明确报告读取进度;一个Boolean桶放不下这些阶段状态。

最后,若页面只需要计数,可以使用counting下游,避免保存所有明细;若需要回显失败原因,则需要保留相应记录并限制批次大小。结果形状由用途决定。不要在拿到最大值或计数以后,又期待能从那份结果恢复每条输入,归约已经丢弃的信息不会因为两个分区键都存在而回来。

换一个下游,空分区的值也会变

默认partitioningBy把每组收成List,空分区自然对应空列表。counting得到0;maxBy则得到Optional.empty。它们都保留true与false两个键,但“这一组没有数据”的值表达不同。

调用者不能写出一个假定所有分区值都可get或可unbox的工具方法。样本直接核对空输入的最大值不存在,而不是给它填一个0。最大值真的为0与没有观测值不是一回事,尤其当后续要做告警、排序或展示时。

自定义下游收集器还应考虑它的空累积器如何完成。这个行为不一定等于null,也不一定与调用方想象的默认值一致。接口需要稳定形式时,应在输出适配层明确映射,并保留必要的缺失状态。

分组与分区适合不同的结果形状

按Boolean分类的groupingBy通常只为实际遇到的键创建结果,不能因为它们泛型类型一样,就假设与partitioningBy的键集合相同。前者是在观察到的分类上建组,后者在固定的真假二分模型上归约。

选择应跟随需求。如果页面总要同时展示通过和拒绝两项,固定分区很方便;如果分类将扩展为通过、拒绝、跳过、待复核四态,Boolean就不足以表达模型,不该继续把缺失键当成第三种状态。改成明确枚举或结果记录更容易维护。

也不应把predicate写成捕获所有错误后返回false。非法输入与程序故障需要不同反馈,单个布尔桶无法保留每条失败原因。分区只是组织已经得到的判断结果,判断如何形成仍需要独立验证。

这类回归只要三组样本就能抓住核心:空输入、全true、全false;再为所用下游补一份空结果断言。保住这些精确结构以后,Map大小就不再被误用成数据量,结果协议也能解释为什么一个空批次仍有两份分类栏位。

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