son - tipo comodin java
¿Cuáles son los beneficios del borrado de tipos de Java? (10)
Tipo Erasure es bueno
Mantengamos los hechos
Muchas de las respuestas hasta ahora están demasiado preocupadas por el usuario de Twitter. Es útil mantenerse enfocado en los mensajes y no en el mensajero. Hay un mensaje bastante consistente incluso con solo los extractos mencionados hasta ahora:
Es curioso cuando los usuarios de Java se quejan de borrado de tipo, que es lo único que Java acierta, mientras ignoran todas las cosas que se equivocaron.
Obtengo grandes beneficios (por ejemplo, parametricidad) y ningún costo (el supuesto costo es un límite de imaginación).
nuevo T es un programa roto. Es isomorfo a la afirmación de que "todas las proposiciones son verdaderas". No soy grande en esto.
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 si podemos razonar que la máquina hará algo que realmente queremos. Un 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 remedios como pruebas automáticas 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 sugiere que hay una manera mucho mejor.
Entonces, nuestro objetivo es tener programas correctos con los que podamos razonar clara y rigurosamente de manera que correspondan con la forma en que la máquina realmente ejecutará el programa. Esto, sin embargo, no es el único objetivo. También queremos que nuestra lógica tenga un grado de expresividad. Por ejemplo, hay mucho que podemos expresar con lógica proposicional. Es bueno tener una cuantificación universal (∀) y existencial (from) a partir de algo así como la lógica de primer orden.
Usando sistemas de tipo para razonar
Estos objetivos pueden ser abordados muy bien por los sistemas de tipos. Esto es especialmente claro debido a la correspondencia de Curry-Howard . Esta correspondencia a menudo se expresa con la siguiente analogía: los tipos son para los programas como los teoremas son para las pruebas.
Esta correspondencia es algo profunda. Podemos tomar expresiones lógicas y traducirlas mediante la correspondencia a tipos. Entonces, si tenemos un programa con la misma firma de tipo que compila, hemos demostrado que la expresión lógica es universalmente cierta (una tautología). Esto se debe a que la correspondencia es bidireccional. La transformación entre los mundos de tipo / programa y teorema / prueba es mecánica, y en muchos casos puede ser automática.
Curry-Howard juega muy bien con lo que nos gustaría hacer con las especificaciones de un programa.
¿Los sistemas de tipos son útiles en Java?
Incluso con una comprensión de Curry-Howard, algunas personas les resulta fácil descartar el valor de un sistema de tipo, cuando
- es extremadamente difícil trabajar con
- corresponde (a través de Curry-Howard) a una lógica con expresividad limitada
- está roto (lo que lleva a la caracterización de sistemas como "débil" o "fuerte").
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).
En cuanto al segundo punto, Java pasa a corresponder casi a una lógica de primer orden. Los genéricos utilizan el sistema de tipo 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 agradable poder decir que las funciones de 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 frecuentemente citado sobre la parametricidad es el Teorema de Philip Wadler gratis! . Lo interesante de este trabajo es que solo con la firma de tipo, podemos probar algunas invariantes muy interesantes. Si tuviéramos que escribir pruebas automatizadas para estas invariantes, estaríamos perdiendo nuestro tiempo. Por ejemplo, para List<A>
, desde la firma de tipo solo para flatten
<A> List<A> flatten(List<List<A>> nestedLists);
podemos razonar eso
flatten(nestedList.map(l -> l.map(any_function)))
≡ flatten(nestList).map(any_function)
Es un ejemplo simple, y probablemente puedas razonar sobre ello de manera informal, pero es aún más agradable cuando obtenemos formalmente esas pruebas de forma gratuita del sistema de tipo y las verifica el compilador.
No borrar puede conducir a abusos
Desde la perspectiva de la implementación del lenguaje, los genéricos de Java (que corresponden a los tipos universales) juegan un papel muy importante 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 de sonido implementado sin defectos. Java definitivamente tiene algunas características de lenguaje que nos permiten romper nuestro razonamiento. Estos incluyen pero no están limitados a:
- efectos secundarios con un sistema externo
- reflexión
Los genéricos no borrados están relacionados de muchas maneras con la reflexión. Sin borrado, hay información de tiempo de ejecución que se lleva con la implementación que podemos usar para diseñar nuestros algoritmos. Lo que esto significa es que estáticamente, cuando razonamos sobre los programas, no tenemos la imagen completa. La reflexión amenaza seriamente la corrección de cualquier prueba que razonamos sobre estáticamente. No es coincidencia que la reflexión también conduzca a una variedad de defectos complicados.
Entonces, ¿cuáles son las formas en que los genéricos no borrados pueden ser "útiles"? Consideremos el uso mencionado en el tweet:
<T> T broken { return new T(); }
¿Qué sucede si T no tiene un constructor no-arg? En algunos idiomas, lo que obtienes es nulo. O tal vez omita el valor nulo y vaya directamente a generar una excepción (que los valores nulos parecen conducir de todos modos). Debido a que nuestro lenguaje está completamente terminado, es imposible razonar sobre qué llamadas a broken
involucrarán tipos "seguros" con constructores sin argumentos y cuáles no. Perdimos la certeza de que nuestro programa funciona universalmente.
Borrar significa que hemos razonado (así que borremos)
Por lo tanto, si queremos razonar acerca de nuestros programas, se recomienda encarecidamente que no empleen funciones de lenguaje que amenacen nuestro razonamiento. Una vez que hacemos eso, ¿por qué no simplemente descartar los tipos en tiempo de ejecución? No son necesarios. Podemos obtener cierta eficiencia y simplicidad con la satisfacción de que ningún lanzamiento fallará o que faltarán métodos en la invocación.
Borrar borra el razonamiento.
Leí un tweet hoy que decía:
Es curioso cuando los usuarios de Java se quejan de borrado de tipo, que es lo único que Java acierta, mientras ignoran todas las cosas que se equivocaron.
Por lo tanto, mi pregunta es:
¿Hay beneficios del borrado de tipos de Java? ¿Cuáles son los beneficios de estilo técnico o de programación que (posiblemente) ofrece, aparte de las implementaciones de JVM, preferencia por compatibilidad con versiones anteriores y rendimiento en tiempo de ejecución?
Esta no es una respuesta directa (OP preguntó "cuáles son los beneficios", estoy respondiendo "cuáles son los inconvenientes")
Comparado con el sistema de tipo C #, el borrado de tipo Java es un verdadero dolor para dos raesons
No puedes implementar una interfaz dos veces
En C #, puede implementar tanto IEnumerable<T1>
como IEnumerable<T2>
forma segura, especialmente si los dos tipos no comparten un ancestro común (es decir, su ancestro es Object
).
Ejemplo práctico: en Spring Framework, no puede implementar ApplicationListener<? extends ApplicationEvent>
ApplicationListener<? extends ApplicationEvent>
varias veces. Si necesita diferentes comportamientos basados en T
, debe probar instanceof
No puedes hacer T nuevo ()
Como otros comentaron, hacer un new T()
solo puede hacerse mediante reflexión, asegurándose de los parámetros requeridos por el constructor. C # le permite hacer new T()
solo si restringe T
a constructor sin parámetros. Si T
no respeta esa restricción, se genera un error de compilació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 exigir, por ejemplo, un constructor con string,string
paramétricas). Pero el último es especulación
La razón por la que el borrado de tipos es bueno es que las cosas que hace imposibles son perjudiciales. La prevención de la inspección de los argumentos de tipo en el tiempo de ejecución facilita la comprensión y el razonamiento sobre los programas.
Una observación que encuentro un tanto contra-intuitiva 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:
public List<Integer> XXX(final List<Integer> l);
¿Cuáles son las posibles implementaciones de esta función? Muchisimo. Puede decir muy poco sobre lo que hace esta función. Podría estar revertiendo la lista de entrada. Podría combinar cosas juntas, sumarlas y devolver una lista de la mitad del tamaño. Hay muchas otras posibilidades que podrían imaginarse. Ahora considera:
public <T> List<T> XXX(final List<T> l);
¿Cuántas implementaciones de esta función hay? Dado que la implementación no puede conocer el tipo de elementos, ahora se puede excluir una gran cantidad de implementaciones: los elementos no se pueden combinar, agregar a la lista o filtrar, etc. Estamos limitados a cosas como: identidad (sin cambios en la lista), elementos caídos o inversión de la lista. Esta función es más fácil de razonar solo en función de 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 elementos como verificaciones de instanceof
y / o conversiones en tipos arbitrarios, nuestro razonamiento basado en la firma de tipo puede volverse fácilmente inútil. 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 hacks de tiempo de ejecución, las firmas de métodos parametrizados se vuelven mucho menos útiles para nosotros.
Si Java no tiene borrado de tipo (es decir, los tipos de argumentos se reificaron en tiempo de ejecución), esto simplemente permitiría más engaños de este tipo. 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
se reificaba, podría hacerlo incluso si la lista estuviera vacía. Los tipos reificados solo aumentarían las (ya muchas) posibilidades para impedir nuestra comprensión del código.
La borradura de tipo hace que el idioma sea menos "poderoso". Pero algunas formas de "poder" son realmente dañinas.
Lo único que no veo considerado aquí es que el polimorfismo de tiempo de ejecución de OOP depende fundamentalmente de la reificación de tipos en tiempo de ejecución. Cuando un lenguaje cuya columna vertebral se mantiene en su lugar por los tipos devueltos 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 sucedió a la comunidad Java; es por eso que la borradura de tipo ha atraído tanta controversia y, en última instancia, por qué hay planes para deshacerla en una versión futura de Java . Encontrar algo gracioso en esa queja de los usuarios de Java delata una sincera incomprensión del espíritu de Java, o una broma deliberadamente desaprobadora.
El reclamo "borrado es lo único que Java tiene derecho" implica la afirmación de que "todos los lenguajes basados en el despacho dinámico contra el tipo de argumento de función en tiempo de ejecución son fundamentalmente defectuosos". Aunque ciertamente es un reclamo legítimo por sí mismo, e incluso puede considerarse una crítica válida de todos los lenguajes de OOP, incluido Java, no puede presentarse como un punto fundamental para evaluar y criticar características dentro del contexto de Java , donde el polimorfismo de tiempo de ejecución es axiomático.
En resumen, aunque se puede afirmar válidamente que "el borrado de tipos es el camino a seguir en el diseño del lenguaje", las posiciones que soportan el borrado de tipos dentro de Java están fuera de lugar simplemente porque es demasiado tarde para eso y lo fue incluso en el momento histórico cuando Oak fue adoptado por Sun y renombrado a Java.
En cuanto a si la tipificación estática en sí misma es la dirección correcta 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, claramente derivada 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 habla con la máquina y explica lo que queremos de ella. En esta visión, el programa es una entidad dinámica, de crecimiento orgánico, un opuesto dramático del edifico cuidadosamente erigido de un programa estáticamente estátizado.
Parecería natural considerar los lenguajes dinámicos como un paso en esa dirección: la consistencia del programa surge de abajo hacia arriba, sin restricciones a priori que lo impongan de arriba hacia abajo. Este paradigma se puede ver como un paso hacia el modelado del proceso por el cual nosotros, los humanos, nos convertimos en lo que somos a través del desarrollo y el aprendizaje.
Los tipos son una construcción utilizada 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 es 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, hacer que funcione más rápido, generar un binario más pequeño, etc. El borrado de los parámetros de tipo facilita esto.
Java interrumpe el tipado estático al permitir que se consulte la información de tipo en el tiempo de ejecución: reflexión, instancia de etc. Esto le permite construir programas que no se pueden verificar estáticamente, omiten el sistema de tipos. También pierde oportunidades para la optimización estática.
El hecho de que los parámetros de tipo se borren impide que se creen algunas instancias de estos programas incorrectos, sin embargo, se rechazarían más programas incorrectos si se borrase más información de tipo y se eliminaran la reflexión y la instancia de las instalaciones.
La eliminación es importante para mantener la propiedad de "parametricidad" de un tipo de datos. Digamos que tengo un tipo "List" parametrizado sobre el componente tipo T. ie List <T>. Ese tipo es una proposición de que este tipo de lista funciona de forma idéntica para cualquier tipo T. El hecho de que T sea un parámetro de tipo abstracto, ilimitado significa que no sabemos nada sobre este tipo, por lo que no se puede hacer nada especial para casos especiales de T.
por ejemplo, decir que tengo una lista xs = asList ("3"). Agrego un elemento: xs.add ("q"). Termino con ["3", "q"]. Como esto es paramétrico, puedo suponer que List xs = asList (7); xs.add (8) termina con [7,8] Sé por el tipo que no hace una cosa por String y una cosa por 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 con los valores "3" y "7". No hay posibilidad de agregar un "2" o "z" a la lista porque la función no podría construirlo. Ninguno de estos otros valores sería razonable agregar, y la parametricidad evita que se construyan estos programas incorrectos.
Básicamente, el borrado evita algunos medios de violar la parametricidad, eliminando así las posibilidades de programas incorrectos, que es el objetivo de la tipada estática.
Un punto adicional que ninguna de las otras respuestas parece haber considerado: si realmente necesita genéricos con mecanografía en tiempo de ejecución , puede implementarlo usted mismo así:
public class GenericClass<T>
{
private Class<T> targetClass;
public GenericClass(Class<T> targetClass)
{
this.targetClass = targetClass;
}
Esta clase es capaz de hacer todo lo que sería posible por defecto si Java no usara borrado: puede asignar nuevas T
s (suponiendo que T
tenga un constructor que coincida con el patrón que espera usar) o matrices de T
s , puede probar dinámicamente en tiempo de ejecución si un objeto en particular es una T
y cambiar el comportamiento según eso, y así sucesivamente.
Por ejemplo:
public T newT () {
try {
return targetClass.newInstance();
} catch(/* I forget which exceptions can be thrown here */) { ... }
}
private T value;
/** @throws ClassCastException if object is not a T */
public void setValueFromObject (Object object) {
value = targetClass.cast(object);
}
}
Una cosa buena es que no hubo necesidad de cambiar JVM cuando se introdujeron los genéricos. Java implementa genéricos solo a nivel de compilador.
Una publicación posterior del mismo usuario en la misma conversación:
nuevo T es un programa roto. Es isomorfo a la afirmación de que "todas las proposiciones son verdaderas". No soy grande en esto.
(Esto fue en respuesta a una afirmación de otro usuario, a saber, que "parece que en algunas situaciones la ''nueva T'' sería mejor", y la idea es que la new T()
es imposible debido al borrado de tipo. (Esto es discutible, incluso si T
estuviera disponible en tiempo de ejecución, podría ser una clase o interfaz abstracta, o podría ser Void
, o podría carecer de un constructor no-arg, o su constructor no-arg podría ser privado (por ejemplo, porque se supone que es un singleton class), o su 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 borrado al menos podría escribir T.class.newInstance()
, que maneja esos problemas.))
Este punto de vista, que los tipos son isomorfos a las proposiciones, sugiere que el usuario tiene antecedentes en la teoría de tipos formal. (S) Es muy probable que no le gusten los "tipos dinámicos" o los "tipos de tiempo de ejecución" y preferiría un Java sin downcasts y instanceof
y reflection, y así sucesivamente. (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 tener en cuenta, por cierto, que el usuario está trolling: mientras que (s) probablemente prefiere sinceramente (estáticamente) los idiomas escritos, (s) no está tratando sinceramente de convencer a otros de esa vista. Más bien, el objetivo principal del tuit original era burlarse de los que no estaban de acuerdo, y después de que algunos de los que estaban en desacuerdo intervinieron, el usuario publicó tweets de seguimiento tales como "la razón por la cual java tiene borrado tipo es que Wadler y otros saben qué lo están haciendo, a diferencia de los usuarios de Java ". Desafortunadamente, esto hace que sea difícil saber qué está pensando en realidad; pero, afortunadamente, también significa que no es muy importante hacerlo. Las personas con una profundidad real en sus puntos de vista generalmente no recurren a trolls que son bastante libres de contenido.
evita la saturación de código similar a c ++ porque el mismo código se usa para varios tipos; sin embargo, el borrado de tipo requiere despacho virtual, mientras que el enfoque c ++ - bloat de código puede hacer genéricos no distribuidos virtualmente
(Aunque ya escribí una respuesta aquí, revisando esta pregunta dos años más tarde, me doy cuenta de que hay otra forma completamente diferente de responderla, así que dejo intacta la respuesta anterior y agrego esta).
Es altamente discutible si el proceso realizado en Java Generics merece el nombre "borrado de tipo". Como los tipos genéricos no se borran sino que se reemplazan por sus contrapartes en bruto, una mejor opción parece ser "mutilación tipo".
La característica esencial del borrado de tipos en su sentido común es forzar al tiempo de ejecución a mantenerse dentro de los límites del sistema de tipo estático al "cegar" la estructura de los datos a los que accede. Esto le da plena potencia al compilador y le permite probar teoremas basados solo en tipos estáticos. También ayuda al programador restringiendo los grados de libertad del código, dando más poder al razonamiento simple.
El borrado de tipos de Java no lo consigue; daña el compilador, como en este ejemplo:
void doStuff(List<Integer> collection) {
}
void doStuff(List<String> collection) // ERROR: a method cannot have
// overloads which only differ in type parameters
(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 objeto y razonar al respecto, pero dado que su percepción del verdadero tipo está paralizada por el borrado, las infracciones de tipo estático son triviales y difíciles de evitar.
Para complicar aún más las cosas, las firmas de tipo original y borrado coexisten y se consideran en paralelo durante la compilación. Esto se debe a que todo el proceso no se trata de eliminar información de tipo del tiempo de ejecución, sino de utilizar 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:
public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll)
(El extends Object
redundante extends Object
debe agregarse para conservar la compatibilidad con versiones anteriores de la firma borrada).
Ahora, con eso en mente, volvamos a la cita:
Es gracioso cuando los usuarios de Java se quejan de borrado de tipo, que es lo único que Java tiene derecho
¿Qué hizo Java exactamente? ¿Es la palabra misma, independientemente de su significado? Para contrastar, observe el tipo int
humilde: nunca se realiza ninguna comprobación del tipo de tiempo de ejecución, o incluso es posible, y la ejecución es siempre perfectamente segura. Ese es el tipo de borrado cuando se hace bien: ni siquiera sabes que está allí.