Definicja Java Enum

151

Wydawało mi się, że całkiem dobrze rozumiem typy generyczne Javy, ale potem w java.lang.Enum natknąłem się na następujące rzeczy:

class Enum<E extends Enum<E>>

Czy ktoś mógłby wyjaśnić, jak interpretować ten parametr typu? Dodatkowe punkty za udostępnienie innych przykładów, w których można zastosować podobny parametr typu.

Dónal
źródło
9
Oto wyjaśnienie, które lubię najbardziej: Groking Enum (aka Enum & lt; E extends Enum & lt; E >>)
Alan Moore,
To pytanie ma lepsze odpowiedzi: stackoverflow.com/a/3068001/2413303
EpicPandaForce

Odpowiedzi:

105

Oznacza to, że argument typu dla wyliczenia musi pochodzić z wyliczenia, które samo ma ten sam argument typu. Jak to się stało? Poprzez uczynienie argumentu type nowym typem. Więc jeśli mam wyliczenie o nazwie StatusCode, byłoby to równoważne z:

public class StatusCode extends Enum<StatusCode>

Teraz, jeśli sprawdzisz ograniczenia, mamy Enum<StatusCode>- więc E=StatusCode. Sprawdźmy: Erozszerza sięEnum<StatusCode> ? Tak! W porządku.

Możesz zadawać sobie pytanie, o co w tym wszystkim chodzi :) Cóż, oznacza to, że API dla Enum może odnosić się do siebie - na przykład, będąc w stanie powiedzieć, że Enum<E>implementuje Comparable<E>. Klasa bazowa jest w stanie dokonywać porównań (w przypadku wyliczeń), ale może upewnić się, że porównuje ze sobą tylko właściwy rodzaj wyliczeń. (EDYCJA: Cóż, prawie - zobacz edycję na dole.)

Użyłem czegoś podobnego w moim porcie C # ProtocolBuffers. Istnieją „wiadomości” (niezmienne) i „konstruktorzy” (zmienne, używane do tworzenia wiadomości) - i występują jako pary typów. Stosowane interfejsy to:

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>

Oznacza to, że z wiadomości możesz uzyskać odpowiedni konstruktor (np. Aby skopiować wiadomość i zmienić kilka bitów), a od konstruktora możesz otrzymać odpowiednią wiadomość, gdy skończysz ją budować. To dobra robota, ale użytkownicy API nie muszą się tym przejmować - jest to horrendalnie skomplikowane i wymagało kilku iteracji, aby dostać się tam, gdzie jest.

EDYCJA: Zauważ, że nie powstrzymuje cię to przed tworzeniem dziwnych typów, które używają argumentu typu, który sam w sobie jest w porządku, ale nie jest tego samego typu. Celem jest raczej zapewnienie korzyści we właściwym przypadku niż ochrona przed złem przypadkiem.

Więc jeśli i Enumtak nie były obsługiwane „specjalnie” w Javie, można (jak zauważono w komentarzach) utworzyć następujące typy:

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

Secondwdrożyłby Comparable<First>raczej niż Comparable<Second>... ale Firstsamo byłoby w porządku.

Jon Skeet
źródło
1
@artsrc: Nie pamiętam od razu, dlaczego musi to być ogólne zarówno w kreatorze, jak i w wiadomości. Jestem prawie pewien, że nie poszedłbym tą trasą, gdybym tego nie potrzebował :)
Jon Skeet
1
@SayemAhmed: Tak, to nie zapobiega temu aspektowi mieszania typów. Dodam o tym notatkę.
Jon Skeet,
1
„Użyłem czegoś podobnego w moim porcie C # ProtocolBuffers”. Ale jest inaczej, ponieważ konstruktorzy mają metody instancji, które zwracają typ parametru typu. Enumnie ma żadnych metod wystąpienia, które zwracają typ parametru typu.
newacct
1
@JonSkeet: Biorąc pod uwagę, że klasy wyliczeniowe są zawsze generowane automatycznie, twierdzę, że class Enum<E>jest to wystarczające we wszystkich przypadkach. W Generics powinieneś używać bardziej restrykcyjnego ograniczenia tylko wtedy, gdy jest to rzeczywiście konieczne do zapewnienia bezpieczeństwa typów.
newacct
1
@JonSkeet: Również, jeśli Enumpodklasy nie zawsze wygenerowany automatycznie, tylko dlatego, że trzeba class Enum<E extends Enum<?>>przez class Enum<E>to możliwość dostępu ordinaldo compareTo(). Jednak jeśli się nad tym zastanowić, z językowego punktu widzenia nie ma sensu pozwalać na porównywanie dwóch różnych typów wyliczeń za pomocą ich liczb porządkowych. Dlatego implementacja Enum.compareTo()tego użycia ordinalma sens tylko w kontekście Enumautomatycznie generowanych podklas. Gdybyś mógł ręcznie tworzyć podklasy Enum, compareToprawdopodobnie musiałoby to być abstract.
newacct
27

