3

I just try to understand streams in Java and I stuck at sorting phase. My purpose is to get most expensive vege pizza with one stream. At this point I get pizza price but I can't sort it. Can anyone tell me how I should do it ?

I try with this :

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

This code returns me unsorted values of pizza.

Michael
  • 41,989
  • 11
  • 82
  • 128
kacper1230
  • 55
  • 6
  • 4
    Why don't you simply use `Stream.max`? – lexicore Mar 06 '18 at 12:54
  • After map instead of sorted ? – kacper1230 Mar 06 '18 at 12:55
  • 4
    No. You'll need first to map `Pizza` to something like `Entry` and then `max` on `Entry::getValue`. With your code you can't find pizza anyway as `flatMap` already completely destroyed stream of pizzas. :) – lexicore Mar 06 '18 at 13:02
  • @lexicore There's no need to store a `(Pizza, price)` pair. You can perfectly find the vegan pizza with the max price by carefully coding the comparator. – fps Mar 06 '18 at 15:02
  • @FedericoPeraltaSchaffner I never spoke of finding the vegan pizza. My point is that in order to find the cheapest pizza you need both pizza and size. You can't "restore" pizza from the stream of ingredients or calculated prices. – lexicore Mar 06 '18 at 16:05
  • @lexicore Please see the accepted answer. That was exactly my point. – fps Mar 06 '18 at 17:58

3 Answers3

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

If you want Ingredient.getPrice() to return a double, you would use Stream.mapToDouble() in Pizza.price() and Comparator.comparingDouble() in Pizza.mostExpensiveVegetarianPizza().

gdejohn
  • 7,451
  • 1
  • 33
  • 49
  • 1
    That's an expensive comparator. Might be more efficient to cache the sum before sorting. – shmosel Mar 06 '18 at 21:27
  • @shmosel Yeah, [Bubletan's answer](https://stackoverflow.com/a/49133031/464306) addresses that. I just wanted to show a simple approach illustrating usage of the stream API. – gdejohn Mar 06 '18 at 21:30
4

To find the pizza with the highest price you need to either compute the price of each pizza every time the prices are compared or have an object that stores both the pizza and the price.

Here's a solution that uses an anonymous object to hold the temporary state where we need both the pizza and its price:

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
shmosel
  • 49,289
  • 6
  • 73
  • 138
Bubletan
  • 3,833
  • 6
  • 25
  • 33
  • This `o -> o.price` won't work. You need some intermediary class to hold pizza and price. – lexicore Mar 06 '18 at 16:12
  • 2
    @lexicore It does work, thanks to type inference. You can't denote the type of `o` in `o -> o.price`, but it does have a type, and that type has an `int` field named `price`, and Java can take advantage of that. Basically it works for the same reason that you can do `new Object() { void foo() {} }.foo()`. – gdejohn Mar 06 '18 at 16:45
  • @gdejohn Fascinating. I was wrong, I am sorry. I've tested it in Eclipse and got a compilation error (have it right before my eyes). But `javac` and Ideone compile it without problems. Too bad I can't undo the downvote now. – lexicore Mar 06 '18 at 17:09
  • @lexicore You're right, it doesn't seem to compile in Eclipse. Alternatively could just write it as a local class or use some existing type like `Map.Entry` (price gets boxed but not a big deal here). – Bubletan Mar 06 '18 at 17:30
  • @Bubletan Yes, sure, a local class would do. I was just in wrong that it does not work. – lexicore Mar 06 '18 at 17:31
  • Wow, I knew you could do `new Object() { int i; }.i`, but I didn't realize it could be inferred across methods like that. Mind=Blown. – shmosel Mar 06 '18 at 21:37
  • 2
    @shmosel it has been discussed in [this Q&A](https://stackoverflow.com/q/43987285/2711488). Note that in Java 10, you will be able to declare variables of anonymous types, like `var name = new Object() { /* declare some members */ }` and access the members via `name`. – Holger Mar 07 '18 at 09:20
3

I've made a short functional example. I have encapsulated some streams inside Pizza class to improve legibility.

INGREDIENT

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

}

PIZZA

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() +"$]";
}
}

MAIN

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

OUTPUT

Pizza [name=special; cost=51.25$]

If you do not like clean code, you can use instead

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

EDIT: Expression for selecting max valued pizza using a reduce is based on listing 5.8 (page 110) from the book "Java 8 in action" by Urma, Fusco and Mycroft. A great book! :-)

RubioRic
  • 2,442
  • 4
  • 28
  • 35