【问题标题】:Java 8 - how sum many fields into a dto?Java 8 - 一个 dto 中有多少个字段?
【发布时间】:2017-03-27 14:32:21
【问题描述】:

假设我有一个这样的类 Item:

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

然后我有一个包含许多项目的列表,我想知道每个值的总和:

所以,我知道我可以做类似的事情

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

但是,这样我需要对每个值都做同样的事情,我想知道是否有某种方法可以将每个属性汇总为一个 Dto like:

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

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

这可能吗?

提前致谢。

【问题讨论】:

  • 您需要创建新的统计类。
  • 每个值分开,意味着结果的3个总和值或三个不同的值到同一个总和?

标签: java java-8 java-stream


【解决方案1】:

我没有测试,但我认为您可以在Item 上实现添加功能,例如:

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

   return newItem;
}

然后做:

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

【讨论】:

  • 不会编译:Item 不能分配给 TotalItem。
  • 这很容易解决。要么不使用 TotalItem,因为它完全相同,要么实现“toTotalItem()”
  • 为什么Item 会关心将其转换为TotalItem 的可能性?这违反了 SRP。
  • 是否必须使用 TotalItem 来表示聚合?
  • 如果你想使用“减少”,你必须首先将 List 映射到 List (这并不漂亮)。没有解决它。
【解决方案2】:

下面的方法怎么样?

TotalItem t = new TotalItem();

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

【讨论】:

  • 这个不好,因为有副作用
  • @maxpovver 有什么副作用?
  • 在 foreach 中它会更改该 foreach 之外的变量。如果在创建总项目时添加一些新代码,它可能会被破坏
  • 您为什么要添加会更改TotalItem 并破坏它的代码?你显然不想那样做。如果解决方案很简单,请使用它。不要试图解释不可能的异常情况,从而使问题复杂化。
  • 这只是执行常规增强 for 循环的快捷方式。我可以理解对 map() 或 reduce() 方法内部副作用的关注,但 forEach() 自然似乎是为了这样的目的。
【解决方案3】:

我假设 Item/TotalItem 是非常大的对象,这将使手动编写 toTotalItem 和 summarise(TotalItem,TotalItem) 成为一项庞大而费力的工作。一个完全是样板且容易出错的。

将数据结构更改为列表或映射 - 这使得摘要更简单,但以代码的可读性和类型安全为代价。

使用反射迭代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

上面的这段代码并不整洁,但应该显示这个概念。可以用同样的概念来概括。

这会减少您需要编写的代码量,但会降低运行速度。反射也很难正确和神奇(一些开发人员不喜欢)。

更快的选项是添加摘要的自定义注释。然而,这将是一大块工作。如果您有大量需要此功能的对象,那么它可能是有意义的。就像反射一样,很难做到正确和神奇(一些开发人员不喜欢)。幸运的是,您不需要构建步骤,因为 Javac 本身就支持注解处理。

【讨论】:

  • (这是对抱怨解决方案的已删除评论的回复)我见过 Item 和 TotalItem 非常大的情况。因此,它们两者的总和是一个庞大的、完全乏味的、容易出错的样板代码。自动化(就像 Apache Commons 对 toString/equals 所做的那样)是有意义的。它需要比上面写的代码好很多。这是显示概念的示例代码。当时我玩过注释解决方案,但从未完成原型。
  • 我的意思是反射有时是巫术,我更喜欢更明确。我并不是说这个解决方案行不通。
  • 是的。确实。但是包含在一个经过良好测试的库(如 Apache Commons toString/Equals)中,它运行得非常好。我个人讨厌样板代码。我们有电脑为我们做样板。那么为什么不使用它们呢。我个人认为自定义注释方法(如 Project Lombok)将是解决此问题的正确方法。一个@Summarizable注解和一些关于如何汇总字段的注解。
【解决方案4】:

这个答案的灵感来自于 JDK 做类似操作的方式。也就是说,我将引用DoubleSummaryStatistics 类。

首先,我们为将要收集的 BigDecimal 信息定义一个持有者:

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);
  }
}

这将提供一个很好的通用实用程序,适用于从 BigDecimal 值的任意序列中收集摘要。

然后,我们可以为我们的 Item 定义一个 Summary 类:

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.
}

最后我们像这样使用这个善良:

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

这种方法的缺点:

  1. 开发时间比临时解决方案略长。

被专业人士远远超过,或者至少我对此深信不疑:

  1. 遵循关注点分离原则:Item Stats 实际上并不关心如何收集特定字段的摘要:他们可以相信 BigDecimalSummary 有效
  2. 可测试:每个部分都可以在自己的套件中进行测试。您可以相信,每个字段的工作方式都是一样的,因为它们使用相同的 API。
  3. 灵活:get(Function...) 方法提供了大量可能性:如果需要,您可以收集第一个字段的总和、第二个字段的平均值和第三个字段的最小值。

【讨论】:

    猜你喜欢
    • 2010-09-18
    • 1970-01-01
    • 2011-03-10
    • 1970-01-01
    • 2018-01-03
    • 1970-01-01
    • 1970-01-01
    • 2015-03-19
    • 1970-01-01
    相关资源
    最近更新 更多