【问题标题】:How to sum up the individual fields of the object list and return the results as a single object如何汇总对象列表的各个字段并将结果作为单个对象返回
【发布时间】:2020-02-08 17:49:34
【问题描述】:

我有一个方法可以计算我们从 API 请求调用中接收到的对象列表的营养成分。

方法如下:

public Nutrients nutrientsCalculator(DailyMeals dailyMeals) {

    String foodNamesForRequest = prepareFoodNamesForRequest(dailyMeals);

    HttpEntity<NutrientsBodyForRequest> requestBody = prepareRequestForAPICall(foodNamesForRequest);

    ResponseEntity<List<FoodNutritional>> response =
        //create request here

    if (nonNull(response.getBody())) {

      double totalFat = response.getBody()
          .stream()
          .map(FoodNutritional::getTotalFat)
          .mapToDouble(Double::doubleValue)
          .sum();

      double totalProtein = response.getBody()
          .stream()
          .map(FoodNutritional::getProtein)
          .mapToDouble(Double::doubleValue)
          .sum();

      double totalCarbohydrates = response.getBody()
          .stream()
          .map(FoodNutritional::getTotalCarbohydrate)
          .mapToDouble(Double::doubleValue)
          .sum();

      double totalDietaryFiber = response.getBody()
          .stream()
          .map(FoodNutritional::getDietaryFiber)
          .mapToDouble(Double::doubleValue)
          .sum();

      return Nutrients.builder()
          .carbohydrates(totalCarbohydrates)
          .protein(totalProtein)
          .fat(totalFat)
          .dietaryFiber(totalDietaryFiber)
          .build();
    }
    return new Nutrients();
  }

我的 FoodNutritional.class 看起来像:

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
class FoodNutritional {

  @JsonProperty("food_name")
  private String foodName;

  @JsonProperty("brand_name")
  private String brandName;

  @JsonProperty("serving_qty")
  private Integer servingQuantity;

  @JsonProperty("serving_unit")
  private String servingUnit;

  @JsonProperty("serving_weight_grams")
  private String servingWeightGrams;

  @JsonProperty("nf_calories")
  private Double calories;

  @JsonProperty("nf_total_fat")
  private Double totalFat;

  @JsonProperty("nf_saturated_fat")
  private Double saturatedFat;

  @JsonProperty("nf_cholesterol")
  private Double cholesterol;

  @JsonProperty("nf_sodium")
  private Double sodium;

  @JsonProperty("nf_total_carbohydrate")
  private Double totalCarbohydrate;

  @JsonProperty("nf_dietary_fiber")
  private Double dietaryFiber;

  @JsonProperty("nf_sugars")
  private Double sugars;

  @JsonProperty("nf_protein")
  private Double protein;

  @JsonProperty("nf_potassium")
  private Double potassium;
}

我的方法解决方案有效,但我开始思考是否有可能摆脱这种方法的 sum 流方法样板。

我想要实现的只是对单个字段求和:totalFatproteindietaryFibertotalCarbohydrate,并将它们作为新对象的组件字段返回。

对于如何提高当前版本代码质量的建议,我将不胜感激。

编辑:

周末我花了一点时间找到了一种不同的附加方法,它可以满足要求并且功能更强大。最后,我创建了两个静态方法,例如:


private static Nutrients reduceNutrients(Nutrients n1, Nutrients n2) {
    return Nutrients.builder()
        .protein(n1.getProtein() + n2.getProtein())
        .carbohydrates(n1.getCarbohydrates() + n2.getCarbohydrates())
        .dietaryFiber(n1.getDietaryFiber() + n2.getDietaryFiber())
        .fat(n1.getFat() + n2.getFat())
        .build();
  }

  private static Nutrients fromFoodNutritionalToNutrients(FoodNutritional foodNutritional) {
    return Nutrients.builder()
        .dietaryFiber(foodNutritional.getDietaryFiber())
        .carbohydrates(foodNutritional.getTotalCarbohydrate())
        .fat(foodNutritional.getTotalFat())
        .protein(foodNutritional.getProtein())
        .build();
  }

毕竟我是这样使用它的:

Stream<FoodNutritional> foodNutritionalStream = Optional.ofNullable(response.getBody()).stream()
        .flatMap(List::stream);

