Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Filter values from a list based on priority

Tags:

java

java-8

I have a list of valid values for a type:

Set<String> validTypes = ImmutableSet.of("TypeA", "TypeB", "TypeC");

From a given list I want to extract the first value which has a valid type. In this scenario I would write something of this sort:

public class A{
    private String type;
    private String member;
}

List<A> classAList;
classAList.stream()
    .filter(a -> validTypes.contains(a.getType()))
    .findFirst();

However I would like to give preference to TypeA, i.e. if classAList has TypeA and TypeB, I want the object which has typeA. To do this one approach I've is:

Set<String> preferredValidTypes = ImmutableSet.of("TypeA");
classAList.stream()
    .filter(a -> preferredValidTypes.contains(a.getType()))
    .findFirst()
    .orElseGet(() -> {
        return classAList.stream()
        .filter(a -> validTypes.contains(a.getType()))
        .findFirst();
    }

Is there a better approach?

like image 577
user1692342 Avatar asked May 08 '19 07:05

user1692342


4 Answers

filter list by type, order by type, collect to list, then just get first element

List<A> collect = classAList.stream()
                  .filter(a -> validTypes.contains(a.getType()))
                  .sorted(Comparator.comparing(A::getType))
                  .collect(Collectors.toList());
System.out.println(collect.get(0));
like image 124
Adrian Avatar answered Nov 12 '22 13:11

Adrian


You can use a custom comparator like:

Comparator<A> comparator = (o1, o2) -> {
    if (preferredValidTypes.contains(o1.getType()) && !preferredValidTypes.contains(o2.getType())) {
        return 1;
    } else if (!preferredValidTypes.contains(o1.getType()) && preferredValidTypes.contains(o2.getType())) {
        return -1;
    } else {
        return 0;
    }
};

to sort the list and then findFirst from that list with your conditiion.

like image 22
Naman Avatar answered Nov 12 '22 13:11

Naman


i don't like the answers already given which use Comparator. Sorting is an expensive operation. You can do it with one walk through the list. Once you find a preferred value, you can break out, otherwise you continue to the end to find a valid.

In this case anyMatch can provide the possibility to break out from the stream processing:

MyVerifier verifier=new MyVerifier(validTypes,preferredValidTypes);
classAList.stream()
    .anyMatch(verifier);
System.out.println("Preferred found:"+verifier.preferred);
System.out.println("Valid found:"+verifier.valid);

public static class MyVerifier implements Predicate<A> {
    private Set<String> validTypes;
    private Set<String> preferredValidTypes;
    A preferred=null;
    A valid=null;

    public MyVerifier(Set<String> validTypes, Set<String> preferredValidTypes) {
        super();
        this.validTypes = validTypes;
        this.preferredValidTypes = preferredValidTypes;
    }

    @Override
    public boolean test(A a) {
        if(preferred==null && preferredValidTypes.contains(a.getType())) {
            preferred=a;
            // we can stop because we found the first preferred
            return true;
        } else if(valid==null && validTypes.contains(a.getType())) {
            valid=a;
        }
        return false;
    }

}
like image 1
Conffusion Avatar answered Nov 12 '22 13:11

Conffusion


One can, of course, define two lists, one with all valid types, and one with the preferred types.

However, here is another approach. Define one list, or actually, a Map, with the keys being the valid types, and the boolean values being whether the type is preferred.

Map<String, Boolean> validTypes = ImmutableMap.of(
    "TypeA", false,
    "TypeB", false,
    "TypeC", true
);

Using AtomicReference

One option is the following:

AtomicReference<A> ref = new AtomicReference<>();
listOfAs.stream()
    .filter(t -> validTypes.containsKey(t.getType()))
    .anyMatch(t -> validTypes.get(ref.updateAndGet(u -> t).getType()));

AtomicReference now contains a preferred A if available, or another valid A, or if the stream is empty, then it contains null. This stream operation short-circuits if an A with a preferred type is found.

The drawback of this option is that it creates side-effects, which is discouraged.

Using distinct()

Another suggestion would be the following. It uses the same map structure, using a boolean to indicate which values are preferred. However, it does not create side effects.

Map<Boolean, A> map = listOfAs.stream()
    .filter(t -> validTypes.containsKey(t.getType()))
    .map(t -> new Carrier<>(validTypes.get(t.getType()), t))
    .distinct()
    .limit(2)
    .collect(Collectors.toMap(Carrier::getKey, Carrier::getValue));

It works as follows.

  1. filter discards any element that is not a valid type.
  2. Then, each element is mapped to a Carrier<Boolean, A> instance. A Carrier is a Map.Entry<K, V> which implements its equals and hashCode methods regarding only the key; the value does not matter. This is necessary for the following step,
  3. distinct(), which discards any duplicate element. This way, only one preferred type and only one valid type is found.
  4. We limit the stream to have 2 elements, one for each boolean. This is because the stream, which is lazy, stops evaluating after both booleans are found.
  5. At last, we collect the Carrier elements into a Map.

The map contains now the following elements:

  • Boolean.TRUE => A with a preferred type
  • Boolean.FALSE => A with a valid type

Retrieve the appropriate element using

A a = map.getOrDefault(true, map.get(false)); // null if not found
like image 1
MC Emperor Avatar answered Nov 12 '22 13:11

MC Emperor