Hoy leí un tweet que decía:
Es gracioso cuando los usuarios de Java se quejan del borrado de tipos, que es lo único que Java hizo bien, ignorando todas las cosas que hizo mal.
Entonces mi pregunta es:
¿Hay beneficios del borrado de tipos de Java? ¿Cuáles son los beneficios técnicos o de estilo de programación que (posiblemente) ofrece además de la preferencia de implementaciones de JVM por compatibilidad con versiones anteriores y rendimiento en tiempo de ejecución?
java
type-erasure
vertti
fuente
fuente
Respuestas:
Tipo de borrado es bueno
Vamos a ceñirnos a los hechos
Muchas de las respuestas hasta ahora están demasiado relacionadas con el usuario de Twitter. Es útil concentrarse en los mensajes y no en el mensajero. Hay un mensaje bastante consistente incluso con solo los extractos mencionados hasta ahora:
Un objetivo: programas razonables
Estos tweets reflejan una perspectiva que no está interesada en si podemos hacer que la máquina haga algo , sino más bien en si podemos razonar que la máquina hará algo que realmente queremos. El buen razonamiento es una prueba. Las pruebas se pueden especificar en notación formal o algo menos formal. Independientemente del lenguaje de especificación, deben ser claros y rigurosos. Las especificaciones informales no son imposibles de estructurar correctamente, pero a menudo tienen fallas en la programación práctica. Terminamos con soluciones como pruebas automatizadas y exploratorias para compensar los problemas que tenemos con el razonamiento informal. Esto no quiere decir que las pruebas sean intrínsecamente una mala idea, pero el usuario de Twitter citado está sugiriendo que hay una manera mucho mejor.
Entonces, nuestro objetivo es tener programas correctos sobre los que podamos razonar de manera clara y rigurosa de una manera que se corresponda con la forma en que la máquina ejecutará el programa. Este, sin embargo, no es el único objetivo. También queremos que nuestra lógica tenga cierto grado de expresividad. Por ejemplo, hay mucho que podemos expresar con lógica proposicional. Es bueno tener una cuantificación universal (∀) y existencial (∃) a partir de algo como la lógica de primer orden.
Usar sistemas de tipos para razonar
Estos objetivos se pueden abordar muy bien mediante sistemas de tipos. Esto es especialmente claro debido a la correspondencia Curry-Howard . Esta correspondencia se expresa a menudo con la siguiente analogía: los tipos son para los programas como los teoremas para las demostraciones.
Esta correspondencia es algo profunda. Podemos tomar expresiones lógicas y traducirlas a través de la correspondencia a tipos. Entonces, si tenemos un programa con el mismo tipo de firma que compila, hemos probado que la expresión lógica es universalmente verdadera (una tautología). Esto se debe a que la correspondencia es bidireccional. La transformación entre el tipo / programa y los mundos de teorema / prueba es mecánica y, en muchos casos, puede automatizarse.
Curry-Howard juega muy bien con lo que nos gustaría hacer con las especificaciones de un programa.
¿Son útiles los sistemas de tipos en Java?
Incluso con una comprensión de Curry-Howard, a algunas personas les resulta fácil descartar el valor de un sistema de tipos, cuando
Con respecto al primer punto, quizás los IDE hagan que el sistema de tipos de Java sea lo suficientemente fácil de trabajar (eso es muy subjetivo).
Con respecto al segundo punto, Java pasa a corresponder casi a una lógica de primer orden. Los genéricos dan uso al sistema de tipos equivalente a la cuantificación universal. Desafortunadamente, los comodines solo nos dan una pequeña fracción de cuantificación existencial. Pero la cuantificación universal es un buen comienzo. Es bueno poder decir que las funciones
List<A>
funcionan universalmente para todas las listas posibles porque A no tiene restricciones. Esto lleva a lo que el usuario de Twitter está hablando con respecto a la "parametricidad".¡Un artículo que se cita con frecuencia sobre la parametricidad es Philip Wadler's Theorems gratis! . Lo interesante de este artículo es que solo con la firma de tipos, podemos probar algunas invariantes muy interesantes. Si tuviéramos que escribir pruebas automatizadas para estos invariantes, estaríamos perdiendo mucho el tiempo. Por ejemplo, para
List<A>
, desde la firma de tipo solo paraflatten
podemos razonar que
Ese es un ejemplo simple, y probablemente pueda razonar al respecto de manera informal, pero es aún mejor cuando obtenemos tales pruebas de forma formal y gratuita del sistema de tipos y el compilador las verifica.
No borrar puede dar lugar a abusos
Desde la perspectiva de la implementación del lenguaje, los genéricos de Java (que corresponden a tipos universales) influyen mucho en la parametricidad utilizada para obtener pruebas sobre lo que hacen nuestros programas. Esto llega al tercer problema mencionado. Todas estas ganancias de prueba y corrección requieren un sistema de tipo sólido implementado sin defectos. Java definitivamente tiene algunas características de lenguaje que nos permiten romper nuestro razonamiento. Estos incluyen pero no se limitan a:
Los genéricos no borrados están relacionados de muchas formas con la reflexión. Sin borrado, hay información de tiempo de ejecución que se transporta con la implementación que podemos usar para diseñar nuestros algoritmos. Lo que esto significa es que, estadísticamente, cuando razonamos sobre los programas, no tenemos la imagen completa. La reflexión amenaza gravemente la corrección de las pruebas sobre las que razonamos estáticamente. No es una coincidencia que la reflexión también conduzca a una variedad de defectos delicados.
Entonces, ¿de qué maneras los genéricos no borrados podrían ser "útiles"? Consideremos el uso mencionado en el tweet:
¿Qué sucede si T no tiene un constructor sin argumentos? En algunos idiomas, lo que obtienes es nulo. O tal vez omita el valor nulo y vaya directamente a generar una excepción (a la que los valores nulos parecen conducir de todos modos). Debido a que nuestro lenguaje es Turing completo, es imposible razonar sobre qué llamadas a
broken
involucrarán tipos "seguros" con constructores sin argumentos y cuáles no. Hemos perdido la certeza de que nuestro programa funciona universalmente.Borrar significa que hemos razonado (así que borremos)
Entonces, si queremos razonar sobre nuestros programas, se recomienda encarecidamente que no empleemos características del lenguaje que amenacen fuertemente nuestro razonamiento. Una vez que hacemos eso, ¿por qué no descartar los tipos en tiempo de ejecución? No son necesarios. Podemos obtener algo de eficiencia y simplicidad con la satisfacción de que no fallarán los moldes o de que pueden faltar métodos en la invocación.
Borrar fomenta el razonamiento.
fuente
Los tipos son una construcción que se utiliza para escribir programas de una manera que permite al compilador verificar la corrección de un programa. Un tipo es una proposición sobre un valor: el compilador verifica que esta proposición sea verdadera.
Durante la ejecución de un programa, no debería haber necesidad de información de tipo; esto ya ha sido verificado por el compilador. El compilador debe tener la libertad de descartar esta información para realizar optimizaciones en el código: hacerlo funcionar más rápido, generar un binario más pequeño, etc. El borrado de los parámetros de tipo facilita esto.
Java rompe la escritura estática al permitir que se consulte la información de tipos en tiempo de ejecución (reflexión, instancia de, etc.). Esto le permite construir programas que no pueden verificarse estáticamente; omiten el sistema de tipos. También pierde oportunidades de optimización estática.
El hecho de que se borren los parámetros de tipo evita que se construyan algunas instancias de estos programas incorrectos; sin embargo, no se permitirían más programas incorrectos si se borrara más información de tipo y se eliminaran el reflejo y la instancia de las instalaciones.
El borrado es importante para mantener la propiedad de "parametricidad" de un tipo de datos. Digamos que tengo un tipo "Lista" parametrizado sobre el tipo de componente T. es decir, Lista <T>. Ese tipo es una proposición de que este tipo de Lista funciona de manera idéntica para cualquier tipo T. El hecho de que T sea un parámetro de tipo abstracto e ilimitado significa que no sabemos nada sobre este tipo, por lo tanto, no podemos hacer nada especial para casos especiales de T.
por ejemplo, digamos que tengo una Lista xs = asList ("3"). Agrego un elemento: xs.add ("q"). Termino con ["3", "q"]. Dado que esto es paramétrico, puedo asumir que List xs = asList (7); xs.add (8) termina con [7,8]. Sé por el tipo que no hace nada para String y nada para Int.
Además, sé que la función List.add no puede inventar valores de T de la nada. Sé que si mi asList ("3") tiene un "7" agregado, las únicas respuestas posibles se construirían a partir de los valores "3" y "7". No hay posibilidad de que se agregue un "2" o una "z" a la lista porque la función no podría construirla. Ninguno de estos otros valores sería sensato de agregar, y la parametricidad evita que se construyan estos programas incorrectos.
Básicamente, el borrado evita algunos medios de violar la parametricidad, eliminando así la posibilidad de programas incorrectos, que es el objetivo de la escritura estática.
fuente
(Aunque ya escribí una respuesta aquí, revisando esta pregunta dos años después, me doy cuenta de que hay otra forma completamente diferente de responderla, así que dejo la respuesta anterior intacta y agrego esta).
Es muy discutible si el proceso realizado en Java Generics merece el nombre de "borrado de tipo". Dado que los tipos genéricos no se borran sino que se reemplazan con sus contrapartes en bruto, una mejor opción parece ser la "mutilación de tipos".
La característica por excelencia del borrado de tipos en su sentido comúnmente entendido es forzar al tiempo de ejecución a permanecer dentro de los límites del sistema de tipos estáticos haciéndolo "ciego" a la estructura de los datos a los que accede. Esto le da toda la potencia al compilador y le permite probar teoremas basados únicamente en tipos estáticos. También ayuda al programador al restringir los grados de libertad del código, dando más poder al razonamiento simple.
El borrado de tipos de Java no logra eso; paraliza el compilador, como en este ejemplo:
(Las dos declaraciones anteriores colapsan en la misma firma de método después del borrado).
Por otro lado, el tiempo de ejecución aún puede inspeccionar el tipo de un objeto y razonar sobre él, pero dado que su conocimiento del tipo verdadero se ve afectado por el borrado, las violaciones de tipo estático son triviales de lograr y difíciles de prevenir.
Para hacer las cosas aún más complicadas, las firmas de tipo original y borrada coexisten y se consideran en paralelo durante la compilación. Esto se debe a que todo el proceso no se trata de eliminar la información de tipo del tiempo de ejecución, sino de convertir un sistema de tipo genérico en un sistema de tipo sin formato heredado para mantener la compatibilidad con versiones anteriores. Esta joya es un ejemplo clásico:
(Se
extends Object
tuvo que agregar el redundante para preservar la compatibilidad con versiones anteriores de la firma borrada).Ahora, con eso en mente, revisemos la cita:
¿Qué hizo exactamente Java? ¿Es la palabra en sí, independientemente del significado? Por contraste, eche un vistazo al
int
tipo humilde : nunca se realiza ninguna verificación de tipo en tiempo de ejecución, ni siquiera es posible, y la ejecución siempre es perfectamente segura para los tipos. Así es como se ve el tipo de borrado cuando se hace correctamente: ni siquiera sabes que está ahí.fuente
Lo único que no veo que se considere aquí en absoluto es que el polimorfismo en tiempo de ejecución de OOP depende fundamentalmente de la cosificación de tipos en tiempo de ejecución. Cuando un lenguaje cuya columna vertebral se mantiene en su lugar por tipos refinados introduce una extensión importante a su sistema de tipos y lo basa en el borrado de tipos, la disonancia cognitiva es el resultado inevitable. Esto es precisamente lo que le pasó a la comunidad Java; es por eso que el borrado de tipos ha atraído tanta controversia y, en última instancia, por qué hay planes para deshacerlo en una versión futura de Java . Encontrar algo gracioso en esa queja de los usuarios de Java delata un malentendido honesto del espíritu de Java o una broma de desprecio consciente.
La afirmación "el borrado es lo único que Java hizo bien" implica la afirmación de que "todos los lenguajes basados en el envío dinámico contra el tipo de ejecución del argumento de función son fundamentalmente defectuosos". Aunque ciertamente es un reclamo legítimo en sí mismo, y que incluso puede considerarse como una crítica válida de todos los lenguajes de programación orientada a objetos, incluido Java, no puede presentarse como un punto fundamental desde el cual evaluar y criticar características dentro del contexto de Java , donde el polimorfismo en tiempo de ejecución es axiomático.
En resumen, si bien se puede afirmar válidamente que "el borrado de tipos es el camino a seguir en el diseño del lenguaje", las posiciones que admiten el borrado de tipos dentro de Java están fuera de lugar simplemente porque es mucho, demasiado tarde para eso y ya lo había sido incluso en el momento histórico. cuando Oak fue adoptado por Sun y renombrado a Java.
En cuanto a si la mecanografía estática en sí es la dirección adecuada en el diseño de lenguajes de programación, esto encaja en un contexto filosófico mucho más amplio de lo que creemos que constituye la actividad de programación . Una escuela de pensamiento, que se deriva claramente de la tradición clásica de las matemáticas, ve los programas como instancias de un concepto matemático u otro (proposiciones, funciones, etc.), pero hay una clase de enfoques completamente diferente, que ven la programación como una forma de hablar con la máquina y explicar lo que queremos de ella. Desde este punto de vista, el programa es una entidad dinámica que crece orgánicamente, un dramático opuesto del edificio cuidadosamente construido de un programa de tipo estático.
Parecería natural considerar los lenguajes dinámicos como un paso en esa dirección: la coherencia del programa surge de abajo hacia arriba, sin restricciones a priori que lo impongan de manera descendente. Este paradigma puede verse como un paso hacia el modelado del proceso mediante el cual nosotros, los humanos, nos convertimos en lo que somos a través del desarrollo y el aprendizaje.
fuente
Una publicación posterior del mismo usuario en la misma conversación:
(Esto fue en respuesta a una declaración de otro usuario, a saber, que "parece que en algunas situaciones 'nueva T' sería mejor", la idea es que
new T()
es imposible debido al borrado de tipo (esto es discutible, incluso siT
estuviera disponible en tiempo de ejecución, podría ser una clase o interfaz abstracta, o podría serVoid
, o podría carecer de un constructor sin argumentos, o su constructor sin argumentos podría ser privado (por ejemplo, porque se supone que es una clase singleton), o su El constructor no-arg podría especificar una excepción marcada que el método genérico no detecta o especifica, pero esa era la premisa. Independientemente, es cierto que sin borrar al menos podría escribirT.class.newInstance()
, lo que maneja esos problemas.))Esta visión de que los tipos son isomórficos a las proposiciones sugiere que el usuario tiene experiencia en la teoría formal de tipos. Es muy probable que no le gusten los "tipos dinámicos" o los "tipos en tiempo de ejecución" y preferiría un Java sin abatimientos
instanceof
y reflexión, etc. (Piense en un lenguaje como Standard ML, que tiene un sistema de tipos muy rico (estático) y cuya semántica dinámica no depende de ningún tipo de información).Vale la pena teniendo en cuenta, por cierto, que el usuario es curricán: tiempo (s) lo más probable es sinceramente prefiere (estáticamente), lenguajes de tipo (s) que se no sinceramente tratando de persuadir a otros de ese punto de vista. Más bien, el propósito principal del tweet original era burlarse de los que no estaban de acuerdo, y después de que algunos de esos en desacuerdo intervinieran, el usuario publicó tweets de seguimiento como "la razón por la que Java tiene borrado de tipo es que Wadler y otros saben qué que están haciendo, a diferencia de los usuarios de java ". Desafortunadamente, esto hace que sea difícil averiguar qué está pensando en realidad; pero, afortunadamente, también probablemente signifique que no es muy importante hacerlo. Las personas con una profundidad real en sus puntos de vista generalmente no recurren a trolls que son tan libres de contenido.
fuente
Una cosa buena es que no era necesario cambiar la JVM cuando se introdujeron los genéricos. Java implementa genéricos solo a nivel de compilador.
fuente
La razón por la que el borrado de tipo es algo bueno es que las cosas que hace imposibles son perjudiciales. Evitar la inspección de argumentos de tipo en tiempo de ejecución facilita la comprensión y el razonamiento sobre los programas.
Una observación que encontré algo contraria a la intuición es que cuando las firmas de funciones son más genéricas, se vuelven más fáciles de entender. Esto se debe a que se reduce el número de posibles implementaciones. Considere un método con esta firma, que de alguna manera sabemos que no tiene efectos secundarios:
¿Cuáles son las posibles implementaciones de esta función? Muchisimo. Puede decir muy poco sobre lo que hace esta función. Podría invertir la lista de entrada. Podría ser emparejar entradas, sumarlas y devolver una lista de la mitad del tamaño. Hay muchas otras posibilidades que se podrían imaginar. Ahora considere:
¿Cuántas implementaciones de esta función hay? Dado que la implementación no puede conocer el tipo de los elementos, ahora se puede excluir una gran cantidad de implementaciones: los elementos no se pueden combinar, agregar a la lista o filtrar, et al. Estamos limitados a cosas como: identidad (sin cambios en la lista), eliminar elementos o invertir la lista. Es más fácil razonar sobre esta función basándose solo en su firma.
Excepto… en Java siempre puedes engañar al sistema de tipos. Debido a que la implementación de ese método genérico puede usar cosas como
instanceof
comprobaciones y / o conversiones de tipos arbitrarios, nuestro razonamiento basado en la firma de tipo puede volverse inútil fácilmente. La función podría inspeccionar el tipo de elementos y hacer cualquier cantidad de cosas en función del resultado. Si se permiten estos trucos en tiempo de ejecución, las firmas de métodos parametrizados se vuelven mucho menos útiles para nosotros.Si Java no tuviera borrado de tipo (es decir, los argumentos de tipo se cosificaron en tiempo de ejecución), esto simplemente permitiría más travesuras de este tipo que perjudican el razonamiento. En el ejemplo anterior, la implementación solo puede violar las expectativas establecidas por la firma de tipo si la lista tiene al menos un elemento; pero si
T
estuviera cosificado, podría hacerlo incluso si la lista estuviera vacía. Los tipos reificados solo aumentarían las (ya muchas) posibilidades de impedir nuestra comprensión del código.El borrado de tipo hace que el lenguaje sea menos "poderoso". Pero algunas formas de "poder" son realmente dañinas.
fuente
instanceof
impiden nuestra capacidad de razonar sobre lo que hace el código en función de los tipos. Si Java reificara los argumentos de tipo, este problema empeoraría. Borrar tipos en tiempo de ejecución tiene el efecto de hacer que el sistema de tipos sea más útil.Esta no es una respuesta directa (OP preguntó "cuáles son los beneficios", yo respondo "cuáles son los contras")
En comparación con el sistema de tipo C #, el borrado de tipo Java es una verdadera molestia por dos razones
No puedes implementar una interfaz dos veces
En C # puede implementar ambos
IEnumerable<T1>
y deIEnumerable<T2>
forma segura, especialmente si los dos tipos no comparten un ancestro común (es decir, su ancestro sí lo esObject
).Ejemplo práctico: en Spring Framework, no puede implementar
ApplicationListener<? extends ApplicationEvent>
varias veces. Si necesita diferentes comportamientos en función de loT
que necesita para probarinstanceof
No puedes hacer una nueva T ()
(y necesitas una referencia a Class para hacer eso)
Como comentaron otros, hacer el equivalente de
new T()
solo se puede hacer a través de la reflexión, solo invocando una instancia deClass<T>
, asegurándose de los parámetros requeridos por el constructor. C # le permite hacerlonew T()
solo si se limitaT
a un constructor sin parámetros. SiT
no respeta esa restricción, se genera un error de compilación .En Java, a menudo se verá obligado a escribir métodos que se parecen a los siguientes
Los inconvenientes del código anterior son:
ReflectiveOperationException
se lanza en tiempo de ejecución.Si yo fuera el autor de C #, habría introducido la capacidad de especificar una o más restricciones de constructor que son fáciles de verificar en tiempo de compilación (por lo que puedo requerir, por ejemplo, un constructor con
string,string
parámetros). Pero el último es especulaciónfuente
Un punto adicional que ninguna de las otras respuestas parece haber considerado: si realmente necesita genéricos con escritura en tiempo de ejecución , puede implementarlo usted mismo de esta manera:
Luego, esta clase puede hacer todas las cosas que se podrían lograr de forma predeterminada si Java no usara el borrado: puede asignar nuevos
T
s (suponiendo queT
tenga un constructor que coincida con el patrón que espera usar), o matrices deT
s, puede probar dinámicamente en tiempo de ejecución si un objeto en particular es unT
y cambiar el comportamiento dependiendo de eso, y así sucesivamente.Por ejemplo:
fuente
evita el exceso de código similar a C ++ porque el mismo código se usa para varios tipos; sin embargo, el borrado de tipos requiere envío virtual, mientras que el enfoque c ++ - code-bloat puede hacer genéricos no enviados virtualmente
fuente
La mayoría de las respuestas están más preocupadas por la filosofía de programación que por los detalles técnicos reales.
Y aunque esta pregunta tiene más de 5 años, la pregunta aún persiste: ¿Por qué es deseable el borrado de tipo desde un punto de vista técnico? Al final, la respuesta es bastante simple (en un nivel superior): https://en.wikipedia.org/wiki/Type_erasure
Las plantillas de C ++ no existen en tiempo de ejecución. El compilador emite una versión totalmente optimizada para cada invocación, lo que significa que la ejecución no depende de la información del tipo. Pero, ¿cómo trata un JIT con diferentes versiones de la misma función? ¿No sería mejor tener solo una función? No quisiera que el JIT tuviera que optimizar todas las diferentes versiones del mismo. Bueno, pero ¿qué pasa con la seguridad de tipos? Supongo que tiene que salir por la ventana.
Pero espere un segundo: ¿Cómo lo hace .NET? ¡Reflexión! De esta manera, solo tienen que optimizar una función y también obtener información del tipo de tiempo de ejecución. Y es por eso que los genéricos .NET solían ser más lentos (aunque han mejorado mucho). ¡No estoy argumentando que eso no sea conveniente! Pero es costoso y no debe usarse cuando no es absolutamente necesario (no se considera costoso en lenguajes tipados dinámicamente porque el compilador / intérprete se basa en la reflexión de todos modos).
De esta manera, la programación genérica con borrado de tipo es casi cero sobrecarga (aún se requieren algunas verificaciones / conversiones en tiempo de ejecución): https://docs.oracle.com/javase/tutorial/java/generics/erasure.html
fuente