Jaka jest przewaga wyliczenia Java w porównaniu z klasą z publicznymi statycznymi polami końcowymi?

147

Bardzo dobrze znam C #, ale zaczynam więcej pracować w Javie. Spodziewałem się, że wyliczenia w Javie są w zasadzie równoważne z tymi w C #, ale najwyraźniej tak nie jest. Początkowo byłem podekscytowany, gdy dowiedziałem się, że wyliczenia Java mogą zawierać wiele fragmentów danych, co wydaje się bardzo korzystne ( http://docs.oracle.com/javase/tutorial/java/javaOO/enum.html ). Jednak od tego czasu odkryłem, że brakuje wielu funkcji, które są trywialne w C #, takich jak możliwość łatwego przypisania elementowi wyliczenia określonej wartości, aw konsekwencji możliwość konwersji liczby całkowitej na wyliczenie bez przyzwoitego wysiłku ( tj. Konwertuj wartość całkowitą na pasującą wartość Java Enum ).

Więc moje pytanie brzmi: czy wyliczenia Java mają jakąś korzyść w porównaniu z klasą z kilkoma publicznymi statycznymi polami końcowymi? A może po prostu zapewnia bardziej zwartą składnię?

EDYCJA: Pozwól, że wyrażę się bardziej jasno. Jaka jest przewaga wyliczeń Java w porównaniu z klasą z kilkoma publicznymi statycznymi polami końcowymi tego samego typu ? Na przykład w przykładzie planet w pierwszym łączu, jaka jest przewaga wyliczenia nad klasą z tymi stałymi publicznymi:

public static final Planet MERCURY = new Planet(3.303e+23, 2.4397e6);
public static final Planet VENUS = new Planet(4.869e+24, 6.0518e6);
public static final Planet EARTH = new Planet(5.976e+24, 6.37814e6);
public static final Planet MARS = new Planet(6.421e+23, 3.3972e6);
public static final Planet JUPITER = new Planet(1.9e+27, 7.1492e7);
public static final Planet SATURN = new Planet(5.688e+26, 6.0268e7);
public static final Planet URANUS = new Planet(8.686e+25, 2.5559e7);
public static final Planet NEPTUNE = new Planet(1.024e+26, 2.4746e7);

O ile wiem, odpowiedź Casablanki jest jedyną, która to spełnia.

Craig W
źródło
4
@Bohemian: To nie może być duplikat, ponieważ OP wspomniał tylko o public static finalpolach, które mogą być wpisanymi wartościami, a niekoniecznie ints.
casablanca
1
@Shahzeb Hardly. Wyraźnie używanie wyliczeń zamiast stałych łańcuchowych jest BARDZO dobrym pomysłem i jest więcej niż zalecane. Bezpieczeństwo typów, nie wymaga statycznych funkcji narzędziowych i tak dalej .. absolutnie nie ma powodu, aby zamiast tego używać łańcuchów.
Voo
1
@Voo Tak, wiedziałem, że będą nieporozumienia. A oto jedna w 49 sekund. Enumy są świetne (a ja je uwielbiam i używam ich bardzo często), ale jakiego rodzaju bezpieczeństwa potrzebujesz, deklarując stałą lub używając jej. Tworzenie wyliczenia za każdym razem, gdy trzeba zadeklarować stałą dla literału String, jest przesadą.
Shahzeb
4
@Shahzeb Jeśli masz jedną zmienną, na pewno użyj ciągu znaków, niewiele może się tu wydarzyć (pojedyncza wartość jest raczej bezcelowa jako parametr). Ale zauważ, że mówimy o stałych S, więc teraz mówimy o prawdopodobnie przekazywaniu ich do funkcji itp. Czy potrzebujemy bezpieczeństwa typów? Cóż, nie, ale większość ludzi uważa, że ​​„wszystko jest w void*dobrym stylu” w stylu c i może to powstrzymać błędy (zwłaszcza jeśli przekazuje więcej niż jeden parametr wyliczenia / ciągu znaków!). Umieszcza również stałe w ich własnej przestrzeni nazw, itp. W przeciwieństwie do tego, posiadanie zwykłych zmiennych nie ma prawdziwej korzyści.
Voo
3
@Bohemian: Nie wiem jak. W przypadku ints nie ma bezpieczeństwa typu, ponieważ można przekazać dowolną wartość. Z drugiej strony obiekty typowane nie różnią się od wyliczeń pod względem bezpieczeństwa typów.
casablanca

Odpowiedzi:

78

Technicznie rzecz biorąc, można by rzeczywiście postrzegać wyliczenia jako klasę z kilkoma typami stałymi, i tak faktycznie jest sposób, w jaki stałe wyliczeniowe są implementowane wewnętrznie. Użycie a enumdaje jednak przydatne metody ( Enum javadoc ), które w innym przypadku musiałbyś zaimplementować samodzielnie, takie jak Enum.valueOf.

Casablanka
źródło
14
Istnieje również .values()do iteracji po liście wartości.
h3xStream
1
Wydaje się, że to właściwa odpowiedź, choć niezbyt satysfakcjonująca. Moim zdaniem nie było opłacalne, aby Java dodawała obsługę wyliczeń tylko dla bardziej zwartej składni i dostępu do metod Enum.
Craig W
7
@Craig, twój instynkt ma rację - to jest bardzo zła odpowiedź, ponieważ całkowicie pominęła cel wyliczeń. Zobacz moje komentarze pod pytaniem, aby poznać niektóre powody.
Bohemian
1
@Bohemian: Nie przegapiłem celu wyliczeń - używam ich cały czas. Zobacz moją odpowiedź na Twój komentarz powyżej.
casablanca
1
Metoda Enum.valuesOf zwraca ten sam obiekt, jeśli zostanie wywołana dwukrotnie?
Emre Aktürk
104
  1. Bezpieczeństwo typu i bezpieczeństwo wartości.
  2. Gwarantowany singleton.
  3. Umiejętność definiowania i nadpisywania metod.
  4. Możliwość korzystania z wartości w switchrachunku casesprawozdaniu bez zastrzeżeń.
  5. Wbudowana sekwencjonowanie wartości za pośrednictwem ordinal().
  6. Serializacja według nazwy, a nie wartości, co zapewnia pewien stopień ochrony w przyszłości.
  7. EnumSeti EnumMapzajęcia.
Markiz Lorne
źródło
19
Powiedziawszy to wszystko, za każdym razem, gdy umieszczałem kod w Enum, żałowałem tego.
Markiz Lorne
4
Dlaczego tego żałowałeś? Nigdy…
glglgl
2
@glglgl Becasue umieściło kod specyficzny dla aplikacji w miejscu, w którym czułem, że tak naprawdę nie należy, to tak naprawdę definiuje tylko zestaw wartości. Gdybym miał to zrobić ponownie, umieściłbym to w jednym z wielu switchstwierdzeń, które były pierwotną motywacją do użycia Enumw ogóle.
Markiz Lorne
72

Nikt nie wspomniał o możliwości ich wykorzystania w switchwypowiedziach; To też dorzucę.

Pozwala to na używanie dowolnie złożonych wyliczeń w czysty sposób bez używania instanceof, potencjalnie mylących ifsekwencji lub wartości przełączających innych niż łańcuch / int. Przykładem kanonicznym jest automat stanowy.

Dave Newton
źródło
W każdym razie nie wspomniałeś o żadnych korzyściach z wyliczeń w porównaniu z polami statycznymi. Możesz użyć wystarczającej liczby typów w instrukcjach przełączania z polami statycznymi. Op Potrzebujesz prawdziwych funkcjonałów lub różnic w wydajności
Genaut
@Genaut Zaletą jest to, że wyliczenia mają większą funkcjonalność niż string lub int - pytanie dotyczyło różnic, które podałem. OP jest już świadomy tego, czym są wyliczenia, i nikt inny nie wspomniał o instrukcjach przełączania, kiedy opublikowałem to 4,5 roku temu, a przynajmniej kilka osób uznało, że dostarczyło nowe informacje ¯_ (ツ) _ / ¯
Dave Newton
44

