Định nghĩa Java Enum


151

Tôi nghĩ rằng tôi đã hiểu các khái quát về Java khá tốt, nhưng sau đó tôi đã tìm thấy những điều sau đây trong java.lang.Enum:

class Enum<E extends Enum<E>>

Ai đó có thể giải thích làm thế nào để giải thích tham số loại này? Điểm thưởng cho việc cung cấp các ví dụ khác về nơi có thể sử dụng tham số loại tương tự.


9
Đây là lời giải thích tôi thích nhất: Groking Enum (còn gọi là Enum & lt; E kéo dài Enum & lt; E >>)
Alan Moore

Câu hỏi này có câu trả lời tốt hơn: stackoverflow.com/a/3068001/2413303
EpicPandaForce

Câu trả lời:


105

Nó có nghĩa là đối số kiểu cho enum phải xuất phát từ một enum mà chính nó có cùng đối số kiểu. Làm thế nào điều này có thể xảy ra? Bằng cách làm cho đối số kiểu chính nó. Vì vậy, nếu tôi có một enum được gọi là StatusCode, nó sẽ tương đương với:

public class StatusCode extends Enum<StatusCode>

Bây giờ nếu bạn kiểm tra các ràng buộc, chúng tôi đã có Enum<StatusCode>- vì vậy E=StatusCode. Hãy kiểm tra: có Emở rộng Enum<StatusCode>không? Đúng! Chúng tôi ổn.

Bạn cũng có thể tự hỏi ý nghĩa của vấn đề này là gì :) Vâng, điều đó có nghĩa là API cho Enum có thể tự tham chiếu - ví dụ, có thể nói rằng Enum<E>thực hiện điều đó Comparable<E>. Lớp cơ sở có thể thực hiện các phép so sánh (trong trường hợp enum) nhưng nó có thể đảm bảo rằng nó chỉ so sánh đúng loại enum với nhau. (EDIT: Chà, gần như - xem phần chỉnh sửa ở phía dưới.)

Tôi đã sử dụng một cái gì đó tương tự trong cổng C # của ProtocolBuffers. Có "thông điệp" (không thay đổi) và "người xây dựng" (có thể thay đổi, được sử dụng để xây dựng một thông điệp) - và chúng có dạng cặp. Các giao diện liên quan là:

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>

Điều này có nghĩa là từ một tin nhắn, bạn có thể có được một nhà xây dựng thích hợp (ví dụ: lấy một bản sao của tin nhắn và thay đổi một số bit) và từ một nhà xây dựng, bạn có thể nhận được một tin nhắn phù hợp khi bạn xây dựng xong nó. Đó là một công việc tốt mà người dùng API không cần thực sự quan tâm về vấn đề này - nó phức tạp khủng khiếp và đã thực hiện một số lần lặp để đến nơi.

EDIT: Lưu ý rằng điều này không ngăn bạn tạo các loại lẻ sử dụng đối số loại mà bản thân nó ổn, nhưng không phải là cùng loại. Mục đích là để cho lợi ích trong đúng trường hợp chứ không phải là bảo vệ bạn khỏi những sai trường hợp.

Vì vậy, nếu Enumkhông được xử lý "đặc biệt" trong Java, bạn có thể (như đã lưu ý trong các nhận xét) tạo các loại sau:

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

Secondsẽ thực hiện Comparable<First>chứ không phải Comparable<Second>... nhưng Firstbản thân nó sẽ ổn thôi.


1
@artsrc: Tôi không thể nhớ chính xác lý do tại sao nó cần phải chung chung trong cả trình tạo và thông báo. Tôi khá chắc chắn rằng tôi sẽ không đi theo con đường đó nếu tôi không cần đến nó :)
Jon Skeet

1
@SayemAhmed: Vâng, nó không ngăn cản khía cạnh trộn lên các loại. Tôi sẽ thêm một lưu ý về điều này.
Jon Skeet

1
"Tôi đã sử dụng một cái gì đó tương tự trong cổng ProtocolBuffers C # của tôi." Nhưng điều đó khác nhau bởi vì các nhà xây dựng có các phương thức cá thể trả về kiểu tham số kiểu. Enumkhông có bất kỳ phương thức thể hiện nào trả về kiểu tham số kiểu.
newacct

1
@JonSkeet: Cho rằng các lớp enum luôn được tự động tạo, tôi khẳng định class Enum<E>là đủ trong mọi trường hợp. Và trong Generics, bạn chỉ nên sử dụng một ràng buộc hạn chế hơn nếu thực sự cần thiết để đảm bảo an toàn loại.
newacct

