java generics effective-java

java - Class.asSubclass firma



generics effective-java (3)

Creo que para entender esto, primero hay que entender las diferencias sutiles entre los tipos y las clases: los objetos tienen clases, las referencias tienen tipos .

Clases vs Tipos

Creo que un ejemplo es una buena manera de explicar lo que quiero decir aquí. Supongamos la existencia de la siguiente jerarquía de clases:

class Mammal {} class Feline extends Mammal {} class Lion extends Feline {} class Tiger extends Feline {}

Gracias al polimorfismo de subtipo pudimos declarar múltiples referencias al mismo objeto.

Tiger tiger = new Tiger(); //! Feline feline = tiger; Mammal mammal = feline;

Curiosamente, si les preguntáramos a cada una de estas referencias el nombre de su clase, todos responderían con la misma respuesta: "Tigre".

System.out.println(tiger.getClass().getName()); //yields Tiger System.out.println(feline.getClass().getName()); //yields Tiger System.out.println(mammal.getClass().getName()); //yield Tiger

Esto significa que la clase real del objeto es fija , su clase es la que usamos para crear una instancia cuando usamos el operador "nuevo" en nuestro código anterior.

Las referencias, por otro lado, pueden tener diferentes tipos , polimorfos compatibles con la clase real de objeto (es decir, un tigre en este caso).

Entonces, los objetos tienen una clase fija mientras que las referencias tienen tipos compatibles.

Esto tiende a ser confuso, ya que los nombres de clase son la misma cosa que usamos para nombrar los tipos de nuestras referencias, pero semánticamente hablando hay una diferencia sutil, como podemos ver arriba.

Quizás la parte más confusa es la comprensión de que las clases también son objetos y, por lo tanto, pueden tener referencias compatibles propias. Por ejemplo:

Class<Tiger> tigerClass = null; Class<? extends Tiger> someTiger = tigerClass; Class<? extends Feline> someFeline = tigerClass; Class<? extends Mammal> someMammal = tigerClass;

En mi código anterior, el objeto al que se hace referencia es un objeto de clase (que dejé nulo por el momento), y estas referencias que se usan aquí tienen diferentes tipos para alcanzar ese objeto hipotético.

Entonces, verán, la palabra "clase" aquí se usa para nombrar un "tipo de referencia" que apunta a un objeto de clase real cuyo tipo es compatible con cualquiera de estas referencias.

Sin embargo, en mi ejemplo anterior no pude definir dicho objeto de clase, ya que inicialicé la variable original a nula. Esto es intencional, y en un momento veremos por qué.

Acerca de invocar getClass y getSubclass en referencias

Siguiendo el ejemplo de @ Jatin que considera el método getClass , ahora, consideremos la siguiente pieza de código polimórfico:

Mammal animal = new Tiger();

Ahora sabemos que, independientemente del hecho de que nuestra referencia animal sea ​​de tipo Mammal , la clase real del objeto es, y siempre será, Tiger (es decir, Clase).

¿Qué deberíamos obtener si hacemos esto?

? type = mammal.getClass()

¿Debería type una Class<Mammal> o una Class<Tiger> ?

Bueno, el problema es que cuando el compilador ve una referencia de tipo Mammal , y no puede saber cuál es la clase real del objeto al que apunta esta referencia. Eso solo podría ser determinado en tiempo de ejecución, ¿verdad? De hecho, puede ser un mamífero, pero puede ser cualquiera de sus subclases, como un tigre, ¿verdad?

Entonces, cuando pedimos su clase, no obtenemos la Class<Mammal> porque el compilador no puede estar seguro. En su lugar obtenemos una Class<? extends Mammal> Class<? extends Mammal> , y esto tiene mucho más sentido, porque, después de todo, el compilador sabe que, en base a las reglas del polimorfismo de subtipo, la referencia dada puede estar apuntando a un mamífero o cualquiera de sus subtipos.

En este punto, probablemente pueda ver los sutiles matices de usar la clase de palabras aquí. Parecería que lo que realmente estamos obteniendo del método getClass() es algún tipo de referencia de tipo que usamos para señalar la clase real del objeto original como ya hemos explicado anteriormente.

Bueno, lo mismo podría decirse acerca del método asSubclass . Por ejemplo:

