kinad
kinad

Reputation: 73

Generate a SortedMap based on the contents of two Lists

I am trying to generate a SortedMap using streams directly from two ArrayLists that come from a two different sources. My goal is to have the SortedMap store a Double attribute from the second list as key and the object from the first list as the value, if those objects have a matching attribute, which is checked by a helper Object.

So far, I can get it accomplished using:

SortedMap<Double, FirstObject> myMap = new TreeMap<>(Double::compareTo);

List<FirstObject> myList = firstDao.get(someId).stream()
    .filter(firstobject -> secondDao.get(firstObject.getObjectId())
        .stream()
        .anyMatch(secondObject -> {
            if (Helper.check(secondObject).matches()) {
                myMap.put(
                    secondObject.getEfficiency(), firstObject
                );
            }
            return Helper.check(secondObject).matches();
    }))
    .collect(Collectors.toList());

I have no use for myList that I am generating with that code, but so far it's the only way I have been able to populate the Map.

Is there a way I can populate directly to the SortedMap without having to generate that list?

Upvotes: 2

Views: 90

Answers (1)

Alexander Ivanchenko
Alexander Ivanchenko

Reputation: 29028

Instead of creating a filter that causes side-effects you need to use collect.

One of the ways to do that is to create an intermediate Map which would associate each object returned by firstDao with the matching pair returned by secondDao.

Then create a stream over the entries of the intermediate map. Filter out the entry with an empty key (with no matching pair). And then apply collect(Collectors.toMap()).

NavigableMap<Double, FirstObject> myMap = firstDao.get(tableId).stream()
    .collect(Collectors.toMap( // creates a map `Map<Optional<Double>,FirstObject>`
        firstObject -> secondDao.get(firstObject.getObjectId()).stream()
            .filter(secondObject -> firstObject.getAttribute().equals(secondObject.getAttribute()))
            .findFirst()
            .map(FirstObject::getEfficiency),
        Function.identity(),
        (left, right) -> left // duplicated keys would appear when there are more then one object having no matching pair (the key would be an empty optional), so we need to guard against that case by providing a merge function to resulve duplicates
    ))
    .entrySet().stream()
    .filter(entry -> entry.getKey().isPresent())
    .collect(Collectors.toMap(
        entry -> entry.getKey().get(),    // extracting a key (efficiency)
        Map.Entry::getValue,              // extracting a value
        (left, right) -> { throw new AssertionError("duplicates are not expected"); }, // since the source is an intermediate map we don't expect duplicates
        TreeMap::new
    ));

Another slightly more concise way of addressing this problem would be to create a custom collector by utilizing Collector.of() (the overall logic remains the same):

NavigableMap<Double, FirstObject> myMap = firstDao.get(tableId).stream()
    .collect(Collector.of(
        TreeMap::new,                                          // mutable container of the collector
        (NavigableMap<Double, FirstObject> map, FirstObject firstObject) -> 
            secondDao.get(firstObject.getObjectId()).stream()  // population the mutable container
                .filter(secondObject -> next.getAttribute().equals(secondObject.getAttribute()))
                .findFirst()
                .map(FirstObject::getEfficiency)
                .ifPresent(efficiency -> map.put(efficiency, firstObject)),
        (left, right) -> { left.putAll(right); return left; } // merging containers in parallel, I'm assuming that there's no duplicates and hence there's no special policy
    ));

Sidenote: when you need a TreeMap it's better to use NavigableMap as an abstract type. This interface extends SortedMap and offers a wide range of methods which are not accessible with SortedMap.


Addressing the comment posted by OP

This code provided in the question creates a stream of objects retrieved by the means of the firstDao and the following filter will update the map myMap if the predicate is meat by adding a new entry (or replacing the value of the existing one) with efficiency of the very first encountered matching object from the stream created using secondDao.get() as a source.

    .filter(firstobject -> secondDao.get().stream()
        .anyMatch(secondObject -> {
            if (someCondition) {
                myMap.put(secondObject.getEfficiency(), firstObject);
            }
            return someCondition;
    }))

anyMatch - is a short-circuit operation and in case if there are the same id but different efficiency, they will be ignored.

This snippet from the solution above behaves in exactly the same way: findFirst will pick the very first object for which someCondition will be evaluated to true (the case with an empty optional is being treated separately afterwards, and equals/hashCode of the optional is delegates to equals/hashCode implementation of its value, so there's no room for discrepancies in the result).

firstObject -> secondDao.get().stream()
    .filter(secondObject -> someCondition)
    .findFirst()
    .map(FirstObject::getEfficiency)

And I assume it's the expected behavior because there is no mention that code provided in the question is malfunctioning in some ways, as well as there's no sample data (which might lead to the opposite conclusion).

Hence, the claim that the above solution doesn't produce the expected result contradicts the current state of the question, since it's based on the logic of the original code.

Until the desired result isn't specified (by providing a sample data, or describing what's wrong with the original code) I see no possibility to improve the answer.

Upvotes: 5

Related Questions