operador objeto metodos manejo instanciar genericos genericas generica diamante datos con comparar clases arreglos java generics

metodos - objeto t en java



¿Por qué los métodos genéricos y los tipos genéricos tienen una sintaxis de introducción de tipos diferente? (7)

Creo que es porque puedes declarar que es un tipo de devolución:

<T> T doStuff(T t) { // Do stuff with T return t; }

Debe declarar el tipo antes de declarar el tipo de devolución, porque no puede usar algo que aún no está definido. Por ejemplo, no puede usar una variable x antes de declararla en algún lugar. Me gusta (cualquier idioma) seguir algunas reglas lógicas, es más fácil usarlo y, en algún punto, solo se sabe lo que se puede esperar de él. Este es el caso de Java, tiene algunas probabilidades, pero en general sigue algunas reglas. Y el que no se puede usar algo antes de declararlo es una regla muy fuerte en Java, y para mí es muy bueno, porque produce menos WTF cuando se trata de entender el código Java, por eso creo que esto es el razonamiento detrás de esto Pero no sé quién es exactamente responsable de esa decisión, una cita de wikipedia:

En 1998, Gilad Bracha, Martin Odersky, David Stoutamire y Philip Wadler crearon Generic Java, una extensión del lenguaje Java para admitir tipos genéricos. [3] Generic Java se incorporó a Java (2004, Java 5) con la incorporación de comodines.

Creo que deberíamos pedirle a alguien mencionado en la cita anterior que obtenga la respuesta definitiva, por qué es así.

No creo que tenga nada que ver con la compatibilidad con versiones anteriores de Java.

Mientras estudiaba genéricos, noté una diferencia en la sintaxis de introducción de tipos entre los métodos genéricos y los tipos genéricos (clase o interfaz) que me confundían.

La sintaxis para un método genérico es

<T> void doStuff(T t) { // Do stuff with T }

Los doctores dicen

La sintaxis de un método genérico incluye un parámetro de tipo, dentro de corchetes angulares, y aparece antes del tipo de retorno del método

La sintaxis para un tipo genérico es

class Stuff<T> { // Do stuff with T T t; }

Los doctores dicen

La sección de parámetros de tipo, delimitada por corchetes angulares (<>), sigue el nombre de la clase . Especifica los parámetros de tipo

Porque ni dice por qué debe venir antes o después.

Para ser coherentes entre sí, esperaba que la sintaxis del método sea
void doStuff<T>(T t) {} o la sintaxis de tipo (para la clase) para ser class <T>Stuff {} , pero obviamente no es el caso.

¿Por qué uno tiene que ser presentado antes, y el otro después?

He usado genéricos principalmente en la forma de List<String> y argumenté que la lista <String>List podría parecer extraña, pero ese es un argumento subjetivo, además de los métodos es así también. Puede llamar a doStuff así this.<String>doStuff("a string");

Buscando una explicación técnica, pensé que quizás <T> debía introducirse en un método antes de especificar el tipo de devolución porque T podría ser el tipo de devolución y el compilador tal vez no sea capaz de mirar hacia delante de esa manera, pero eso sonaba extraño porque los compiladores son inteligente.

Me imagino que hay una explicación para esto más allá de "los diseñadores de idiomas simplemente lo hicieron de esa manera", pero no pude encontrarlo.


Java Generics se introdujo con Java 1.5. La idea de las nuevas características del lenguaje es nunca romper las versiones anteriores. Debemos tener en cuenta que los genéricos son una característica de seguridad de tipo para el lenguaje / desarrollador. Con eso, se introdujeron dos tipos nuevos de tipos parameterized types y type variables .

Los valores y tipos de referencia de JLS 4.3 propone la siguiente sintaxis para TypeArgument y TypeVariable .

ReferenceType: ClassOrInterfaceType TypeVariable ArrayType

ClassOrInterfaceType: ClassType InterfaceType

ClassType: TypeDeclSpecifier TypeArgumentsopt

InterfaceType: TypeDeclSpecifier TypeArgumentsopt

TypeDeclSpecifier: TypeName
ClassOrInterfaceType. Identificador

TypeName: identificador TypeName. Identificador

TipoVariable: identificador

ArrayType: escriba []

Los ejemplos son como estos

Vector<String> Seq<Seq<A>> Seq<String>.Zipper<Integer> Collection<Integer> Pair<String,String>

y para tipos parametrizados

Vector<String> x = new Vector<String>(); Vector<Integer> y = new Vector<Integer>(); return x.getClass() == y.getClass();

Siempre que no se dé un límite, lo asumirá como un java.lang.Object y con el borrado de tipo será, por ejemplo, Vector<Object> por lo que es compatible con versiones anteriores de Java.

La sintaxis de los métodos genéricos donde la clase misma no es genérica tiene la siguiente sintaxis.

De JLs 8.4 Declaraciones de método

MétodoDeclaración: MethodHeader MethodBody

MethodHeader: MethodModifiersopt TypeParametersopt Result MethodDeclarator Throwsopt

MethodDeclarator: Identificador (FormalParameterListopt)

Un ejemplo se parece a esto

public class GenericMethod { public static <T> T aMethod(T anObject) { return anObject; } public static void main(String[] args) { String greeting = "Hi"; String reply = aMethod(greeting); } }

Lo que resulta con borrado de tipo a

public class GenericMethod { public static Object aMethod(Object anObject) { return anObject; } public static void main(String[] args) { String greeting = "Hi"; String reply = (String) aMethod(greeting); } }

Y una vez más es compatible con versiones anteriores de Java. Ver ambos documentos de propuesta para un razonamiento más profundo

Adición de genéricos al lenguaje de programación Java: Especificación del borrador del participante

Especialización de tipos genéricos de Java

Sobre la parte técnica. Los pasos para crear un programa Java es compilar el archivo .java . Uno haría eso con el comando javac para generar los archivos de clase. JavacParser analiza el archivo completo con la especificación anterior y genera el bytecode. Vea here el código fuente de JavacParser.

Tomemos el siguiente archivo Test.java

class Things{} class Stuff<T>{ T t; public <U extends Things> U doStuff(T t, U u){ return u; }; public <T> T doStuff(T t){ return t; }; }

Para mantenerlo compatible con versiones anteriores, la JVM no modificó sus atributos previos para los archivos de clase. Agregaron un nuevo atributo y lo llamaron Signature . Del papel propsal

Cuando se usa como un atributo de un método o campo, una firma proporciona el tipo completo (posiblemente genérico) de ese método o campo. Cuando se usa como un atributo de clase, una firma indica los parámetros de tipo de la clase, seguidos por su supertipo, seguido de todas sus interfaces. La sintaxis de tipo en firmas se extiende a tipos parametrizados y variables de tipo. También hay una nueva sintaxis de firma para los parámetros de tipo formales. Las extensiones de sintaxis para cadenas de firmas son las siguientes:

La JVM Spec 4.3.4 define la siguiente sintaxis

MethodTypeSignature: FormalTypeParametersopt (TypeSignature *) ReturnType ThrowsSignature *

ReturnType: TypeSignature VoidDescriptor

ThrowsSignature: ^ ClassTypeSignature ^ TypeVariableSignature

Test.class archivo Test.class con javap -v obtenemos lo siguiente:

class Stuff<T extends java.lang.Object> extends java.lang.Object minor version: 0 major version: 52 flags: ACC_SUPER Constant pool: #1 = Methodref #3.#20 // java/lang/Object."<init>":()V #2 = Class #21 // Stuff #3 = Class #22 // java/lang/Object #4 = Utf8 t #5 = Utf8 Ljava/lang/Object; #6 = Utf8 Signature #7 = Utf8 TT; #8 = Utf8 <init> #9 = Utf8 ()V #10 = Utf8 Code #11 = Utf8 LineNumberTable #12 = Utf8 doStuff #13 = Utf8 (Ljava/lang/Object;LThings;)LThings; #14 = Utf8 <U:LThings;>(TT;TU;)TU; #15 = Utf8 (Ljava/lang/Object;)Ljava/lang/Object; #16 = Utf8 <T:Ljava/lang/Object;>(TT;)TT; #17 = Utf8 <T:Ljava/lang/Object;>Ljava/lang/Object; #18 = Utf8 SourceFile #19 = Utf8 Test.java #20 = NameAndType #8:#9 // "<init>":()V #21 = Utf8 Stuff #22 = Utf8 java/lang/Object { T t; descriptor: Ljava/lang/Object; flags: Signature: #7 // TT; Stuff(); descriptor: ()V flags: Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 4: 0 public <U extends Things> U doStuff(T, U); descriptor: (Ljava/lang/Object;LThings;)LThings; flags: ACC_PUBLIC Code: stack=1, locals=3, args_size=3 0: aload_2 1: areturn LineNumberTable: line 8: 0 Signature: #14 // <U:LThings;>(TT;TU;)TU; public <T extends java.lang.Object> T doStuff(T); descriptor: (Ljava/lang/Object;)Ljava/lang/Object; flags: ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: aload_1 1: areturn LineNumberTable: line 11: 0 Signature: #16 // <T:Ljava/lang/Object;>(TT;)TT; } Signature: #17 // <T:Ljava/lang/Object;>Ljava/lang/Object; SourceFile: "Test.java"

El método

public <U extends Things> U doStuff(T t, U u){ return u; };

traduce a la Firma de para indicar que es un método genérico

Signature: #14 // <U:LThings;>(TT;TU;)TU;

Si utilizamos una clase no genérica para las versiones anteriores de Java 1.5, por ejemplo

public String doObjectStuff(Object t, String u){ return u; }

se traduciría a

public java.lang.String doObjectStuff(java.lang.Object, java.lang.String); descriptor: (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String; flags: ACC_PUBLIC Code: stack=1, locals=3, args_size=3 0: aload_2 1: areturn LineNumberTable: line 12: 0

La única diferencia entre ambos es que uno tiene el campo de atributo Signature que indica que, de hecho, es un método genérico, mientras que las otras versiones previas de Java 1.5 no lo tienen. Pero ambos tienen el mismo atributo de descriptor

Non-Generic method descriptor: (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String; Generic method descriptor: (Ljava/lang/Object;LThings;)LThings;

Lo que lo hace compatible con versiones anteriores. Entonces la respuesta sería como sugirió

"los diseñadores de idiomas lo hicieron de esa manera"

con la adición de

"Los diseñadores de idiomas lo hicieron de esa manera, para hacerlo compatible con versiones anteriores sin agregar mucho código "

EDITAR: Sobre el comentario de que debería ser fácil de manejar la sintaxis diferente Encontré un pasaje en el libro Java Generics and Collections de Philip Wadler, Maurice Naftalin

Los genéricos en Java se parecen a las plantillas en C ++. Solo hay dos cosas importantes a tener en cuenta sobre la relación entre los genéricos de Java y las plantillas de C ++: sintaxis y semántica. La sintaxis es deliberadamente similar y la semántica es deliberadamente diferente.
Sintácticamente, se eligieron corchetes porque son familiares para los usuarios de C ++, y porque los corchetes serían difíciles de analizar. Sin embargo, hay una diferencia en la sintaxis. En C ++, los parámetros anidados requieren espacios adicionales, por lo que puede ver cosas como esta: Lista <Lista> [...] etc.

Mira aquí


La razón es porque el tipo genérico y el tipo parametrizado se manejan de forma diferente durante la compilación. Uno se ve como parámetros de tipo Eliding y el otro es Eliding tipo argumentos durante el proceso de borrado.

Generics se agrega a Java en 2004 dentro de la versión oficial J2SE 5.0. EN una documentación de Oracle " Uso y programación de genéricos en J2SE 5.0 " declaró

Entre bastidores

Los genéricos son implementados por el compilador de Java como una conversión front-end llamada borrado, que es el proceso de traducir o reescribir el código que usa genéricos en código no genérico (es decir, mapea la nueva sintaxis a la especificación JVM actual). En otras palabras, esta conversión borra toda la información de tipo genérico; toda la información entre corchetes angulares se borra. Por ejemplo, LinkedList se convertirá en LinkedList. Los usos de otras variables de tipo se reemplazan por el límite superior de la variable de tipo (por ejemplo, Objeto), y cuando el código resultante no es del tipo correcto, se inserta una conversión al tipo apropiado.

La clave está en el proceso de Type Erasure . No se realizaron cambios en la JVM para admitir los genéricos, por lo que Java no recuerda la compilación pasada del tipo genérico.

En un llamado Cost of Erasure publicado por la Universidad de Nueva Orleans, se rompieron los pasos de Erasure para nosotros:

Los pasos realizados durante el borrado de tipo incluyen:

  • Parámetros de tipo Eliding: cuando el compilador encuentra la definición de un tipo o método genérico, elimina todas las apariciones de cada parámetro de tipo reemplazándolo por su límite más a la izquierda, o Object si no se especifica ningún límite.

  • Eliding tipos de argumentos: cuando el compilador encuentra un tipo parametrizado, una instanciación de un tipo genérico, elimina los argumentos de tipo. Por ejemplo, el tipo List<String> se traduce a List .

Para el método genérico, el compilador busca la definición de tipo genérico que está más a la izquierda . Y literalmente significa más hacia la izquierda y es por eso que los parámetros de tipo delimitado aparecen antes del tipo de retorno del método. Para la clase o interfaz genérica, el compilador busca el tipo parametrizado que, a diferencia del tipo genérico, no está más a la izquierda de la clase definición, pero en su lugar sigue el className. El compilador luego elimina los argumentos de tipo para que JVM pueda entenderlo.

Si selecciona la sección del Apéndice del documento Cost Of Erasure . Muestra muy bien cómo el compilador maneja la interfaz y los métodos genéricos.

Métodos de puente

Al compilar una clase o interfaz que extiende una clase parametrizada o implementa una interfaz parametrizada, el compilador puede necesitar crear un método sintético, llamado método de puente, como parte del proceso de borrado de tipo. Normalmente no necesita preocuparse por los métodos de puente, pero puede que le sorprenda si aparece en un seguimiento de pila.

Nota: Además, el compilador a veces puede necesitar insertar métodos de puente sintético. Bridge Methods forma parte del proceso de borrado de tipo. Los métodos Bridge son responsables de asegurarse de que las firmas de métodos coincidan después del borrado de tipos. Obtenga más información al respecto en Efectos de borrado de tipo y métodos de puente

Editar: Como OP señala que mi conclusión de "left most bound" significa literalmente que los medios más hacia la izquierda no son lo suficientemente sólidos. (OP dijo en su pregunta que no estaba interesado en el tipo de respuesta "Creo") así que hice un poco de excavación y encontré este GenericsFAQ . A partir del ejemplo, parece que el orden de los parámetros de tipo sí importa. es decir, <T extends Cloneable & Comparable<T>> vuelve Cloneable después de que se escribe enrase pero no Comparable

aquí hay otro ejemplo directamente de Oracle Erasure of Generic Type

En el siguiente ejemplo, la clase genérica de nodo utiliza un parámetro de tipo delimitado:

public class Node<T extends Comparable<T>> { ... }

El compilador de Java reemplaza el parámetro de tipo delimitado T con la primera clase vinculada, Comparable .

Creo que la forma más correcta desde el punto de vista técnico es decir tipo borrar reemplazar el tipo vinculado con la primera clase encuadernada (u Object si T no tiene límites) sucede que la primera clase encuadernada es la que está más a la izquierda debido a la sintaxis en Java.


La respuesta de hecho se encuentra en la especificación de GJ , que ya se ha vinculado, cita del documento, p.14:

La convención de pasar parámetros antes del nombre del método se hace necesaria al analizar restricciones: con la convención más convencional de "parámetros de tipo después del nombre del método", la expresión f (a<b,c>(d)) tendría dos análisis posibles.

Implementación a partir de comentarios:

f(a<b,c>(d)) puede analizarse como cualquiera de f(a < b, c > d) (dos booleanos de comparaciones pasadas a f) o f(a<B, C>(d)) ( llamada de a con los argumentos de tipo B y C y argumento de valor d pasado a f) . Creo que esta también podría ser la razón por la cual Scala eligió usar [] lugar de <> para genéricos.


Mi fuerte suposición es que es porque, como dijiste para un método, el parámetro genérico también puede ser el tipo de devolución de una función:

public <RETURN_TYPE> RETURN_TYPE getResult();

Entonces, en el momento en que el compilador alcanza el tipo de devolución de la función, su tipo ya se ha encontrado (como en, sabe que es un tipo genérico).

Si tuvieras una sintaxis como

public RETURN_TYPE getResult<RETURN_TYPE>();

requeriría un segundo barrido para analizar.

Para las clases, esto no es un problema, porque todas las referencias al tipo genérico aparecen dentro del bloque de definición de clase, es decir, después de que se haya declarado el tipo genérico.


No hay una razón teórica profunda para esto: este parece ser el caso de "los diseñadores de idiomas simplemente lo hicieron de esa manera". C #, por ejemplo, usa exactamente la sintaxis que se pregunta por qué Java no implementa. El siguiente código:

private T Test<T>(T abc) { throw new NotImplementedException(); }

compilará C # es lo suficientemente similar a Java que esto implicaría que no hay ninguna razón teórica por la que Java tampoco haya podido implementar lo mismo (especialmente dado que ambos lenguajes implementaron genéricos desde el principio en su desarrollo).

La ventaja de la sintaxis de Java tal como está ahora es que es marginalmente más fácil implementar un analizador LL (1) para los métodos que usan la sintaxis actual.


Por lo que sé, los genéricos de Java, cuando se introdujeron, se basaron en la idea de los genéricos de GJ (una extensión del lenguaje de programación de Java que admite tipos genéricos). Por lo tanto, la sintaxis fue tomada de GJ, ver Especificación GJ .

Esta es una respuesta formal a su pregunta, pero no una respuesta a su pregunta en el contexto de GJ. Pero está claro que no tiene nada que ver con la sintaxis de C ++ porque en la sección de parámetros de C ++ precede tanto class palabra clave de la class como el tipo de retorno del método.