values - Por qué en Java enum se declara como Enum<E se extiende Enum<E>>
java enum with values (2)
Posible duplicado:
definición de Java Enum
Una pregunta mejor formulada, que no se considera un duplicado:
¿Qué sería diferente en Java si la declaración Enum no tuviera la parte recursiva?
si los diseñadores del lenguaje usaran simplemente Enum <E extiende Enum> ¿cómo afectaría eso al lenguaje?
La única diferencia ahora sería que alguien podría escribir
A extends Enum<B>
pero dado que no está permitido en Java extender enumeraciones que seguirían siendo ilegales. También estaba pensando en alguien que le proporcione a jvm un bytecode que define smth como la extensión de una enumeración, pero los genéricos no pueden afectarlo a medida que se borran.
Entonces, ¿cuál es el objetivo de tal declaración?
¡Gracias!
Editar por simplicidad, veamos un ejemplo:
interface MyComparable<T> { int myCompare(T o); } class MyEnum<E extends MyEnum> implements MyComparable<E> { public int myCompare(E o) { return -1; } } class FirstEnum extends MyEnum<FirstEnum> {} class SecondEnum extends MyEnum<SecondEnum> {}
¿Qué pasa con esta estructura de clase? ¿Qué se puede hacer que "MyEnum <E extiende MyEnum <E >>" restringiría?
La única diferencia ahora sería que alguien podría escribir
A extends Enum<B>
pero dado que no está permitido en Java extender enumeraciones que seguirían siendo ilegales. También estaba pensando en alguien que le proporcione a jvm un bytecode que define smth como la extensión de una enumeración, pero los genéricos no pueden afectarlo a medida que se borran.
Tienes razón en que el lenguaje evitaría que alguien lo haga. Sin embargo, el beneficio de hacer que E extends Enum<E>
lugar de simplemente extends Enum
es que algunos de los métodos en Enum
ahora devolverán el tipo correcto y específico.
El cartel pidió algo que no funcionaría. Si Enum
se definió como Enum<E>
, entonces podrías hacer esto:
// Would be legal, because element types of `DaysOfWeek` and `Color` both
// extend `Enum<E>`, not `Enum<E extends Enum<E>>`.
DaysOfWeek d = Enum.valueOf(Color.class, "Purple");
Esta es una pregunta común, y comprensiblemente. Eche un vistazo a esta parte de las preguntas frecuentes sobre medicamentos genéricos para la respuesta (y de hecho, lea todo el documento con el que se sienta cómodo, está bastante bien hecho e informativo).
La respuesta corta es que obliga a la clase a parametrizarse en sí misma; esto es necesario para que las superclases definan métodos, usando el parámetro genérico, que funcionan de manera transparente ("nativamente", si se quiere) con sus subclases.
Edición: como ejemplo (no), por ejemplo, considere el método clone()
en Object
. Actualmente, está definido para devolver un valor de tipo Object
. Gracias a los tipos de retorno covariantes, las subclases específicas pueden (y a menudo lo hacen) definir que devuelven una clase más específica, pero esto no se puede aplicar y, por lo tanto, no se puede inferir para una clase arbitraria.
Ahora, si Object se definiera como Enum, es decir, Object<T extends Object<T>>
entonces tendría que definir todas las clases como algo así como public class MyFoo<MyFoo>
. En consecuencia, puede declararse que clone()
devuelve un tipo de T
y puede garantizar en tiempo de compilación que el valor devuelto sea siempre exactamente la misma clase que el objeto en sí (ni siquiera las subclases coincidirían con los parámetros).
Ahora, en este caso, Object no está parametrizado de esta manera porque sería extremadamente molesto tener este equipaje en todas las clases cuando el 99% de ellas no lo utilicen en absoluto. Pero para algunas jerarquías de clase puede ser muy útil. He usado una técnica similar antes con tipos de analizadores de expresiones abstractas y recursivas con varias implementaciones. Esta construcción hizo posible escribir código que era "obvio" sin tener que enviarlo a todas partes, o copiar y pegar solo para cambiar las definiciones de clase concretas.
Editar 2 (¡Para responder tu pregunta!):
Si Enum se definió como Enum<E extends Enum>
, entonces, como dices con razón, alguien podría definir una clase como A extends Enum<B>
. Esto derrota el punto de la construcción genérica, que es asegurar que el parámetro genérico sea siempre el tipo exacto de la clase en cuestión. Dando un ejemplo concreto, Enum declara su método compareTo como
public final int compareTo(E o)
En este caso, como usted definió A
para extender Enum<B>
, las instancias de A
solo podrían compararse con las instancias de B
(cualquiera que sea B), lo que no es muy útil. Con el constructo adicional, sabes que cualquier clase que amplíe Enum es comparable solo contra sí misma. Y, por lo tanto, puede proporcionar implementaciones de métodos en la superclase que siguen siendo útiles y específicas en todas las subclases.
(Sin este truco de genéricos recursivos, la única otra opción sería definir compareTo como public final int compareTo(Enum o)
. Esto no es realmente lo mismo, ya que entonces uno podría comparar un java.math.RoundingMode
contra un java.lang.Thread.State
sin la queja del compilador, que de nuevo no es muy útil.)
De acuerdo, Enum
de Enum
, ya que parece que nos estamos colgando. En cambio, aquí hay una clase abstracta:
public abstract class Manipulator<T extends Manipulator<T>>
{
/**
* This method actually does the work, whatever that is
*/
public abstract void manipulate(DomainObject o);
/**
* This creates a child that can be used for divide and conquer-y stuff
*/
public T createChild()
{
// Some really useful implementation here based on
// state contained in this class
}
}
Tendremos varias implementaciones concretas de esto: SaveToDataManipulator, SpellCheckingManipulator, lo que sea. Además, también queremos que las personas definan las suyas propias, ya que es una clase súper útil. ;-)
Ahora - notará que estamos usando la definición genérica recursiva y luego createChild
T
desde el método createChild
. Esto significa que:
1) Sabemos y el compilador sabe que si llamo:
SpellCheckingManipulator obj = ...; // We have a reference somehow
return obj.createChild();
entonces el valor devuelto es definitivamente un SpellCheckingManipulator
, aunque está usando la definición de la superclase. Los genéricos recursivos aquí permiten que el compilador sepa lo que es obvio para nosotros, por lo que no tiene que seguir emitiendo los valores de retorno (como generalmente tiene que ver con clone()
, por ejemplo).
2) Tenga en cuenta que no declare el método como definitivo, ya que tal vez algunas subclases específicas querrán anularlo con una versión más adecuada para ellos. La definición de genéricos significa que, independientemente de quién crea una nueva clase o cómo se define, aún podemos afirmar que el retorno de, por ejemplo, BrandNewSloppilyCodedManipulator.createChild()
seguirá siendo una instancia de BrandNewSloppilyCodedManipulator
. Si un desarrollador descuidado intenta definirlo para devolver solo el Manipulator
, el compilador no los dejará. Y si intentan definir la clase como BrandNewSloppilyCodedManipulator<SpellCheckingManipulator>
, tampoco los dejará.
Básicamente, la conclusión es que este truco es útil cuando se quiere proporcionar alguna funcionalidad en una superclase que de alguna manera se vuelve más específica en subclases. Al declarar la superclase de esta manera, está bloqueando el parámetro genérico para que cualquier subclase sea la subclase misma. Esta es la razón por la que puede escribir un compareTo
genérico compareTo
o createChild
en la superclase y evitar que se vuelva demasiado vago cuando se trata de subclases específicas.