考虑此类:-
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
是否可以简化将getCreatedYearMonth(Report report)
转换为Timestamp
的方法YearMonth
?当前,它将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));
}
似乎没有快速的方法可以将时间戳转换为YearMonth。我们可以从时间戳记中获取年月的字符串表示形式,如Amith所示。
答案 0 :(得分:4)
您不应将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)));
外部Collector
是groupingBy
:我们希望每个YearMonth
中都有东西。下游收集器将仅在同一月份看到Report
。
下游Collector
是collectingAndThen
,因为maxBy
收集器将产生Optional<Report>
。但是我们已经知道每个月至少有一个Report
,因此我们只需对其进行包装。
最里面的收集器只是通过Timestamp
获得最大值。
答案 1 :(得分:1)
您可以将Java流与下面的有状态谓词一起使用,以按月过滤第一个报告。
注意:-不要使用parallelStream()运行此函数,因为它不是线程安全的,并且还假定该列表按日期排序,以获得按月和年顺序选择第一个的期望结果。< / p>
重点
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);
}
}
TESTABLE(完整)代码
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 + "]";
}
}
答案 2 :(得分:1)
要回答第一个问题:
使用标准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的扩展。