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.


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.


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)


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

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”.


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(){...}
}

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.


Nic, w czym pokazałeś, compareToani nic nie getDeclaringClasswymaga extends Enum<E>ograniczenia.
— newacct

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.