在 FreeMarker 模板引擎中,集合操作是非常常见的功能,尤其在处理 Java 中的 ListMap、数组等结构时。本文将系统性地介绍 FreeMarker 中的集合操作函数:size, first, last, reverse, sort, sort_by,并涵盖核心概念、操作步骤、常见错误、注意事项、使用技巧、最佳实践与性能优化等内容,帮助开发者快速掌握并高效实践。


一、核心概念

操作 说明
size 获取集合的元素个数
first 获取集合的第一个元素
last 获取集合的最后一个元素
reverse 反转集合顺序
sort 对集合进行排序(按自然顺序)
sort_by 按照指定属性排序集合(适用于对象集合)

⚠️ 注意: 这些内建函数仅适用于可迭代对象,如 List、数组、Set(部分支持)等。


二、操作步骤(详细)

步骤 1:准备数据模型

确保传递给模板的数据中包含集合类型(如 ListMap 等)。

List<String> fruits = Arrays.asList("apple", "banana", "orange");
List<Map<String, Object>> users = new ArrayList<>();
users.add(Map.of("name", "Alice", "age", 25));
users.add(Map.of("name", "Bob", "age", 22));
users.add(Map.of("name", "Charlie", "age", 30));

Map<String, Object> model = new HashMap<>();
model.put("fruits", fruits);
model.put("users", users);

步骤 2:在模板中使用集合操作

2.1 获取集合大小 ?size

集合大小:${fruits?size}

输出:

集合大小:3

2.2 获取第一个元素 ?first

第一个元素:${fruits?first}

输出:

第一个元素:apple

2.3 获取最后一个元素 ?last

最后一个元素:${fruits?last}

输出:

最后一个元素:orange

2.4 反转集合 ?reverse

反转后的集合:
<#list fruits?reverse as fruit>
  ${fruit}
</#list>

输出:

反转后的集合:
orange
banana
apple

2.5 自然排序 ?sort

自然排序后的集合:
<#list fruits?sort as fruit>
  ${fruit}
</#list>

输出(按字母顺序):

apple
banana
orange

2.6 按属性排序 ?sort_by

按年龄排序的用户:
<#list users?sort_by("age") as user>
  ${user.name} - ${user.age}
</#list>

输出:

Bob - 22
Alice - 25
Charlie - 30

三、格式与语法说明

3.1 ?sort 的排序规则

  • 对字符串按字母顺序排序(区分大小写)
  • 对数字按数值大小排序
  • 对布尔值:false < true
  • Map 或 JavaBean,需要指定 ?sort_by 字段

3.2 ?sort_by 的嵌套属性

支持嵌套属性访问:

<#list users?sort_by("address.city") as user>

四、常见错误与注意事项

4.1 类型错误:非集合类型使用集合操作

错误示例:

${"hello"?size}

错误原因: 字符串不是集合类型。

解决办法: 确保操作对象是 ListMap、数组等。

4.2 ?sort_by 字段不存在或类型不一致

错误示例:

<#list users?sort_by("gender") as user>

错误原因: gender 属性在某些对象中不存在。

解决办法:

  • 确保所有对象都有该字段
  • 或使用 ! 提供默认值:?sort_by("gender!unknown")

4.3 ?reverseMap 无效

Map 不是有序结构,使用 reverse 可能无效果或抛异常(取决于版本)。


五、使用技巧

5.1 使用 <#assign> 缓存处理后的集合

<#assign sortedUsers = users?sort_by("age")>

避免重复计算,提高性能。

5.2 多条件排序(模拟)

FreeMarker 本身不支持多字段排序,但可以借助 Java 工具类或模板逻辑模拟:

model.put("multiSort", (Comparator<Map<String, Object>>) (o1, o2) -> {
    int cmp = Integer.compare((Integer) o1.get("age"), (Integer) o2.get("age"));
    if (cmp == 0) {
        cmp = ((String) o1.get("name")).compareTo((String) o2.get("name"));
    }
    return cmp;
});

模板中:

<#assign sortedUsers = users?sort(multiSort)>

5.3 使用 ?chunk 分组显示集合

<#list fruits?chunk(2) as group>
  <#list group as fruit>${fruit} </#list><br>
</#list>

输出:

apple banana
orange

六、最佳实践

实践 说明
✅ 尽量在 Java 层处理复杂排序 提升模板可读性和性能
✅ 使用 ?assign 缓存中间结果 避免重复计算
✅ 对 Map 排序前转换为 List 保证排序逻辑清晰
✅ 使用 ?chunk 实现分页展示 如表格分组、轮播图等场景
✅ 为 sort_by 提供默认值 ?sort_by("age!0") 避免空值报错

七、性能优化建议

优化点 说明
❌ 避免在 <#list> 中重复调用 ?sort / ?reverse 重复排序影响性能
✅ 提前在 Java 层排序 适合大数据量
✅ 使用 <#assign> 缓存结果 避免重复计算
✅ 对大集合进行分页处理 避免一次性渲染过多数据

八、总结

操作 用途 适用对象
?size 获取集合长度 List、Map、Array
?first 获取第一个元素 List、Array
?last 获取最后一个元素 List、Array
?reverse 反转集合顺序 List、Array
?sort 自然排序 List(支持 Comparable)
?sort_by 按字段排序 List、List

九、附录:常用表达式示例汇总

表达式 输出示例
${fruits?size} 3
${fruits?first} apple
${fruits?last} orange
${fruits?reverse} orange, banana, apple
${fruits?sort} apple, banana, orange
${users?sort_by("age")} 按年龄排序
${users?sort_by("name")} 按名字排序
${users?sort_by("age")?first.name} 获取最小年龄用户名称

十、扩展建议

如需更复杂集合操作(如分组、过滤、合并等),推荐:

  • 在 Java 层使用 Stream APIGuava 等工具类处理后再传入模板
  • 在模板中使用宏(<#macro>)封装常用逻辑,提高复用性

如需进一步封装,可以创建一个 CollectionTool 类,提供 filter, map, groupBy, limit 等功能,供模板调用。


结语: FreeMarker 的集合操作虽简单,但掌握其细节和最佳实践,可以极大提升模板开发效率和性能表现。结合 Java 层处理和模板逻辑,能构建出更强大、灵活的视图层逻辑。