使用Java 8 Stream API查找枚举值

47
假设有一个简单的枚举类型叫做Type,定义如下:
enum Type{
    X("S1"),
    Y("S2");

    private String s;

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

使用静态方法和for循环(假设该方法定义在枚举内部)轻松找到给定s的正确枚举,例如:

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));
}

我认为使用Stream API表达的功能等效于以下内容:

我认为使用Stream API表达的功能等效于以下内容:

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));});
}

我们该如何更好、更简单地编写这段代码呢?这段代码感觉有些勉强,不太清晰。尤其是 reduce() 方法,它似乎很笨拙且被滥用了,因为它不会累加任何值,也不执行任何计算,并且始终只返回 t1(如果过滤器返回一个值——否则就是灾难性的),更不用说 t2 是多余的且令人困惑的了。然而我无法在 Stream API 中找到任何直接从 Stream<T> 返回 T 的方法。

有更好的方式吗?


7
我知道这个评论不会被任何人点赞,但是尽管Java 8很棒,你并不需要在每个问题上都使用Stream。你的for循环方法比任何使用Stream的方法更清晰(也更快)。 - Paul Boddington
4
嗯,我认为这是一个好评论,但如果我给它点赞,那么你评论中的第一句话就会变成错误的,这意味着我需要再次取消点赞,然后我又会认为这是一个好评论,所以我又需要点赞,但是然后第一句话又会错...我觉得我快要抛出"StackOverflowException"了... - ajb
@pbabcdefp - 这可能是个人意见问题,但我越来越喜欢使用Lambda而不是迭代,并且清晰往往胜过效率。我确信我尝试过findFirst(),但在IDEA中得到了一些奇怪的编译错误,因此我写了reduce()变体。无论如何,我已经给你所有答案点赞了,但我觉得“first”比“any”更清晰,所以我选择了它。谢谢你的帮助! - quantum
9个回答

96
我会使用 findFirst 代替:
return Arrays.stream(Type.values())
            .filter(e -> e.s.equals(val))
            .findFirst()
            .orElseThrow(() -> new IllegalStateException(String.format("Unsupported type %s.", val)));

尽管在这种情况下使用Map可能更好:
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;
    }
}

我从这个答案中学到了这个技巧。基本上,类加载器会在枚举类之前初始化静态类,这使得你可以在枚举构造函数中填充Map。非常方便!希望能对你有所帮助! :)

5
这是一个非常巧妙的技巧,我特别喜欢JVM保证了串行映射的填充 - 太棒了。只有一个小建议 - 我们可以通过去掉未在其他地方使用的s字段来使代码更加简洁。 - quantum
如果你保证只有一个(或零个)匹配项,那么使用findAny()可能会更快地得到答案(尽管我猜枚举是否足够大以使搜索并行化是值得怀疑的),而不是使用findFirst() - slim
由于流是有序的,因此在存在多个匹配项的情况下,“findFirst()”表现出与Java 8之前的原始代码相同的行为。尽管我怀疑枚举值和名称之间存在双射映射,但我不希望在“findFirst()”和“findAny()”之间在性能方面有太大差异(正如您所说,这是值得商榷的)。话虽如此,我会使用第二种方法。它使用更多的内存,但查找时间更短,可能值得一试 :) - Alexis C.
第二种方法的时间复杂度为O(1),而其他所有方法的时间复杂度都为O(n)。谢谢您的答案。 - Shridutt Kothari

21

接受的答案效果不错,但是如果您想避免使用临时数组创建新流,可以使用EnumSet.allOf()

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

8
查看JDK源代码,Arrays.stream(Type.values()) 内部会克隆一个数组并创建一个新流,而 EnumSet.allOf(Type.class).stream() 内部则创建一个新的EnumSet,将所有枚举值添加到其中,然后创建一个新流。这个解决方案看起来更好,但使用它的决策不应只基于有多少对象被创建的假设。 - kapex
1
@kapex 因为枚举是常量,所以你一直在复制。另一方面,EnumSet 可以引入像 DISTINCTNONNULL 这样的标志,这些标志稍后可以被流利用,它不仅仅是关于对象。 - Eugene
@Eugene 对,EnumSet可以提供优化流的提示,这将是使用它的更好理由。你所说的“因为枚举是常量,所以你仍然会一直复制”是什么意思?枚举常量定义了枚举类型的枚举实例。据我所知,复制枚举数组的方式与复制任何其他对象类型的数组的方式相同。 - kapex
@kapex 我的意思是 values 总是返回一个新数组,当然是通过复制,因为返回由原始数组支持的数组将意味着可能改变枚举本身,这显然是不可能的。 - Eugene
e.s.equals(val)这里的s的值是多少? - Girdhar Singh Rathore
1
最后一行应该是 .orElseThrow(() -> new IllegalStateException(String.format("不支持的类型 %s。", val))); - lorraine batol

6
Arrays.stream(Type.values()).filter(v -> v.s.equals(val)).findAny().orElseThrow(...);

4
使用findAny()而不是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)));
}

4
orElseThrow方法需要一个Supplier类型的参数,该参数负责提供异常对象而不是抛出异常。因此,你应该使用.orElseThrow(() -> new IllegalStateException …)来代替.orElseThrow(() -> {throw new IllegalStateException … })。在使用受检异常时,你会注意到这种差异。 - Holger
@Holger - 发现得好,已修复。谢谢! - Todd

3

我认为Alexis C.(Alexis C.的答案)的第二个回答在复杂度方面是比较好的。它不是每次使用

寻找代码时都需要以O(n)的时间复杂度进行搜索。

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

你可以在类加载时将所有元素放入映射中,以O(n)的时间复杂度完成,然后使用映射以常数时间O(1)访问类型的代码。
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
我知道这个问题很老了,但我从一个重复的问题中来到这里。我的答案并不严格回答OP关于如何使用Java Streams解决问题的问题。相反,这个答案扩展了被接受的答案提出的基于Map的解决方案,使其更加(在我看来)易于管理。
所以,这里是我的建议:我建议引入一个特殊的帮助器类,我将其命名为EnumLookup
假设Type枚举略微改进(有意义的字段名称+getter),我像下面这样将一个EnumLookup常量注入其中:
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;
    }
}

使用方式变得(在我看来)真的很易读:
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
}

最后,这是EnumLookup辅助类的代码:

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
}

请注意:
  1. 这里使用了Guava的ImmutableMap,但是常规的HashMapLinkedHashMap也可以使用。

  2. 如果您关心上述方法中缺乏惰性初始化,您可以延迟构建EnumLookup直到第一次调用byCode方法(例如使用延迟保持者模式,就像接受的答案中所示)。


1

您需要一个获取String s的getter,但这是我使用的模式:

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);
}

没有for循环,只有流。与每次调用方法时构建流相反,快速查找。


1
你需要一个获取字符串s的getter方法。 在下面的示例中,这个方法是getDesc()
public static StatusManifestoType getFromValue(String value) {
    return Arrays.asList(values()).stream().filter(t -> t.getDesc().equals(value)).findAny().orElse(null);
}

0

我暂时无法添加评论,因此我发表一个回答来补充上面的answer,仅遵循相同的思路,但使用Java 8方式:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接