Podstawową zaletą jest bezpieczeństwo typów. W przypadku zestawu stałych można użyć dowolnej wartości tego samego typu wewnętrznego, wprowadzając błędy. W przypadku wyliczenia można używać tylko odpowiednich wartości.

Na przykład

public static final int SIZE_SMALL  = 1;
public static final int SIZE_MEDIUM = 2;
public static final int SIZE_LARGE  = 3;

public void setSize(int newSize) { ... }

obj.setSize(15); // Compiles but likely to fail later

vs

public enum Size { SMALL, MEDIUM, LARGE };

public void setSize(Size s) { ... }

obj.setSize( ? ); // Can't even express the above example with an enum
Jim Garrison
źródło
3
klasy są również bezpieczne pod względem typu ...: / (zakładając, że pola statyczne są typu klasy kontenera)
h3xStream
Nadal możesz przekazać mu niepoprawną wartość, ale byłby to błąd czasu kompilacji, który jest znacznie łatwiejszy do wykrycia.
zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
2
Możesz zadzwonić setSize(null)w drugim przykładzie, ale prawdopodobnie zakończy się niepowodzeniem znacznie wcześniej niż błąd pierwszego przykładu.
Jeffrey
42

Jest mniej zamieszania. Weźmy Fontna przykład. Ma konstruktora, który przyjmuje żądaną nazwę Font, jej rozmiar i styl ( new Font(String, int, int)). Do dziś nie pamiętam, czy styl, czy rozmiar są najważniejsze. Jeżeli Fontnie stosuje się enumdo wszystkich swoich różnych stylów ( PLAIN, BOLD, ITALIC, BOLD_ITALIC), jego konstruktor będzie wyglądać Font(String, Style, int), uniemożliwiając wszelkie nieporozumienia. Niestety, enumnie było go w pobliżu, kiedy Fontklasa została utworzona, a ponieważ Java musi zachować kompatybilność wsteczną, zawsze będziemy nękani tą niejednoznacznością.

Oczywiście jest to tylko argument przemawiający za używaniem stałych enumzamiast public static final. Wyliczenia są również idealne dla singletonów i implementują domyślne zachowanie, umożliwiając późniejsze dostosowywanie (tj. Wzorzec strategii ). Przykładem tego ostatniego jest java.nio.file„s OpenOptiona StandardOpenOption: jeśli deweloper chciał stworzyć własne niestandardowe OpenOption, potrafił.

Jeffrey
źródło
Twój przypadek „Czcionka” jest nadal niejednoznaczny, jeśli poproszono o dwie takie same wyliczenia. Kanoniczną odpowiedzią na ten problem jest to, co wiele języków nazywa nazwanymi parametrami . Ta lub lepsza obsługa sygnatur metod IDE.
aaaaaa
1
@aaaaaa Nie widziałem wielu przypadków, w których konstruktor wziąłby dwa takie same enumbez użycia varargs lub a, Setaby wziąć dowolną ich liczbę.
Jeffrey,
@aaaaaa Główny problem z nazwanymi parametrami polega na szczegółach implementacji (nazwa parametru). Potrafię stworzyć interfejs interface Foo { public void bar(String baz); }. Ktoś sprawia klasy, która wywołuje bar: someFoo.bar(baz = "hello");. Zmieniam podpis Foo::barna public void bar(String foobar). Teraz osoba, która dzwoniła, someFoobędzie musiała zmodyfikować swój kod, jeśli nadal chce, aby działał.
Jeffrey
Nie pamiętam też, żebym widział kolejne typy wyliczeń, ale pomyślałem, że typowym może być DAY_OF_WEEK lub coś podobnego. I dobra uwaga na temat interfejsu - nie pomyślałem o tym. Osobiście wziąłbym ten problem za powszechną niejednoznaczność spowodowaną nienazwanymi parametrami, wymagającymi silniejszego wsparcia IDE. Rozumiem jednak, że to wezwanie do oceny i że zerwanie zmian interfejsu API jest czymś, co należy poważnie rozważyć.
aaaaaa
26

Istnieje wiele dobrych odpowiedzi, ale nikt nie wspomina, że ​​istnieją wysoce zoptymalizowane implementacje klas / interfejsów Collection API specjalnie dla wyliczeń :

Te specyficzne klasy wyliczenia akceptują tylko Enumwystąpienia ( EnumMapjedyne akceptowaneEnum s tylko jako klucze) i jeśli to możliwe, powracają do kompaktowej reprezentacji i manipulacji bitami w ich implementacji.

Co to znaczy?

Jeśli nasz Enumtyp ma nie więcej niż 64 elementy (większość rzeczywistych Enumprzykładów będzie się do tego kwalifikować), implementacje przechowują elementy w jednej longwartości, każda Enumrozpatrywana instancja będzie powiązana z bitem o długości 64-bitowej long. Dodanie elementu do elementu EnumSetto po prostu ustawienie odpowiedniego bitu na 1, a usunięcie go to po prostu ustawienie tego bitu na 0. Testowanie, czy element jest w elemencie, Setto tylko jeden test maski bitowej! Teraz musisz to pokochać Enum!

icza
źródło
1
Wiedziałem już o tych dwóch, ale właśnie wiele się nauczyłem z twojej What does this mean?sekcji. Wiedziałem, że jest podział we wdrażaniu w rozmiarze 64, ale tak naprawdę nie wiedziałem dlaczego
Christopher Rucinski
15

przykład:

public class CurrencyDenom {
   public static final int PENNY = 1;
 public static final int NICKLE = 5;
 public static final int DIME = 10;
public static final int QUARTER = 25;}

Ograniczenie stałych Java

1) Brak bezpieczeństwa typu : Przede wszystkim nie jest bezpieczny; możesz przypisać dowolną prawidłową wartość int do int, np. 99, chociaż nie ma monety, która by reprezentowała tę wartość.

2) Brak sensownego drukowania : wartość drukowania którejkolwiek z tych stałych wypisze jej wartość numeryczną zamiast znaczącej nazwy monety np. Kiedy wydrukujesz NICKLE, wypisze "5" zamiast "NICKLE"

3) Brak przestrzeni nazw : aby uzyskać dostęp do stałej currencyDenom, musimy poprzedzić nazwę klasy, np. CurrencyDenom.PENNY zamiast po prostu używać PENNY, chociaż można to również osiągnąć za pomocą importu statycznego w JDK 1.5

Zaleta enum

1) Wyliczenia w Javie są bezpieczne dla typów i mają własną przestrzeń nazw. Oznacza to, że Twoje wyliczenie będzie miało typ, na przykład „Waluta” w poniższym przykładzie i nie możesz przypisać żadnej wartości innej niż określona w stałych wyliczeniowych.

public enum Currency {PENNY, NICKLE, DIME, QUARTER};

Currency coin = Currency.PENNY; coin = 1; //compilation error

2) Enum w Javie są typami referencyjnymi, takimi jak klasa lub interfejs i możesz zdefiniować konstruktor, metody i zmienne wewnątrz java Enum, co czyni go bardziej wydajnym niż Enum w C i C ++, jak pokazano w następnym przykładzie typu Java Enum.

3) Możesz określić wartości stałych wyliczeniowych w czasie tworzenia, jak pokazano na poniższym przykładzie: publiczne wyliczenie Waluta {PENNY (1), NICKLE (5), DIME (10), QUARTER (25)}; Ale aby to zadziałało, musisz zdefiniować zmienną składową i konstruktora, ponieważ PENNY (1) w rzeczywistości wywołuje konstruktor, który akceptuje wartość int, patrz poniższy przykład.

public enum Currency {
    PENNY(1), NICKLE(5), DIME(10), QUARTER(25);
    private int value;

    private Currency(int value) {
            this.value = value;
    }
}; 

Źródła: https://javarevisited.blogspot.com/2011/08/enum-in-java-example-tutorial.html

Politycznie niezależny
źródło
11

Jak już zauważyłeś, pierwszą zaletą wyliczeń jest prostota składni. Ale głównym celem wyliczeń jest zapewnienie dobrze znanego zestawu stałych, które domyślnie tworzą zakres i pomagają w przeprowadzaniu bardziej kompleksowej analizy kodu poprzez sprawdzanie bezpieczeństwa typu i wartości.

Te atrybuty wyliczeń pomagają zarówno programiście, jak i kompilatorowi. Na przykład, powiedzmy, że widzisz funkcję, która akceptuje liczbę całkowitą. Co ta liczba całkowita może oznaczać? Jakie wartości możesz przekazać? Tak naprawdę nie wiesz od razu. Ale jeśli widzisz funkcję, która akceptuje wyliczenie, znasz bardzo dobrze wszystkie możliwe wartości, które możesz przekazać.

W przypadku kompilatora wyliczenia pomagają określić zakres wartości i jeśli nie przypiszesz specjalnych wartości do elementów członkowskich wyliczenia, będą to zakresy od 0 w górę. Pomaga to w automatycznym śledzeniu błędów w kodzie poprzez sprawdzanie bezpieczeństwa typu i nie tylko. Na przykład kompilator może ostrzec, że nie obsługujesz wszystkich możliwych wartości wyliczenia w instrukcji switch (tj. Gdy nie maszdefault wielkości liter i obsługujesz tylko jedną z N wartości wyliczenia). Ostrzega również, gdy konwertujesz dowolną liczbę całkowitą na wyliczenie, ponieważ zakres wartości wyliczenia jest mniejszy niż liczba całkowita, a to z kolei może powodować błędy w funkcji, która tak naprawdę nie akceptuje liczby całkowitej. Ponadto generowanie tabeli skoków dla przełącznika staje się łatwiejsze, gdy wartości są od 0 w górę.

Dotyczy to nie tylko języka Java, ale także innych języków ze ścisłym sprawdzaniem typu. C, C ++, D, C # to dobre przykłady.


źródło
4

Wyliczenie jest niejawnie ostateczne, z prywatnymi konstruktorami, wszystkie jego wartości są tego samego typu lub podtypu, wszystkie jego wartości można uzyskać za pomocą values(), pobrać name()lub ordinal()wartość lub można wyszukać wyliczenie według numeru lub nazwy.

Możesz także zdefiniować podklasy (chociaż teoretycznie ostateczne, coś, czego nie możesz zrobić w żaden inny sposób)

enum Runner implements Runnable {
    HI {
       public void run() {
           System.out.println("Hello");
       }
    }, BYE {
       public void run() {
           System.out.println("Sayonara");
       }
       public String toString() {
           return "good-bye";
       }
    }
 }

 class MYRunner extends Runner // won't compile.
Peter Lawrey
źródło
4

Zalety enum:

  1. Wyliczenia są bezpieczne dla typów, pola statyczne nie
  2. Istnieje skończona liczba wartości (nie można przekazać nieistniejącej wartości wyliczenia. Jeśli masz statyczne pola klas, możesz popełnić ten błąd)
  3. Każde wyliczenie może mieć przypisanych wiele właściwości (pól / metod pobierających) - hermetyzacja. Również kilka prostych metod: YEAR.toSeconds () lub podobne. Porównaj: Colors.RED.getHex () z Colors.toHex (Colors.RED)

„np. możliwość łatwego przypisania elementowi wyliczenia określonej wartości”

enum EnumX{
  VAL_1(1),
  VAL_200(200);
  public final int certainValue;
  private X(int certainValue){this.certainValue = certainValue;}
}

„iw konsekwencji możliwość konwersji liczby całkowitej na wyliczenie bez znacznego wysiłku” Dodaj metodę konwertującą int na wyliczenie, która to robi. Po prostu dodaj statyczny HashMap <Integer, EnumX> zawierający mapowanie wyliczenia java .

Jeśli naprawdę chcesz przekonwertować ord = VAL_200.ordinal () z powrotem na val_200, po prostu użyj: EnumX.values ​​() [ord]

mabn
źródło
3

Inną ważną różnicą jest to, że kompilator java traktuje static finalpola typów pierwotnych i ciągów jako literały. Oznacza to, że te stałe stają się wbudowane. Jest podobny do C/C++ #definepreprocesora. Zobacz to pytanie SO . Tak nie jest w przypadku wyliczeń.