Poniżej znajduje się zmodyfikowana wersja wyjaśnienia z książki Java Generics and Collections : We have an Enumspecified

enum Season { WINTER, SPRING, SUMMER, FALL }

który zostanie rozszerzony do klasy

final class Season extends ...

gdzie ...ma być jakoś sparametryzowaną klasą bazową dla wyliczeń. Zastanówmy się, co to ma być. Cóż, jednym z wymagań Seasonjest to, że powinien on implementować Comparable<Season>. Więc będziemy potrzebować

Season extends ... implements Comparable<Season>

Czego możesz użyć do ...tego, aby to zadziałało? Biorąc pod uwagę, że ma to być parametryzacja Enum, jedyny wybór jest Enum<Season>taki, aby mieć:

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

Tak Enumjest sparametryzowany na typach takich jak Season. Wyciągnij z Seasoni otrzymujesz, że parametr Enumjest dowolnego typu, który spełnia

 E extends Enum<E>

Maurice Naftalin (współautor, Java Generics and Collections)

Maurice Naftalin
źródło
1
@newacct OK, teraz rozumiem: chcesz, aby wszystkie wyliczenia były instancjami Enum <E>, prawda? (Ponieważ jeśli są to wystąpienia podtypu Enum, powyższy argument ma zastosowanie). Ale wtedy nie będziesz już mieć wyliczeń bezpiecznych dla typu, więc w ogóle stracisz sens posiadania wyliczeń.
Maurice Naftalin,
1
@newacct Nie chcesz nalegać, że Seasonimplementuje Comparable<Season>?
Maurice Naftalin
2
@newacct Spójrz na definicję Enum. Aby porównać jedną instancję z drugą, musi porównać ich liczby porządkowe. Zatem argument compareTometody musi być zadeklarowany jako Enumpodtyp, w przeciwnym razie kompilator (poprawnie) powie, że nie ma liczby porządkowej.
Maurice Naftalin
2
@MauriceNaftalin: Gdyby Java nie zabraniała ręcznego tworzenia podklas Enum, byłoby to możliwe class OneEnum extends Enum<AnotherEnum>{}, nawet przy Enumzadeklarowaniu sposobu w tej chwili. Nie miałoby sensu porównywanie jednego typu wyliczenia z innym, więc Enumi compareTotak nie miałoby to sensu, jak zostało zadeklarowane. Granice nie pomagają w tym.
newacct
2
@MauriceNaftalin: Gdyby powodem była liczba porządkowa, to public class Enum<E extends Enum<?>>też by wystarczyło.
newacct
6

Można to zilustrować prostym przykładem i techniką, której można użyć do zaimplementowania połączonych wywołań metod dla podklas. W poniższym przykładzie setNamezwraca, Nodewięc łańcuch nie zadziała dla 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
}