Mammal tiger = new Tiger(); Class<? extends Mammal> tigerAsMammal = tiger.getClass(); Class<? extends Feline> tigerAsFeline = tigerAsMammal.asSubclass(Feline.class);

Cuando invocamos asSubclass obtenemos una referencia al tipo real de clase al que apunta nuestra referencia, pero el compilador ya no puede estar seguro de cuál debería ser esa naturaleza real, y por lo tanto, ¿obtiene una referencia más laxer como Class<? extends Feline> Class<? extends Feline> . Esto es lo máximo que el compilador puede asumir sobre la naturaleza original del objeto, y es por eso que esto es todo lo que obtenemos.

¿Qué pasa con el nuevo Tiger (). GerClass ()?

Podríamos esperar que la única forma de obtener una Class<Tiger> (sin wirldcards) sea accediendo al objeto original, ¿verdad? Me gusta:

Class<Tiger> tigerClass = new Tiger().getClass()

Lo curioso, sin embargo, siempre alcanzamos el objeto tigre a través de un tipo de referencia, ¿no? En Java nunca tenemos acceso directo a los objetos. Dado que siempre estamos llegando a un objeto a través de sus referencias, no hay forma de que el compilador pueda hacer suposiciones sobre el tipo real de la referencia que se devuelve.

Es por eso que incluso este código produciría Class<? extend Tiger> Class<? extend Tiger>

Class<? extends Tiger> tigerClass = new Tiger().getClass();

Es decir, el compilador no ofrece garantías sobre lo que el new operador puede devolver aquí. Por todo lo que importa, puede devolver un objeto compatible con Tiger, pero no necesariamente uno cuya clase sea Tiger.

Esto se vuelve más claro si cambia el new operador por un método de fábrica.

TigerFactory factory = new TigerFactory(); Class<? extends Tiger> tigerClass = tigerFactory.newTiger().getClass();

Y nuestra fábrica de tigres:

class TigerFactory { public Tiger newTiger(){ return new Tiger(){ } //notice this is an anonymous class } }

Espero que esto contribuya de alguna manera a la discusión.

Mi pregunta es bastante teórica ... Esta es la firma de Class.asSubclass ( Javadoc ):

public <U> Class<? extends U> asSubclass(Class<U> clazz)

¿Por qué se utilizan los genéricos comodín en el tipo de retorno? Desde mi entendimiento de los genéricos, una mejor firma podría ser:

public <U> Class<U> asSubclass(Class<U> clazz)

porque puedes asegurarlo

Class<? extends U>

a un más simple

Class<U>

Bloch en su libro "Java efectiva" recomienda (página 137, artículo 28):

No utilice tipos de comodín como tipos de retorno. En lugar de proporcionar flexibilidad adicional a sus usuarios, los obligaría a usar tipos de comodines en el código del cliente.

¿Cuál es la razón detrás de esta elección? ¿Qué me estoy perdiendo? Muchas gracias por adelantado.

Edit: Como sugiere @egelev, podría haber formulado mi pregunta de otra manera ... de hecho, devolver el parámetro de entrada "tal como está" no tendría sentido. Entonces, el problema real es: ¿Cuál es la utilidad real del método Class.asSubclass, en comparación con una conversión normal? Ambos lanzarían una ClassCastException en caso de problemas de lanzamiento.

PUEDE haber sido agregado para evitar la conversión no verificada en una situación específica: cuando pasa el resultado del método asSubclass directamente a otro método que solicita un parámetro de tipo restringido, como aquí (tomado de Effective Java, página 146):

AnnotatedElement element; ... element.getAnnotation(annotationType.asSubclass(Annotation.class));

La firma del método anterior es:

<T extends Annotation> T getAnnotation(Class<T> annotationClass);

Me parece que el método asSubclass es solo una forma de realizar una conversión no verificada (¡de hecho!) Sin una advertencia adecuada del compilador ...

Y esto, al final, vuelve a proponer mi pregunta anterior: la firma.

public <U> Class<U> asSubclass(Class<U> clazz)

¡Sería igualmente efectivo (aunque extraño, lo admito)! Sería totalmente compatible con el ejemplo de getAnnotation, y no restringiría el código del cliente, lo que obligaría a usar genéricos de comodín sin sentido.

Edit2: Creo que mi pregunta general ha sido resuelta; muchas gracias. Si alguien tiene otros buenos ejemplos sobre la corrección de la firma asSubclass, agréguelos a la discusión, me gustaría ver un ejemplo completo utilizando asSubclass con mi firma y, evidentemente, no funciona.


En el caso del tipo de retorno Class<? extends U> Class<? extends U> . Vamos a intentar entender, la firma getClass :

AbstractList<String> ls = new ArrayList<>(); Class<? extends AbstractList> x = ls.getClass();

Ahora el compilador nos había permitido hacer:

Class<AbstractList> x = ls.getClass();

Esto habría estado mal. Porque en el tiempo de ejecución, ls.getClass sería ArrayList.class y no AbstractList.class . Ni ls.getClass puede devolver la Class<ArrayList> porque ls es de tipo AbstractList <> y no Arraylist

Así que el compilador, ahora dice - ¡Ok! No puedo devolver la Class<ArrayList> ni puedo devolver la Class<AbstractList> . Pero como sé que ls es un AbstractList, el objeto de clase real solo puede ser un subtipo de AbstractList. Así que Class<? extends AbstractList> Class<? extends AbstractList> es una apuesta muy segura. Por comodín: no puedes hacer:

AbstractList<String> ls = new ArrayList<>(); Class<? extends AbstractList> x = ls.getClass(); Class<ArrayList<?>> xx = x; Class<AbstractList<?>> xxx = x;

La misma lógica se aplica a su pregunta. Supongamos que fuera declarado como:

public <U> Class<U> asSubClass(Class<U> c)

Lo siguiente habría compilado:

List<String> ls = new ArrayList<>(); Class<? extends List> x = ls.getClass(); Class<AbstractList> aClass = x.asSubclass(AbstractList.class); //BIG ISSUE

Por encima de aClass en tiempo de ejecución está la Class<Arraylist> y no la Class<AbstractList> . ¡Así que esto no debe ser permitido! Class<? extends AbstractList> Class<? extends AbstractList> es la mejor apuesta.

El primer pensamiento que tuve al analizar la pregunta fue, ¿por qué no se declaró como:

public <U extends T> Class<? extends U> asSubClass(Class<U> c)

tiene más sentido tener una restricción de tiempo de compilación en los argumentos que puedo aprobar. Pero la razón por la que creo que no se prefería era que esto hubiera roto la compatibilidad con versiones anteriores del código pre-java5. Por ejemplo, un código como el siguiente, que compilado con pre-Java5 ya no se compilaría si se declarara asSubClass como se muestra arriba.

Class x = List.class.asSubclass(String.class); //pre java5 // this would have not compiled if asSubclass was declared above like

Comprobación rápida:

public static <T, U extends T> Class<? extends U> asSubClaz(Class<T> t, Class<U> c){ return t.asSubClass(c); } public static <T, U> Class<? extends U> asSubClazOriginal(Class<T> t, Class<U> c){ return t.asSubClass(c); } asSubClazOriginal(List.class, String.class); asSubClaz(List.class, String.class); //error. So would have broken legacy code

PS: Para la pregunta editada, ¿por qué asSubClass lugar de cast? - Porque el reparto es traición. Por ejemplo:

List<String> ls = new ArrayList<>(); Class<? extends List> x = ls.getClass(); Class<AbstractList> aClass = (Class<AbstractList>) x;

Lo anterior siempre tendría éxito porque los genéricos son borrados. Así que su clase echó a una clase. Pero aClass.equals(ArrayList.class) daría falso. Así que definitivamente el elenco está mal. En caso de que necesite seguridad de tipo, puede usar asSubClaz arriba


La idea detrás de este método es exactamente agregar este comodín. Por lo que entiendo, el propósito de este método es convertir this objeto de clase en un objeto de clase que represente cualquier subclase del parámetro clazz . Es por eso que el resultado es declarado como Class<? extends U> Class<? extends U> (la clase de algo que extiende U). De lo contrario, no tendría sentido, ya que su tipo de retorno será exactamente el mismo que el tipo de su único parámetro, es decir, no hará nada más que return clazz; . Hace una verificación de tipo de tiempo de ejecución y convierte el parámetro a la Class<? extends U> Class<? extends U> (por supuesto que lanzará la excepción ClassCastException si el objetivo de la invocación, es decir, this , no representa la clase de un derivado de U). Este es el mejor enfoque que el tipo simple (Class<? extends U>) de conversión, porque este último hace que el compilador produzca una advertencia.