SpEl表达式之强大的集合选择(Collection Selection)和集合投影(Collection Projection)

集合选择(Collection Selection)

Selection 是一个强大的表达式语言功能,它让你通过从一个源集合的条目中进行选择,将其转化为另一个集合。

选择使用的语法是 .?[selectionExpression]。它对集合进行过滤,并返回一个新的集合,其中包含原始元素的一个子集。

常规使用

  1. 如果list则循环的是每个元素,通过#this获取元素本身,也可以直接调用元素的方法和公共属性,调用方法或属性可省略#this,返回一个list
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.util.List;
public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 调用this
        var res1 = p.parseExpression("#root.?[#this<2]").getValue(List.of(1,2,3));
        assert res1 instanceof List<?> l && l.size()==1 && l.getFirst().equals(1);
        // 调用方法
        var res2 = p.parseExpression("#root.?[#this.intValue()<3]").getValue(List.of(1,3),List.class);
        assert  res2.size()==1 && res2.getFirst().equals(1);
        // 调用方法或属性可省略this
        var res3 = p.parseExpression("#root.?[intValue()<3]").getValue(List.of(1,3),List.class);
        assert res3.getFirst().equals(res2.getFirst());
        // 没匹配到时不会返回null,而是返回空集合
        var res4 = p.parseExpression("#root.?[#this<1]").getValue(List.of(1));
        assert res4 instanceof List l && l.isEmpty();
    }
}
  1. 如果是array,则返回一个array,需要注意的是即便传入的是原始类型数组,在转换后会被自动包装,其他情况类似list
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 结果将自动包装
        var res1 = p.parseExpression("#root.?[#this<2]").getValue(new int[]{1,2,3});
        assert res1.getClass().isArray() && res1.getClass().getComponentType().equals(Integer.class) && ((Integer[])res1).length==1 && ((Integer[])res1)[0]==1;
        // 自身不会被包装
        var res2 = p.parseExpression("#root.class.componentType").getValue(new int[]{1});
        assert res2.equals(int.class);
    }
}
  1. 如果是map,则循环的是每个entry,也可以通过#this获取entry本身,或者通过getKey()(可简化为key),getValue()(可简化为value),返回一个map,其他情况和list相同
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.util.Map;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 如果是map.则循环的是每个entry,也可以通过#this获取entry本身,或者通过getKey()或key,getValue()或value获取kv
        var res1 = p.parseExpression("#root.?[value=='aa']").getValue(Map.of('a',"aa",'b',"bb"));
        assert res1 instanceof Map<?,?> m && m.size()==1 && m.get('a').equals("aa");
        // 也可以用常规的方法表达式
        var res2 = p.parseExpression("#root.?[#this.getKey().toString()=='a']").getValue(Map.of('a',"aa"));
        assert res2 instanceof Map<?,?> m && m.size()==1 && m.containsKey('a');
    }
}

等价方法

投影功能类似于于stream.filter.collect操作

java 复制代码
// 数组类型等价方法
public <T> Object[] selection(T[] obj, Predicate<T> mapper){
    return Arrays.stream(obj).filter(mapper).toArray();
}
// 列表类型等价方法
public <T> List<T> selection(List<T> obj, Predicate<T> mapper){
    return obj.stream().filter(mapper).collect(Collectors.toList());
}
// 映射类型等价方法
public <K,V> Map<K, V> selection(Map<K,V> obj, Predicate<? super Map.Entry<K,V>> mapper){
    return obj.entrySet().stream().filter(mapper).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

选择首个匹配的元素或最后一个匹配的元素

使用表达式.^[selectionExpression]获取第一个匹配元素,使用表达式.$[selectionExpression]获取最后一个匹配元素.

  1. 如果是listarray将返回匹配的元素,而不是listarray,原始类型将自动装箱,如果没找到会返回null
java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.util.List;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 返回首个匹配的元素,如果是原始类型也会被自动包装
        var res1 = p.parseExpression("#root.^[#this<3]").getValue(List.of(1,3));
        assert res1 instanceof Integer i && i==1;
        // 如果找不到合适的元素,则返回null
        var res2 = p.parseExpression("#root.^[#this<1]").getValue(new int[]{1});
        assert res2 == null;
        // 使用条件true选择首个元素
        var res3 = p.parseExpression("#root.^[true]").getValue(new int[]{1});
        assert res3.equals(1);
        // 返回最后一个匹配的元素,只是把^换成$
        var res4 = p.parseExpression("#root.$[#this<3]").getValue(List.of(1,2,3));
        assert res4.equals(2);
    }
}
  1. 如果是map则返回的是一个只有一个entrymap,如果没有找到返回null,需要注意的是HashMap是不排序的,LinkedHashMap才是排序的
java 复制代码
import cn.hutool.core.map.MapUtil;
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.util.LinkedHashMap;
import java.util.Map;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 返回首个匹配的元素,如果是原始类型也会被自动包装,需要注意的是HashMap是不排序的,LinkedHashMap是排序的
        var res1 = p.parseExpression("#root.^[value<3]").getValue(
                MapUtil.builder(new LinkedHashMap<>()).put('a',1).put('b',2).put('c',3).build());
        assert res1 instanceof Map m && m.size()==1 && m.get('a').equals(1);
        // 返回最后一个匹配的元素
        var res2 = p.parseExpression("#root.$[value<3]").getValue(
                MapUtil.builder(new LinkedHashMap<>()).put('a',1).put('b',2).put('c',3).build());
        assert res2 instanceof Map m && m.size()==1 && m.get('b').equals(2);
        // 如果没有找到返回null
        var res3 = p.parseExpression("#root.$[value<3]").getValue(Map.of('a',3));
        assert res3 == null;
    }
}

