简体   繁体   中英

Convert ArrayList<String> to Set<ScopeItem> with Java streams

I want to convert an ArrayList<String> to Set<ScopeItem> with Java streams .

ScopeItem is a enum;
items is an ArrayList<String>;

Set<ScopeItem> scopeItems = items.stream()
                    .map(scopeString -> ScopeItem.valueOf(scopeString))
                    .filter(Objects::nonNull)
                    .collect(Collectors.toSet());

On a string that isn't in the enum this throws the following:

java.lang.IllegalArgumentException: No enum const...

Ideally, I would like to skip past any Strings that don't match.

I think maybe a using flatmap? Any ideas how to do it?

You could add the following method to your ScopeItem :

public static ScopeItem valueOfOrNull(String name) {
    try {
        return valueOf(name);
    } catch (IllegalArgumentException e) {
        // no such element
        return null;
    }
}

and use that to map your enum values:

.map(scopeString -> ScopeItem.valueOfOrNull(scopeString))

Subsequent .filter() on non-null values (which you already have) will filter-out those nulls that correspond to non-matching strings.

You can put a try-catch inside your map to return null instead of throwing an exception:

Set<ScopeItem> scopeItems = items.stream()
    .map(scopeString ->
        {
           try
           {
              return ScopeItem.valueOf(scopeString);
           }
           catch (IllegalArgumentException e)
           {
              return null;
           }
        })
    .filter(Objects::nonNull)
    .collect(Collectors.toSet());

You could also use filter beforehand to check whether the array of values contains the string you're looking for:

Set<ScopeItem> scopeItems = items.stream()
    .filter(scopeString -> Arrays.stream(ScopeItem.values())
                               .anyMatch(scopeItem -> scopeItem.name().equals(scopeString)))
    .map(ScopeItem::valueOf)
    .collect(Collectors.toSet());

Unlike others, I won't recommend using exceptions, as I feel they should be used for exceptional situations, and not for something that will likely to occur. A simple solution, is to have a static set of acceptable strings, and simply check, if a string you want to use valueOf with is in said set.

package test;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

public class Test {

    public static enum ScopeItem {
        ScopeA,
        ScopeB;

        private static Set<String> castableStrings;

        static {
            castableStrings = new HashSet<>();
            for (ScopeItem i : ScopeItem.values()) {
                castableStrings.add(i.name());
            }
        }

        public static boolean acceptable(String s) {
            return castableStrings.contains(s);
        }
    }

    public static void main(String[] args) {

       List<String> items = Arrays.asList("ScopeA", "RandomString", "ScopeB");
       Set<ScopeItem> scopeItems = items.stream()
               .filter(ScopeItem::acceptable)
               .map(ScopeItem::valueOf)
               .collect(Collectors.toSet());

       System.out.println(scopeItems.size());
    }

}

There's a more elegant approach here. You don't have to add any new fields to your enum; you can simply run a stream against it as well and determine if there's any matches in your collection.

The below code assumes an enum declaration of:

enum F {
    A, B, C, D, E
}

and looks as thus:

List<String> bad = Arrays.asList("A", "a", "B", "b", "C", "c");

final Set<F> collect = bad.stream()
                                .filter(e -> Arrays.stream(F.values())
                                                     .map(F::name)
                                                     .anyMatch(m -> Objects.equals(e, m)))
                                .map(F::valueOf)
                                .collect(Collectors.toSet());

Two parts to pay attention to here:

  • We do the internal filter on the values of our enum, and map that to a String through F::name .
  • We determine if there's any match on the elements of our base collection with the elements of our enum in a null-safe way ( Objects.equals does The Right Thing™ with nulls here)

You could use Apache Common's commons-lang3 EnumUtils.getEnum() instead of valueOf() . This returns null if there is no matching enum entry (which you can then filter exactly as you have in your code).

The easiest and clearest method would be to filter your Enum's values() method on items.contains() :

Set<ScopeItem> enumVals = Arrays.stream(ScopeItem.values())
        .filter(e -> items.contains(e.name()))
        .collect(Collectors.toSet());

No added functionality just to get the stream to do what you want, and it is obvious what this does at a glance.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM