使用Stream实现统计计算

问题引入

 在之前的工作中,笔者曾经结果部门领导的一个需求,当时正在进行态势的项目开发,要求是能够统计出一些指标以反映态势的实时的状态,类似于DashBoard的功能。当时笔者因为一些其他的工作,所以一开始没有理解其意思,结果就被喷了一脸,之后理解了其意图是想要开发一个能够反映态势实时状态的指标。

这样的问题场景其实在程序员编程生活中经常出现。应该如何应对和解决呢。

实现过程

思想过程

 在这里我们要实现的目标是统计出每种类型的实时目标,目标从生产者哪里源源不断的出现,统计程序作为消费者,计算出在整个态势画面内高、中、低危险区域的个数。一种想法是我们要构建一个Map<type, Integer> statistics;当我们发现一个新的目标出现在某个区域时,我们把相应映射的计数器增加1,这对于一个从未进入态势画面的目标,很方便。可当目标从一个敏感区域进入另一个敏感区域呢?该怎么处理?比如说从低到中,从中到高,从高到中,从中到底,(正常情况下不考虑直接从高到底和从低到高),在实现时,需要维持一个中间结构Map<type, List > realTimeScenario;来维持每个区域内的目标,realTimeScenario随着时间的变动,目标的变化,数据结构也在不断的变化。因此这样的代码逻辑实现起来如下:

Map<type, Integer> statistic(Target target){
    // 获取目标的威胁等级
	Type type = target.getType();
	
    
    // 从realTimeScenario中基于targetId获取其原来所在的级别
    Type preType = getPreThreatenType(realTimeScenario, target);
	if (preType != null) {
        // 之前出现在态势区域内
        realTimeScenario.get(preType).remove(target);
    }
    realTimeScenario.get(Type).add(target);
    // 根据实时画面的目标列表统计出每个危险等级的统计个数
    return getStatic(realTimeScenario);
}

 上述的代码是可以实现的,主干逻辑也是没有问题的,但这里由于类型个数只有3中低、中、高还可以忍受,但如果统计的类型更多呢?尤其是在计算之前目标的威胁等级时,需要遍历整个realTimeScenario这几乎是无法忍受的,而且这种开销每次目标上来时都需要进行O(n)次比较,而且写的代码由于多种情况,又丑又长,令人无法忍受。因此综合考虑上述伪代码,因此我们摒弃这种代码而且由于目标一直在不断的上报,我们也无法直接对statistics进行加锁,导致代码效率更低。有没有更好的方式呢?

List+Stream的方式

 当时在实现这些代码时,第一直观印象就是前一节维持Map<type, List > realTimeScenario数据结构。这是解决该问题最直接有效的方式。但是有没有更有效的方式呢?

  • 直接按照威胁等级进行分类,需要考虑威胁等级的变化。
  • 倘若把态势区域作为一个整体考虑呢?就是我们把低、中、高的目标维持在一起?

我记得当时我思考这个问题的时候,是在从沈塘桥出来向宿舍走去的路上,当时车流并不多,晚上八九点钟的样子,也不知道是什么原因就突然灵光一现,我们可以维持整体的目标作为一个List这很直接,上来一个目标,也很简单,如果不包含,就直接添加,如果原来包含了,就移除之后,再添加,这样代码写起来也很简单。

这其实也是一种List转Map实现统计的转化方式。

使用策略模式移除if-else也有异曲同工之妙

阿里Java分词则采用了从Map转换为List以按照值进行排序

上述两篇文章,均可以思考和体会,如果采用一种方式,理解起来有点费劲,或者代码写起来也不好理解,那么我们便可以采用这种转换的方式,思考有没有其他等价的方式能够方便的解决。

那不多数,我们如何实现这个问题呢?

Target唯一性判断

 由于我们要使用List维持一组Target以反映实施态势场景画面,而目标的属性是以目标批号彼此区分的,也就是说,我们并不关注上报的目标的其他属性信息,目标的唯一性判断仅依靠目标批号就可以执行。

package com.cetc52.situation.domain.entity;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import java.util.Objects;

/**
 * 融合目标信息实体
 *
 * @author songquanheng
 * 2021/9/16 11:07
 */
@Entity
@Table(name = "tshd_target")
@Getter
@Setter
public class TargetEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * 该目标是否被跟踪
     */
    @Transient
    private Integer tracked = 0;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TargetEntity that = (TargetEntity) o;
        return Objects.equals(targetNo, that.targetNo);
    }

    @Override
    public int hashCode() {
        return Objects.hash(targetNo);
    }
}

使用List维持态势区域内所有目标

 有了上述的Target的唯一性校验之后,我们便可以使用Set进一步提升目标的查询效率(之前态势画面中是否包含目标)。因为Set天然就有包含的语音。我们不采用List的来维持画面,就是因为List查找的效率更低,所以不优美。