Seyed Jalal Hosseini
źródło
2

Korzystając z wyliczenia, uzyskujesz sprawdzanie czasu kompilacji prawidłowych wartości. Spójrz na to pytanie.

zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
źródło
2

Największą zaletą jest to, że enum Singletony są łatwe do napisania i bezpieczne dla wątków:

public enum EasySingleton{
    INSTANCE;
}

i

/**
* Singleton pattern example with Double checked Locking
*/
public class DoubleCheckedLockingSingleton{
     private volatile DoubleCheckedLockingSingleton INSTANCE;

     private DoubleCheckedLockingSingleton(){}

     public DoubleCheckedLockingSingleton getInstance(){
         if(INSTANCE == null){
            synchronized(DoubleCheckedLockingSingleton.class){
                //double checking Singleton instance
                if(INSTANCE == null){
                    INSTANCE = new DoubleCheckedLockingSingleton();
                }
            }
         }
         return INSTANCE;
     }
}

oba są podobne i same obsłużyły serializację, implementując

//readResolve to prevent another instance of Singleton
    private Object readResolve(){
        return INSTANCE;
    }

więcej

Premraj
źródło
0

Myślę, że enumnie można final, ponieważ kompilator pod maską generuje podklasy dla każdego enumwpisu.

Więcej informacji Ze źródła

Sitansu
źródło
Wewnętrznie nie są one ostateczne, ponieważ - jak mówisz - wewnętrznie mogą być podklasy. Ale niestety nie możesz ich samodzielnie podklasować, np. W celu rozszerzenia o własne wartości.
glglgl
0

Publikowane tutaj wyliczenia mają wiele zalet i właśnie teraz tworzę takie wyliczenia, o które zadano w pytaniu. Ale mam wyliczenie z 5-6 polami.

enum Planet{
EARTH(1000000, 312312321,31232131, "some text", "", 12),
....
other planets
....

W tego rodzaju przypadkach, gdy masz wiele pól w wyliczeniach, bardzo trudno jest zrozumieć, która wartość należy do którego pola, ponieważ musisz zobaczyć konstruktor i gałkę oczną.

Klasa ze static finalstałymi i używanie Builderwzorca do tworzenia takich obiektów sprawia, że ​​jest bardziej czytelny. Ale stracisz wszystkie inne zalety używania wyliczenia, jeśli ich potrzebujesz. Wadą takich klas jest, trzeba dodać Planetręcznie do obiektów list/setoPlanets.

I wciąż wolą enum nad taką klasą, jak values()jest przydatna i nigdy nie wiadomo, czy trzeba je stosować w switchlub EnumSetczy EnumMapw przyszłości :)

Bikas Katwal
źródło
0

Główny powód: wyliczenia pomagają pisać dobrze ustrukturyzowany kod, w którym semantyczne znaczenie parametrów jest jasne i silnie wpisane w czasie kompilacji - ze wszystkich powodów, które podały inne odpowiedzi.

Quid pro quo: w Javie po wyjęciu z pudełka tablica elementów Enum jest ostateczna. Zwykle jest to dobre, ponieważ pomaga docenić bezpieczeństwo i testowanie, ale w niektórych sytuacjach może to być wada, na przykład jeśli rozszerzasz istniejący kod bazowy, być może z biblioteki. W przeciwieństwie do tego, jeśli te same dane znajdują się w klasie z polami statycznymi, możesz łatwo dodać nowe instancje tej klasy w czasie wykonywania (może być również konieczne napisanie kodu, aby dodać je do dowolnej iterowalnej dla tej klasy). Ale to zachowanie Enum można zmienić: używając refleksji możesz dodawać nowych członków w czasie wykonywania lub zastępować istniejących członków, chociaż powinno to prawdopodobnie robić tylko w wyspecjalizowanych sytuacjach, w których nie ma alternatywy: tj. Jest to hackowe rozwiązanie i może powodować nieoczekiwane problemy, zobacz moją odpowiedźCzy mogę dodawać i usuwać elementy wyliczenia w czasie wykonywania w Javie .

radfast
źródło