El lenguaje de programación Java y la plataforma de software Java han sido criticados por decisiones de diseño que incluyen la implementación de genéricos, programación orientada a objetos forzada, el manejo de números sin signo, la implementación de aritmética de punto flotante y un historial de vulnerabilidades de seguridad en la implementación principal de Java VM, HotSpot . El software escrito en Java, especialmente sus primeras versiones, ha sido criticado por su rendimiento en comparación con el software escrito en otros lenguajes de programación. Los desarrolladores también han señalado que se deben tener en cuenta las diferencias en varias implementaciones de Java al escribir programas Java complejos que deben funcionar con todas ellas. [1]
Java introdujo excepciones controladas, en las que un método debe declarar las excepciones controladas que lanza en la firma del método. Esto puede generar un código repetitivo innecesariamente extenso . Ningún lenguaje importante ha seguido a Java en la implementación de excepciones controladas.
Cuando se añadieron los genéricos a Java 5.0, ya existía un amplio marco de clases (muchas de las cuales ya estaban en desuso ), por lo que se implementaron los genéricos mediante el borrado de tipos para permitir la compatibilidad con la migración y la reutilización de estas clases existentes. Esto limitó las características que se podían proporcionar, en comparación con otros lenguajes. [2] [3]
Debido a que los genéricos se implementan mediante borrado de tipo, el tipo real de un parámetro de plantilla E no está disponible en tiempo de ejecución. Por lo tanto, las siguientes operaciones no son posibles en Java: [4]
clase pública MiClase < E > { public static void miMétodo ( Objeto elemento ) { if ( elemento instanciade E ) { //Error del compilador ... } E elemento2 = new E (); //Error del compilador E [] iArray = new E [ 10 ] ; //Error del compilador } }
Además, en 2016, se encontró el siguiente ejemplo que revelaba que Java no era sólido y, a su vez, hacía que las JVM que arrojaban ClassCastExceptions o cualquier otro tipo de error de tiempo de ejecución no fueran técnicamente conformes. [5] Esto se corrigió en Java 10.
clase Nullless < T , U > { clase Constrain < B extiende U > {} final Constrain <? super T > constrain ; final U u ; Sin valores nulos ( T t ) { u = coerce ( t ); constrain = getConstrain (); } < B extiende U > U upcast ( Restringir < B > restringir , B b ) { devolver b ; } U coaccionar ( T t ) { devolver upcast ( restringir , t ); } Restringir <? super T > obtenerConstrain () { devolver restringir ; } public static void main ( String [] args ) { String cero = new Nullless < Integer , String > ( 0 ). u ; } }
Por diseño, Java alienta a los programadores a pensar en una solución en términos de sustantivos (clases) que interactúan entre sí, y a pensar en los verbos (métodos) como operaciones que pueden realizarse sobre o por ese sustantivo. [6] Steve Yegge sostiene que esto causa una restricción innecesaria en la expresividad del lenguaje porque una clase puede tener múltiples funciones que operan sobre ella, pero una función está ligada a una clase y nunca puede operar sobre múltiples tipos. [7]
Muchos otros lenguajes multiparadigma admiten funciones como una construcción de nivel superior. Cuando se combinan con otras características como la sobrecarga de funciones (un verbo, varios sustantivos) y funciones genéricas (un verbo, una familia de sustantivos con ciertas propiedades), el programador puede decidir si resolver un problema específico en términos de sustantivos o verbos. La versión 8 de Java introdujo algunas características de programación funcional.
Java carece de tipos nativos de enteros sin signo . Los datos sin signo se generan a menudo a partir de programas escritos en C , y la falta de estos tipos impide el intercambio directo de datos entre C y Java. Los números grandes sin signo también se utilizan en varios campos de procesamiento numérico, incluida la criptografía, lo que puede hacer que Java sea más incómodo de usar para estas tareas. [8] Aunque es posible evitar este problema utilizando código de conversión y tipos de datos más grandes, hace que el uso de Java sea engorroso para manejar datos sin signo. Si bien un entero con signo de 32 bits se puede utilizar para almacenar un valor sin signo de 16 bits sin pérdida, y un entero con signo de 64 bits un entero sin signo de 32 bits, no hay un tipo más grande para almacenar un entero sin signo de 64 bits. En todos los casos, la memoria consumida puede duplicarse y, por lo general, cualquier lógica que dependa del desbordamiento del complemento a dos debe reescribirse. Si se abstraen, las llamadas a funciones se vuelven necesarias para muchas operaciones que son nativas de algunos otros lenguajes. Como alternativa, es posible utilizar los enteros con signo de Java para emular enteros sin signo del mismo tamaño, pero esto requiere un conocimiento detallado de las operaciones bit a bit . [9] Se proporcionó cierto soporte para tipos de enteros sin signo en JDK 8, pero no para bytes sin signo y sin soporte en el lenguaje Java. [10]
Java ha sido criticado por no soportar operadores definidos por el usuario. [ cita requerida ] La sobrecarga de operadores mejora la legibilidad, [11] por lo que su ausencia puede hacer que el código Java sea menos legible, especialmente para clases que representan objetos matemáticos, como números complejos y matrices. Java solo tiene un uso no numérico de un operador: +
y +=
para la concatenación de cadenas. Sin embargo, esto lo implementa el compilador, que genera código para crear instancias de StringBuilder. Es imposible crear sobrecargas de operadores definidas por el usuario.
Java carece de tipos de valor compuestos, como las estructuras en C, paquetes de datos que se manipulan directamente en lugar de indirectamente a través de referencias. Los tipos de valor a veces pueden ser más rápidos y más pequeños que las clases con referencias. [12] [13] [14] Por ejemplo, Java HashMap
se implementa como una matriz de referencias a HashMap.Entry
objetos, [15] que a su vez contienen referencias a objetos clave y valor. Buscar algo requiere una doble desreferenciación ineficiente. Si Entry
fuera un tipo de valor, la matriz podría almacenar pares clave-valor directamente, eliminando la primera indirección, aumentando la localidad de referencia y reduciendo el uso de memoria y la fragmentación del montón . Además, si Java admitiera tipos primitivos genéricos, las claves y los valores podrían almacenarse en la matriz directamente, eliminando ambos niveles de indirección.
Se ha criticado a Java por no admitir matrices de 2 31 (aproximadamente 2.1 mil millones) o más elementos. [16] [17] Esta es una limitación del lenguaje; la Especificación del lenguaje Java , Sección 10.4, establece que:
Las matrices deben indexarse mediante valores int... Un intento de acceder a un componente de matriz con un valor de índice largo genera un error en tiempo de compilación. [18]
Para soportar matrices de gran tamaño también se necesitarían cambios en la JVM. [19] Esta limitación se manifiesta en áreas como las colecciones limitadas a 2 mil millones de elementos [20] y la incapacidad de mapear en memoria segmentos de archivos continuos mayores a 2 GB. [21] Java también carece (fuera de sus matrices 2D) de matrices multidimensionales (bloques individuales de memoria asignados de forma contigua a los que se accede mediante una única indirección), lo que limita el rendimiento para la computación científica y técnica. [13]
Las matrices y los primitivos son algo especiales y deben tratarse de forma diferente a las clases. Esto ha sido criticado [22] porque requiere muchas variantes de funciones al crear bibliotecas de propósito general.
Per Brinch Hansen argumentó en 1999 [23] que la implementación del paralelismo en Java en general, y los monitores en particular, no proporcionan las garantías y los mecanismos de cumplimiento necesarios para una programación paralela segura y confiable. Si bien un programador puede establecer convenciones de diseño y codificación , el compilador no puede intentar hacerlas cumplir, por lo que el programador puede escribir código inseguro o poco confiable sin darse cuenta.
Java proporciona un mecanismo para la serialización de objetos , donde un objeto puede representarse como una secuencia de bytes que incluye sus campos de datos, junto con información de tipo sobre sí mismo y sus campos. Después de que un objeto se serializa, puede deserializarse más tarde; es decir, la información de tipo y los bytes que representan sus datos se pueden utilizar para recrear el objeto en la memoria. [24] Esto plantea riesgos de seguridad teóricos y reales muy graves. [25] [26]
Aunque la aritmética de punto flotante de Java se basa en gran medida en el estándar IEEE 754 ( estándar para aritmética de punto flotante binario ), algunas características estándar obligatorias no son compatibles incluso cuando se utiliza el strictfp
modificador, como los indicadores de excepción y los redondeos dirigidos. Los tipos de precisión extendidos definidos por IEEE 754 (y compatibles con muchos procesadores) no son compatibles con Java. [27] [28] [29]
Java no admite tuplas de forma nativa , lo que da lugar a una proliferación de implementaciones de terceros que deben ser importadas y manejadas por el programador. [30]
En 2008, el Centro de Soporte de Tecnología de Software del Departamento de Defensa de los Estados Unidos publicó un artículo en el "Journal of Defense Software Engineering" en el que se analizaba la falta de idoneidad de Java como primer lenguaje de enseñanza. Las desventajas eran que los estudiantes "no tenían ninguna noción de la relación entre el programa fuente y lo que el hardware haría realmente" y la imposibilidad "de desarrollar un sentido del coste en tiempo de ejecución de lo que está escrito porque es extremadamente difícil saber qué ejecutará finalmente cualquier llamada de método". [31] En 2005, Joel Spolsky criticó a Java como una parte demasiado centrada en los planes de estudio de las universidades en su ensayo The Perils of JavaSchools . [32] Otros, como Ned Batchelder, no están de acuerdo con Spolsky por criticar las partes del lenguaje que le resultaban difíciles de entender, afirmando que el comentario de Spolsky era más bien un "discurso subjetivo". [33]
Antes de 2000, cuando se implementó HotSpot VM en Java 1.3, hubo muchas críticas sobre su rendimiento. Se ha demostrado que Java se ejecuta a una velocidad comparable con el código nativo optimizado, y las implementaciones de JVM modernas se evalúan regularmente como una de las plataformas de lenguaje más rápidas disponibles, por lo general no más de tres veces más lentas que C y C++. [34]
El rendimiento ha mejorado sustancialmente desde las primeras versiones. [35] Se ha demostrado que el rendimiento de los compiladores JIT en relación con los compiladores nativos es bastante similar en algunas pruebas optimizadas. [35] [36] [37]
El bytecode de Java puede ser interpretado en tiempo de ejecución por una máquina virtual o compilado en tiempo de carga o de ejecución para convertirlo en código nativo que se ejecuta directamente en el hardware de la computadora. La interpretación es más lenta que la ejecución nativa, pero la compilación en tiempo de carga o de ejecución tiene una penalización inicial en el rendimiento. Todas las implementaciones de JVM modernas utilizan el enfoque de compilación, por lo que después del tiempo de arranque inicial el rendimiento es similar al del código nativo.
En 2005, el diseñador y programador de juegos John Carmack llegó a la siguiente conclusión sobre Java en los teléfonos móviles : "El mayor problema es que Java es realmente lento. En un nivel puro de CPU/memoria/pantalla/comunicaciones, la mayoría de los teléfonos móviles modernos deberían ser plataformas de juego considerablemente mejores que una Game Boy Advance. Con Java, en la mayoría de los teléfonos te quedas con aproximadamente la potencia de CPU de una IBM PC original de 4,77 MHz (sic) , y un pésimo control sobre todo". [38]
La plataforma Java ofrece una arquitectura de seguridad [39] diseñada para permitir que el usuario ejecute código de bytes no confiable en un entorno protegido contra software malicioso o mal escrito. Esta función de "entorno protegido" tiene como objetivo proteger al usuario al restringir el acceso a las funciones y API de la plataforma que podrían ser explotadas por malware , como el acceso al sistema de archivos local o la red, o la ejecución de comandos arbitrarios.
En 2010, hubo un aumento significativo de software malicioso que atacaba las fallas de seguridad en los mecanismos de sandbox utilizados por las implementaciones de Java, incluida la de Oracle. Estas fallas permiten que un código no confiable eluda las restricciones del sandbox, exponiendo al usuario a ataques. Las fallas se solucionaron mediante actualizaciones de seguridad, pero aún se explotaban en máquinas sin las actualizaciones. [40]
Los críticos han sugerido que los usuarios no actualizan sus instalaciones de Java porque no saben que las tienen o cómo actualizarlas. Muchas organizaciones restringen la instalación de software por parte de los usuarios, pero son lentas a la hora de implementar las actualizaciones. [40] [41]
Oracle ha sido criticado por no proporcionar rápidamente actualizaciones para errores de seguridad conocidos. [42] Cuando Oracle finalmente lanzó un parche para fallas ampliamente explotadas en Java 7, eliminó Java 6 de las máquinas de los usuarios, a pesar de ser ampliamente utilizado por aplicaciones empresariales que Oracle había declarado que no estaban afectadas por las fallas. [43]
En 2007, un equipo de investigación dirigido por Marco Pistoia expuso otra falla importante del modelo de seguridad de Java, [44] basada en la inspección de pila . Cuando se accede a un recurso sensible a la seguridad, el administrador de seguridad activa un código que recorre la pila de llamadas, para verificar que la base de código de cada método en ella tiene autoridad para acceder al recurso. Esto se hace para evitar ataques de diputado confuso , que tienen lugar cada vez que un programa legítimo, más privilegiado, es engañado por otro para que haga un mal uso de su autoridad. El problema del diputado confuso es un tipo específico de escalada de privilegios . Pistoia observó que cuando se accede a un recurso sensible a la seguridad, el código responsable de adquirir el recurso puede ya no estar en la pila. Por ejemplo, un método ejecutado en el pasado puede haber modificado el valor de un campo de objeto que determina qué recurso utilizar. Esa llamada de método puede ya no estar en la pila cuando se inspecciona.
Algunos permisos son implícitamente equivalentes a los de Java AllPermission
. Entre ellos se incluyen el permiso para cambiar el administrador de seguridad actual (y reemplazarlo por uno que podría potencialmente eludir la inspección de la pila), el permiso para instanciar y utilizar un cargador de clases personalizado (que podría elegir asociarse AllPermission
a una clase maliciosa al cargarla) y el permiso para crear un permiso personalizado (que podría declararse tan poderoso como AllPermission
a través de su implies
método). Estos problemas están documentados en los dos libros de Pistoia sobre seguridad en Java. [45] [46]
Antes de Java 7, los instaladores no eliminaban las instalaciones antiguas de Java. Era común en un sistema Windows ver múltiples instalaciones de Java en la misma computadora. [47] [48] [49] Se permitían múltiples instalaciones y podían ser utilizadas por programas que dependían de versiones específicas, incluidos programas maliciosos. Este problema se solucionó en Java 7: con el permiso del usuario, el instalador elimina las instalaciones anteriores. [50]
La compilación JIT utiliza fundamentalmente datos ejecutables, por lo que plantea desafíos de seguridad y posibles vulnerabilidades de seguridad.
Hasta ahora, la promesa de "escribir una vez, ejecutar en todas partes" de Java no se ha hecho realidad. La mayor parte de una aplicación Java migrará entre la mayoría de las implementaciones de Java, pero aprovechar una característica específica de la máquina virtual causa problemas de portabilidad.
Las matrices multidimensionales rectangulares verdaderas son las estructuras de datos más importantes para la computación científica y de ingeniería.
...no es posible en Java tener matrices con más de 2 31 entradas...
los estudiantes les resultaba difícil escribir programas que no tuvieran una interfaz gráfica, no tenían idea de la relación entre el programa fuente y lo que realmente haría el hardware y (lo más perjudicial) no entendían en absoluto la semántica de los punteros, lo que hacía que el uso de C en la programación de sistemas fuera muy desafiante.
Ya es bastante malo que las JavaSchools no logren eliminar a los niños que nunca serán grandes programadores, algo que las escuelas podrían justificadamente decir que no es su problema. La industria, o al menos los reclutadores que usan grep, seguramente están clamando por que se enseñe Java. Pero las JavaSchools también fallan en entrenar los cerebros de los niños para que sean lo suficientemente hábiles, ágiles y flexibles para hacer un buen diseño de software.
¿Por qué Joel elige punteros y recursión como los dos conceptos de control? ¿Porque los encontró difíciles? Como señala Tim Bray, Java es perfectamente apto para la recursión, y la concurrencia puede ser un concepto más importante y difícil de dominar en cualquier caso. El énfasis en la recursión en los lenguajes Lisp es un poco exagerado y no se traslada a otras culturas de programación. ¿Por qué la gente piensa que es tan importante para la ingeniería de software? No me malinterpreten: me encanta la recursión cuando es la herramienta adecuada para el trabajo, pero eso no sucede tan a menudo como para justificar el enfoque de Joel en ella como un concepto fundamental.
Mientras buscamos conceptos difíciles que separen a los hombres de los niños, ¿qué pasa con el que nos llevó a Joel y a mí a una pelea hace dos años: las excepciones. Básicamente, no le gustan porque lo confunden. ¿Es esto diferente a que a un experto en Java no le gusten los punteros? Sí, puedes evitar las excepciones y usar retornos de estado, pero también puedes esforzarte mucho para evitar los punteros. ¿Significa eso que deberías hacerlo? Joel tiene los conceptos que le gustan (punteros y recursión) y lamenta su declive, pero no parece darse cuenta de que hay conceptos más nuevos que nunca ha entendido y con los que los jóvenes de Java se sienten cómodos.