Java Streams reduce后如何对值进行排序

3
我只是在尝试理解Java中的流操作,卡在了排序阶段。我的目的是使用一个流获取最贵的素食比萨。现在我已经获得了比萨价格,但是无法对其进行排序。有人能告诉我该怎么做吗?
我尝试了以下代码:
pizzas.stream()
    .flatMap(pizza -> Stream.of(pizza.getIngredients())
        .filter(list -> list.stream().noneMatch(Ingredient::isMeat))
            .map(list -> list.stream().map(Ingredient::getPrice).reduce(0,(a, b) -> a + b))
            .sorted((o1, o2) -> o1.intValue() - o2.intValue())
            )
    .forEach(System.out::println);

这段代码会返回给我未排序的披萨数值。

4
дЄЇдїАдєИдЄНзЫіжО•дљњзФ®Stream.maxеСҐпЉЯ - lexicore
使用 map 之后,是否应该使用 sorted? - kacper1230
4
不行,你需要先将“Pizza”映射为类似于“Entry<Pizza,Double>”的东西,然后在“Entry :: getValue”上使用“max”。使用你的代码,无论如何都无法找到披萨,因为“flatMap”已经完全破坏了披萨对象流。 :) - lexicore
@lexicore 不需要存储 (Pizza, price) 对。通过仔细编写比较器,您可以完美地找到最高价格的素食披萨。 - fps
@FedericoPeraltaSchaffner,我从未谈论过寻找素食比萨。我的观点是,为了找到最便宜的比萨,你需要知道比萨和大小。你不能从配料或计算出的价格流中“恢复”比萨。 - lexicore
@lexicore,请查看被接受的答案。那正是我的观点。 - fps
3个回答

5
import java.util.Collection;
import java.util.Comparator;

interface Pizza {
    interface Ingredient {
        boolean isMeat();

        int getPrice();
    }

    Collection<Ingredient> getIngredients();

    static boolean isVegetarian(Pizza pizza) {
        return pizza.getIngredients().stream().noneMatch(Ingredient::isMeat);
    }

    static int price(Pizza pizza) {
        return pizza.getIngredients().stream().mapToInt(Ingredient::getPrice).sum();
    }

    static Pizza mostExpensiveVegetarianPizza(Collection<Pizza> pizzas) {
        return pizzas.stream()
                     .filter(Pizza::isVegetarian)
                     .max(Comparator.comparingInt(Pizza::price))
                     .orElseThrow(() -> new IllegalArgumentException("no veggie pizzas"));
    }
}

如果你希望 Ingredient.getPrice() 返回一个 double,那么在 Pizza.price() 中使用 Stream.mapToDouble(),在 Pizza.mostExpensiveVegetarianPizza() 中使用 Comparator.comparingDouble()

1
那是一个昂贵的比较器。在排序之前缓存总和可能更有效率。 - shmosel
@shmosel 是的,Bubletan的回答解决了这个问题。我只是想展示一种简单的方法,说明流API的用法。 - gdejohn

4
要找到价格最高的披萨,您需要每次比较价格时计算每个披萨的价格,或者有一个对象来存储披萨和价格。这里有一个解决方案,它使用匿名对象来保存临时状态,其中我们需要披萨和其价格:
Optional<Pizza> pizza = pizzas.stream()

        .filter(p -> p.getIngredients().stream()
                .noneMatch(Ingredient::isMeat)) // filter

        .map(p -> new Object() { // anonymous object to hold (pizza, price)

            Pizza pizza = p; // store pizza

            int price = p.getIngredients().stream()
                    .mapToInt(Ingredient::getPrice).sum(); // store price
        })

        .max(Comparator.comparingInt(o -> o.price)) // find the highest price
        .map(o -> o.pizza); // get the corresponding pizza

这个 o -> o.price 是行不通的。你需要一个中间类来保存披萨和价格。 - lexicore
2
@lexicore 它确实有效,这要归功于类型推断。您无法表示 o -> o.price 中的 o 的类型,但它确实具有一种类型,并且该类型具有名为 priceint 字段,Java 可以利用它。基本上,它之所以有效,是因为您可以执行 new Object() { void foo() {} }.foo() 的相同原因。 - gdejohn
@gdejohn 很有趣。我错了,很抱歉。我在Eclipse中测试了它,得到了一个编译错误(就在我的眼前)。但是javac和Ideone可以编译它而没有问题。太遗憾了,现在我无法取消投票了。 - lexicore
@lexicore 你说得对,在Eclipse中似乎无法编译。或者可以将其编写为本地类,或使用一些现有类型,如Map.Entry(价格被装箱,但这里并不是什么大问题)。 - Bubletan
2
@shmosel 这个问题已经在这个问答中讨论过了。请注意,在Java 10中,您将能够声明匿名类型的变量,例如 var name = new Object() { /* declare some members */ } 并通过 name 访问成员。 - Holger
显示剩余2条评论

