跳转到正文

集合与分页

集合工具处理应用已经取得的数据,不执行数据库查询。多数入口位于 com.own.constant.util.list,组合分页入口为 com.own.constant.util.PageSimulateUtil

需要去重、拆分或批量分组时从 ListUtil 开始;需要按多个规则排序时用 ListSortModel;需要构树时根据对象是否已有 children 字段选择入口;需要读取多列表的部分组合时用候选组合分页

去重与拆分

下面分别去除重复值、解析逗号分隔的 ID、每两个元素分成一批。片段可放在 main 中,import 放在类定义前:

java
import com.own.constant.util.list.ListUtil;
import java.util.List;

var unique = ListUtil.distinct(List.of("A", "B", "A")); // [A, B]
var ids = ListUtil.Spilt.toLong("1,2,,3");              // [1, 2, 3]
var groups = ListUtil.groupList(List.of(1, 2, 3, 4, 5), 2);
// [[1, 2], [3, 4], [5]]
入口行为与边界
ListUtil.distinct(list)保留首次出现顺序,返回可变新列表;null/空输入返回可变空列表
distinct(list, keyExtractor)按 key 保留首项,允许一个 null key
retain(listOfLists)计算交集,按首列表顺序去重;输入为空、任一子列表为空或交集为空时为 Optional.empty()
listNonValueIndex(list, extractor)返回提取值非空白的索引,不是缺失值索引
groupList(list, length)Guava 分组视图,原列表变化会影响结果;非空输入要求 length 大于 0
groupList(list, length, converter)懒分组,访问时才转换;列表/函数须非 null,length 大于 0,size 随源列表变化
Spilt.str(text, splitLen)按字符数量拆分;先校验 splitLen 大于 0,再处理空白输入
Spilt.to/toInteger/toLong默认逗号分隔,可传分隔正则或转换函数;过滤空白项,不裁剪保留项

Spilt 保留现有拼写。分隔参数按正则解释,按字面量 | 分隔要传 "\\|"toLong(" 1 ") 会因数字两侧空格失败。Spilt.to 系列对非空白输入返回不可修改列表,对空白输入返回可变空列表,需要修改时统一复制为 ArrayList

com.own.constant.function.CustomFunction.distinctByKey(extractor) 创建带状态的 Predicate,适用于 Stream 过滤。它使用 ConcurrentHashMap,提取 null key 会失败,且并行 Stream 下不保证保留输入首项;每次去重都应新建 Predicate。CustomFunction.merge() 则返回保留第一个参数的 BinaryOperator,可用于 Map 收集时解决重复键。

排序与分组映射

返回排序副本

使用 ListSortModel<T> 按添加顺序组合显式顺序和 Comparator:

java
import com.own.constant.util.list.ListSortModel;
import java.util.Comparator;
import java.util.List;

record Item(String type, int score) {}

var sorted = new ListSortModel<Item>()
        .add(Item::type, List.of("VIP", "NORMAL"))
        .add(Comparator.comparingInt(Item::score))
        .sort(List.of(new Item("NORMAL", 1), new Item("VIP", 3), new Item("VIP", 2)));
// VIP/2、VIP/3、NORMAL/1

显式顺序中未知 key 排在已知 key 后;sort 不修改原列表,非空结果不可修改,null/空输入原样返回。

新写多规则排序可以从上面的 ListSortModel 开始;需要直接改变原列表或写入序号时,使用下一节的 ListUtil.Sort

已使用 ListSortUtil.hybrid 的项目

ListSortUtil.hybrid(list, rules...) 是相同排序任务的另一入口。通过 HybridSortMethod.builder() 设置 keyList + extractorkeyExtractor:同一条规则两者都提供时优先显式顺序,自然顺序为升序且 null 在末尾。多条规则按参数顺序逐级比较,非空结果也是不可修改的新列表。

调整原列表或元素

ListUtil.Sort 方法修改范围
sort(list, keyList, extractor)clear/addAll 重排原列表;未知 key 保持原顺序放末尾,不能传不可修改列表
setSortOrder(list, setter[, start])按当前列表顺序写序号,默认从 0 开始
collate(list, comparator, setter[, start])排序副本后写元素序号;原列表顺序不变,原元素会改变
removeRepeatCollate(list, valueGetter, orderGetter, setter)按已有序号检查相邻业务值,将相等项序号归到首项

MapUtil.group(item, keyFunction, valueFunction) 可将一个值分发到多个 key;也可使用 group(keys, value[, existingMap])。它向列表追加,重复 key 会重复添加,null key 跳过;传入既有 Map 时会就地修改。

DefaultValueHashMap<K,V> 位于 com.own.constant.util,通过 Supplier 创建缺省值。get 使用 computeIfAbsent,不存在或当前值为 null 都可能调用工厂并写回;它不是只读查询,也没有并发保护。

构建与遍历树

已有对象只包含 id 和 parentId 时,可用 TreeNode 包装,而不必增加 children 字段:

java
import com.own.constant.util.list.TreeNode;
import java.util.List;