1
@JonSkeet: Ngoài ra, nếu Enumcác lớp con được không phải lúc nào autogenerated, lý do duy nhất bạn sẽ cần class Enum<E extends Enum<?>>hơn class Enum<E>là khả năng truy cập ordinalcho compareTo(). Tuy nhiên, nếu bạn nghĩ về nó, điều đó không có nghĩa gì từ quan điểm ngôn ngữ để cho phép bạn so sánh hai loại enum khác nhau thông qua các quy tắc của chúng. Do đó, việc thực hiện Enum.compareTo()sử dụng đó ordinalchỉ có ý nghĩa trong bối cảnh các Enumlớp con được tự động tạo. Nếu bạn có thể phân lớp thủ công Enum, compareTocó lẽ sẽ phải như vậy abstract.
newacct

27

Sau đây là phiên bản sửa đổi của lời giải thích từ cuốn sách Java Generics and Collection : Chúng tôi có một Enumtuyên bố

enum Season { WINTER, SPRING, SUMMER, FALL }

sẽ được mở rộng thành một lớp

final class Season extends ...

trong đó ...là lớp cơ sở được tham số hóa nào đó cho Enums. Chúng ta hãy tìm ra những gì phải có. Vâng, một trong những yêu cầu Seasonlà nó nên thực hiện Comparable<Season>. Vì vậy, chúng tôi sẽ cần

Season extends ... implements Comparable<Season>

Những gì bạn có thể sử dụng cho ...điều đó sẽ cho phép điều này làm việc? Cho rằng nó phải là một tham số hóa Enum, sự lựa chọn duy nhất là Enum<Season>, để bạn có thể có:

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

Vì vậy, Enumđược tham số hóa trên các loại như Season. Tóm tắt từ Seasonvà bạn nhận được rằng tham số của Enumbất kỳ loại nào thỏa mãn

 E extends Enum<E>

Maurice Naftalin (đồng tác giả, Java Generics and Collection)


1
@newacct OK, tôi hiểu rồi: bạn muốn tất cả các enum là phiên bản của Enum <E>, phải không? .
Maurice Naftalin

1
@newacct Bạn không muốn nhấn mạnh rằng Seasonthực hiện Comparable<Season>?
Maurice Naftalin

2
@newacct Nhìn vào định nghĩa của Enum. Để so sánh một trường hợp với một trường hợp khác, nó phải so sánh các chức vụ của họ. Vì vậy, đối số của compareTophương thức phải được khai báo là một Enumkiểu con, hoặc trình biên dịch sẽ (chính xác) nói rằng nó không có một thứ tự.
Maurice Naftalin

2
@MauriceNaftalin: Nếu Java không cấm phân lớp thủ công Enum, thì có thể có class OneEnum extends Enum<AnotherEnum>{}, ngay cả với cách Enumkhai báo ngay bây giờ. Sẽ không có ý nghĩa gì khi có thể so sánh một loại enum với loại khác, vì vậy dù sao thì EnumcompareTocũng sẽ không có ý nghĩa như tuyên bố. Các giới hạn không cung cấp bất kỳ trợ giúp cho điều này.
newacct

2
@MauriceNaftalin: Nếu thứ tự là lý do, thì public class Enum<E extends Enum<?>>cũng sẽ đủ.
newacct

6

Điều này có thể được minh họa bằng một ví dụ đơn giản và một kỹ thuật có thể được sử dụng để thực hiện các cuộc gọi phương thức chuỗi cho các lớp con. Trong một ví dụ dưới đây setNametrả về một Nodechuỗi như vậy sẽ không hoạt động cho 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
}

Vì vậy, chúng ta có thể tham chiếu một lớp con trong một khai báo chung, để Citybây giờ trả về đúng loại:

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

Giải pháp của bạn có một nhóm không được kiểm tra: return (CHILD) this;Xem xét thêm phương thức get This (): protected CHILD getThis() { return this; } Xem: angelikalanger.com/GenericsFAQ/FAQSections/ Kẻ
Roland

@Roland cảm ơn vì một liên kết, tôi đã mượn một ý tưởng từ nó. Bạn có thể giải thích cho tôi hoặc trực tiếp đến một bài viết giải thích tại sao đây là một thực hành xấu trong trường hợp cụ thể này? Phương thức trong liên kết yêu cầu nhập nhiều hơn và đây là đối số chính tại sao tôi tránh điều này. Tôi chưa bao giờ thấy lỗi cast trong trường hợp này + Tôi biết rằng có một số lỗi cast không thể tránh khỏi - tức là khi một người lưu trữ các đối tượng có nhiều loại vào cùng một bộ sưu tập. Vì vậy, nếu các diễn viên không được kiểm tra không quan trọng và thiết kế hơi phức tạp ( Node<T>không phải vậy), tôi sẽ bỏ qua chúng để tiết kiệm thời gian.
Andrey Chaschev

