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
взгляните на sitepoint.com/self-types-with-javas-generics
Арне Бурмейстер

Ответы:

105

Это означает, что аргумент типа для enum должен происходить от enum, который сам имеет такой же аргумент типа. Как это может случиться? Делая аргумент типа новым самим типом. Так что если бы у меня был enum с именем StatusCode, это было бы эквивалентно:

public class StatusCode extends Enum<StatusCode>

Теперь, если вы проверите ограничения, у нас есть Enum<StatusCode>- так E=StatusCode. Давайте проверим: Eрасширяется Enum<StatusCode>ли? Да! Мы в порядке.

Вы можете спросить себя, в чем смысл этого :) Ну, это означает, что API для Enum может ссылаться на себя - например, быть в состоянии сказать, что Enum<E>реализует Comparable<E>. Базовый класс может выполнять сравнения (в случае перечислений), но он может убедиться, что он сравнивает только правильные типы перечислений друг с другом. (РЕДАКТИРОВАТЬ: Ну, почти - см. Редактирование внизу.)

Я использовал нечто подобное в моем 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 не нужно на самом деле заботиться об этом - это ужасно сложно и потребовалось несколько итераций, чтобы добраться до места, где он находится.

РЕДАКТИРОВАТЬ: Обратите внимание, что это не мешает вам создавать нечетные типы, которые используют аргумент типа, который сам по себе, но не тот же тип. Цель состоит в том, чтобы дать преимущества в правильном случае, а не защитить вас от неправильного случая.

Таким образом, если бы 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 Collections)

Морис Нафталин
источник
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 (" node "). setSquare (1); `Если вы посмотрите на байт-код Java, то увидите, что из-за стирания типа оператор return (SELF) this;компилируется return this;, поэтому вы можете просто его пропустить.
Роланд
@Roland спасибо, это то, что мне было нужно - обновлю пример, когда я буду свободен.
Андрей Чащев
Следующая ссылка также хороша: angelikalanger.com/GenericsFAQ/FAQSections/…
Роланд
3

Вы не единственный, кто интересуется, что это значит; см. хаотический Java-блог .

«Если класс расширяет этот класс, он должен передать параметр E. Границы параметра E предназначены для класса, который расширяет этот класс тем же параметром E».

kpirkkal
источник
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(){...}
}
nozebacle
источник
Это все еще позволяет мне создавать class Foo extends Node<City>Foo, не связанный с City.
newacct
1
Конечно, и это не так? Я так не думаю. Базовый контракт, предоставленный Узлом <City>, все еще выполняется, только ваш подкласс Foo менее полезен, так как вы начинаете работать с Foos, но выводите Города из ADT. Для этого может быть сценарий использования, но, скорее всего, проще и полезнее просто сделать универсальный параметр таким же, как подкласс. Но в любом случае, дизайнер имеет такой выбор.
МДМА
@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, и не параметризован с необработанным типом (он параметризован сам по себе).

Это актуально, если у вас есть 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>объекту.

Более ясный пример - тот, на который я ответил на этот вопрос, где вы не можете избежать этой конструкции, если хотите указать общую границу.

EpicPandaForce
источник
Ничего из того, что вы показали compareToили не getDeclaringClassтребует extends Enum<E>привязки.
newacct
0

Согласно википедии, этот шаблон называется Curiously recurring template pattern . По сути, используя шаблон CRTP, мы можем легко ссылаться на тип подкласса без приведения типов, что означает, что с помощью шаблона мы можем имитировать виртуальную функцию.

Ханьчжоу Тан
источник