Визначення Java Enum


151

Я думав, що я досить добре розумію Java-дженерики, але потім у java.lang.Enum натрапив на таке:

class Enum<E extends Enum<E>>

Може хтось пояснить, як інтерпретувати параметр цього типу? Бонусні бали за надання інших прикладів, де може бути використаний аналогічний параметр типу.


9
Ось пояснення, яке мені найбільше подобається: Groking Enum (він же Enum & lt; E розширює Enum & lt; E >>)
Алан Мур

На це питання краще відповісти: stackoverflow.com/a/3068001/2413303
EpicPandaForce

Відповіді:


105

Це означає, що аргумент типу для enum має випливати з enum, який сам має аргумент того ж типу. Як це може статися? Зробивши аргумент типу самим новим типом. Отже, якщо у мене є перелік під назвою StatusCode, це було б рівнозначно:

public class StatusCode extends Enum<StatusCode>

Тепер якщо ви перевірите обмеження, у нас є Enum<StatusCode>- так E=StatusCode. Давайте перевіримо: чи EпоширюєтьсяEnum<StatusCode> ? Так! Ми все в порядку.

Ви, можливо, запитаєте себе, в чому суть цього :) Ну, це означає, що API для Enum може посилатися на себе - наприклад, бути в змозі сказати, що Enum<E>реалізує Comparable<E>. Базовий клас здатний проводити порівняння (у разі перерахунків), але він може переконатися, що він лише порівнює правильний вид перерахунків між собою. (EDIT: Ну, майже - дивіться правки внизу.)

Я використовував щось подібне в моєму C # порту ProtocolBuffers. Існують "повідомлення" (незмінні) та "будівельники" (мутабельні, використовуються для створення повідомлення) - і вони надходять як пари типів. Задіяні інтерфейси:

public interface IBuilder<TMessage, TBuilder>
  where TMessage : IMessage<TMessage, TBuilder> 
  where TBuilder : IBuilder<TMessage, TBuilder>

public interface IMessage<TMessage, TBuilder>
  where TMessage : IMessage<TMessage, TBuilder> 
  where TBuilder : IBuilder<TMessage, TBuilder>

Це означає, що з повідомлення ви можете отримати відповідного конструктора (наприклад, взяти копію повідомлення і змінити деякі біти), а від конструктора ви зможете отримати відповідне повідомлення, коли закінчите його створювати. Корисним робочим користувачам API не потрібно насправді дбати про це - це жахливо складно, і потрібно було кілька ітерацій, щоб дістатися до місця, де він є.

EDIT: Зауважте, що це не заважає вам створювати непарні типи, які використовують аргумент типу, який сам по собі є нормальним, але який не є тим самим. Метою є надання переваг у правильному випадку, а не захист від неправильної справи.

Тож якби Enumдо Java все-таки не зверталися "спеціально", ви могли (як зазначено в коментарях) створити такі типи:

public class First extends Enum<First> {}
public class Second extends Enum<First> {}

Secondбуде реалізовуватись, Comparable<First>а не Comparable<Second>... але Firstсама була б добре.


1
@artsrc: Я не можу напевно пригадати, чому це має бути загальним як у програмі, так і в повідомленні. Я майже впевнений, що не пішов би по цьому маршруту, якби він мені не знадобився :)
Джон Скіт

1
@SayemAhmed: Так, це не заважає цьому аспекту змішати типи. Додам до цього записку.
Джон Скіт

1
"Я використовував щось подібне в моєму C # порту ProtocolBuffers." Але це по-іншому, оскільки у будівельників є методи екземплярів, які повертають тип параметра типу. Enumне має методів примірника, які б повертали тип параметра типу.
newacct

1
@JonSkeet: Зважаючи на те, що класи перерахунків завжди автогенеруються, я стверджую, що class Enum<E>цього достатньо у всіх випадках. А в Generics вам слід використовувати обмеженіші обмеження лише тоді, коли це дійсно необхідно для забезпечення безпеки типу.
newacct

1
@JonSkeet: Крім того , якщо Enumпідкласи не завжди генеруватися автоматично, єдина причина , вам потрібно буде class Enum<E extends Enum<?>>через class Enum<E>це можливість отримати доступ ordinalдо compareTo(). Однак якщо ви подумаєте про це, з мовної точки зору немає сенсу дозволяти вам порівнювати два різні типи переліків через їх порядки. Тому реалізація Enum.compareTo()цього використання ordinalмає сенс лише в контексті Enumавтогенерації підкласів. Якби ви могли вручну підклас Enum, compareToмабуть, це було б abstract.
newacct

27

Далі йде модифікована версія пояснення з книги Java Generics and Collections : У нас є Enumзаявлений

enum Season { WINTER, SPRING, SUMMER, FALL }

який буде розширений до класу

final class Season extends ...

де ...повинен бути якийсь параметризований базовий клас для Enums. Давайте розберемося, що це має бути. Ну, одна з вимог до цього Season- це те, що він повинен реалізовуватися Comparable<Season>. Отже, нам буде потрібно

Season extends ... implements Comparable<Season>

Що б ви могли використовувати для ...цього, що дозволило б це працювати? Зважаючи на те, що це має бути параметризація Enum, єдиний вибір полягає в Enum<Season>тому, щоб ви могли мати:

Season extends Enum<Season>
Enum<Season> implements Comparable<Season>

Так Enumпараметризується на такі типи Season. Абстрактно від Seasonі ви отримуєте, що параметр Enumбудь-якого типу задовольняє

 E extends Enum<E>

Моріс Нафталін (співавтор, Java Generics and Collection)


1
@newacct Добре, я розумію зараз: ви хочете, щоб усі переліки були екземплярами Enum <E>, правда? (Оскільки, якщо вони є примірниками підтипу Enum, застосовується аргумент, наведений вище.) Але тоді у вас більше не буде безпечних для переліку типів, тому втрачаючи сенс взагалі перераховувати.
Моріс Нафталін

1
@newacct Не хочете наполягати на Seasonзастосуванні Comparable<Season>?
Моріс Нафталін

2
@newacct Подивіться на визначення Enum. Щоб порівняти один екземпляр з іншим, він повинен порівняти їх порядки. Отже, аргумент compareToметоду повинен був бути оголошений як Enumпідтип, або компілятор скаже (правильно), що він не має порядкового.
Моріс Нафталін

2
@MauriceNaftalin: Якщо Java не заборонила вручну підкласифікувати Enum, то це було б можливо class OneEnum extends Enum<AnotherEnum>{}, навіть із тим, як Enumзаявлено зараз. Це не має особливого сенсу , щоб мати можливість порівняти один тип перерахування з іншого, так , то Enum«S compareToне матиме сенсу , як заявлено в будь-якому випадку. Межі не допомагають у цьому.
newacct

2
@MauriceNaftalin: Якби порядкова причина була, то public class Enum<E extends Enum<?>>також було б достатньо.
newacct

6

Це можна проілюструвати простим прикладом та технікою, яка може бути використана для реалізації ланцюгових викликів методів для підкласів. У наведеному нижче прикладі setNameповертає, Nodeтак що ланцюжок не буде працювати для City:

class Node {
    String name;

    Node setName(String name) {
        this.name = name;
        return this;
    }
}

class City extends Node {
    int square;

    City setSquare(int square) {
        this.square = square;
        return this;
    }
}

public static void main(String[] args) {
    City city = new City()
        .setName("LA")
        .setSquare(100);    // won't compile, setName() returns Node
}

Таким чином, ми могли б посилатися на підклас у загальній декларації, щоб Cityтепер повертати правильний тип:

abstract class Node<SELF extends Node<SELF>>{
    String name;

    SELF setName(String name) {
        this.name = name;
        return self();
    }

    protected abstract SELF self();
}

class City extends Node<City> {
    int square;

    City setSquare(int square) {
        this.square = square;
        return self();
    }

    @Override
    protected City self() {
        return this;
    }

    public static void main(String[] args) {
       City city = new City()
            .setName("LA")
            .setSquare(100);                 // ok!
    }
}

Ваше рішення має неперевірений return (CHILD) this;склад : Розгляньте можливість додавання методу getThis (): protected CHILD getThis() { return this; } Див.: Angelikalanger.com/GenericsFAQ/FAQSections/…
Roland

@Roland дякую за посилання, я запозичив ідею з нього. Чи можете ви пояснити мені або звернутися до статті, яка пояснює, чому це погана практика в даному конкретному випадку? Метод у посиланні вимагає більше вводити текст, і це головний аргумент, чому я цього уникаю. Я ніколи не бачив помилок прикидання в цьому випадку + я знаю, що існують деякі неминучі помилки відкидання - тобто коли в одній колекції зберігаються об'єкти декількох типів. Тож якщо неперевірені касти не критичні, а дизайн дещо складний ( Node<T>це не так), я їх ігнорую, щоб заощадити час.
Андрій Чащев

Ваша редакція не відрізняється від раніше, окрім додавання синтаксичного цукру, врахуйте, що наступний код насправді буде компілюватися, але видасть помилку часу виконання: `Node <City> node = new Node <City> () .setName (" вузол "). setSquare (1); `Якщо ви подивитеся на байт-код Java, то побачите, що через стирання типу оператор return (SELF) this;складається в return this;, тому ви можете просто залишити його.
Roland

@Roland спасибі, це те, що мені було потрібно - оновлю приклад, коли я вільний.
Андрій Чащев

Наступне посилання також хороше: angelikalanger.com/GenericsFAQ/FAQSections/…
Roland

3

Ти не єдиний, хто цікавиться, що це означає; дивіться хаотичний блог Java .

"Якщо клас розширює цей клас, він повинен передавати параметр E. Межі параметра E призначені для класу, який розширює цей клас з тим же параметром E".


1

Цей пост повністю роз’яснив мені ці проблеми «рекурсивних родових типів». Я просто хотів додати ще один випадок, коли потрібна саме ця структура.

Припустимо, у вас є загальні вузли в загальному графіку:

public abstract class Node<T extends Node<T>>
{
    public void addNeighbor(T);

    public void addNeighbors(Collection<? extends T> nodes);

    public Collection<T> getNeighbor();
}

Тоді ви можете мати графіки спеціалізованих типів:

public class City extends Node<City>
{
    public void addNeighbor(City){...}

    public void addNeighbors(Collection<? extends City> nodes){...}

    public Collection<City> getNeighbor(){...}
}

Це все ще дозволяє мені створити місце, class Foo extends Node<City>де Foo не має відношення до міста.
newacct

1
Впевнений, і це неправильно? Я не думаю, що так. Базовий контракт, наданий Node <City>, все ще виконується, лише ваш підклас Foo менш корисний, оскільки ви починаєте працювати з Foos, але отримуєте міста з ADT. Для цього може бути випадок використання, але, швидше за все, простіше і корисніше просто зробити загальний параметр таким же, як підклас. Але в будь-якому випадку у дизайнера є такий вибір.
mdma

@mdma: Я згоден. Тож яке використання надає зв'язане, просто class Node<T>?
newacct

1
@nozebacle: Ваш приклад не демонструє, що "саме ця структура необхідна". class Node<T>повністю відповідає вашому прикладу.
newacct

1

Якщо ви подивитеся на Enumвихідний код, він має таке:

public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;
    }

    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }

    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    } 
}

По-перше, що E extends Enum<E>означає? Це означає, що параметр типу - це те, що поширюється на Enum, і не параметризується з необробленим типом (параметризується сам по собі).

Це актуально, якщо у вас є перерахунки

public enum MyEnum {
    THING1,
    THING2;
}

на яку, якщо я правильно знаю, перекладається

public final class MyEnum extends Enum<MyEnum> {
    public static final MyEnum THING1 = new MyEnum();
    public static final MyEnum THING2 = new MyEnum();
}

Це означає, що MyEnum отримує такі методи:

public final int compareTo(MyEnum o) {
    Enum<?> other = (Enum<?>)o;
    Enum<MyEnum> self = this;
    if (self.getClass() != other.getClass() && // optimization
        self.getDeclaringClass() != other.getDeclaringClass())
        throw new ClassCastException();
    return self.ordinal - other.ordinal;
}

І ще важливіше,

    @SuppressWarnings("unchecked")
    public final Class<MyEnum> getDeclaringClass() {
        Class<?> clazz = getClass();
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<MyEnum>)clazz : (Class<MyEnum>)zuper;
    }

Це робить getDeclaringClass()ролях належнимClass<T> об'єкта.

Більш чітким прикладом є той, на який я відповів на це запитання, де ви не можете уникнути цієї конструкції, якщо ви хочете вказати загальне обмеження.


Нічого, що ви показали compareToабо не getDeclaringClassвимагаєте, extends Enum<E>пов'язане.
newacct

0

Згідно з wikipedia, ця закономірність називається цікаво повторюваним шаблоном . В основному, використовуючи схему CRTP, ми можемо легко посилатися на тип підкласу без кастингу типів, а значить, використовуючи шаблон, ми можемо імітувати віртуальну функцію.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.