Chỉnh sửa của bạn không khác so với trước ngoài việc thêm một số đường cú pháp, hãy xem xét rằng đoạn mã sau sẽ thực sự biên dịch nhưng lại gây ra lỗi thời gian chạy: `Node <City> node = new Node <City> () .setName (" node "). setSapes (1); `Nếu bạn nhìn vào mã byte java, bạn sẽ thấy rằng do xóa kiểu, câu lệnh return (SELF) this;được biên dịch thành return this;, vì vậy bạn có thể bỏ nó đi.
Roland

@Roland cảm ơn, đây là những gì tôi cần - sẽ cập nhật ví dụ khi tôi rảnh.
Andrey Chaschev

Liên kết sau đây cũng tốt: angelikalanger.com/GenericsFAQ/FAQSections/ Khăn
Roland

3

Bạn không phải là người duy nhất tự hỏi điều đó có nghĩa là gì; xem blog Java hỗn loạn .

Nếu một lớp mở rộng lớp này, nó sẽ vượt qua một tham số E. Giới hạn của tham số E dành cho một lớp mở rộng lớp này với cùng tham số E.


1

Bài đăng này đã hoàn toàn làm rõ cho tôi những vấn đề về 'loại chung đệ quy'. Tôi chỉ muốn thêm một trường hợp khác trong đó cấu trúc cụ thể này là cần thiết.

Giả sử bạn có các nút chung trong biểu đồ chung:

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

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

    public Collection<T> getNeighbor();
}

Sau đó, bạn có thể có đồ thị của các loại chuyên ngành:

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

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

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

Nó vẫn cho phép tôi tạo một class Foo extends Node<City>nơi mà Foo không liên quan đến Thành phố.
newacct

1
Chắc chắn, và đó là sai? Tôi không nghĩ vậy. Hợp đồng cơ sở do Node <City> cung cấp vẫn được vinh danh, chỉ có lớp con Foo của bạn ít hữu ích hơn kể từ khi bạn bắt đầu làm việc với Foos nhưng đưa Thành phố ra khỏi ADT. Có thể có một trường hợp sử dụng cho điều này, nhưng rất có thể đơn giản và hữu ích hơn để chỉ làm cho tham số chung giống như lớp con. Nhưng dù bằng cách nào, nhà thiết kế có sự lựa chọn đó.
mdma

@mdma: Tôi đồng ý. Vì vậy, những gì sử dụng không ràng buộc cung cấp, hơn chỉ class Node<T>?
newacct

1
@nozebacle: Ví dụ của bạn không chứng minh rằng "cấu trúc cụ thể này là cần thiết". class Node<T>là hoàn toàn phù hợp với ví dụ của bạn.
newacct

1

Nếu bạn nhìn vào Enummã nguồn, nó có các mục sau:

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

Điều đầu tiên đầu tiên, E extends Enum<E>có nghĩa là gì? Điều đó có nghĩa là tham số loại là thứ gì đó kéo dài từ Enum và không được tham số hóa bằng loại thô (nó được tham số hóa bởi chính nó).

Điều này có liên quan nếu bạn có enum

public enum MyEnum {
    THING1,
    THING2;
}

mà, nếu tôi biết chính xác, được dịch thành

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

Vì vậy, điều này có nghĩa là MyEnum nhận được các phương thức sau:

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

Và thậm chí quan trọng hơn,

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

Điều này làm cho getDeclaringClass()diễn viên đến đúng Class<T>đối tượng.

Một ví dụ rõ ràng hơn là một ví dụ mà tôi đã trả lời cho câu hỏi này , nơi bạn không thể tránh cấu trúc này nếu bạn muốn chỉ định một ràng buộc chung.


Không có gì bạn thể hiện trong compareTohoặc getDeclaringClassyêu cầu extends Enum<E>ràng buộc.
newacct

0

Theo wikipedia, mẫu này được gọi là mẫu khuôn mẫu định kỳ . Về cơ bản, bằng cách sử dụng mẫu CRTP, chúng ta có thể dễ dàng tham khảo loại lớp con mà không cần truyền kiểu, có nghĩa là bằng cách sử dụng mẫu, chúng ta có thể bắt chước hàm ảo.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.