等价方法

表达式.^[selectionExpression]类似于stream.filter.findFirst方法

java 复制代码
// 数组类型等价方法
public <T> T selectionFirst(T[] obj, Predicate<T> mapper){
    return Arrays.stream(obj).filter(mapper).findFirst().orElse(null);
}
// 列表类型等价方法
public <T> T selectionFirst(List<T> obj, Predicate<T> mapper){
    return obj.stream().filter(mapper).findFirst().orElse(null);
}
// 映射类型等价方法
public <K,V> Map<K, V> selectionFirst(Map<K,V> obj, Predicate<? super Map.Entry<K,V>> mapper){
    var s = obj.entrySet().stream().filter(mapper).findFirst().orElse(null);
    if (s==null) return null;
    return Map.of(s.getKey(),s.getValue());
}
// 列表类型获取最后一个匹配元素的等价方法
public <T> T selectionLast(List<T> obj, Predicate<T> mapper){
    return obj.stream().filter(mapper).toList().reversed().stream().findFirst().orElse(null);
}

不希望返回null

可以通过埃尔维斯(Elvis)运算符来避免返回null,其语法是objMayNull?:obj,如果objMayNull为空则返回obj,否则返回objMayNull

java 复制代码
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import java.util.List;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        var ctx = new StandardEvaluationContext();
        ctx.setRootObject(List.of(1));
        ctx.setVariable("defaultV",0);
        // 当无法筛选到合适的元素时,返回默认值0(defaultV)
        var res1 = p.parseExpression("#root.^[#this<1]?:#defaultV").getValue(ctx);
        assert Integer.valueOf(0).equals(res1);
    }
}

集合投影(Collection Projection)

投影让一个集合驱动一个子表达式的评估,其结果是一个新的集合.投射的语法是 .![projectionExpression].

遍历每一个元素并计算然后返回一个listarray,注意list投影后返回list,map投影后也是返回list,只有array投影后返回array,且返回值内的原始类型会被自动装箱.

java 复制代码
import cn.hutool.core.collection.ListUtil;
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.util.List;
import java.util.Map;

public class MainTests {
    @Test
    public void test(){
        var p = new SpelExpressionParser();
        // 投影list返回list,将每个元素乘2,返回[2,4],int会被包装为Integer
        var res1 = p.parseExpression("#root.![#this*2]").getValue(ListUtil.of(1,2));
        assert res1 instanceof List<?> l && l.size()==2 && l.getFirst().equals(2) && l.getLast().equals(4) && l.getFirst().getClass().equals(Integer.class);
        // 投影array返回array
        var res2 = p.parseExpression("#root.![#this*2]").getValue(new int[]{3});
        assert res2.getClass().isArray() && ((Integer[])res2)[0]==6;
        // 投影map返回list,需要注意的是如果map不是已排序的,返回的list也将不是
        var res3 = p.parseExpression("#root.![value]").getValue(Map.of('a',1,'b',2));
        assert res3 instanceof List<?> l && l.contains(1) && l.contains(2);
    }
}

等价方法

投影功能类似于于stream.map.collect操作

java 复制代码
// 数组类型等价方法
public <T> Object[] projection(T[] obj, Function<T,Object> mapper){
    return Arrays.stream(obj).map(mapper).toArray();
}
// 列表类型等价方法
public <T,R> List<R> projection(List<T> obj, Function<T,R> mapper){
    return obj.stream().map(mapper).collect(Collectors.toList());
}
// 映射类型等价方法
public <K,V,R> List<R> projection(Map<K,V> obj, Function<? super Map.Entry<K,V>, R> mapper){
    return obj.entrySet().stream().map(mapper).toList();
}
相关推荐
高兴达11 分钟前
Spring boot入门工程
java·spring boot·后端
萧曵 丶13 分钟前
Spring @TransactionalEventListener
java·数据库·spring·事务·transactional·异步
笑衬人心。13 分钟前
HTTPS详解:原理 + 加解密过程 + 面试问答
java·网络协议·http·面试·https
蓝澈112115 分钟前
弗洛伊德(Floyd)算法-各个顶点之间的最短路径问题
java·数据结构·动态规划
再见晴天*_*24 分钟前
logback 日志不打印
java·服务器·logback
幽络源小助理32 分钟前
SpringBoot基于JavaWeb的城乡居民基本医疗信息管理系统
java·spring boot·学习
欧阳有财35 分钟前
[java八股文][Mysql面试篇]日志
java·mysql·面试
TDengine (老段)44 分钟前
使用 StatsD 向 TDengine 写入
java·大数据·数据库·时序数据库·iot·tdengine·涛思数据
真实的菜1 小时前
JVM类加载系统详解:深入理解Java类的生命周期
java·开发语言·jvm
N_NAN_N1 小时前
类图+案例+代码详解:软件设计模式----原型模式
java·设计模式·原型模式