5

Can't figure out how to read out average weight of animals in a list for a specific building.

I've written another method that gets the names of the animals per kind of animal, so I know my persistency is working.

Zoo:

public class Zoo {
    private List<Animal> animals;
    private List<Building> buildings;

    public Zoo() {
        this.animals = new PersistencyController().giveAnimals();
        this.gebouwen = new PersistencyController().giveBuildings();
    }

 public List<Animal> giveAnimalsByKind(String kindName) {
        return animals.stream().filter(animal -> animal.getKind().getName() == kindName).sorted(Comparator.comparing(Animal::getWeight)).collect(Collectors.toList());
    }

    public double giveAvgWeightForAnimalsInBuilding(String buildingName) {
        return animals.stream().collect(Collectors.averagingDouble(Animal::getWeight));
    }
}

Animal:

public class Animal implements Serializable {

    private int nr;
    private String name;
    private double weight;
    private Kind kind;

    public Animal(int nr, String name, double weight, Kind kind) {
        this.nr = nr;
        this.name = name;
        this.weight= weight;
        this.kind= kind;
    }

    public double getWeight() {
        return weight;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }
}

Building:

public class Building{

    private String naam;
    private int capaciteit;
    private final List<Animal> animals = new ArrayList<>();

    public Building(String naam, int capaciteit) {
        this.naam = naam;
        this.capaciteit = capaciteit;
    }
}

I want to get the average weight of the animals per building, so the idea is I pass a building's name. Every Building object stores a list of animals, and I need the building object because it stores the name of the building, but I can't figure out how to access it using streams. I know that in the code I posted, I'm not actually using buildingName yet, but that's because I can't even get the average weight of the animals normally. weight is stored as a double in Animal and my persistency is working, as I've tested this in other ways. Anyone experienced with streams explaining how to filter by building and get this average weight out would be much appreciated.

Michiel Leegwater
  • 1,172
  • 4
  • 11
  • 27
Zirael
  • 108
  • 7
  • 2
    Use [Collectors.groupingBy](https://docs.oracle.com/javase/10/docs/api/java/util/stream/Collectors.html#groupingBy(java.util.function.Function,java.util.stream.Collector)) :) The downstream collector will be Collectors.averagingDouble as before. P.S. You might want to provide some more details of the Building and Animal classes as part of a [reprex]. – Avi Aug 20 '19 at 16:06
  • What do you mean by "I can't even get the average weight of the animals normally"? Your code does calculate the average weight of the animals. – dumbPotato21 Aug 20 '19 at 16:12
  • I added some more code. @ImaginaryPumpkin I figure this should at least just calculate the animals' weight without specifying the exact building, but the double I get as a result is '0.0' – Zirael Aug 20 '19 at 16:17
  • Hmm, I wrote some dummy code, and it gave me the correct average. Are you sure something isn't wrong with the list? – dumbPotato21 Aug 20 '19 at 16:22
  • 1
    For example, if the list is empty, you will get an average of 0. You should debug the contents of your list (maybe write an informative `toString` method for `Animal` to help with this). – Avi Aug 20 '19 at 16:24
  • What should be the key of your map? Whether it is building or building name? – Ravindra Ranwala Aug 20 '19 at 16:37
  • I added some more working code. 'giveAnimalsByKind' works, so I know the list isn't empty and the animals have weights. The key of the map should be the name of the building – Zirael Aug 20 '19 at 16:40
  • 2
    Aside `animal.getKind().getName() == kindName` should use `equals` for String comparison. – Naman Aug 20 '19 at 16:59

1 Answers1

5

If you have the liberty of using Java9, I would suggest you using the flatMapping collector to get this thing done. Here's how it looks.

Map<String, Double> avgWeightByBuildingName = buildings.stream()
    .collect(Collectors.groupingBy(Building::getName,
        Collectors.flatMapping(b -> b.getAnimals().stream(), 
            Collectors.averagingDouble(Animal::getWeight))));

However here's the java8 solution which looks a bit more verbose compared to the former.

Map<String, Double> avgWeightByBuildingName = buildings.stream()
    .flatMap(b -> b.getAnimals().stream()
        .map(a -> new AbstractMap.SimpleEntry<>(b.getName(), a.getWeight())))
    .collect(Collectors.groupingBy(Map.Entry::getKey, 
        Collectors.averagingDouble(Map.Entry::getValue)));

Alternatively consider writing your own flatMapping collector by following this answer. Here's one such implementation that I came up with after looking at the above answer and the JDK 9 source code.

static <T, U, A, R> Collector<T, ?, R> flatMapping(Function<? super T,
            ? extends Stream<? extends U>> mapper, Collector<? super U, A, R> downstream) {
    BiConsumer<A, ? super U> downstreamAccumulator = downstream.accumulator();
    return Collector.of(downstream.supplier(), (r, t) -> {
        try (Stream<? extends U> result = mapper.apply(t)) {
            result.sequential().forEach(u -> downstreamAccumulator.accept(r, u));
        }
    }, downstream.combiner(), downstream.finisher(), 
            downstream.characteristics().toArray(new Characteristics[0]));
}

Moreover this has a straight-forward migration strategy for newer Java versions as stated in the below comment.

Ravindra Ranwala
  • 20,744
  • 6
  • 45
  • 63
  • 1
    also worth mentioning that this gives a complete building name to average animal weight mapping, and if the query was based on a building name, one can rather implement using a `filter` and `findFirst` assuming it to be unique – Naman Aug 20 '19 at 17:03
  • @Naman The OP stated that `The key of the map should be the name of the building` in his last comment. So my answer is based on that. – Ravindra Ranwala Aug 20 '19 at 17:12
  • 1
    Since the `flatMapping` collector is no magic, you can add an implementation to your codebase for Java 8 (see the end of [this answer](https://stackoverflow.com/a/39131049/2711488)), to use the simpler code and have a straight-forward migration strategy for newer Java versions (when using `import static`, you only have to change the import to switch to the standard API version). – Holger Aug 21 '19 at 08:59