使用Java 8 Stream API查找枚举值

Finding enum value with Java 8 Stream API

假设有一个名为type的简单枚举定义如下:

1
2
3
4
5
6
7
8
9
10
enum Type{
    X("S1"),
    Y("S2");

    private String s;

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

对于给定的s查找正确的枚举,通常使用静态方法with for循环来完成(假设该方法在枚举中定义),例如:

1
2
3
4
5
6
7
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));
}

我认为用流API表示的这个函数等价物应该是这样的:

1
2
3
4
5
6
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是否有多余和混乱。然而,在流API中,我找不到任何东西可以简单地从Stream直接返回T

有更好的方法吗?

  • 我知道这个评论不会被任何人投票赞成,但是和Java 8一样伟大,你不必为每个问题使用EDCOX1和0的S。您的for-loop方法比使用Streams的任何方法都更清晰(更快)。
  • @pbabcdefp好吧,我认为这是一个很好的评论,但是如果我投了反对票,那么你评论中的第一个短语就会是错误的,这意味着我必须再次投反对票,然后我会认为这是一个很好的评论,所以我必须投反对票,但是第一个短语会是错误的……我想我就要扔了。
  • @pbabcdefp——这可能是一个观点问题,但我发现lambda越来越优于迭代,而clearity几乎总是胜过efficiency。我很肯定,我尝试过findFirst(),在思想上出现了一些奇怪的编译错误,并编写了reduce()变体。不管怎样,我对你所有的答案都投了反对票,但我觉得firstany更清楚,所以我同意了。谢谢你的帮助!


我用findFirst代替:

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


虽然在这种情况下,Map可能更好:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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。非常方便!

希望有帮助!:)

  • 这是一个非常巧妙的技巧,我特别喜欢JVM如何保证序列图的数量-非常好。只是一个小建议——我们可以通过去掉字段s,使代码更加紧凑,因为它在其他地方是不使用的。
  • findAny()而不是findFirst()吗?--如果保证一个(或零个)匹配,那么EDOCX1[1]可能会更快得到答案(尽管我猜枚举是否足够大以使搜索并行化是值得怀疑的)。
  • SLIM作为流排序,EDCOX1×2表示在多个匹配的情况下与原来的Java 8代码相同的行为。虽然我怀疑枚举值和名称之间存在双目标映射,但我不希望在findFirst()findAny()之间的性能有太大差异(正如您所说,这是有问题的)。也就是说,我将使用第二种方法。它使用更多的内存,但由于查找时间更好,因此可能值得使用:)


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

1
2
3
4
5
EnumSet.allOf(Type.class)
       .stream()
       .filter(e -> e.s.equals(val))
       .findFirst()
       .orElseThrow(String.format("Unsupported type %s.", val));

  • 从JDK源来看,Arrays.stream(Type.values())在内部克隆一个数组,然后创建一个新的流,而EnumSet.allOf(Type.class).stream()在内部创建一个新的枚举集,向它添加所有枚举值,然后创建一个新的流。这个解决方案对我来说看起来更好,但是使用它的决定不应该仅仅基于对创建了多少对象的假设。
  • @Kapex因为Enun是常量,所以你一直在复制。另一方面,一个EnumSet可以引入像DISTINCTNONNULL这样的标志,这些标志可以在流的后面被利用,而不仅仅是对象。
  • @EugeneRight,Enumset可以为优化流提供提示,这将是更好的使用它的原因。您所说的"由于枚举是常量,所以您一直在复制。"是什么意思?枚举常量定义枚举类型的枚举实例。据我所知,复制枚举数组的工作方式与复制任何其他对象类型的数组的工作方式相同。
  • @kapex我的意思是values总是通过复制返回一个新的数组,因为返回一个由原始数组支持的数组意味着改变枚举本身的可能性,这显然是不可能的。
  • e.s.等于(val)这里s的值是多少?


findAny()代替reduce怎么样?

1
2
3
4
5
6
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)));
}

  • orElseThrow希望有一个Supplier,顾名思义,它提供了异常,而不是抛出异常,所以应该使用.orElseThrow(() -> new IllegalStateException …),而不是.orElseThrow(() -> {throw new IllegalStateException … })。当使用选中的异常时,您会注意到不同之处。
  • @霍尔格-很好的接球,固定。谢谢!


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

我认为亚历克西斯C的第二个答案(亚历克西斯C的答案)在复杂性方面是好的。而不是每次使用

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

您可以在加载类时使用o(n)time,方法是将所有元素放入映射中,然后使用映射以固定时间o(1)访问类型的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
}
}

我知道这个问题很古老,但我是从一个复制品来的。我的答案不是严格回答OP关于如何使用Java流解决问题的问题。相反,这个答案扩展了公认答案中提出的基于Map的解决方案,使其更易于管理。

所以这里是:我提议引入一个特殊的助手类,我将它命名为EnumLookup

假设Type枚举写得稍微好一点(有意义的字段名+getter),我向它注入一个EnumLookup常量,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
    }
}

然后,用法变得(同样,imo)真正可读:

1
2
3
4
5
6
7
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助手类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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
}

注意:

  • 我在这里用了番石榴的ImmutableMap,但可以用常规的HashMapLinkedHashMap

  • 如果您不介意上面的方法中没有延迟初始化,那么您可以延迟生成EnumLookup,直到第一次调用byCode方法(例如,使用lazy holder惯用法,就像在接受的答案中一样)。

    • 超级整洁,我建议把这个介绍给我们的项目!!!!


    你需要一个字符串的getter,但这是我使用的模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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);
    }

    不适用于循环,只适用于流。快速查找,而不是每次调用方法时都生成流。


    你需要一个字符串的getter。在下面的示例中,此方法是getDesc()

    1
    2
    3
    public static StatusManifestoType getFromValue(String value) {
        return Arrays.asList(values()).stream().filter(t -> t.getDesc().equals(value)).findAny().orElse(null);
    }

    我不能添加评论,所以我发布一个答案来补充上述答案,只是遵循相同的想法,但使用Java 8的方法:

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