技术tips-1)java steam的一些好用方法的总结,如分组后自定义排序等。

本文探讨在简化数据库查询逻辑的背景下,如何利用Java Stream进行数据处理。通过创建班级、爱好和学生表的示例,展示如何进行数据排序、分组以及按条件筛选。文章提供了多个测试用例,包括根据字段排序、按字段分组并选取特定记录等操作,同时也对比了Stream和Guava Multimaps在处理大量数据时的性能。
摘要由CSDN通过智能技术生成


背景

越来越多的场景下,从数据库获取数据被要求简单、不得包含更多的业务逻辑,而是建议单纯的打中【索引】取【合理数量】的数据至内存中,再通过代码进行二次处理。

在这一样的背景下,通过steam相关方法进行二次数据处理感觉是一个较为方便的方式。


场景

我们构建相关场景,并建立相关表进行后续案列表述。
相关项目地址: github(含sql语句)


1.数据表相关
  • 班级表
CREATE TABLE `t_test_class`
(
    `id`          bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `name`        varchar(32) NOT NULL COMMENT '名称',
    `create_time` timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `modify_time` timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    PRIMARY KEY (`id`) COMMENT '主键'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='班级表';
  • 爱好表
CREATE TABLE `t_test_hobby`
(
    `id`          bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `desc`        varchar(32) NOT NULL COMMENT '描述',
    `create_time` timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `modify_time` timestamp   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    PRIMARY KEY (`id`) COMMENT '主键'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='爱好';
  • 学生表
CREATE TABLE `t_test_student`
(
    `id`               bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `class_id`         bigint(20) unsigned NOT NULL COMMENT '班级id',
    `hobby_id`         bigint(20) unsigned NOT NULL COMMENT '兴趣班id',
    `age`              int(11) unsigned NOT NULL COMMENT '年龄',
    `last_attend_time` timestamp NULL DEFAULT NULL COMMENT '最后一次参加兴趣班时间',
    `create_time`      timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `modify_time`      timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
    PRIMARY KEY (`id`) COMMENT '主键'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='学生表';

模型比较简单,这次的案例主要涉及学生相关事项;学生:班级=1:1 , 学生:爱好=1:1。

为了后续扩展相关案例比较方便,搭建了一个springboot+mybatis plus 的一个案例,做数据填充跟持久层数据交互。


2.代码程序相关
  1. 基础测试引导类 :cn.lcy.stream#BaseTransactionTest
@Slf4j
@Rollback(value = true)
@SpringBootTest(classes = StreamApplication.class)
public class BaseTransactionTest extends AbstractTransactionalTestNGSpringContextTests {

    @Test
    public void testEnv() throws InterruptedException {
        Thread.sleep(2000L);
        System.out.println("test env running");
    }

}
  1. cn.lcy.stream.demo1#StreamApplicationTests extends BaseTransactionTest
    数据初始化,方便后续测试。
@BeforeClass(description = "初始化数据")
    public void initData() {

        Long classId = 10L;
        Long hobbyId = 100L;

        //create students;
        ArrayList<StudentDO> firstConditionHobbyAndClass = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            StudentDO studentDO = new StudentDO();
            studentDO.setClassId(classId + RandomUtils.nextInt(5));
            studentDO.setHobbyId(hobbyId + RandomUtils.nextInt(5));
            studentDO.setAge(RandomUtils.nextInt(18));
            studentDO.setLastAttendTime(DateTime.now().plusHours(RandomUtils.nextInt(10)).toDate());
            firstConditionHobbyAndClass.add(studentDO);
        }

        studentDataSupport.saveBatch(firstConditionHobbyAndClass);

    }

case 1 :

从简单开始,第一个案例是根据一个字段或者多个字段对List等进行排序。

    @Test(description = "case1 : 根据字段排序")
    public void sortTest() {

        //当前的全量学生
        List<StudentDO> requestData = studentDataSupport.list();

        List<StudentDO> sortList = requestData.stream().sorted(
                Comparator.comparing(StudentDO::getAge)     //首先根据age排序
                        .thenComparing(StudentDO::getHobbyId).reversed()//其次根据id编号排序
        ).collect(Collectors.toList());

        List<StudentDO> sortList2 = requestData.stream().sorted(Comparator.comparing(StudentDO::getAge))
                .collect(Collectors.toList());

        log.info("sortList response = {} ", sortList);
        log.info("sortList2 response = {} ", sortList);

    }

case 2 :

下面是一个分组的归并;
当groupBy的字段于list中的元素重复时,使用List进行装载。
目前发现两种比较好用的方式,分别是streamguava中Multimaps

简单测试了下,10万数据量下两种方法耗时对于业务开发可以忽略不计。

    @Test(description = "case2 : 根据hobbyId对students进行分组,如果hobbyId重复,则使用集合进行装填;")
    public void extractedByHobbyId() {

        //当前的全量学生
        List<StudentDO> requestData = studentDataSupport.list();

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        //test stream
        Map<Long/*hobby_id*/, List<StudentDO>/*hobby_id对应的students*/> result = requestData.stream()
                .collect(
                        Collectors.toMap(
                                StudentDO::getHobbyId, // key wrapper
                                s -> {                  // value wrapper
                                    List<StudentDO> list = new ArrayList<>();
                                    list.add(s);
                                    return list;
                                },
                                (List<StudentDO> value1, List<StudentDO> value2) -> {   //mergeFunc
                                    value1.addAll(value2);
                                    return value1;
                                }
                        )
                );
        stopWatch.stop();
        log.info("stream : elapsed[{}]", stopWatch.getTime());
        log.info("stream : size[{}]", result.size());// elapsed[27]

        stopWatch.reset();
        stopWatch.start();
        //test Multimaps
        ImmutableListMultimap<Long, StudentDO> index = Multimaps.index(requestData, StudentDO::getHobbyId);
        log.info("guava : elapsed[{}]", stopWatch.getTime());//elapsed[58]

    }

case 3:

根据condition进行分组,感觉蛮好用但是比较冷僻的方法。

    @Test(description = "case3 : 根据表达式进行分组")
    public void testCondition() {

        List<StudentDO> requestData = studentDataSupport.list();

        Map<Boolean/*true 有兴趣班*/, List<StudentDO>> haveHobbyStudents =
                requestData.stream().collect(Collectors.groupingBy(e -> e.getHobbyId() > 0));

    }

case 4:

重点根据字段进行分组,对于分组后的数据进行排序。

  • 题设:根据hobby_id进行分组,随后对分组后的数据选取last_attend_time字段最大的记录。
    @Test(description = "case4 : 根据字段分组随后进行排序并选取top1或者topN")
    public void groupAndThenSort() {

        //当前的全量学生
        List<StudentDO> requestData = studentDataSupport.list();

        //方法一: 先分组,随后从组内寻找【指定】数据
        Collector<StudentDO, ?, Optional<StudentDO>> sortFunction = Collectors.reducing((c1, c2) -> c1.getAge() > c2.getAge() ? c1 : c2);
        Map<Long/*hobby_id*/, StudentDO> groupAndSorting1 = requestData.stream()
                .collect(Collectors.groupingBy(StudentDO::getHobbyId, // 先根据HobbyId分组
                        Collectors.collectingAndThen(sortFunction, Optional::get)));// 随后排序
                        
        //方法二: 当key冲突时,以怎样的方式保留唯一key相关的数据
        Map<Long/*hobby_id*/, StudentDO> groupAndSorting2 = requestData.stream()
                .collect(Collectors.toMap(StudentDO::getHobbyId, Function.identity(), (c1, c2) -> c1.getAge() > c2.getAge() ? c1 : c2));// 随后排序

        //case 3: 类似的sql
        /**
         * SELECT *
         * FROM   (SELECT id,
         *                hobby_id,
         *                class_id,
         *                last_attend_time,
         *                IF(@bak = hobby_id, @rounum := @rounum + 1, @rounum := 1) AS
         *                row_number,
         *                @bak := hobby_id
         *         FROM   (SELECT id,
         *                        hobby_id,
         *                        class_id,
         *                        last_attend_time
         *                 FROM   t_test_student
         *                 ORDER  BY hobby_id ASC,
         *                           last_attend_time DESC) a,
         *                (SELECT @rounum := 0,
         *                        @bak := 0) b) c
         * WHERE  c.row_number < 2
         */

    }

  • groupAndSorting1 是做groupby随后进行andThen调用一个func,func中进行stream reduce计算。
  • groupAndSorting2 是将groupBy至一个map中的思路,其中做处理的方式主要表述在key重复的时候,如何选取。
  • SQL的方式比较通用,但同样写法与受众度比较小众,我们分拆来理解下。

SQL 解释:总体思路是想数据先以期望得方式进行排序,随后添加相关计数器,对出现的有序数据进行计数。因为原数据已拍过序,所有计数器约大的数据说明越在尾端。

  1. 当前语句很简单的根据hobby_id 与 last_attend_time 进行排序,这一步得到的结果已经得到了相关期望顺序。
    在这里插入图片描述
  2. 当前语句为声明两个变量,变量一为rownum,初始值为0。变量二为bak,用于记录当前记录读到的hobby_id。在这里插入图片描述
  3. 当前语句看似麻烦,实则理解起来不为麻烦。
    IF(@bak = hobby_id, @rounum := @rounum + 1, @rounum := 1) 共由三部分内容组成.

3.1 :@bak = hobby_id :为条件表达式,指bak变量当前的值若等于hobby_id。
3.2 :@rounum := @rounum + 1: 如果表达式成立,对当前记录的rounum进行+1。
3.3 :@rounum := 1:如果表达式不成立,则使rounum等于1.
在这里插入图片描述
因为在前一步骤中,已经对数据进行过相应排序,而rownumber对应的数字越大说明排序越后。

  1. 最终将上述语句套起来,选取需要的topN条即可。
    在这里插入图片描述
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值