package com.cetc52.situation.service;

import com.cetc52.situation.domain.entity.TargetEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * 目标威胁统计服务
 *
 * @author songquanheng
 * 2021/12/9 17:19
 */
@Service
@Slf4j
public class StatisticThreatenService {

    private Set<TargetEntity> targets = new CopyOnWriteArraySet<>();
    private Map<Integer, Long> getThreatMap() {
        return targets.stream()
            .collect(Collectors.groupingBy(TargetEntity::getTargetThreatLevel, Collectors.counting()));
    }

    private long getCount(Map<Integer, Long> collect, int threatLevel) {
        return collect.getOrDefault(threatLevel, 0L);
    }


    public void removeTarget(Long stayDuration) {
        targets.removeIf(getFuseTargetDeletePredicate(stayDuration));
    }

    /**
     * 删除目标条件
     *
     * @return
     */
    private Predicate<TargetEntity> getFuseTargetDeletePredicate(Long removeTime) {
        return fuseTargetEntity -> fuseTargetEntity.getTimestap() + removeTime < System.currentTimeMillis();
    }

    /**
     * 更新目标信息,主要用于更新目标时间
     * 1. 根据targetNo删除原目标信息
     * 2. 添加目标当前信息
     * @param targetEntity
     */
    public synchronized void update(TargetEntity targetEntity) {
        targets.remove(targetEntity);
        targets.add(targetEntity);
    }

}

上述代码有几个点是需要注意的,首先是上报了一个新的点。

更新态势画面
/**
     * 更新目标信息,主要用于更新目标时间
     * 1. 根据targetNo删除原目标信息
     * 2. 添加目标当前信息
     * @param targetEntity
     */
    public synchronized void update(TargetEntity targetEntity) {
        targets.remove(targetEntity);
        targets.add(targetEntity);
    }

 尽管CopyOnWriteArraySet 的targes是线程安全的,但是在多线程环境下,对于updates的实现而言,两个语句均使用了共享状态targets,并且均是对其的更新,因此我们要对update函数添加synchronized,因为两个语句均是直接操作内存,对于set而言也比较高效,也就没有必要使用synchronized代码块来进一步细分了。

因此由于逻辑简单干脆,我们即可以省略对于包含的判断,直接调用**targets.remove(targetEntity);**来移除目标之前的状态,因为是Set嘛,如果之前不包含次目标,该语句执行不产生实际作用。

List到Map的转换
private Map<Integer, Long> getThreatMap() {
        return targets.stream()
            .collect(Collectors.groupingBy(TargetEntity::getTargetThreatLevel, Collectors.counting()));
    }

 上述代码中使用了Stream表达式,在编程生活中,集合遍历、过滤、规约、统计和匹配均可以优雅的使用Lambda表达式和Stream,不可不会,不可不擅长使用。上述的代码直接通过List完成了根据威胁等级进行分组,直接统计出了每个分组的数量,非常的优雅。

JDK8中Lambda深入理解和stream实践较为详细的阐述了JDK8 Lambda表达式的用法。

JDK8:Lambda表达式操作List集合也描述了许多使用Lambda操作List集合的直接使用。

下载

使用Stream实现统计是本文的资源。

其中也包含了笔者使用XMind对于Lambda表达式的总结。

总结

 上述代码片段的核心其实只有两个,统计时如果直接按照类型进行统计比较麻烦,则可以统计全部,再按照类型进行分组,这是核心的思想。另外就是工欲善其事必先利其器。其实也是比较简单的一种场景。多个线程操作一个共享状态变量,一个线程逐渐累积变化,另外一个线程,基于时间移除过期的目标(当变化到了一定的时间,定期刷新),这种场景其实非常多。

 最近笔者也很苦恼,因为周六周日自己其实应该写专利的,但却因为内心抗拒应该做的事情,却选择了逃避,选择了看韩剧,周六周日连续看了两天,之后又非常的难过。有点痛苦。笔者不喜欢这样失控的自己,我希望的是啊,我未来的每一天都能够干干净净的,不会沉迷于电视剧,知道自己在做什么?在朝着什么样的目标,解决什么样的问题,在不断的积累。

 亲爱的读者,或许你也有类似的苦恼吧,这世界上其实只有两件事,应该做的事情,想要做的事情,其实我们应该选择的是应该做的事情,一定非常抗拒吧,但就是这些抗拒之中,包含着成长搜需要的养料。希望大家都能从痛苦的事情中获得进步,不要逃避无聊和抗拒,坚持去做正确的事、苦难的事情、让你焦虑的事情。

2022年1月10日22:06:34于文一路38弄

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值