Moglibyśmy więc odwołać się do podklasy w ogólnej deklaracji, tak aby Cityteraz zwracał prawidłowy typ:

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!
    }
}
Andrey Chaschev
źródło
Twoje rozwiązanie ma niesprawdzoną obsadę: return (CHILD) this;rozważ dodanie metody getThis (): protected CHILD getThis() { return this; } Zobacz: angelikalanger.com/GenericsFAQ/FAQSections/…
Roland
@Roland dzięki za link, pożyczyłem od niego pomysł. Czy mógłbyś mi wyjaśnić lub skierować do artykułu wyjaśniającego, dlaczego jest to zła praktyka w tym konkretnym przypadku? Metoda w linku wymaga więcej wpisywania i to jest główny argument, dlaczego tego unikam. Nigdy nie widziałem błędów rzutowania w tym przypadku + Wiem, że są pewne nieuniknione błędy rzutowania - np. Gdy przechowuje się obiekty wielu typów w tej samej kolekcji. Więc jeśli niesprawdzone odlewy nie są krytyczne, a projekt jest nieco skomplikowany ( Node<T>tak nie jest), ignoruję je, aby zaoszczędzić czas.
Andrey Chaschev
Twoja edycja nie różni się zbytnio od poprzedniej, oprócz dodania trochę cukru składniowego, weź pod uwagę, że następujący kod faktycznie się skompiluje, ale zgłosi błąd w czasie wykonywania: `Węzeł <City> node = new Node <City> () .setName (" node "). setSquare (1); `Jeśli spojrzysz na kod bajtowy java, zobaczysz, że ze względu na typ erasure instrukcja return (SELF) this;jest wkompilowana return this;, więc możesz ją po prostu pominąć.
Roland
@Roland dzięki, to jest to, czego potrzebowałem - zaktualizuję przykład gdy będę wolny.
Andrey Chaschev
Poniższy link jest również dobry: angelikalanger.com/GenericsFAQ/FAQSections/…
Roland
3

Nie tylko ty zastanawiasz się, co to znaczy; zobacz blog Chaotic Java .

„Jeśli klasa rozszerza tę klasę, powinna przekazać parametr E. Granice parametru E dotyczą klasy, która rozszerza tę klasę o ten sam parametr E”.

kpirkkal
źródło
1

Ten post całkowicie wyjaśnił mi problem „rekurencyjnych typów ogólnych”. Chciałem tylko dodać kolejny przypadek, w którym ta konkretna struktura jest konieczna.

Załóżmy, że masz ogólne węzły na ogólnym wykresie:

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

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

    public Collection<T> getNeighbor();
}

Następnie możesz mieć wykresy wyspecjalizowanych typów:

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

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

    public Collection<City> getNeighbor(){...}
}
nozebacle
źródło
Nadal pozwala mi stworzyć miejsce, w class Foo extends Node<City>którym Foo nie jest związane z City.
newacct
1
Jasne, a czy to źle? Nie sądzę. Podstawowy kontrakt dostarczony przez Node <City> jest nadal honorowany, tylko twoja podklasa Foo jest mniej użyteczna, odkąd zaczynasz pracę z Foos, ale usuwasz Miasta z ADT. Może to być przypadek użycia, ale najprawdopodobniej prostsze i bardziej przydatne jest po prostu uczynienie parametru generycznego takim samym, jak podklasa. Ale tak czy inaczej, projektant ma taki wybór.
mdma
@mdma: Zgadzam się. Więc jaki pożytek daje związanie, po prostu class Node<T>?
newacct
1
@nozebacle: Twój przykład nie pokazuje, że „ta konkretna konstrukcja jest konieczna”. class Node<T>jest w pełni zgodny z twoim przykładem.
newacct
1

Jeśli spojrzysz na Enumkod źródłowy, ma on następujące cechy:

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);
    } 
}

Po pierwsze, co to E extends Enum<E>znaczy? Oznacza to, że parametr typu jest czymś, co pochodzi od Enum i nie jest sparametryzowany przez typ surowy (jest sparametryzowany przez siebie).

Jest to istotne, jeśli masz wyliczenie

public enum MyEnum {
    THING1,
    THING2;
}

który, jeśli dobrze wiem, jest tłumaczony na

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

Oznacza to, że MyEnum otrzymuje następujące metody:

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;
}

A co ważniejsze,

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

To sprawia, że getDeclaringClass()rzut do właściwego Class<T>obiektu.

O wiele jaśniejszym przykładem jest ten, na który odpowiedziałem na to pytanie, w którym nie można uniknąć tej konstrukcji, jeśli chcesz określić ogólne ograniczenie.

EpicPandaForce
źródło
Nic, w czym pokazałeś, compareToani nic nie getDeclaringClasswymaga extends Enum<E>ograniczenia.
newacct
0

Według Wikipedii ten wzór nazywa się Ciekawie powtarzający się wzór szablonu . Zasadniczo, używając wzorca CRTP, możemy łatwo odwołać się do typu podklasy bez rzutowania typów, co oznacza, że ​​używając wzorca, możemy imitować funkcję wirtualną.

Hanzhou Tang
źródło