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

Переопределение абстрактного метода или использование одного единственного метода в перечислениях?

Рассмотрим ниже enums, что лучше? Оба они могут использоваться точно так же, но каковы их преимущества друг над другом?

1. Переопределение абстрактного метода:

public enum Direction {
    UP {
        @Override
        public Direction getOppposite() {
            return DOWN;
        }
        @Override
        public Direction getRotateClockwise() {
            return RIGHT;
        }
        @Override
        public Direction getRotateAnticlockwise() {
            return LEFT;
        }
    },
    /* DOWN, LEFT and RIGHT skipped */
    ;
    public abstract Direction getOppposite();
    public abstract Direction getRotateClockwise();
    public abstract Direction getRotateAnticlockwise();
}

2. Используя один метод:

public enum Orientation {
    UP, DOWN, LEFT, RIGHT;
    public Orientation getOppposite() {
        switch (this) {
        case UP:
            return DOWN;
        case DOWN:
            return UP;
        case LEFT:
            return RIGHT;
        case RIGHT:
            return LEFT;
        default:
            return null;
        }
    }
    /* getRotateClockwise and getRotateAnticlockwise skipped */
}

Изменить: Я действительно надеюсь увидеть некоторые хорошо обоснованные/разработанные ответы с доказательствами/источниками конкретных заявлений. Большинство существующих ответов на производительность не очень убедительно из-за отсутствия доказательств.

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

4b9b3361

Ответ 1

Забудьте об эффективности в этом сравнении; это потребует поистине масштабной переписи, чтобы иметь значимую разницу в производительности между двумя методологиями.

Вместо этого сосредоточьтесь на ремонтопригодности. Предположим, вы закончили кодирование своего перечня Direction и, в конце концов, перейдете к более престижному проекту. Тем временем другому разработчику предоставляется право собственности на ваш старый код, включая Direction - позвоните ему Джимми.

В какой-то момент требования диктуют, что Джимми добавляет два новых направления: FORWARD и BACKWARD. Джимми устал и перегружен работой и не удосуживается полностью изучить, как это повлияет на существующую функциональность - он просто делает это. Посмотрим, что происходит сейчас:

1. Переопределение абстрактного метода:

Jimmy немедленно получает ошибку компилятора (на самом деле он, вероятно, заметил бы, что метод переопределяется прямо под объявлениями enum constant). В любом случае проблема обнаруживается и фиксируется во время компиляции.

2. Используя один метод:

Jimmy не получает ошибку компилятора или даже неполное предупреждение о переключении с его IDE, так как ваш switch уже имеет случай default. Позже во время выполнения определенный фрагмент кода вызывает FORWARD.getOpposite(), который возвращает null. Это вызывает неожиданное поведение и в лучшем случае быстро вызывает выброс NullPointerException.

Вернитесь назад и притворитесь, что вместо этого вы добавили некоторую будущую проверку:

default:
    throw new UnsupportedOperationException("Unexpected Direction!");

Даже тогда проблема не будет обнаружена до выполнения. Надеюсь, проект будет проверен правильно!

Теперь ваш пример Direction довольно прост, поэтому этот сценарий может показаться преувеличенным. На практике, однако, перечисления могут перерасти в проблему обслуживания так же легко, как и другие классы. В более крупной, старой кодовой базе с множественной устойчивостью разработчиков к рефакторингу является законной проблемой. Многие люди говорят об оптимизации кода, но они могут забыть, что время разработки также необходимо оптимизировать - и это включает в себя кодирование для предотвращения ошибок.

Изменить: Заметка под Пример JLS §8.9.2-4, похоже, согласен:

Телом класса, зависящим от констант, присваивается поведение константам. [Этот] шаблон намного безопаснее, чем использование оператора switch в базовом типе... поскольку шаблон исключает возможность забыть добавить поведение для новой константы (поскольку объявление перечисления вызовет ошибку времени компиляции).

Ответ 2

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

Я предлагаю следующий шаблон (применительно к вашей проблеме):

public enum Direction {
    UP, RIGHT, DOWN, LEFT;

    static {
      Direction.UP.setValues(DOWN, RIGHT, LEFT);
      Direction.RIGHT.setValues(LEFT, DOWN, UP);
      Direction.DOWN.setValues(UP, LEFT, RIGHT);
      Direction.LEFT.setValues(RIGHT, UP, DOWN);
    }

    private void setValues(Direction opposite, Direction clockwise, Direction anticlockwise){
        this.opposite = opposite;
        this. clockwise= clockwise;
        this. anticlockwise= anticlockwise;
    }

    Direction opposite;
    Direction clockwise;
    Direction anticlockwise;

    public final Direction getOppposite() { return opposite; }
    public final Direction getRotateClockwise() { return clockwise; }
    public final Direction getRotateAnticlockwise() { return anticlockwise; }
}

С таким дизайном вы:

  • никогда не забывайте устанавливать направление, потому что оно выполняется конструктором (в случае case)

  • имеют незначительные затраты на вызов метода, потому что метод является окончательным, а не виртуальным

  • чистый и короткий код

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

Ответ 3

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

Ответ 4

Второй вариант, вероятно, будет немного быстрее, так как > 2-арный полиморфизм заставит полный виртуальный вызов функции на интерфейс, против прямого вызова и индекса для последнего.

Первая форма - объектно-ориентированный подход.

Вторая форма - подход с учетом соответствия шаблону.

Таким образом, первая форма, объектно-ориентированная, упрощает добавление новых перечислений, но трудно добавить новые операции. Вторая форма делает обратное

Большинство опытных программистов, которых я знаю, рекомендуют использовать сопоставление шаблонов по объектной ориентации. Поскольку перечисления закрыты, добавление новых перечислений не является вариантом; поэтому я бы определенно пошел с последним подходом.

