Поиск значения перечисления с помощью Java 8 Stream API

Предположим, что существует простое перечисление с именем Type, определенное следующим образом:

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

    private String s;

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

Поиск правильного перечисления для данного s тривиально выполняется статическим методом с циклом for (предполагается, что метод определен внутри перечисления), например:

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, будет примерно таким:

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 ничего, что просто каким-то образом напрямую возвращает T из Stream<T>.

Есть ли способ лучше?


person quantum    schedule 06.01.2015    source источник
comment
Я знаю, что никто не поддержит этот комментарий, но как бы хороша ни была Java 8, вам не нужно использовать Stream для каждой отдельной проблемы. Ваш подход к циклу for понятнее (и быстрее), чем любой подход, использующий Streams.   -  person Paul Boddington    schedule 07.01.2015
comment
@pbabcdefp Ну, я думаю, что это хороший комментарий, но если бы я проголосовал за него, то первая фраза в вашем комментарии была бы неправильной, а это значит, что мне пришлось бы снова проголосовать за него, тогда я бы снова подумал, что это хороший комментарий, так что тогда мне пришлось бы проголосовать за это, но тогда первая фраза была бы неправильной ... я думаю, что собираюсь бросить StackOverflowException ...   -  person ajb    schedule 07.01.2015
comment
@pbabcdefp - это, вероятно, вопрос мнения, но я нахожу лямбда-выражения все более и более предпочтительными для итераций, а ясность почти всегда превосходит эффективность. Я был уверен, что попробовал findFirst() и получил несколько странных ошибок компиляции в IDEA, и написал вариант reduce(). В любом случае, я проголосовал за все ваши ответы, но чувствовал, что first понятнее, чем any, поэтому я согласился. Спасибо за вашу помощь!   -  person quantum    schedule 07.01.2015


Ответы (8)


Вместо этого я бы использовал 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 в самом конструкторе перечисления. Очень кстати !

Надеюсь, поможет ! :)

person Alexis C.    schedule 06.01.2015
comment
Это очень изящный трюк, мне особенно нравится, как JVM гарантирует заполнение последовательной карты - отлично. Только одно небольшое предложение - мы можем сделать код еще более компактным, избавившись от поля s, так как оно больше нигде не используется. - person quantum; 07.01.2015
comment
findAny() вместо findFirst()? -- если вам гарантировано одно (или ноль) совпадение, то потенциально findAny() получит ответ раньше (хотя я думаю, сомнительно, будет ли когда-либо перечисление достаточно большим для распараллеливания поиска) - person slim; 02.03.2017
comment
@slim Поскольку поток упорядочен, findFirst() демонстрирует то же поведение, что и исходный код до Java 8, в случае нескольких совпадений. Хотя я подозреваю биективное сопоставление между значениями и именами перечисления, я не ожидаю большой разницы в производительности между findFirst() и findAny() (и, как вы сказали, это сомнительно). Тем не менее, я бы использовал второй подход. Он использует больше памяти, но может стоить, так как время поиска лучше :) - person Alexis C.; 02.03.2017

Принятый ответ работает хорошо, но если вы хотите избежать создания нового потока с временным массивом, вы можете использовать EnumSet.allOf().

EnumSet.allOf(Type.class)
       .stream()
       .filter(e -> e.s.equals(val))
       .findFirst()
       .orElseThrow(String.format("Unsupported type %s.", val));
person Pär Eriksson    schedule 16.05.2016
comment
Глядя на исходный код JDK, Arrays.stream(Type.values()) внутренне клонирует массив, а затем создает новый поток, а EnumSet.allOf(Type.class).stream() внутренне создает новый EnumSet, добавляет к нему все значения перечисления, а затем создает новый поток. Это решение кажется мне более привлекательным, но решение об его использовании не должно основываться только на предположениях о том, сколько объектов создается. - person kapex; 28.02.2017
comment
@kapex, поскольку enun являются константами, вы все еще копируете их все время. С другой стороны, EnumSet может вводить флаги, такие как DISTINCT или NONNULL, которые могут быть использованы позже потоком, это не только об объектах - person Eugene; 17.09.2018
comment
@Eugene Правильно, EnumSet мог бы предложить подсказки для оптимизированной потоковой передачи, это было бы лучшей причиной для его использования. Что вы имеете в виду, поскольку перечисление является константой, которую вы все время копируете? Константы перечисления определяют экземпляры перечисления типа перечисления. Насколько я знаю, копирование массивов перечислений работает так же, как копирование массивов любых других типов объектов. - person kapex; 21.09.2018
comment
@kapex я имею в виду, что values всегда возвращает новый массив, конечно, путем копирования, потому что возврат массива, поддерживаемого исходным массивом, будет означать возможность изменить само перечисление, а этого, очевидно, не произойдет. - person Eugene; 21.09.2018
comment
esequals(val) какое здесь значение s? - person Girdhar Singh Rathore; 10.01.2019
comment
последняя строка должна была быть .orElseThrow(() -> new IllegalStateException(String.format(Unsupported type %s., val))); - person villager; 08.07.2020

Как насчет использования 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)));
}
person Todd    schedule 06.01.2015
comment
orElseThrow ожидает Supplier, который, как следует из названия, предоставляет исключение, а не генерирует исключение, поэтому вместо .orElseThrow(() -> {throw new IllegalStateException … }) следует использовать .orElseThrow(() -> new IllegalStateException …). Вы заметите разницу при использовании проверенного исключения. - person Holger; 07.01.2015
comment
@Holger - Хороший улов, исправлено. Спасибо! - person Todd; 07.01.2015

Я думаю, что второй ответ Алексиса С. (ответ Алексиса С.) является хорошим с точки зрения сложности. Вместо поиска в 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);
}
}
person Bastien Escouvois    schedule 20.02.2019

Я знаю, что этот вопрос старый, но я пришел сюда из дубликата. Мой ответ не является строго ответом на вопрос ОП о том, как решить проблему с помощью Java Streams. Вместо этого этот ответ расширяет решение на основе Map, предложенное в принятом ответе, чтобы стать более (ИМХО) управляемым.

Итак, вот оно: я предлагаю ввести специальный вспомогательный класс, который я назвал EnumLookup.

Предполагая, что перечисление Type написано немного лучше (осмысленное имя поля + геттер), я ввожу в него константу 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;
    }
}

Затем использование становится (опять же, IMO) действительно читаемым:

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. Я использовал ImmutableMap от Guava. здесь, но вместо него можно использовать обычные HashMap или LinkedHashMap.

  2. Если вы возражаете против отсутствия ленивой инициализации в приведенном выше подходе, вы можете отложить сборку EnumLookup до первого вызова метода byCode (например, с помощью идиома ленивого держателя, как в принятом ответе)

person Tomasz Linkowski    schedule 13.09.2018

Вам понадобится геттер для String s, но я использую этот шаблон:

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, только потоки. Быстрый поиск вместо построения потока при каждом вызове метода.

person Myles Wehr    schedule 18.04.2019

Я пока не могу добавить комментарий, поэтому отправляю ответ в дополнение к приведенному выше ответу, просто следуя той же идее но с использованием подхода java 8:

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

Вам нужен геттер для String s. В приведенном ниже примере это метод getDesc():

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