Java 8 - how sum many fields into a dto?

2019-07-23 19:22发布

问题:

Let's say I have a class Item like this:

class Item {  
    private long id;  
    private BigDecimal value1;  
    private BigDecimal value2;  
    private BigDecimal value3;  
}  

Then I have a list with many itens, I want to know the sum of each of the values:

So, I know I could do something like

BigDecimal v1 = list.stream().map(Item::value1).reduce(BigDecimal.ZERO, BigDecimal::add);

However, this way I would need to do the same for each value, I'd like to know if there's some way of summing each attribute into only one Dto like:

class TotalItem {  
    private BigDecimal value1;  
    private BigDecimal value2;  
    private BigDecimal value3;  
}  

TotalItem t = list.stream().map(???).reduce(BigDecimal.ZERO, BigDecimal::add);

Is this possible?

thanks in advance.

回答1:

I didn't test it but I think that you can implement add function on Item like:

public Item add(Item other) {
   Item newItem = new Item(this.value1 + other.value1,
              this.value2 + other.value2,
              this.value3 + other.value3);

   return newItem;
}

and then do:

Item t = list.stream().reduce(BigDecimal.ZERO, Item::add);


回答2:

How about the following way?

TotalItem t = new TotalItem();

list.stream().forEach(item -> {
   t.value1+ = item.value1;
   t.value2+ = item.value2;
   t.value3+ = item.value3;
});


回答3:

I'm making the assumption that Item/TotalItem are very large objects, which would make writing a toTotalItem and a summarise(TotalItem,TotalItem) by hand a large and laborious job. One that is completely boilerplate and easy to get wrong.

Change the data structure to be a list or map - This makes summarisation simpler, at the cost of readability of the code and type safety.

Use reflection to iterate over the fields.

TotalItem toTotalItem(Item item) {
    Field[] targetFields = TotalItem.class.getFields();
    Collection<Field> sourceFields = Item.class.getFields().stream()
       .filter(x=>fieldNameIsIn(x, targetFields)           
       .collect(Collectors.toList());

    TotalItem returnItem = new TotalItem();
    for(Field field : sourceFields) {
        toTargetField(field, targetFields).set(returnItem, (BigDecimal) field.get(item));
    }
    return returnItem;
}
boolean fieldNameIsIn(Field sourceField, Field[] targetFields) // exercise for the reader
Field toTargetField(Field sourceField, Field[] targetFields) // exercise for the reader

This code above is not neat, but should show the concept. The same concept could be used to summarise.

This reduces the amount of code you need to write, but at the cost of runtime speed. Reflection is also hard to get right and magic (which some developers do not like).

The faster option would be a custom annotation which adds in summarisation. However this would be large chunk of work. If you have a large number of objects that need this, then it may make sense. Like reflection it is hard to get right and magic (which some developers do not like). Luckily you do not need a build step as Javac supports annotation processing natively.



回答4:

This answer is inspired by JDK way of doing similar operations. Namely, I'll be referencing DoubleSummaryStatistics class.

First, we define a holder for information about BigDecimals we'll be collecting:

public class BigDecimalSummaryStats {
  private long count;
  private MathContext mc;
  private BigDecimal sum = BigDecimal.ZERO;
  private BigDecimal max;
  private BigDecimal min;

  public BigDecimalSummaryStats(MathContext mathCtx) {
    mc = requireNonNull(mathCtx);
  }

  public Supplier<BigDecimalSummaryStats> supplier(MathContext ctx) {
    return () -> new BigDecimalSummaryStats(ctx);
  }

  public void accept(BigDecimal value) {
    requireNonNull(value);
    count++;
    sum = sum.add(value, mc);
    min = min.min(value);
    max = max.max(value);
  }

  public void combine(BigDecimalSummaryStats other) {
    requireNonNull(other);
    count += other.count;
    sum = sum.add(other.sum, mc);
    min = min.min(other.min);
    max = max.max(other.max);
  }

  public long getCount() {
    return count;
  }

  public BigDecimal getSum() {
    return sum;
  }
  public BigDecimal getMax() {
    return max;
  }
  public BigDecimal getMin() {
    return min;
  }
  public BigDecimal getAverage() {
    long c = getCount();
    return c == 0 ? BigDecimal.ZERO : getSum().divide(BigDecimal.valueOf(c), mc);
  }
}

This will provide with nice general utility suitable for collecting summary from arbitrary sequences of BigDecimal values.

Then, we can define a Summary class for our Item:

public class ItemSummaryStats {
  private BigDecimalSummaryStats value1;
  private BigDecimalSummaryStats value2;
  private BigDecimalSummaryStats value3;
  // ... other fields as needed
  public ItemSummaryStats(MathContext math) {
    value1 = new BigDecimalSummaryStats(math);
    value2 = new BigDecimalSummaryStats(math);
    value3 = new BigDecimalSummaryStats(math);
  }

  public void accept(Item item) {
    value1.accept(item.value1);
    value2.accept(item.value2);
    value3.accept(item.value3);
    // ... other fields as needed
  }
  public void combine(ItemSummaryStats other) {
    value1.combine(other.value1);
    value2.combine(other.value2);
    value3.combine(other.value3);
  }

  public TotalItem get(
     Function<BigDecimalSummaryStats, BigDecimal> v1Mapper,
     Function<BigDecimalSummaryStats, BigDecimal> v2Mapper,
     Function<BigDecimalSummaryStats, BigDecimal> v3Mapper) {

     TotalItem t = new TotalItem();
     t.value1 = v1Mapper.get(value1);
     t.value2 = v2Mapper.get(value2);
     t.value3 = v3Mapper.get(value3);
     return t;
  }

  public TotalItem getSum() {
    return get(BigDecimalSummaryStats::getSum,
               BigDecimalSummaryStats::getSum,
               BigDecimalSummaryStats::getSum);
  }
  public TotalItem getAverage() {
    return get(BigDecimalSummaryStats::getAverage,
               BigDecimalSummaryStats::getAverage,
               BigDecimalSummaryStats::getAverage);
  }
  public TotalItem getMin() {
    return get(BigDecimalSummaryStats::getMin,
               BigDecimalSummaryStats::getMin,
               BigDecimalSummaryStats::getMin);
  }
  //.... other methods basically all the same. You get the idea.
}

And finally we use this goodness like this:

TotalItem totals = list.stream().collect(
      Collector.of(() -> new ItemStatsSummary(MathContext.DECIMAL64),
                   ItemStatsSummary::accept,
                   ItemStatsSummary::combine,
                   ItemStatsSummary::getSum)
    )

Cons of this approach:

  1. Slightly longer development time than adhoc solutions.

Are far outweighed by the pros, or at least I'm convinced of it:

  1. Follows separation of concerns principle: Item Stats are not actually concerned how to collect summary of specific field: they can trust that BigDecimalSummary works
  2. Testable: Each part can be tested in its own suite. You can trust that every field will work the same since they use the same API.
  3. Flexible: get(Function...) method exposes a big list of possibilities: you can collect sum of first field, an averabe of second and min of third if needed.