3
我已经制作了一个简短的功能示例。我将一些流封装在Pizza类中,以提高可读性。
成分
public class Ingredient {
private String name;
private double price;
private boolean meat;

public Ingredient(String name, double price, boolean meat) {
    this.name = name;
    this.price = price;
    this.meat = meat;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public double getPrice() {
    return price;
}

public void setPrice(double price) {
    this.price = price;
}

public boolean isMeat() {
    return meat;
}

public void setMeat(boolean meat) {
    this.meat = meat;
}

}

披萨

public class Pizza {

private String name;
private List<Ingredient> ingredients;

public Pizza(String name, List<Ingredient> ingredients) {
    this.name = name;
    this.ingredients = ingredients;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}


public List<Ingredient> getIngredients() {
    return ingredients;
}


public void setIngredients(List<Ingredient> ingredients) {
    this.ingredients = ingredients;
}

public boolean isVegan() {
    return  (ingredients != null) ? ingredients.stream().noneMatch(Ingredient::isMeat)
                                  : false;
}

public double getTotalCost() {
    return  (ingredients != null) ? ingredients.stream().map(Ingredient::getPrice)
                                                        .reduce(0.0, Double::sum)
                                  : 0;
}

@Override
public String toString() {
    return "Pizza [name=" + name + "; cost=" + getTotalCost() +"$]";
}
}

主要内容

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

public class VeganPizzaPlace {

public static void checkMostExpensiveVeganPizza(List<Pizza> pizzas) {
    if (pizzas != null) {

        Optional<Pizza> maxVegan =
                pizzas.stream()
                      .filter(Pizza::isVegan)
                      .max(Comparator.comparingDouble(Pizza::getTotalCost));

        if (maxVegan.isPresent()) {
            System.out.println(maxVegan.get().toString());
        } else {
            System.out.println("No vegan pizzas in the menu today");
        }
    }
}

public static void main (String[] args) {
    List<Pizza> pizzas = new ArrayList<Pizza>();

    Ingredient tomato   = new Ingredient("tomato", 0.50, false);
    Ingredient cheese   = new Ingredient("cheese", 0.75, false);
    Ingredient broccoli = new Ingredient("broccoli", 50.00, false);
    Ingredient ham      = new Ingredient("ham", 10.00, true);

    List<Ingredient> ingredientsMargherita = new ArrayList<Ingredient>();
    ingredientsMargherita.add(tomato);
    ingredientsMargherita.add(cheese);

    Pizza margherita = new Pizza("margherita", ingredientsMargherita);

    List<Ingredient> ingredientsSpecial = new ArrayList<Ingredient>();
    ingredientsSpecial.add(tomato);
    ingredientsSpecial.add(cheese);
    ingredientsSpecial.add(broccoli);

    Pizza special = new Pizza("special", ingredientsSpecial);

    List<Ingredient> ingredientsProsciutto = new ArrayList<Ingredient>();
    ingredientsProsciutto.add(tomato);
    ingredientsProsciutto.add(cheese);
    ingredientsProsciutto.add(ham);

    Pizza prosciutto = new Pizza("prosciutto", ingredientsProsciutto);

    pizzas.add(margherita);
    pizzas.add(special);
    pizzas.add(prosciutto);

    checkMostExpensiveVeganPizza(pizzas);
}
}

输出

披萨 [名称=special; 价格=51.25美元]

如果您不喜欢干净的代码,可以使用以下代码代替

Optional<Pizza> maxVegan =
                pizzas.stream()
                      .filter(p -> p.getIngredients().stream().noneMatch(Ingredient::isMeat))
                      .reduce((p1, p2) -> p1.getIngredients().stream().map(Ingredient::getPrice).reduce(0.0, Double::sum)
                                        < p2.getIngredients().stream().map(Ingredient::getPrice).reduce(0.0, Double::sum) ? p1 : p2);

编辑:使用reduce选择最大值披萨的表达式基于 Urma、Fusco 和 Mycroft 的书《Java 8实战》(第110页,列表5.8)。这是一本非常好的书!:-)


你的“streams”代码(最后一个代码片段)是目前为止所有展示中最好的,我已经点赞了。 - lexicore
谢谢,@lexicore。我最喜欢我的另一个版本。最后的代码片段中有太多流操作。 - RubioRic

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接