46

Suppose there is a simple enum called Type defined like this:

enum Type{
    X("S1"),
    Y("S2");

    private String s;

    private Type(String s) {
        this.s = s;
    }
}

Finding the correct enum for given s is trivially done with static method with for-loop (assume the method is defined inside enum), e.g.:

private static Type find(String val) {
        for (Type e : Type.values()) {
            if (e.s.equals(val))
                return e;
        }
        throw new IllegalStateException(String.format("Unsupported type %s.", val));
}

I think the functional equivalent of this expressed with Stream API would be something like this:

private static Type find(String val) {
     return Arrays.stream(Type.values())
            .filter(e -> e.s.equals(val))
            .reduce((t1, t2) -> t1)
            .orElseThrow(() -> {throw new IllegalStateException(String.format("Unsupported type %s.", val));});
}

How could we write this better and simpler? This code feels coerced and not very clear. The reduce() especially seems clunky and abused as it doesn't accumulate anything, performs no calculation and always simply returns t1 (provided the filter returns one value - if it doesn't that's clearly a disaster), not to mention t2 is there superfluous and confusing. Yet I couldn't find anything in Stream API that simply somehow returns directly a T from a Stream<T>.

Is there a better way?

Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
quantum
  • 3,000
  • 5
  • 41
  • 56
  • 7
    I know that this comment will not be upvoted by anyone, but as great as Java 8 is you don't have to use `Stream`s for every single problem. Your for loop approach is clearer (and faster) than any approach using `Stream`s. – Paul Boddington Jan 06 '15 at 21:24
  • 4
    @pbabcdefp Well, I think it's a good comment, but if I upvoted it then the first phrase in your comment would be wrong, which means I'd have to downvote it again, then I'd think it's a good comment again, so then I'd have to upvote it, but then the first phrase would be wrong ... I think I'm about to throw `StackOverflowException`... – ajb Jan 06 '15 at 21:33
  • @pbabcdefp - that is probably a matter of opinion but I am finding lambdas more and more preferable to iterations and clarity trumps efficiency almost always. I was positive I had tried the `findFirst()` and have gotten some strange compile errors in IDEA and wrote the `reduce()` variant. At any rate, I have upvoted all your answers but felt `first` is clearer than `any` so I went with it. Thanks for your help! – quantum Jan 06 '15 at 21:55

9 Answers9

96

I would use findFirst instead:

return Arrays.stream(Type.values())
            .filter(e -> e.s.equals(val))
            .findFirst()
            .orElseThrow(() -> new IllegalStateException(String.format("Unsupported type %s.", val)));


Though a Map could be better in this case:
enum Type{
    X("S1"),
    Y("S2");

    private static class Holder {
        static Map<String, Type> MAP = new HashMap<>();
    }

    private Type(String s) {
        Holder.MAP.put(s, this);
    }

    public static Type find(String val) {
        Type t = Holder.MAP.get(val);
        if(t == null) {
            throw new IllegalStateException(String.format("Unsupported type %s.", val));
        }
        return t;
    }
}

I learnt this trick from this answer. Basically the class loader initializes the static classes before the enum class, which allows you to fill the Map in the enum constructor itself. Very handy !

Hope it helps ! :)

Community
  • 1
  • 1