Ответ 5

Значения перечисления можно рассматривать как независимые классы. Поэтому, рассматривая Object-Oriented Concepts, каждый enum должен определять свое поведение. Поэтому я бы рекомендовал первый подход.

Ответ 6

Первая версия, вероятно, намного быстрее. Компилятор Java JIT может применять к нему агрессивные оптимизации, потому что enum являются окончательными (поэтому все методы в них final тоже). Код:

Orientation o = Orientation.UP.getOppposite();

должен фактически стать (во время выполнения):

Orientation o = Orientation.DOWN;

то есть. компилятор может удалить служебные данные для вызова метода.

С точки зрения дизайна, это правильный способ сделать это с помощью OO: переместить знания близко к объекту, который ему нужен. Поэтому UP должен знать об этом наоборот, а не о каком-то другом коде.

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

EDIT Мой первый аргумент зависит от того, насколько интеллектуальным является JIT-компилятор. Мое решение проблемы будет выглядеть так:

public enum Orientation {
    UP, DOWN, LEFT, RIGHT;

    private static Orientation[] opposites = {
        DOWN, UP, RIGHT, LEFT
    };

    public Orientation getOpposite() {
        return opposites[ ordinal() ];
    }
}

Этот код является компактным и быстрым, независимо от того, что JIT может или может сделать. Он четко передает намерение, и, учитывая правила ординалов, он всегда будет работать.

Я также предлагаю добавить тест, который гарантирует, что при вызове getOpposite() для каждого значения перечисления вы всегда получаете другой результат, и ни один из результатов не является null. Таким образом, вы можете быть уверены, что у вас есть все дела.

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

вот еще один способ сделать это:

public enum Orientation {
    UP(1), DOWN(0), LEFT(3), RIGHT(2);

    private int opposite;

    private Orientation( int opposite ) {
        this.opposite = opposite;
    }

    public Orientation getOpposite() {
        return values()[ opposite ];
    }
}

Мне не нравится этот подход. Слишком сложно читать (вам нужно подсчитать индекс каждого значения в голове) и слишком легко ошибиться. Для перечисления и метода, который вы можете вызвать, потребуется значение unit test за каждое значение (так что 4 * 3 = 12 в вашем случае).

Ответ 7

Вы также могли бы просто реализовать его как-то так (вам нужно сохранить константы перечисления в соответствующем порядке):

public enum Orientation {

    UP, RIGHT, DOWN, LEFT; //Order is important: must be clock-wise

    public Orientation getOppposite() {
        int position = ordinal() + 2;
        return values()[position % 4];
    }
    public Orientation getRotateClockwise() {
        int position = ordinal() + 1;
        return values()[position % 4];
    }
    public Orientation getRotateAnticlockwise() {
        int position = ordinal() + 3; //Not -1 to avoid negative position
        return values()[position % 4];
    }
}

Ответ 8

Ответ: зависит от

  • Если ваши определения методов просты

    Это относится к вашим очень простым примерным методам, которые просто жестко кодируют вывод enum для каждого ввода перечисления

    • реализовать определения, специфичные для значения перечисления, рядом с этим значением перечисления
    • реализовать определения, общие для всех значений перечисления в нижней части класса в "общей области"; если одна и та же сигнатура метода должна быть доступна для всех значений перечисления, но ни одна/часть логики не является общей, используйте определения абстрактных методов в общей области.

    то есть. Вариант 1

    Почему?

    • читаемость, согласованность, ремонтопригодность: код, непосредственно связанный с определением, находится рядом с определением
    • проверка времени компиляции, если абстрактные методы, объявленные в общей области, но не указанные в области значений enum

    Обратите внимание, что пример Север/Юг/Восток/Запад можно считать представляющим собой очень простое состояние (текущего направления), а методы, противоположные /rotateClockwise/rotateAnticlockwise, могут рассматриваться как представляющие пользовательские команды для изменения состояния. Что вызывает вопрос, что вы делаете для реальной, типично сложной государственной машины?

  • Если ваши определения методов сложны:

    State-Machines часто сложны, полагаясь на текущее состояние (перечисляемое значение), ввод команд, таймеры и довольно большие правила количества и бизнес-исключения для определения нового состояния (перечисляемого значения). Другие редкие времена, методы могут даже определять вывод нулевой стоимости посредством вычислений (например, категоризацию научного/инженерного/страхового рейтинга). Или он может использовать структуры данных, такие как карта, или сложную структуру данных, подходящую для алгоритма. Когда логика сложна, требуется дополнительная осторожность, и баланс между "общей" логикой и логикой "enum value-specific" изменяется.

    • избегайте излишнего объема кода, сложности и повторяющихся разделов "вырезать и вставлять" рядом с значением перечисления
    • попытайтесь реорганизовать как можно больше логики в общую область - возможно, поставив здесь 100% логики, но если это невозможно, используйте шаблон Gang Of Four "Template Method", чтобы максимизировать количество общей логики, но гибко разрешить небольшое количество конкретной логики против каждого значения перечисления. то есть насколько возможно Вариант 1, с небольшим количеством Варианта 2, разрешенным

    Почему?

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

    • Примечание. Вы можете включить логику ALL в отдельный класс-помощник, но я лично не вижу в этом никаких преимуществ (не производительность/ремонтопригодность/удобочитаемость). Это немного разрушает инкапсуляцию, и как только у вас есть вся логика в одном месте, какая разница, чтобы добавить простое определение перечисления обратно в начало класса? Разделение кода на несколько классов - это другое дело, и его следует поощрять там, где это необходимо.