Nutrients nutrients = foodNutritionalStream
        .map(NutrientsCalculatorService::fromFoodNutritionalToNutrients)
        .reduce(NutrientsCalculatorService::reduceNutrients)
        .orElseThrow(() -> new CustomException("custom_exception");

不过,还是要感谢 @Koziołek@Nir Levy@Naman 成为我的缪斯女神。感谢每一个承诺。

【问题讨论】:

  • 看看Java流中的reduction。您必须创建一个零初始化标识(初始状态/累加器),然后使用将脂肪、蛋白质等添加到累加器的二进制操作来减少流。
  • @madhead 另一种有趣的方法,我肯定会检查这种方法,因为我还没有使用这个 Collectors 方法。
  • 它已经在这里了,在Nir Levy's answer。恕我直言,它是性能最高的,因为它只会迭代您的收藏一次,总结所有营养。
  • @madhead 当我写这个答案时,我还没有看到这个答案。现在,在一瞬间,我收到了多种选择。再次感谢您的承诺。

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


【解决方案1】:

如何使用Supplier&lt;Stream&lt;T&gt;&gt;&gt; 创建可重复使用的Stream 类似:

Supplier<Stream<FoodNutritional>> foodNutritionalSupplier = () -> Optional.ofNullable(responseBody)
        .stream()
        .flatMap(List::stream);
return Nutrients.builder()
        .carbohydrates(foodNutritionalSupplier.get().mapToDouble(FoodNutritional::getTotalCarbohydrate).sum())
        .protein(foodNutritionalSupplier.get().mapToDouble(FoodNutritional::getProtein).sum())
        .fat(foodNutritionalSupplier.get().mapToDouble(FoodNutritional::getTotalFat).sum())
        .dietaryFiber(foodNutritionalSupplier.get().mapToDouble(FoodNutritional::getDietaryFiber).sum())
        .build();

其中responseBody 对应于问题中的response.getBody()


或者进一步使用实用程序来抽象出核心逻辑,例如:

private Nutrients nutrientsCalculator(List<FoodNutritional> responseBody) {
    Supplier<Stream<FoodNutritional>> foodNutritionalSupplier =
            () -> Optional.ofNullable(responseBody).stream().flatMap(List::stream);
    // should ideally be as simple as 'responseBody::stream'
    return Nutrients.builder()
            .carbohydrates(sumNutrition(foodNutritionalSupplier, FoodNutritional::getTotalCarbohydrate))
            .protein(sumNutrition(foodNutritionalSupplier, FoodNutritional::getProtein))
            .fat(sumNutrition(foodNutritionalSupplier, FoodNutritional::getTotalFat))
            .dietaryFiber(sumNutrition(foodNutritionalSupplier, FoodNutritional::getDietaryFiber))
            .build();
}

private Double sumNutrition(Supplier<Stream<FoodNutritional>> foodNutritionalSupplier,
                            ToDoubleFunction<FoodNutritional> nutritionTypeFunction) {
    return foodNutritionalSupplier.get().mapToDouble(nutritionTypeFunction).sum();
}

【讨论】:

  • 有趣的方法。在第一个中,一切似乎都正常,但线 .flatMap(List::stream); 带有下划线,带有经典消息 nonstatic method cannot be referenced from a static context 不幸的是,这个集合被 ResponseEntity 包装,所以我认为缺少一个额外的 flatMap 操作,对吧?
  • @Martin 鉴于responseBodyList&lt;FoodNutritional&gt; 类型等价,并且使用Java-11,理想情况下它应该是可推断的。正如我在答案中指出的那样,responseBody 对应于 response.getBody()
  • 你说得对,我没有注意到您在下面的附加评论。
【解决方案2】:

您可以为此使用Stream.reduce 方法,只需要创建一个NutrientsAggregator,它将知道将FoodNutritional 中的值添加到自身并将所有内容相加

public class NutrientsAggregator {
   private double calories;
   private double totalFat;
   private double saturatedFat;
   private double cholesterol;

   public NutrientsAggregator addFoodNutritionalValues(FoodNutritional foodNutrional) {
      this.calories += foodNutrional.getCalories();
      this.totalFat+= foodNutrional.getTotalFat();
      this.saturatedFat+= foodNutrional.getSaturatedFat();
      this.cholesterol+= foodNutrional.getCholesterol();

      return this;
   }
}

比:

NutrientsAggregator result = response.getBody()
          .stream()
          .reduce(new NutrientsAggregator(), 
                  (aggregator, food) -> aggregator.addFoodNutritionalValues(food);

【讨论】:

  • 最佳答案,因为它只需要一次集合迭代。
  • @Nir Levy 感谢您的回答。你确定没有印刷错误?我不能在我的流中使用方法addFoodNutritionalValues。 IntelliJ 建议在 FoodNutritional 类中创建此方法。也许我想将这个类 NutrientsAggregator 添加为 FoodNutritional 类中的内部类?
  • @Martin - 你的班级没有public 是故意的吗?无论如何,我是在这里写的,而不是在 ide 上写的,所以可能是一个小错别字或错误。解释它的要点更像是一个笼统的想法
  • @NirLevy 没问题,我有一个单独的包负责一件事。 aggregatorfood 是 FoodNutritional 类型,毕竟我不能在 aggregator 上调用此方法。 IntelliJ 试图强迫我在 FoodNutritional 类中创建方法,而不是从累加器类中调用它NutrientsAggregator
  • 如果结果与 Stream 的元素类型不同,则不能使用 2 参数 reduce() 方法。
【解决方案3】:

让我们介绍类NutritionAccumulator

class NutritionAccumulator{
    private double fat = 0.;
    private double carbs = 0.;
    private double fiber = 0.;
    private double protein = 0.;

    public NutritionAccumulator() {
    }

    public NutritionAccumulator(double fat, double carbs, double fiber, double protein) {
        this.fat = fat;
        this.carbs = carbs;
        this.fiber = fiber;
        this.protein = protein;
    }

    public NutritionAccumulator add(NutritionAccumulator that){
        return new NutritionAccumulator(this.fat + that.fat,
        this.carbs + that.carbs,
        this.fiber + that.fiber,
        this.protein + that.protein
        );
    }
}

现在我们可以编写简单的流reduce了:

Optional.ofNullable(response.body())
.stream()
.reduce(
                        new NutritionAccumulator(),
                        (acc, fudNut) -> new NutritionAccumulator(
                                fudNut.getTotalFat(),
                                fudNut.getTotalCarbohydrate(),
                                fudNut.getDietaryFiber(),
                                fudNut.getProtein()
                        ).add(acc),
                        NutritionAccumulator::add

                );

最后你可以将上面的结果传递给构建器。

【讨论】:

    猜你喜欢
    • 2020-10-22
    • 2018-05-13
    • 2016-11-21
    • 2014-06-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多