Alexis C.
  • 91,686
  • 21
  • 171
  • 177
  • 5
    That is a very nifty trick, I especially like how the JVM is guaranteeing serial map population - excellent. Just one minor suggestion - we can make the code even more compact by getting rid of field `s` as it is unused elsewhere. – quantum Jan 06 '15 at 22:41
  • `findAny()` rather than `findFirst()`? -- if you're guaranteed one (or zero) matches, then potentially `findAny()` will get the answer sooner (although I guess it's questionable whether an enum will ever be big enough for the search to be parallelised) – slim Mar 02 '17 at 13:15
  • @slim As the stream is ordered, `findFirst()` exhibits the same behavior as the original pre-java 8 code in case of multiple matches. Although I suspect a bijective mapping between enum's values and names, I don't expect much difference in terms of performance between `findFirst()` and `findAny()` (and as you said, it's questionable). That said, I would use the second approach. It uses more memory but it may be worth as the lookup time is better :) – Alexis C. Mar 02 '17 at 17:39
  • 2nd approach is running in Big O(1) while all other approach will be taking BigO(n)... Thanks for the answer – Shridutt Kothari Dec 29 '21 at 09:56
21

The accepted answer works well, but if you want to avoid creating a new stream with a temporary array you could use EnumSet.allOf().

EnumSet.allOf(Type.class)
       .stream()
       .filter(e -> e.s.equals(val))
       .findFirst()
       .orElseThrow(() -> new IllegalStateException(String.format("Unsupported type %s.", val)));
Mike Partridge
  • 5,128
  • 8
  • 35
  • 47
Pär Eriksson
  • 367
  • 2
  • 9
  • 8
    Looking at the JDK source, `Arrays.stream(Type.values())` internally clones an array and then creates a new stream - while `EnumSet.allOf(Type.class).stream()` internally creates a new EnumSet, adds all enum values to it and then creates a new stream. This solution looks nicer to me but the decision to use this shouldn't just be based on assumptions about how many objects are created. – kapex Feb 28 '17 at 10:47
  • 1
    @kapex since enun are constants, you still copies, all the time. An `EnumSet` on the other hand could introduce flags like `DISTINCT` or `NONNULL` that could be leveraged later by the stream, its not *just* about the objects – Eugene Sep 17 '18 at 20:21
  • @Eugene Right, EnumSet could offer hints for optimized streaming, that would be a better reason for using it. What do you mean by "since enum are constants, you still copies, all the time."? Enum constants define enum instances of the enum type. Copying arrays of enums works the same as copying arrays of any other object types as far as I know. – kapex Sep 21 '18 at 10:19
  • @kapex what I mean is that `values` always returns a new array, of course by copying, because returning an array backed by the original array would mean the possibility to alter the enum itself and that cant happen, obviously. – Eugene Sep 21 '18 at 11:14
  • e.s.equals(val) what is value of s here? – Girdhar Singh Rathore Jan 10 '19 at 17:28
  • 1
    the last line should have been .orElseThrow(() -> new IllegalStateException(String.format("Unsupported type %s.", val))); – lorraine batol Jul 08 '20 at 02:53
5
Arrays.stream(Type.values()).filter(v -> v.s.equals(val)).findAny().orElseThrow(...);
sprinter
  • 27,148
  • 6
  • 47
  • 78
4

How about using findAny() instead of reduce?

private static Type find(String val) {
   return Arrays.stream(Type.values())
        .filter(e -> e.s.equals(val))
        .findAny()
        .orElseThrow(() -> new IllegalStateException(String.format("Unsupported type %s.", val)));
}
Todd
  • 30,472
  • 11
  • 81
  • 89
  • 4
    `orElseThrow` expects a `Supplier` which, as the name suggests, *supplies* the exception, not *throwing* the exception, so instead of `.orElseThrow(() -> {throw new IllegalStateException … })` you should use `.orElseThrow(() -> new IllegalStateException …)`. You will notice the difference when using a checked exception. – Holger Jan 07 '15 at 17:51
3

I think the second answer of Alexis C. (Alexis C.'s answer) is the good one in term of complexity. Instead of searching in O(n) each time you look for a code using

return Arrays.stream(Type.values())
        .filter(e -> e.s.equals(val))
        .findFirst()
        .orElseThrow(() -> new IllegalStateException(String.format("Unsupported type %s.", val)));

you could use O(n) time at the loading of the class by putting all elements into the map, and then access to the code of the type in constant time O(1) using the map.

enum Type{
X("S1"),
Y("S2");

private final String code;
private static Map<String, Type> mapping = new HashMap<>();

static {
    Arrays.stream(Type.values()).forEach(type-> mapping.put(type.getCode(), type));
}

Type(String code) {
    this.code = code;
}

public String getCode() {
    return code;
}

public static Type forCode(final String code) {
    return mapping.get(code);
}
}
2

I know this question is old but I came here from a duplicate. My answer is not strictly answering the OP's question about how to solve the problem using Java Streams. Instead, this answer expands the Map-based solution proposed in the accepted answer to become more (IMHO) manageable.

So here it is: I propose to introduce a special helper class that I named EnumLookup.

Assuming the Type enumeration is slightly better written (meaningful field name + getter), I inject an EnumLookup constant to it like below:

enum Type {

    X("S1"),
    Y("S2");

    private static final EnumLookup<Type, String> BY_CODE = EnumLookup.of(Type.class, Type::getCode, "code");

    private final String code;

    Type(String code) {
        this.code = code;
    }

    public String getCode() {
        return code;
    }

    public static EnumLookup<Type, String> byCode() {
        return BY_CODE;
    }
}

The usage then becomes (again, IMO) really readable:

Type type = Type.byCode().get("S1"); // returns Type.X

Optional<Type> optionalType = Type.byCode().find("S2"); // returns Optional(Type.Y)

if (Type.byCode().contains("S3")) { // returns false
    // logic
}

Finally, here's the code of the EnumLookup helper class:

public final class EnumLookup<E extends Enum<E>, ID> {

    private final Class<E> enumClass;
    private final ImmutableMap<ID, E> valueByIdMap;
    private final String idTypeName;

    private EnumLookup(Class<E> enumClass, ImmutableMap<ID, E> valueByIdMap, String idTypeName) {
        this.enumClass = enumClass;
        this.valueByIdMap = valueByIdMap;
        this.idTypeName = idTypeName;
    }

    public boolean contains(ID id) {
        return valueByIdMap.containsKey(id);
    }

    public E get(ID id) {
        E value = valueByIdMap.get(id);
        if (value == null) {
            throw new IllegalArgumentException(String.format(
                    "No such %s with %s: %s", enumClass.getSimpleName(), idTypeName, id
            ));
        }
        return value;
    }

    public Optional<E> find(ID id) {
        return Optional.ofNullable(valueByIdMap.get(id));
    }

    //region CONSTRUCTION
    public static <E extends Enum<E>, ID> EnumLookup<E, ID> of(
            Class<E> enumClass, Function<E, ID> idExtractor, String idTypeName) {
        ImmutableMap<ID, E> valueByIdMap = Arrays.stream(enumClass.getEnumConstants())
                .collect(ImmutableMap.toImmutableMap(idExtractor, Function.identity()));
        return new EnumLookup<>(enumClass, valueByIdMap, idTypeName);
    }

    public static <E extends Enum<E>> EnumLookup<E, String> byName(Class<E> enumClass) {
        return of(enumClass, Enum::name, "enum name");
    }
    //endregion
}

Note that:

  1. I used Guava's ImmutableMap here, but a regular HashMap or LinkedHashMap can be used instead.

  2. If you mind the lack of lazy initialization in the above approach, you can delay building of the EnumLookup until byCode method is first called (e.g. using the lazy-holder idiom, like in the accepted answer)

Tomasz Linkowski
  • 4,386
  • 23
  • 38
1

You'd need a getter for String s, but this is the pattern I use:

private static final Map<String, Type> TYPE_MAP = 
    Collections.unmodifiableMap(
        EnumSet.allOf(Type.class)
        .stream()
        .collect(Collectors.toMap(Type::getS, e -> e)));

public static Type find(String s) {
    return TYPE_MAP.get(s);
}

No for loops, only streams. Quick lookup as opposed to building a stream every time the method is called.

Myles Wehr
  • 11
  • 1
0

I can't add a comment yet, so I am posting an answer to complement the above answer, just following the same idea but using java 8 approach:

public static Type find(String val) {
    return Optional
            .ofNullable(Holder.MAP.get(val))
            .orElseThrow(() -> new IllegalStateException(String.format("Unsupported type %s.", val)));
}
Community
  • 1
  • 1
thiaguten
  • 23
  • 7
0

You need a getter for String s. In the example below this method is getDesc():

public static StatusManifestoType getFromValue(String value) {
    return Arrays.asList(values()).stream().filter(t -> t.getDesc().equals(value)).findAny().orElse(null);
}
Marcell Rico
  • 169
  • 2
  • 5