Я хотел бы сгруппировать коллекцию объектов Rental в установленный срок, но я хочу создать новый объект RentalReport для каждой группы с ключом в качестве предопределенного значения (перечисление) и группой в качестве свойства этого объекта. Я добился этого, подогнав коллекцию по каждому критерию и создав объект RentalReport для каждого, но мне было интересно, можно ли это сделать с помощью метода groupingBy класса Collectors.
Можно ли сгруппировать с помощью предопределенного набора фильтров в java 8, чтобы я мог создать карту, где ключ - это перечисление, а значение - это коллекция объектов Rental. Затем я мог бы перебрать эту карту и сгенерировать объекты RentalReport.
Я создал эту демонстрацию, но настоящая задача включает в себя несколько предложений group by, поэтому было бы здорово, если бы я смог добиться этого путем группировки.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.joda.time.DateTime;
public class TestGrouping {
enum RentalClassification {
UNRESTRICTED, //No restriction restricted=false
OVERDUE, //todays date after due date.
NEARLY_OVERDUE, // todays date after due date + 2 days
NOT_OVERDUE
}
public static void main(String[] args) {
class Rental {
Integer rentalId;
Integer productId;
boolean restricted;
DateTime dueDate;
public Rental(Integer rentalId, Integer productId, boolean restricted, DateTime dueDate){
this.rentalId = rentalId;
this.productId = productId;
this.restricted = restricted;
this.dueDate = dueDate;
}
public Integer getRentalId() {
return rentalId;
}
public void setRentalId(Integer rentalId) {
this.rentalId = rentalId;
}
public Integer getProductId() {
return productId;
}
public void setProductId(Integer productId) {
this.productId = productId;
}
public boolean isRestricted() {
return restricted;
}
public void setRestricted(boolean restricted) {
this.restricted = restricted;
}
public DateTime getDueDate() {
return dueDate;
}
public void setDueDate(DateTime dueDate) {
this.dueDate = dueDate;
}
public String toString(){
return "RentalId:"+this.rentalId+". ProductId:"+this.productId+". Due date:"+this.dueDate+". -";
}
}
class RentalReport {
RentalClassification classification;
List<Rental> rentals;
public RentalReport(RentalClassification classification, List<Rental> rentals) {
this.classification = classification;
this.rentals = rentals;
}
public RentalClassification getClassification() {
return classification;
}
public void setClassification(RentalClassification classification) {
this.classification = classification;
}
public List<Rental> getRentals() {
return rentals;
}
public void setRentals(List<Rental> rentals) {
this.rentals = rentals;
}
public String toString(){
StringBuilder sb = new StringBuilder("Classification:"+this.classification.name()+". Rental Ids:");
this.rentals.forEach(r -> sb.append(r.getRentalId()));
return sb.toString();
}
}
DateTime today = new DateTime();
List<Rental> rentals = Arrays.asList(
new Rental(1,100, true, today.plusDays(-10)),
new Rental(2,101, false, today.plusDays(-10)),
new Rental(3,102, true, today.plusDays(-4)),
new Rental(4,103, true, today.plusDays(-4)),
new Rental(5,104, true, today.plusDays(-4)),
new Rental(6,105, true, today.plusDays(2)),
new Rental(7,106, true, today.plusDays(2)),
new Rental(8,107, true, today.plusDays(2)),
new Rental(9,108, true, today.plusDays(4)),
new Rental(10,109, true, today.plusDays(5))
);
List<RentalReport> rentalReports = new ArrayList<RentalReport>();
List<Rental> unrestrictedRentals = rentals.stream()
.filter(r -> !r.isRestricted())
.collect(Collectors.toList());
rentalReports.add(new RentalReport(RentalClassification.UNRESTRICTED, unrestrictedRentals));
List<Rental> overdueRentals = rentals.stream()
.filter(r -> r.isRestricted() && r.getDueDate().isBefore(new DateTime()))
.collect(Collectors.toList());
rentalReports.add(new RentalReport(RentalClassification.OVERDUE, overdueRentals));
List<Rental> nearlyOverdueRentals = rentals.stream()
.filter(r -> r.isRestricted()
&& r.getDueDate().isAfter(new DateTime())
&& r.getDueDate().isBefore(new DateTime().plusDays(2)))
.collect(Collectors.toList());
rentalReports.add(new RentalReport(RentalClassification.NEARLY_OVERDUE, nearlyOverdueRentals));
List<Rental> notOverdueRentals = rentals.stream()
.filter(r -> r.isRestricted() && r.getDueDate().isAfter(new DateTime()))
.collect(Collectors.toList());
rentalReports.add(new RentalReport(RentalClassification.NOT_OVERDUE, notOverdueRentals));
System.out.println("Rental Reports: "+rentalReports.toString());
}
}
Как насчет такого:
rentals.stream().collect(Collectors.groupingBy(r -> {
if (!r.isRestricted()) {
return RentalClassification.UNRESTRICTED;
}
if (r.isRestricted() && r.getDueDate().isBefore(new DateTime())) {
return RentalClassification.OVERDUE;
}
// and so on
}));
Возможно, стоит добавить эту лямбду в Rental как метод RentalClassification getRentalClassification()
.
Вот подход:
List<RentalClassification,List<Rental>> = rentals
.stream()
.map(r -> getClassifiedRental(r))
.collect(Collectors.groupingBy(SimpleEntry::getKey,Collectors.mapping(SimpleEntry::getValue,Collectors.toList())))
весь фокус в методе getClassifiedRental(Rental r)
:
SimpleEntry<RentalClassification,Rental> getClassifiedRental(Rental r){
if (r -> !r.isRestricted())
return new SimpleEntry<RentalClassification,Rental>(RentalClassification.UNRESTRICTED,r);
if (r -> r.isRestricted() && r.getDueDate().isBefore(new DateTime()))
return new SimpleEntry<RentalClassification,Rental>(RentalClassification.OVERDUE,r);
if (r -> r.isRestricted()
&& r.getDueDate().isAfter(new DateTime())
&& r.getDueDate().isBefore(new DateTime().plusDays(2)))
return new SimpleEntry<RentalClassification,Rental>(RentalClassification.NEARLY_OVERDUE,r);
if (r -> r.isRestricted() && r.getDueDate().isAfter(new DateTime()))
return new SimpleEntry<RentalClassification,Rental>(RentalClassification.NOT_OVERDUE,r);
}
с SimpleEntry
- это кастомная реализация интерфейса Map.Entry
(вам придется написать ее самостоятельно):
public class SimpleEntry implements Entry<RentalClassification, Rental> {
// implementation
}
наличие getClassifiedRental(Rental r)
поможет вам разделить рутину классификации аренды для лучшего, например, тестирования, рефакторинга, например
Вот один из подходов к groupingBy.
Function<Rental, RentalClassification> classifyRental = r -> {
if (!r.isRestricted())
return RentalClassification.UNRESTRICTED;
else if (r.getDueDate().isBefore(new DateTime()))
return RentalClassification.OVERDUE;
else if (r.getDueDate().isAfter(new DateTime())
&& r.getDueDate().isBefore(new DateTime().plusDays(2)))
return RentalClassification.NEARLY_OVERDUE;
else
return RentalClassification.NOT_OVERDUE;
};
Map<RentalClassification, List<Rental>> rentalReportMap = rentals.stream()
.collect(groupingBy(classifyRental));
rentalReportMap
.forEach((classification, rental) -> rentalReports.add(new RentalReport(classification, rental)));
Очевидно, что между вашими 4 константами enum RentalClassification
существует взаимно однозначная связь.
и 4 тестовых лямбда-выражения.
Поэтому имеет смысл интегрировать каждое лямбда-выражение в соответствующую константу enum
.
Каждая константа enum
будет содержать свое тестовое лямбда-выражение в виде
Predicate<Rental>
.
Тип enum
будет иметь метод boolean test(Rental)
, выполняющий тест.
с собственным Predicate
.
И пока мы находимся в этом: из-за метода test
мы можем добавить implements Predicate<Rental>
к типу enum
.
Это окажется полезным в конце.
enum RentalClassification implements Predicate<Rental> {
UNRESTRICTED( //No restriction restricted=false
r -> !r.isRestricted()),
OVERDUE( //todays date after due date.
r -> r.isRestricted()
&& r.getDueDate().isBefore(new DateTime())),
NEARLY_OVERDUE( // todays date after due date + 2 days
r -> r.isRestricted()
&& r.getDueDate().isAfter(new DateTime())
&& r.getDueDate().isBefore(new DateTime().plusDays(2))),
NOT_OVERDUE(
r -> r.isRestricted()
&& r.getDueDate().isAfter(new DateTime()));
private RentalClassification(Predicate<Rental> predicate) {
this.predicate = predicate;
}
private Predicate<Rental> predicate;
@Override
public boolean test(Rental r) {
return predicate.test(r);
}
}
Используя этот улучшенный тип enum
, вы можете написать простой метод классификации Rental
.
Он будет перебирать все константы RentalClassification
пока он не найдет тот, где тест завершится успешно:
static RentalClassification classify(Rental rental) {
for (RentalClassification classification : RentalClassification.values()) {
if (classification.test(rental))
return classification;
}
return null; // should not happen
}
Используя этот метод классификации, вы можете легко создать желаемую карту:
Map<RentalClassification, List<Rental>> map =
rentals.stream()
.collect(Collectors.groupingBy(r -> classify(r)));
В качестве альтернативы возможен несколько иной подход. Он использует константы enum
непосредственно в качестве предикатов фильтрации для получения нескольких списков:
List<Rental> unrestrictedRentals =
rentals.stream()
.filter(RentalClassification.UNRESTRICTED)
.collect(Collectors.toList());
List<Rental> overdueRentals =
rentals.stream()
.filter(RentalClassification.OVERDUE)
.collect(Collectors.toList());
// ...
это приятно, спасибо. Вот как я буду реализовывать свой проект. :)