record Node(Long id, Long parentId, String name) {}

var roots = TreeNode.buildForOriginList(
        List.of(new Node(1L, null, "root"), new Node(2L, 1L, "child")),
        Node::id, Node::parentId);
System.out.println(roots.getFirst().getChildren().getFirst().getItem().name());
// child

BaseTreeNode.buildForOriginList 支持自定义包装工厂,BaseTreeNode.build 则接收已包装的节点。挂载时 children 为 null 会新建列表;parentId 为 null 或父节点不存在的节点作为根返回。不要使用 null id,以免与根标记产生歧义。

已有可变 children 字段时可用 ListUtil.Tree.build

参数形式根节点规则
list, idKey, parentIdKey, childrenKeyparentId 为 null 或父 id 不存在
再传 rootKey选择 id 等于 rootKey 的节点,不是选择其直接子节点
再传 levelKey, rootLevel指定层级作为根
再传 levelKey自动选择最小层级

该入口通过 getter 取得 children 后追加;children 为 null 时跳过挂载,不会创建列表。需要提前初始化可变 children。两类构树工具都不清空已有 children、不检测环,重复构建可能重复挂载;重复 id 的父节点映射保留首项,不表示会移除重复节点。

树上的后续操作

入口实际语义
ListUtil.Tree.level给节点写层级,默认从 1 开始,可传初始层级
ListUtil.Tree.leafLast将真正无子节点的节点标为 true
ListUtil.Tree.leaf将所有后代标为 true,包含中间节点;不标记根
ListUtil.Tree.chain给非根节点写祖先文本链,不包含当前节点
ListUtil.Tree.getByParentId实际按节点 id 搜索,未找到返回 null
BaseTreeNode.recurrenceCheck子节点优先查找,首次匹配后短路;对不匹配的已访问分支调用 consumer
ChildrenUtil.recursion前序遍历,先处理当前节点,再递归 children

遍历方法不检测环,输入应为已经校验的无环结构;leafLast 也不会自动将先前的 true 重置为 false。

候选组合分页

需要把颜色和尺码等多个候选列表组合,又只读取其中一页时,使用 PageSimulateUtil.of。下面读取第一页三项,页码从 1 开始:

java
import com.own.constant.util.PageSimulateUtil;
import java.util.List;

var page = PageSimulateUtil.of(List.of("red", "blue"), List.of("S", "M"),
        1, 3, (color, size) -> color + "-" + size);
System.out.println(page); // [red-S, red-M, blue-S]

完整组合顺序是 red-S → red-M → blue-S → blue-M,最后一维变化最快。将页码改为 2、页大小仍为 3,结果为 [blue-M];改为第 3 页则为空。size() 是当前页数量,没有公开的总量 getter。

需要末尾回绕时,可开启循环模式:

java
import com.own.constant.util.PageSimulateUtil;
import java.util.List;

var cycle = PageSimulateUtil.of(List.of("a", "b"), 2L, 3, true);
System.out.println(cycle); // [b, a, b]

循环模式在总量大于 0 时固定返回 pageSize 项。两种模式下,任一源列表为空时结果都为空。

调用方须保证页码大于等于 1、pageSize 大于等于 0,并限制最大页大小。实现没有主动校验,也不防页码乘法溢出;四、五维索引计算还包含 int 乘积,不能因总量字段为 long 就认为可支持任意大的组合。

列表保留源引用并预计算总量,构造后不要增删或重排源列表。每次 get 都会重新调用转换函数,不缓存结果;遍历、打印、序列化也会触发计算。需要稳定快照时复制当前页,避免在转换函数中发请求、写库或递增计数。它不提供写操作,也不是 Base Business 的数据库分页响应模型。

列表维数、返回类型与重载参数

PageSimulateUtil.of 支持一到五个源列表,返回 PageSimulationList1PageSimulationList5。单列表默认返回原元素,多列表默认返回相应 Tuple2Tuple5,也可传转换函数。

参数顺序(前面先传源列表)行为
pageNum, pageSize普通分页,默认元素/元组
pageNum, pageSize, cycle可选循环模式
pageNum, pageSize, converter普通分页,自定义返回类型
pageNum, pageSize, cycle, converter自定义转换与循环模式

上述模式均支持 int / long 页码。工具工厂的 cycle 在 converter 前面;直接调用实现类构造器时,两者顺序相反。推荐统一使用工具工厂。

随机与累计计算

ListUtil.Random.random(list, number) 复制并打乱后抽取;number 大于等于总量时直接返回原顺序副本。带 extractor 的重载混合约 60% 排序顶部和 40% 剩余随机项,默认高值优先。generateList(total, count[, isReplenish]) 随机拆分正整数,默认最后一项补齐;关闭补齐时总和可能小于 total。这些入口不提供安全随机或公平性保证。

ListUtil.Calc 提供 Integer/Long 前缀和,以及通用 accumulate、逐项 add。结果为新列表;通用 accumulate 对单元素直接返回该元素,不调用初始值 Supplier 或累计函数。