【问题标题】:Java 8 filter considering multiple items of the streamJava 8过滤器考虑流的多个项目
【发布时间】:2018-06-20 01:19:17
【问题描述】:

考虑这个类:-

import java.sql.Timestamp;

public class Report {
    private short value;
    private Timestamp created;
    //Getters, Setters
}

我有一个List 使用ORDER BY created DESC 从数据库获得的报告。

任务是获取每个月的最新报告。我知道它可以在 SQL 级别完成,但由于某种原因我需要在 Java 中完成。

我就是这样解决的:-

    /**
     * Assuming that the reports are sorted with <code>ORDER BY created DESC</code>, this method filters the list so
     * that it contains only the latest report for any month.
     *
     * @param reports Sorted list of reports
     * @return List containing not more than one report per month
     */
    public static List<Report> oneReportPerMonthFilter(List<Report> reports) {
        Map<String, Report> monthlyReports = new HashMap<>();
        reports.forEach(report -> {
            String yearMonth = getCreatedYearMonth(report);
            if (!monthlyReports.containsKey(yearMonth)) {
                monthlyReports.put(yearMonth, report);
            }
        });
        return new ArrayList<>(monthlyReports.values());
    }

    private static String getCreatedYearMonth(Report report) {
        return YearMonth
                .from(ZonedDateTime.of(report.getCreated().toLocalDateTime(), ZoneOffset.UTC))
                .toString();
    }

问题 1

虽然按预期工作,但我必须创建一个Map,然后将values 转换回List。使用 Java 8 Stream API 有更好的方法吗?也许是更“实用”的方式?

问题 2

Timestamp 转换为YearMonth 的方法getCreatedYearMonth(Report report) 可以简化吗?目前它将Timestamp 更改为LocalDateTime,然后更改为ZonedDateTime,然后更改为YearMonth

单元测试:-

@Test
public void shouldFilterOutMultipleReportsPerMonth() {
    Report report1 = new Report();
    report1.setCreated(Timestamp.from(Instant.EPOCH));
    report1.setValue((short) 100);

    Report report2 = new Report();
    report2.setCreated(Timestamp.from(Instant.EPOCH.plus(10, ChronoUnit.DAYS)));
    report2.setValue((short) 200);

    Report report3 = new Report();
    report3.setCreated(Timestamp.from(Instant.EPOCH.plus(40, ChronoUnit.DAYS)));
    report3.setValue((short) 300);

    List<Report> reports = Stream.of(report3, report2, report1).collect(Collectors.toList());

    List<Report> filteredReportList = ExampleClass.oneReportPerMonthFilter(reports);
    Assert.assertEquals(2, filteredReportList.size());
    Assert.assertEquals((short) 300, (short) filteredReportList.get(0).getValue());
    Assert.assertEquals((short) 200, (short) filteredReportList.get(1).getValue());
}

编辑 1

回答

感谢大家的回答。使用 Amith 和 Johannes 的答案,我想出了这个简单易读的版本:-

public static List<Report> oneReportPerMonthFilter(List<Report> reports) {
    Set<YearMonth> found = new HashSet<>();
    return reports.stream()
            .filter(r -> found.add(getCreatedYearMonth(r)))
            .collect(Collectors.toList());
}

private static YearMonth getCreatedYearMonth(Report report) {
    return YearMonth.from(
            report.getCreated()
                    .toInstant()
                    .atZone(ZoneOffset.UTC));
}

似乎没有快速将时间戳转换为年月的方法。我们可以从 Timestamp 中获取年月的字符串表示形式,如 Amith 所示。

【问题讨论】:

  • 对于此类问题,提供MCVE(最小、完整且可验证的示例)会很有帮助,这样我们就可以自己运行代码并验证我们的答案是否正确。
  • @Radiodef 我已经提供了模型类(除了可以生成的 getter/setter)、有问题的方法以及可以运行以测试您的解决方案的单元测试。请告知我还应该添加什么。
  • 为了使代码编译和运行而无需付出太多努力所需的一切。如果您不这样做,我认为您的问题不会被关闭或在这种情况下发生任何事情。这只是一个建议,让您更有可能获得更好的答案。
  • YearMonth 可以直接从LocalDateTime 创建。中间不需要转换成ZonedDateTime。您可以使用Arrays.asList() 获得Report 的列表。

标签: java java-8 functional-programming java-stream


【解决方案1】:

您不应将YearMonth 转换为字符串。只需省略 toString() 部分。此外,我能够将其简化为:

private static YearMonth getCreatedYearMonth(Report report) {
    return YearMonth.from(report.getCreated().toInstant().atZone(ZoneOffset.UTC));
}

为了得到你想要的结果,你必须链接一些收集器:

Map<YearMonth, Report> last = reports.stream()
        .collect(Collectors.groupingBy(r -> getCreatedYearMonth(r),
                Collectors.collectingAndThen(
                        Collectors.maxBy(Comparator.comparing(Report::getCreated)),
                        Optional::get)));

