Подтвердить что ты не робот

Как обеспечить полноту в перечислительном коммутаторе во время компиляции?

У меня есть несколько операторов switch, которые проверяют enum. Все значения enum должны обрабатываться операторами switch оператором case. Во время рефакторинга кода может случиться, что enum сжимается и растет. Когда enum сжимается, компилятор выдает ошибку. Но ошибка не возникает, если растет enum. Состояние соответствия забудется и выдает ошибку времени выполнения. Я хотел бы переместить эту ошибку из времени выполнения для компиляции времени. Теоретически должно быть возможно обнаружить отсутствующие случаи enum во время компиляции. Есть ли способ достичь этого?

Вопрос уже существует Как определить новое значение, добавленное в перечисление и не обрабатываемое в коммутаторе", но оно не содержит ответа только на Работа вокруг Eclipse.

4b9b3361

Ответ 1

В другом решении используется функциональный подход. Вам просто нужно объявить класс enum в соответствии со следующим шаблоном:

public enum Direction {

    UNKNOWN,
    FORWARD,
    BACKWARD;

    public interface SwitchResult {
        public void UNKNOWN();
        public void FORWARD();
        public void BACKWARD();
    }

    public void switchValue(SwitchResult result) {
        switch (this) {
            case UNKNOWN:
                result.UNKNOWN();
                break;
            case FORWARD:
                result.FORWARD();
                break;
            case BACKWARD:
                result.BACKWARD();
                break;
        }
    }
}

Если вы попытаетесь использовать это без какой-либо одной константы перечисления, вы получите ошибку компиляции:

getDirection().switchValue(new Direction.SwitchResult() {
    public void UNKNOWN() { /* */ }
    public void FORWARD() { /* */ }
    // public void BACKWARD() { /* */ } // <- Compilation error if missing
});

Ответ 2

В Эффективной Java Джошуа Блох рекомендует создать абстрактный метод, который будет реализован для каждой константы. Например:

enum Color {
    RED   { public String getName() {return "Red";} },
    GREEN { public String getName() {return "Green";} },
    BLUE  { public String getName() {return "Blue";} };
    public abstract String getName();
}

Это будет функционировать как безопасный коммутатор, заставив вас реализовать этот метод, если вы добавите новую константу.

EDIT: Чтобы устранить некоторую путаницу, здесь эквивалент, используя обычный switch:

enum Color {
    RED, GREEN, BLUE;
    public String getName() {
        switch(this) {
            case RED:   return "Red";
            case GREEN: return "Green";
            case BLUE:  return "Blue";
            default: return null;
        }
    }
}

Ответ 3

Я не знаю о стандартном компиляторе Java, но компилятор Eclipse, безусловно, может быть настроен для предупреждения об этом. Перейдите в раздел Window- > Preferences- > Java- > Compiler- > Errors/Warnings/Enum, который не включен в коммутатор.

Ответ 4

По моему мнению, и если код, который вы собираетесь выполнять, находится за пределами домена вашего перечисления, способ сделать это - создать случай unit test, который будет проходить через ваши элементы в перечислении и выполнить часть кода, который содержит переключатель. Если что-то пойдет не так или нет, как ожидалось, вы можете проверить возвращаемое значение или состояние объекта с утверждением.

Вы можете выполнить тесты как часть некоторого процесса сборки, и на данный момент вы увидите какие-либо аномалии.

В любом случае, модульное тестирование почти обязательно и полезно во многих проектах.

Если код внутри коммутатора принадлежит перечислению, включите его внутри, как предложено в других ответах.

Ответ 5

Возможно, такой инструмент, как FindBugs, пометит такие коммутаторы.

Трудным ответом будет рефакторинг:

Возможность 1: может идти Объектно-ориентированный

Если это возможно, зависит от кода в случаях.

Вместо

switch (language) {
case EO: ... break;
case IL: ... break;
}

создать абстрактный метод:, скажем p

language.p();

или

switch (p.category()) {
case 1: // Less cases.
...
}

Возможность 2: более высокий уровень

При наличии множества переключателей в перечислении типа DocumentType, WORD, EXCEL, PDF,.... Затем создайте WordDoc, ExcelDoc, PdfDoc, расширяя базовый класс Doc. И снова можно работать объектно-ориентированным.

Ответ 6

Вы также можете использовать адаптацию шаблона Visitor к перечислениям, которые не позволяют помещать все типы несвязанных состояний в класс enum.

Сбой времени компиляции произойдет, если один из них изменяет перечисление достаточно осторожным, но он не гарантируется.

У вас по-прежнему будет сбой раньше, чем RTE в заявлении по умолчанию: он не удастся, если загрузится один из классов посетителя, что может произойти при запуске приложения.

Вот какой код:

Вы начинаете с перечисления, которое выглядит так:

public enum Status {
    PENDING, PROGRESSING, DONE
}

Вот как вы его преобразуете для использования шаблона посетителя:

public enum Status {
    PENDING,
    PROGRESSING,
    DONE;

    public static abstract class StatusVisitor<R> extends EnumVisitor<Status, R> {
        public abstract R visitPENDING();
        public abstract R visitPROGRESSING();
        public abstract R visitDONE();
    }
}

Когда вы добавляете новую константу в перечисление, если вы не забудете добавить метод visitXXX в абстрактный класс StatusVisitor, у вас будет непосредственно ошибка компиляции, которую вы ожидаете везде, где вы использовали посетителя (который должен заменять каждый переключатель, который вы сделали на перечислении):

switch(anObject.getStatus()) {
case PENDING :
    [code1]
    break;
case PROGRESSING :
    [code2]
    break;
case DONE :
    [code3]
    break;
}

должен стать:

StatusVisitor<String> v = new StatusVisitor<String>() {
    @Override
    public String visitPENDING() {
        [code1]
        return null;
    }
    @Override
    public String visitPROGRESSING() {
        [code2]
        return null;
    }
    @Override
    public String visitDONE() {
        [code3]
        return null;
    }
};
v.visit(anObject.getStatus());

И теперь уродливая часть, класс EnumVisitor. Это высший класс иерархии посетителей, реализующий метод посещения и приводящий код при запуске (теста или приложения), если вы забыли обновить посетителя-абстракта:

public abstract class EnumVisitor<E extends Enum<E>, R> {

    public EnumVisitor() {
        Class<?> currentClass = getClass();
        while(currentClass != null && !currentClass.getSuperclass().getName().equals("xxx.xxx.EnumVisitor")) {
            currentClass = currentClass.getSuperclass();
        }

        Class<E> e = (Class<E>) ((ParameterizedType) currentClass.getGenericSuperclass()).getActualTypeArguments()[0];
        Enum[] enumConstants = e.getEnumConstants();
        if (enumConstants == null) {
            throw new RuntimeException("Seems like " + e.getName() + " is not an enum.");
        }
        Class<? extends EnumVisitor> actualClass = this.getClass();
        Set<String> missingMethods = new HashSet<>();
        for(Enum c : enumConstants) {
            try {
                actualClass.getMethod("visit" + c.name(), null);
            } catch (NoSuchMethodException e2) {
                missingMethods.add("visit" + c.name());
            } catch (Exception e1) {
                throw new RuntimeException(e1);
            }
        }
        if (!missingMethods.isEmpty()) {
            throw new RuntimeException(currentClass.getName() + " visitor is missing the following methods : " + String.join(",", missingMethods));
        }
    }

    public final R visit(E value) {
        Class<? extends EnumVisitor> actualClass = this.getClass();
        try {
            Method method = actualClass.getMethod("visit" + value.name());
            return (R) method.invoke(this);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Существует несколько способов реализации/улучшения этого кода. Я выбираю идти вверх по иерархии классов, останавливаюсь, когда суперкласс является EnumVisitor и читает параметризованный тип оттуда. Вы также можете сделать это с параметром конструктора, являющимся классом enum.

Вы могли бы использовать более умную стратегию именования, чтобы иметь менее уродливые имена и т.д.

Недостатком является то, что он немного более подробный. Преимущества

  • ошибка времени компиляции [в большинстве случаев в любом случае]
  • работает, даже если вы не являетесь владельцем кода перечисления
  • no dead code (инструкция по умолчанию для всех значений перечисления)
  • sonar/pmd/... не жалуюсь, что у вас есть оператор switch без инструкции по умолчанию

Ответ 7

Проект Enum Mapper предоставляет обработчик аннотации, который во время компиляции будет проверять, что все константы перечисления обрабатываются.
Кроме того, он поддерживает обратный поиск и палитровые карты.

Пример использования:

@EnumMapper
public enum Seasons {
  SPRING, SUMMER, FALL, WINTER
}

Обработчик аннотации будет генерировать класс java Seasons_MapperFull, который можно использовать для сопоставления всех констант перечисления с произвольными значениями.

Вот пример использования, где мы сопоставляем каждую константу перечисления с строкой.

EnumMapperFull<Seasons, String> germanSeasons = Seasons_MapperFull
     .setSPRING("Fruehling")
     .setSUMMER("Sommer")
     .setFALL("Herbst")
     .setWINTER("Winter");

Теперь вы можете использовать mapper для получения значений или обратного поиска

String germanSummer = germanSeasons.getValue(Seasons.SUMMER); // returns "Sommer"
ExtremeSeasons.getEnumOrNull("Sommer");                 // returns the enum-constant SUMMER
ExtremeSeasons.getEnumOrRaise("Fruehling");             // throws an IllegalArgumentException 

Ответ 8

Это вариант подхода Visitor, который дает вам помощь при компиляции при добавлении констант:

interface Status {
    enum Pending implements Status {
        INSTANCE;

        @Override
        public <T> T accept(Visitor<T> v) {
            return v.visit(this);
        }
    }
    enum Progressing implements Status {
        INSTANCE;

        @Override
        public <T> T accept(Visitor<T> v) {
            return v.visit(this);
        }
    }
    enum Done implements Status {
        INSTANCE;

        @Override
        public <T> T accept(Visitor<T> v) {
            return v.visit(this);
        }
    }

    <T> T accept(Visitor<T> v);
    interface Visitor<T> {
        T visit(Done done);
        T visit(Progressing progressing);
        T visit(Pending pending);
    }
}

void usage() {
    Status s = getRandomStatus();
    String userMessage = s.accept(new Status.Visitor<String>() {
        @Override
        public String visit(Status.Done done) {
            return "completed";
        }

        @Override
        public String visit(Status.Progressing progressing) {
            return "in progress";
        }

        @Override
        public String visit(Status.Pending pending) {
            return "in queue";
        }
    });
}

Красивая, а? Я называю это "Rube Goldberg Architecture Solution".

Обычно я просто использую абстрактный метод, но если вы действительно не хотите добавлять методы к вашему перечислению (возможно, потому, что вы вводите циклические зависимости), это способ.

Ответ 9

Если в разных уровнях проекта, которые должны соответствовать друг другу, существует несколько перечислений, это может быть обеспечено тестовым случаем:

private static <T extends Enum<T>> String[] names(T[] values) {
    return Arrays.stream(values).map(Enum::name).toArray(String[]::new);
}

@Test
public void testEnumCompleteness() throws Exception {
    Assert.assertArrayEquals(names(Enum1.values()), names(Enum2.values()));
}