外部CollectorgroupingBy:我们想要每个YearMonth 的东西。下游收集器在同一个月只会看到Reports。
下游CollectorcollectingAndThen,因为maxBy 收集器将产生Optional&lt;Report&gt;。但是我们已经知道每个月至少有一个Report,所以我们只是打开它。
最里面的收集器只是通过Timestamp 获得最大值。

【讨论】:

  • 如果你只想要一份报告列表,你可以List&lt;Report&gt; lastList = new ArrayList&lt;&gt;(last.values());
  • 顺便说一下,这个解决方案适用于并行流。
  • +1 用于使用来自java.timeYearMonth。请注意对时区/偏移量的依赖性:问题使用 JVM 默认时区,答案使用 UTC。对于某些报告创建时间,结果会有所不同。
  • @OleV.V.这可以通过使用YearMonth.from(report.getCreated().toLocalDateTime()); 来解决
【解决方案2】:

您可以使用带有如下状态谓词的 Java 流来按月过滤第一份报告。

注意: - 不要使用 parallelStream() 运行它,因为它不是线程安全,还假设列表按日期排序以获得按月和年顺序选择第一个的所需结果。

亮点

    public static List<Report> oneReportPerMonthFilter(List<Report> reports) {
            Set<String> found = new HashSet<>();
            return reports.stream().filter(r -> found.add(getCreatedYearMonth(r))).collect(Collectors.toList());    
    }

    public static String getCreatedYearMonth(Report report) {
            //Or you can use SimpleDateFormat to extract Year & Month
            Calendar cal = Calendar.getInstance();
            cal.setTime(report.getCreated());
            return "" + cal.get(Calendar.YEAR) + cal.get(Calendar.MONTH);
        }
    }


可测试(完整)代码

import java.sql.Timestamp;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class ReportFilter {

    public static void main(String[] args) {
        Report report1 = new Report();      
        report1.setCreated(Timestamp.from(Instant.EPOCH));
        report1.setValue((short) 100);

        Report report2 = new Report();
        report2.setCreated(Timestamp.from(Instant.EPOCH.plus(10, ChronoUnit.DAYS)));
        report2.setValue((short) 200);

        Report report3 = new Report();
        report3.setCreated(Timestamp.from(Instant.EPOCH.plus(40, ChronoUnit.DAYS)));
        report3.setValue((short) 300);

        Report report4 = new Report();
        report4.setCreated(Timestamp.from(Instant.EPOCH.plus(40, ChronoUnit.DAYS)));
        report4.setValue((short) 400);

        List<Report> reports = Arrays.asList(report1, report2, report3, report4);
        List<Report> filteredReports = oneReportPerMonthFilter(reports); 
        System.out.println(filteredReports);
    }

    public static List<Report> oneReportPerMonthFilter(List<Report> reports) {
        Set<String> found = new HashSet<>();
        return reports.stream().filter(r -> found.add(getCreatedYearMonth(r))).collect(Collectors.toList());    
    }

    public static String getCreatedYearMonth(Report report) {
        //Or you can use SimpleDateFormat to extract Year & Month
        Calendar cal = Calendar.getInstance();
        cal.setTime(report.getCreated());
        return "" + cal.get(Calendar.YEAR) + cal.get(Calendar.MONTH);
    }
}

class Report {
    private Timestamp created;
    private short value;    

    public Timestamp getCreated() {
        return created;
    }

    public void setCreated(Timestamp created){
        this.created = created;
    }


    public short getValue() {
        return value;
    }

    public void setValue(short value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Report [created=" + created + ", value=" + value + "]";
    }
}

【讨论】:

  • 美丽。不过,您可能希望将 Set&lt;Object&gt; 更改为 Set&lt;String&gt;。我将其更改为Set&lt;YearMonth&gt;(我编辑了我的问题以添加我最终确定的解决方案)。
  • 是的,我同意。这是一个快速编译,为您提供可能的选项。并感谢您总结您的答案,这将有助于其他人将来参考。为创建年/月密钥提供的所有解决方案都很好且高效,无论如何它都不是瓶颈。很高兴我能提供帮助。
【解决方案3】:

回答你的第一个问题:

使用标准 API,如果不创建地图,实际上是没有办法做你想做的事的;但是,我确实重新编写了它以使其更惯用。

public static List<Report> oneReportPerMonthFilter2(List<Report> reports) {
    return reports.stream()
        .collect(Collectors.groupingBy(Q50938904::getCreatedYearMonth))
        .values().stream()
            .map(p-> p.get(0))
            .collect(Collectors.toList());
}

如果可以,请考虑查看 StreamEx。它是流 API 的扩展。

【讨论】:

    猜你喜欢
    • 2018-12-17
    • 1970-01-01
    • 2021-02-24
    • 1970-01-01
    • 2022-01-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-05-14
    相关资源
    最近更新 更多