Capítulo 1. Una lengua moderna
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Los mayores retos y las oportunidades más apasionantes para los desarrolladores de software residen hoy en aprovechar el poder de las redes. Las aplicaciones creadas hoy en día, sea cual sea su alcance o público previsto, se ejecutarán casi con toda seguridad en máquinas conectadas por una red global de recursos informáticos. La creciente importancia de las redes está planteando nuevas exigencias a las herramientas existentes y alimentando la demanda de una lista en rápido crecimiento de tipos de aplicaciones completamente nuevas.
Como usuarios de , queremos un software que funcione -de forma coherente, en cualquier lugar, en cualquier plataforma- y que juegue bien con otras aplicaciones. Queremos aplicaciones dinámicas que aprovechen las ventajas de un mundo conectado, capaces de acceder a fuentes de información dispares y distribuidas. Queremos un software verdaderamente distribuido que pueda ampliarse y actualizarse sin problemas. Queremos aplicaciones inteligentes que puedan recorrer la nube por nosotros, buscando información y sirviendo de emisarios electrónicos. Hace tiempo que sabemos qué tipo de software queremos, pero sólo en los últimos años hemos empezado a conseguirlo.
El problema, históricamente, ha sido que las herramientas para crear estas aplicaciones se han quedado cortas. Los requisitos de velocidad y portabilidad han sido, en su mayor parte, mutuamente excluyentes, y la seguridad se ha ignorado o malinterpretado en gran medida. En el pasado, los lenguajes verdaderamente portables eran voluminosos, interpretados y lentos. Estos lenguajes eran populares tanto por su funcionalidad de alto nivel como por su portabilidad. Los lenguajes rápidos solían proporcionar velocidad vinculándose a plataformas concretas, por lo que cumplían el requisito de portabilidad sólo a medias. Hubo incluso algunos lenguajes que empujaban a los programadores a escribir un código mejor y más seguro, pero eran principalmente vástagos de los lenguajes portables y sufrían los mismos problemas. Java es un lenguaje moderno que aborda estos tres frentes: portabilidad, velocidad y seguridad. Por eso sigue siendo un lenguaje dominante en el mundo de la programación casi tres décadas después de su introducción.
Entra en Java
El lenguaje de programación Java se diseñó para ser un lenguaje de programación independiente de la máquina, lo bastante seguro para atravesar redes y lo bastante potente para sustituir al código ejecutable nativo. Java aborda las cuestiones planteadas aquí y desempeñó un papel estelar en el crecimiento de Internet, que nos ha llevado a donde estamos hoy.
Java se ha convertido en la principal plataforma para aplicaciones y servicios web. Estas aplicaciones utilizan tecnologías como la API Java Servlet, Java Web Services y muchos servidores y marcos de aplicaciones Java comerciales y de código abierto muy conocidos. La portabilidad y velocidad de Java la convierten en la plataforma preferida para las aplicaciones empresariales modernas. Los servidores Java que se ejecutan en plataformas Linux de código abierto están en el corazón del mundo empresarial y financiero actual.
Al principio, la mayor parte del entusiasmo de por Java se centraba en sus capacidades para crear aplicaciones integradas para la web, denominadas applets. Pero al principio, los applets y otras interfaces gráficas de usuario (GUI) del lado del cliente escritas en Java eran limitadas. Hoy en día, Java dispone de Swing, un sofisticado conjunto de herramientas para crear interfaces gráficas de usuario. Este desarrollo ha permitido que Java se convierta en una plataforma viable para desarrollar software de aplicación tradicional del lado del cliente, aunque muchos otros contendientes han entrado en este campo tan concurrido.
Este libro te mostrará cómo utilizar Java para realizar tareas de programación del mundo real. En los próximos capítulos te presentaremos una amplia selección de funciones de Java, como procesamiento de textos, redes, manejo de archivos y creación de aplicaciones de escritorio con Swing.
Los orígenes de Java
Las semillas de Java fueron plantadas en 1990 por el patriarca e investigador jefe de Sun Microsystems, Bill Joy. Por aquel entonces, Sun competía en un mercado de estaciones de trabajo relativamente pequeño, mientras Microsoft empezaba a dominar el mundo de los PC basados en Intel. Cuando Sun perdió el tren de la revolución del PC, Joy se retiró a Aspen, Colorado, para trabajar en investigación avanzada. Estaba comprometido con la idea de realizar tareas complejas con software sencillo y fundó la bien llamada Sun Aspen Smallworks.
De los miembros originales del pequeño equipo de programadores que Joy reunió en Aspen, James Gosling será recordado como el padre de Java. Gosling se dio a conocer a principios de la década de 1980 como autor de Gosling Emacs, la primera versión del popular editor Emacs que estaba escrita en C y funcionaba bajo Unix. Gosling Emacs pronto fue eclipsado por una versión libre, GNU Emacs, escrita por el diseñador original de Emacs. Para entonces, Gosling había pasado a diseñar el Sistema de Ventanas Extensible en Red (NeWS) de Sun, que compitió brevemente con el Sistema de Ventanas X por el control del escritorio GUI de Unix en 1987. Aunque hay quien sostiene que NeWS era superior a X, NeWS perdió porque Sun lo mantuvo en propiedad y no publicó el código fuente, mientras que los principales desarrolladores de X formaron el X Consortium y adoptaron el enfoque contrario.
El diseño de NeWS enseñó a Gosling el poder de la integración de un lenguaje expresivo con una interfaz gráfica de usuario de ventanas compatible con la red. También enseñó a Sun que la comunidad de programadores de Internet se negará en última instancia a aceptar estándares propietarios, por muy buenos que sean. El fracaso de NeWS sembró las semillas del sistema de licencias de Java y del código abierto (aunque no del todo "open source"). Gosling aportó lo que había aprendido al naciente proyecto Aspen de Bill Joy. En 1992, el trabajo en el proyecto condujo a la fundación de la filial de Sun FirstPerson, Inc. Su misión era llevar a Sun al mundo de la electrónica de consumo.
El equipo FirstPerson trabajó en el desarrollo de software para aparatos de información, como teléfonos móviles y asistentes digitales personales (PDA). El objetivo era permitir la transferencia de información y aplicaciones en tiempo real a través de redes baratas de infrarrojos y redes tradicionales basadas en paquetes. Las limitaciones de memoria y ancho de banda exigían un código pequeño y eficaz. La naturaleza de las aplicaciones también exigía que fueran seguras y robustas. Gosling y sus compañeros empezaron a programar en C++, pero pronto se vieron confundidos por un lenguaje demasiado complejo, difícil de manejar e inseguro para la tarea. Decidieron empezar de cero, y Gosling empezó a trabajar en algo que apodó "C++ menos menos".
Con el hundimiento del Apple Newton (el primer ordenador de mano de Apple), se hizo evidente que el barco de la PDA aún no había llegado, así que Sun desplazó los esfuerzos de FirstPerson a la televisión interactiva (ITV). El lenguaje de programación elegido para los descodificadores de ITV iba a ser el antepasado cercano de Java, un lenguaje llamado Oak. A pesar de su elegancia y su capacidad para ofrecer una interactividad segura, Oak no pudo salvar la causa perdida de la ITV. Los clientes no lo querían, y Sun abandonó pronto el concepto.
En aquel momento, Joy y Gosling se reunieron para decidir una nueva estrategia para su innovador lenguaje. Era 1993, y la explosión de interés por la web presentaba una nueva oportunidad. Oak era pequeño, seguro, independiente de la arquitectura y orientado a objetos. Da la casualidad de que estos son también algunos de los requisitos de un lenguaje de programación universal y apto para Internet. Sun cambió rápidamente de enfoque y, con un pequeño retoque, Oak se convirtió en Java.
Crecer
No sería exagerado decir en que Java (y su paquete orientado a los desarrolladores, el Kit de Desarrollo de Java o JDK) prendió como la pólvora. Incluso antes de su primer lanzamiento oficial, cuando Java aún no era un producto, casi todos los actores importantes de la industria se subieron al carro de Java. Entre los licenciatarios de Java se encontraban Microsoft, Intel, IBM y prácticamente todos los principales proveedores de hardware y software. Sin embargo, incluso con todo este apoyo, Java recibió muchos golpes y experimentó algunos dolores de crecimiento durante sus primeros años.
Una serie de demandas por incumplimiento de contrato y antimonopolio entre Sun y Microsoft sobre la distribución de Java y su uso en Internet Explorer obstaculizó su implementación en el sistema operativo de escritorio más común del mundo: Windows. La implicación de Microsoft con Java también se convirtió en uno de los focos de un pleito federal más amplio sobre graves prácticas anticompetitivas de la empresa. Un testimonio judicial reveló que el gigante del software había intentado socavar Java introduciendo incompatibilidades en su versión del lenguaje. Mientras tanto, Microsoft introdujo su propio lenguaje derivado de Java llamado C# (C-sharp) como parte de su iniciativa .NET y eliminó Java de la inclusión en Windows. C# se ha convertido en un lenguaje muy bueno por derecho propio, con más innovaciones en los últimos años que Java.
Pero Java sigue extendiéndose en una amplia variedad de plataformas. Cuando empecemos a examinar la arquitectura de Java, verás que gran parte de lo apasionante de Java procede del entorno autónomo de máquina virtual en el que se ejecutan las aplicaciones Java. Java se diseñó cuidadosamente para que esta arquitectura de soporte pueda implementarse tanto en software, para las plataformas informáticas existentes, como en hardware personalizado. Las implementaciones de hardware de Java se utilizan en algunas tarjetas inteligentes y otros sistemas integrados. Incluso puedes comprar dispositivos "llevables", como anillos y placas de identificación, que llevan intérpretes de Java incorporados. Existen implementaciones de software de Java para todas las plataformas informáticas modernas, hasta para los dispositivos informáticos portátiles. En la actualidad, una rama de la plataforma Java es la base del sistema operativo Android de Google, con el que funcionan miles de millones de teléfonos y otros dispositivos móviles.
En 2010, Oracle Corporation compró Sun Microsystems y se convirtió en el administrador del lenguaje Java. En un comienzo algo accidentado de su mandato, Oracle demandó a Google por el uso del lenguaje Java en Android y perdió. En julio de 2011, Oracle lanzó Java Standard Edition 7,1 una importante versión de Java que incluía un nuevo paquete de E/S. En 2017, Java 9 introdujo módulos para abordar algunos problemas de larga data con la forma en que se compilaban, distribuían y ejecutaban las aplicaciones Java. Java 9 también puso en marcha un rápido proceso de actualización que llevó a que algunas versiones de Java se designaran como "soporte a largo plazo" y el resto como versiones estándar a corto plazo. (Más información sobre éstas y otras versiones en "Una hoja de ruta de Java".) Oracle sigue liderando el desarrollo de Java; sin embargo, también ha bifurcado el mundo de Java al trasladar el principal entorno de implementación de Java a una costosa licencia comercial, mientras ofrece una opción subsidiaria gratuita OpenJDK que conserva la accesibilidad que muchos desarrolladores adoran y esperan.
Una máquina virtual
Antes de avanzar mucho más, es útil saber algo más sobre el entorno que Java necesita para hacer su magia. No pasa nada si no entiendes todo lo que tocamos en estas próximas secciones. Cualquier término que no te resulte familiar lo encontrarás en capítulos posteriores. Sólo queremos ofrecerte una visión general del ecosistema de Java. En el núcleo de ese ecosistema está la Máquina Virtual Java (JVM).
Java es tanto un lenguaje compilado como interpretado. El código fuente de Java se convierte en simples instrucciones binarias, de forma muy parecida al código máquina de un microprocesador ordinario. Sin embargo, mientras que el código fuente de C o C++ se reduce a instrucciones nativas para un modelo concreto de procesador, el código fuente de Java se compila en un formato universal: instrucciones para la máquina virtual conocidas como bytecode.
El bytecode de Java es ejecutado por un intérprete en tiempo de ejecución de Java. El sistema en tiempo de ejecución realiza todas las actividades normales de un procesador de hardware, pero lo hace en un entorno seguro y virtual. Ejecuta un conjunto de instrucciones basado en la pila y gestiona la memoria como un sistema operativo. Crea y manipula tipos de datos primitivos y carga e invoca bloques de código recién referenciados. Y lo que es más importante, hace todo esto de acuerdo con una especificación abierta estrictamente definida que puede ser implementada por cualquiera que desee producir una máquina virtual compatible con Java. Juntas, la máquina virtual y la definición del lenguaje proporcionan una especificación completa. No hay ninguna característica del lenguaje Java base que quede sin definir o que dependa de la implementación. Por ejemplo, Java especifica los tamaños y las propiedades matemáticas de todos sus tipos de datos primitivos en lugar de dejarlo a la implementación de la plataforma.
El intérprete Java es relativamente ligero y pequeño; puede implementarse en la forma que se desee para una plataforma concreta. El intérprete puede ejecutarse como una aplicación independiente o puede incrustarse en otra pieza de software, como un navegador web. En conjunto, esto significa que el código Java es implícitamente portátil. El mismo código de bytes de una aplicación Java puede ejecutarse en cualquier plataforma que proporcione un entorno de ejecución Java, como se muestra en la Figura 1-1. No tienes que producir versiones alternativas de tu aplicación para diferentes plataformas, y no tienes que distribuir el código fuente a los usuarios finales.
La unidad fundamental del código Java es la clase. Como en otros lenguajes orientados a objetos, las clases son pequeños componentes modulares de la aplicación que contienen código ejecutable y datos. Las clases Java compiladas se distribuyen en un formato binario universal que contiene bytecode Java y otra información sobre la clase. Las clases pueden mantenerse de forma discreta y almacenarse en ficheros o archivos locales o en un servidor de red. Las clases se localizan y cargan dinámicamente en tiempo de ejecución a medida que las necesita una aplicación.
Además del sistema de tiempo de ejecución específico de la plataforma , Java tiene una serie de clases fundamentales que contienen métodos dependientes de la arquitectura. Estos métodos nativos sirven de pasarela entre la máquina virtual Java y el mundo real. Se implementan en un lenguaje compilado nativamente en la plataforma anfitriona y proporcionan acceso de bajo nivel a recursos como la red, el sistema de ventanas y el sistema de archivos del anfitrión. Sin embargo, la gran mayoría de Java está escrita en el propio Java -a partir de estas partes básicas- y, por tanto, es portátil. Esto incluye importantes herramientas Java, como el compilador Java, también escritas en Java y, por tanto, disponibles en todas las plataformas Java exactamente de la misma forma sin necesidad de portarlas.
Históricamente, los intérpretes de se han considerado lentos, pero Java no es un lenguaje interpretado tradicional. Además de compilar el código fuente en bytecode portable, Java también se ha diseñado cuidadosamente para que las implementaciones de software del sistema de ejecución puedan optimizar aún más su rendimiento compilando bytecode en código máquina nativo sobre la marcha. Esto se denomina compilación dinámica o "justo a tiempo" (JIT). Con la compilación JIT, el código Java puede ejecutarse tan rápido como el código nativo y mantener su transportabilidad y seguridad.
Esta característica JIT es un punto que a menudo se malinterpreta entre quienes quieren comparar el rendimiento de los lenguajes. Sólo hay una penalización de rendimiento intrínseca que el código Java compilado sufre en tiempo de ejecución por motivos de seguridad y diseño de la máquina virtual: la comprobación de los límites de las matrices. Todo lo demás puede optimizarse en código nativo igual que en un lenguaje compilado estáticamente. Yendo más allá, el lenguaje Java incluye más información estructural que muchos otros lenguajes, lo que permite más tipos de optimizaciones. Recuerda también que estas optimizaciones pueden hacerse en tiempo de ejecución, teniendo en cuenta el comportamiento y las características reales de la aplicación. ¿Qué se puede hacer en tiempo de compilación que no se pueda hacer mejor en tiempo de ejecución? Bueno, hay una contrapartida: el tiempo.
El problema de una compilación JIT tradicional es que optimizar el código lleva tiempo. Aunque un compilador JIT puede producir resultados decentes, puede sufrir una latencia significativa cuando se inicia la aplicación. Esto no suele ser un problema para las aplicaciones del lado del servidor de larga ejecución, pero sí lo es para el software del lado del cliente y las aplicaciones que se ejecutan en dispositivos más pequeños con capacidades limitadas. Para solucionarlo, la tecnología del compilador de Java, llamada HotSpot, utiliza un truco llamado compilación adaptativa. Si observas a qué dedican realmente su tiempo los programas, resulta que pasan casi todo su tiempo ejecutando una parte relativamente pequeña del código una y otra vez. El trozo de código que se ejecuta repetidamente puede ser sólo una pequeña fracción del programa total, pero su comportamiento determina el rendimiento global del programa. La compilación adaptativa permite que el tiempo de ejecución de Java aproveche nuevos tipos de optimizaciones que simplemente no pueden hacerse en un lenguaje compilado estáticamente, de ahí la afirmación de que el código Java puede ejecutarse más rápido que C/C++ en algunos casos.
Para aprovechar esta capacidad de adaptación, HotSpot comienza como un intérprete normal de código de bytes de Java, pero con una diferencia: mide (perfila) el código mientras se ejecuta para ver qué partes se ejecutan repetidamente. Una vez que sabe qué partes del código son cruciales para el rendimiento, HotSpot compila esas secciones en código máquina nativo óptimo. Como sólo compila una pequeña parte del programa en código máquina, puede permitirse tomarse el tiempo necesario para optimizar esas partes. Puede que no sea necesario compilar el resto del programa, sólo interpretarlo, para ahorrar memoria y tiempo. De hecho, la máquina virtual de Java puede funcionar en uno de estos dos modos: cliente y servidor, que determinan si hace hincapié en un tiempo de arranque rápido y en la conservación de la memoria o en un rendimiento total. A partir de Java 9, también puedes utilizar la compilación por adelantado (AOT) si es realmente importante minimizar el tiempo de inicio de tu aplicación.
Una pregunta natural que cabe hacerse en este punto es, ¿por qué tirar toda esta buena información de perfiles cada vez que se cierra una aplicación? Bueno, Sun abordó parcialmente este tema con el lanzamiento de Java 5.0 mediante el uso de clases compartidas de sólo lectura que se almacenan persistentemente de forma optimizada. Esto redujo significativamente tanto el tiempo de inicio como la sobrecarga de ejecutar muchas aplicaciones Java en una máquina determinada. La tecnología para hacerlo es compleja, pero la idea es sencilla: optimiza las partes del programa que necesitan ir rápido, y no te preocupes del resto.
Por supuesto, "el resto" contiene código que podría optimizarse aún más. En 2022, el Proyecto Leyden del OpenJDK se puso en marcha con la intención de reducir aún más el tiempo de arranque, minimizar el gran tamaño de las aplicaciones Java y reducir el tiempo que tardan en surtir efecto todas las optimizaciones mencionadas anteriormente. Los mecanismos propuestos por el Proyecto Leyden son bastante complejos, por lo que no los trataremos en este libro. Pero queríamos destacar el trabajo constante que se realiza para desarrollar y mejorar Java y su ecosistema. Incluso unos 30 años después de su debut, Java sigue siendo un lenguaje moderno.
Java comparado con otros lenguajes
Los desarrolladores de Java se basaron en muchos años de experiencia en programación con otros lenguajes a la hora de elegir las funciones. Merece la pena dedicar un momento a comparar Java a alto nivel con algunos de esos lenguajes, tanto en beneficio de quienes tengáis otra experiencia en programación como para los recién llegados que necesiten poner las cosas en contexto. Aunque este libro espera que te sientas cómodo con los ordenadores y las aplicaciones de software en un sentido genérico, no esperamos que tengas conocimientos de ningún lenguaje de programación en particular. Cuando nos referimos a otros lenguajes a modo de comparación, esperamos que los comentarios se expliquen por sí mismos.
Para que un lenguaje de programación sea universal hoy en día se necesitan al menos tres pilares: portabilidad, velocidad y seguridad. La Figura 1-2 muestra cómo se compara Java con algunos de los lenguajes que eran populares cuando se creó.
Puede que hayas oído que Java se parece mucho a C o C++, pero en realidad eso no es cierto, salvo a un nivel superficial. Cuando veas por primera vez el código Java, verás que la sintaxis básica se parece a C o C++. Pero ahí acaban las similitudes. Java no es en absoluto un descendiente directo de C o un C++ de nueva generación. Si comparas las características del lenguaje, verás que en realidad Java tiene más en común con lenguajes altamente dinámicos, como Smalltalk y Lisp. De hecho, la implementación de Java está tan lejos de C nativo como puedas imaginar.
Si estás familiarizado con el panorama actual de los lenguajes, te darás cuenta de que C#, un lenguaje muy popular, no aparece en esta comparación. C# es, en gran medida, la respuesta de Microsoft a Java, es cierto que con una serie de sutilezas añadidas. Dados sus objetivos de diseño y enfoque comunes (como el uso de una máquina virtual, código de bytes y una caja de arena), las plataformas no difieren sustancialmente en cuanto a su velocidad o características de seguridad. C# es más o menos tan portable como Java. Al igual que Java, C# toma mucho prestado de la sintaxis de C, pero en realidad es un pariente más cercano de los lenguajes dinámicos. A la mayoría de los desarrolladores de Java les resulta relativamente fácil aprender C# y viceversa. La mayor parte del tiempo que pasarás de uno a otro será en aprender la biblioteca estándar.
Sin embargo, merece la pena destacar las similitudes superficiales con estos lenguajes. Java se inspira en gran medida en la sintaxis de C y C++, por lo que verás construcciones de lenguaje concisas, incluidas abundantes llaves y puntos y coma. Java se adhiere a la filosofía de C de que un buen lenguaje debe ser compacto; en otras palabras, debe ser lo suficientemente pequeño y regular como para que un programador pueda albergar en su cabeza todas sus capacidades a la vez. Al igual que C es extensible mediante bibliotecas, se pueden añadir paquetes de clases Java a los componentes básicos del lenguaje para ampliar su vocabulario.
C ha tenido éxito porque proporciona un entorno de programación razonablemente repleto de funciones, con un alto rendimiento y un grado aceptable de portabilidad. Java también intenta equilibrar funcionalidad, velocidad y portabilidad, pero lo hace de forma muy diferente. C cambia funcionalidad por portabilidad; Java cambió inicialmente velocidad por portabilidad. Java también aborda cuestiones de seguridad que C no hace (aunque en los sistemas modernos, muchas de esas preocupaciones se abordan ahora en el sistema operativo y el hardware).
Los lenguajes de scripting como Perl, Python y Ruby siguen siendo populares. No hay ninguna razón por la que un lenguaje de scripting no pueda ser adecuado para aplicaciones seguras y en red. Pero la mayoría de los lenguajes de programación no son adecuados para la programación seria a gran escala. El atractivo de los lenguajes de scripting es que son dinámicos; son herramientas potentes para un desarrollo rápido. Algunos lenguajes de programación, como Tcl (más popular cuando se estaba desarrollando Java), también ayudan a los programadores a realizar tareas específicas, como crear rápidamente interfaces gráficas, que otros lenguajes de propósito más general consideran poco manejables. Los lenguajes de scripting también son muy portables, aunque a nivel de código fuente.
No confundir con Java, JavaScript es un lenguaje de scripting basado en objetos desarrollado originalmente por Netscape para el navegador web. Sirve como lenguaje residente en el navegador web para aplicaciones dinámicas, interactivas y basadas en la web. JavaScript toma su nombre de su integración y similitudes con Java, pero la comparación realmente acaba ahí. Sin embargo, existen importantes aplicaciones de JavaScript fuera del navegador, como Node.js,2 y sigue aumentando su popularidad entre los desarrolladores de diversos campos. Para más información sobre JavaScript, consulta JavaScript: La Guía Definitiva de David Flanagan (O'Reilly).
El problema de los lenguajes de programación es que no tienen en cuenta la estructura del programa ni la tipificación de los datos. Tienen sistemas de tipos simplificados y, por lo general, no permiten un alcance sofisticado de variables y funciones. Estas características los hacen menos adecuados para construir aplicaciones grandes y modulares. La velocidad es otro problema de los lenguajes de programación; la naturaleza de alto nivel y normalmente interpretada en código fuente de estos lenguajes a menudo los hace bastante lentos.
Los defensores de los lenguajes de programación individuales discreparían con algunas de estas generalizaciones, y sin duda tendrían razón en algunos casos. Los lenguajes de programación han mejorado en los últimos años, especialmente JavaScript, cuyo rendimiento ha sido objeto de una enorme cantidad de investigación. Pero la contrapartida fundamental es innegable: los lenguajes de script nacieron como alternativas sueltas y menos estructuradas a los lenguajes de programación de sistemas, y en general no son ideales para proyectos grandes o complejos por diversas razones.
Java ofrece algunas de las ventajas esenciales de un lenguaje de scripting: es muy dinámico y tiene las ventajas añadidas de un lenguaje de bajo nivel. Java tiene un potente paquete de expresiones regulares que compite con Perl para trabajar con texto. También tiene características del lenguaje que agilizan la codificación con colecciones, listas de argumentos de variables, importaciones estáticas de métodos y otros azúcares sintácticos que lo hacen más conciso.
El desarrollo incremental con componentes orientados a objetos, combinado con la sencillez de Java, permite desarrollar aplicaciones rápidamente y cambiarlas con facilidad. Los estudios han descubierto que desarrollar en Java es más rápido que en C o C++, basándose estrictamente en las características del lenguaje.3 Java también viene con una gran base de clases básicas estándar para tareas comunes, como la creación de interfaces gráficas de usuario y el manejo de las comunicaciones de red. Maven Central es un recurso externo con una enorme gama de bibliotecas y paquetes que pueden incorporarse rápidamente a tu entorno para ayudarte a abordar todo tipo de nuevos problemas de programación. Junto con estas características, Java tiene las ventajas de escalabilidad e ingeniería de software de los lenguajes más estáticos. Proporciona una estructura segura sobre la que construir marcos de trabajo de nivel superior (e incluso otros lenguajes).
Como ya hemos dicho, el diseño de Java es similar al de lenguajes como Smalltalk y Lisp. Sin embargo, estos lenguajes se utilizaron sobre todo como vehículos de investigación y no para desarrollar sistemas a gran escala. Una de las razones es que estos lenguajes nunca desarrollaron una vinculación portátil estándar a los servicios del sistema operativo, como la biblioteca estándar de C o las clases principales de Java. Smalltalk se compila en un formato de código de bytes interpretado, y puede compilarse dinámicamente en código nativo sobre la marcha, igual que Java. Pero Java mejora el diseño utilizando un verificador de código de bytes para garantizar la corrección del código Java compilado. Este verificador da a Java una ventaja de rendimiento sobre Smalltalk porque el código Java requiere menos comprobaciones en tiempo de ejecución. El verificador de bytecode de Java también ayuda con los problemas de seguridad, algo que Smalltalk no aborda.
A lo largo del resto de este capítulo, presentaremos una vista de pájaro del lenguaje Java. Explicaremos qué hay de nuevo y qué no es tan nuevo en Java y por qué.
Seguridad del diseño
Sin duda, en has oído hablar mucho de que Java está diseñado para ser un lenguaje seguro. Pero, ¿qué entendemos por seguro? ¿Seguro de qué o de quién? Las características de seguridad de Java que más llaman la atención son las que hacen posible nuevos tipos de software dinámicamente portable. Java proporciona varias capas de protección contra código peligrosamente defectuoso, así como contra cosas más traviesas, como virus y caballos de Troya. En la siguiente sección, veremos cómo la arquitectura de la máquina virtual Java evalúa la seguridad del código antes de ejecutarlo y cómo el cargador de clases Java (el mecanismo de carga de código de bytes del intérprete Java) construye un muro alrededor de las clases que no son de confianza. Estas características proporcionan la base para las políticas de seguridad de alto nivel que pueden permitir o no diversos tipos de actividades en función de cada aplicación.
En esta sección, sin embargo, veremos algunas características generales del lenguaje de programación Java. Quizá más importante que las características de seguridad específicas, aunque a menudo se pasa por alto en el barullo de la seguridad, es la seguridad que proporciona Java al abordar problemas comunes de diseño y programación. Java pretende ser lo más seguro posible frente a los errores sencillos que cometemos los propios programadores, así como los que heredamos del software heredado. El objetivo con Java ha sido mantener la sencillez del lenguaje, proporcionar herramientas que hayan demostrado su utilidad y dejar que los usuarios construyan instalaciones más complicadas sobre el lenguaje cuando sea necesario.
Simplifica, simplifica, simplifica...
Con Java, manda la simplicidad. Como Java empezó con una pizarra limpia, evitó características que han resultado complicadas o controvertidas en otros lenguajes. Por ejemplo, Java no permite la sobrecarga de operadores definida por el programador (que, en algunos lenguajes, permite a los programadores redefinir los significados de símbolos básicos como + y -). Java no tiene un preprocesador de código fuente, por lo que no tiene cosas como macros, declaraciones #define
o compilación condicional del código fuente. Estas construcciones existen en otros lenguajes principalmente para soportar dependencias de plataforma, así que, en ese sentido, no deberían ser necesarias en Java. La compilación condicional también se utiliza habitualmente para la depuración, pero las sofisticadas optimizaciones en tiempo de ejecución de Java y características como las aserciones resuelven el problema de forma más elegante.4
Java proporciona una estructura de paquetes bien definida para organizar los archivos de clases. El sistema de paquetes permite al compilador manejar algunas de las funciones de la utilidad make tradicional (una herramienta para crear ejecutables a partir del código fuente). Además, el compilador puede trabajar directamente con las clases Java compiladas, ya que se conserva toda la información de tipo; no se necesitan archivos de "cabecera" de código fuente extraños, como en C/C++. Todo esto significa que el código Java requiere menos contexto para leerlo. De hecho, a veces te resultará más rápido mirar el código fuente Java que consultar la documentación de las clases.
Java también adopta un enfoque diferente respecto a algunas características estructurales que han resultado problemáticas en otros lenguajes. Por ejemplo, Java sólo admite una única jerarquía de clases hereditarias (cada clase sólo puede tener una clase "padre"), pero permite la herencia múltiple de interfaces. Una interfaz, como una clase abstracta en C++, especifica el comportamiento de un objeto sin definir su implementación. Es un mecanismo muy potente que permite al desarrollador definir un "contrato" para el comportamiento del objeto que puede utilizarse y al que se puede hacer referencia independientemente de cualquier implementación concreta del objeto. Las interfaces en Java eliminan la necesidad de herencia múltiple de clases y los problemas asociados.
Como verás en el Capítulo 4, Java es un lenguaje de programación bastante sencillo y elegante, y eso sigue siendo gran parte de su atractivo.
Tipo Seguridad y Método Encuadernación
Un atributo de un lenguaje es el tipo de comprobación de tipos que utiliza. Generalmente, los lenguajes se clasifican en estáticos o dinámicos, lo que se refiere a la cantidad de información sobre variables que se conoce en tiempo de compilación frente a la que se conoce mientras se ejecuta la aplicación .
En un lenguaje estrictamente tipado estáticamente, como C o C++, los tipos de datos están grabados en piedra cuando se compila el código fuente. El compilador se beneficia de ello al disponer de información suficiente para detectar muchos tipos de errores antes de que se ejecute el código. Por ejemplo, el compilador no te permitiría almacenar un valor de coma flotante en una variable entera. Entonces, el código no requiere comprobación de tipos en tiempo de ejecución, por lo que se puede compilar para que sea pequeño y rápido. Pero los lenguajes tipados estáticamente son inflexibles. No admiten colecciones con la misma naturalidad que los lenguajes con comprobación dinámica de tipos, y hacen imposible que una aplicación importe con seguridad nuevos tipos de datos mientras se ejecuta.
En cambio, un lenguaje dinámico como Smalltalk o Lisp tiene un sistema en tiempo de ejecución que gestiona los tipos de objetos y realiza la comprobación de tipos necesaria mientras se ejecuta una aplicación. Este tipo de lenguajes permiten un comportamiento más complejo y son, en muchos aspectos, más potentes. Sin embargo, también suelen ser más lentos, menos seguros y más difíciles de depurar.
Las diferencias entre lenguajes se han comparado con las diferencias entre tipos de automóviles.5 Los lenguajes tipados estáticamente, como C++, son análogos a un coche deportivo: razonablemente seguros y rápidos, pero útiles sólo si conduces por una carretera bien asfaltada. Los lenguajes altamente dinámicos, como Smalltalk, son más parecidos a un todoterreno: te ofrecen más libertad, pero pueden ser algo difíciles de manejar. Puede ser divertido (y a veces más rápido) ir rugiendo por los bosques, pero también puedes quedarte atascado en una zanja o ser mutilado por los osos.
Otro atributo de un lenguaje es la forma en que vincula las llamadas a métodos con sus definiciones. En un lenguaje estático como C o C++, las definiciones de los métodos se vinculan normalmente en tiempo de compilación, a menos que el programador especifique lo contrario. Los lenguajes como Smalltalk, en cambio, se denominan de vinculación tardía porque localizan las definiciones de los métodos dinámicamente en tiempo de ejecución. La vinculación temprana es importante por razones de rendimiento; permite que una aplicación se ejecute sin la sobrecarga que supone la búsqueda de métodos en tiempo de ejecución. Pero la vinculación tardía es más flexible. También es necesaria en un lenguaje orientado a objetos, donde los nuevos tipos pueden cargarse dinámicamente y sólo el sistema en tiempo de ejecución puede determinar qué método ejecutar.
Java proporciona algunas de las ventajas tanto de C++ como de Smalltalk; es un lenguaje estáticamente tipado y de enlace tardío. Cada objeto en Java tiene un tipo bien definido que se conoce en tiempo de compilación. Esto significa que el compilador de Java puede hacer el mismo tipo de comprobación estática de tipos y análisis de uso que C++. Como resultado, no puedes asignar un objeto al tipo incorrecto de variable o llamar a métodos inexistentes de un objeto. El compilador de Java va aún más lejos e impide que utilices variables no inicializadas y que crees sentencias inalcanzables (consulta el Capítulo 4).
Sin embargo, Java también está totalmente tipado en tiempo de ejecución. El sistema de tiempo de ejecución de Java hace un seguimiento de todos los objetos y permite determinar sus tipos y relaciones durante la ejecución. Esto significa que puedes inspeccionar un objeto en tiempo de ejecución para determinar qué es. A diferencia de C o C++, el sistema de tiempo de ejecución de Java comprueba las transformaciones de un tipo de objeto a otro, y es posible utilizar nuevos tipos de objetos cargados dinámicamente con cierto grado de seguridad de tipos. Y como Java utiliza la vinculación tardía, es posible escribir código que sustituya algunas definiciones de métodos en tiempo de ejecución.
Desarrollo incremental
Java lleva consigo toda la información sobre tipos de datos y firmas de métodos desde su código fuente hasta su forma de código de bytes compilado. Esto significa que las clases Java pueden desarrollarse de forma incremental. Tu propio código fuente Java también puede compilarse de forma segura con clases de otras fuentes que tu compilador nunca haya visto. En otras palabras, puedes escribir código nuevo que haga referencia a archivos de clases binarias sin perder la seguridad de tipos que obtienes al tener el código fuente.
Java no sufre el problema de la "clase base frágil". En lenguajes como C++, la implementación de una clase base puede estar efectivamente congelada porque tiene muchas clases derivadas; cambiar la clase base puede requerir recompilar todas las clases derivadas. Éste es un problema especialmente difícil para los desarrolladores de bibliotecas de clases. Java evita este problema ubicando dinámicamente los campos dentro de las clases. Mientras una clase mantenga una forma válida de su estructura original, puede evolucionar sin romper otras clases que deriven de ella o la utilicen.
Gestión dinámica de la memoria
Algunas de las diferencias más importantes de entre Java y los lenguajes de bajo nivel (como C o C++) tienen que ver con la forma en que Java gestiona la memoria. Java elimina las referencias ad hoc a zonas arbitrarias de la memoria(punteros, en otros lenguajes) y añade algunas estructuras de datos de alto nivel al lenguaje. Java también limpia los objetos no utilizados (un proceso conocido como recogida de basura) de forma eficaz y automática. Estas características eliminan muchos problemas de seguridad, portabilidad y optimización que de otro modo serían insalvables.
La recolección de basura por sí sola ha salvado a innumerables programadores de la mayor fuente de errores de programación en C o C++: la asignación y desasignación explícitas de memoria. Además de mantener los objetos en memoria, el sistema de ejecución de Java hace un seguimiento de todas las referencias a esos objetos. Cuando un objeto deja de utilizarse, Java lo elimina automáticamente de la memoria. Puedes, en su mayor parte, simplemente ignorar los objetos que ya no utilices, con la confianza de que el intérprete los limpiará en el momento adecuado.
Java utiliza un sofisticado recolector de basura que se ejecuta en segundo plano, lo que significa que la mayor parte de la recolección de basura tiene lugar durante los tiempos muertos: entre pausas de E/S, clics del ratón o pulsaciones del teclado. Algunos sistemas de tiempo de ejecución, como HotSpot, tienen una recogida de basura más avanzada que puede diferenciar los patrones de uso de los objetos (como los de vida corta frente a los de vida larga) y optimizar su recogida. Ahora, el tiempo de ejecución de Java puede ajustarse automáticamente a la distribución óptima de memoria para distintos tipos de aplicaciones en función de su comportamiento. Con este tipo de perfilado del tiempo de ejecución, la gestión automática de la memoria puede ser mucho más rápida que los recursos gestionados más diligentemente por el programador, algo que a algunos programadores de la vieja escuela aún les cuesta creer.
Hemos dicho que Java no tiene punteros. Estrictamente hablando, esta afirmación es cierta, pero también es engañosa. Lo que Java ofrece son referencias, untipo de puntero más seguro. Una referencia es un manejador fuertemente tipado de un objeto. Todos los objetos de Java, salvo los tipos numéricos primitivos, se acceden mediante referencias. Puedes utilizar referencias para construir todos los tipos normales de estructuras de datos que un programador de C estaría acostumbrado a construir con punteros, como listas enlazadas, árboles, etcétera. La única diferencia es que con las referencias tienes que hacerlo de forma segura.
Las referencias en Java no pueden modificarse del mismo modo que se modifican los punteros en lenguajes como C. Una referencia es algo atómico; no puedes manipular el valor de una referencia salvo asignándolo a un objeto. Las referencias se pasan por valor, y no puedes hacer referencia a un objeto a través de más de un único nivel de indirección. Proteger las referencias es uno de los aspectos más fundamentales de la seguridad de Java. Significa que el código Java tiene que atenerse a las reglas; no puede asomarse a lugares donde no debería para eludir esas reglas.
Por último, debemos mencionar que las matrices (esencialmente listas indexadas) en Java son verdaderos objetos de primera clase. Se pueden asignar dinámicamente como otros objetos. Las matrices conocen su propio tamaño y tipo. Aunque no puedes definir o subclasificar directamente clases de matrices, tienen una relación de herencia bien definida basada en la relación de sus tipos base. Disponer de verdaderas matrices en el lenguaje alivia en gran medida la necesidad de la aritmética de punteros, como la que se utiliza en C o C++.
Tratamiento de errores
Las raíces de Java están en los dispositivos en red y los sistemas embebidos. Para estas aplicaciones, es importante disponer de una gestión de errores robusta e inteligente. Java dispone de un potente mecanismo para gestionar excepciones, similar al de las nuevas implementaciones de C++. Las excepciones proporcionan una forma más natural y elegante de tratar los errores. Las excepciones te permiten separar el código de gestión de errores del código normal, lo que hace que las aplicaciones sean más limpias y legibles.
Cuando se produce una excepción, hace que el flujo de ejecución del programa se transfiera a un bloque de código "catch" previamente designado. La excepción lleva consigo un objeto que contiene información sobre la situación que causó el problema. El compilador de Java exige que un método declare las excepciones que puede generar o las atrape y se ocupe él mismo de ellas. Esto eleva la información sobre errores al mismo nivel de importancia que los argumentos y los tipos de retorno de los métodos. Como programador Java, sabes con precisión qué condiciones excepcionales debes tratar, y cuentas con la ayuda del compilador para escribir un software correcto que no las deje sin tratar.
Hilos
Las aplicaciones modernas de requieren un alto grado de paralelismo. Incluso una aplicación muy monótona puede tener una interfaz de usuario compleja, que requiere actividades concurrentes. A medida que las máquinas se hacen más rápidas, los usuarios se vuelven menos pacientes con las tareas no relacionadas que se apoderan de su tiempo. Los hilos proporcionan un multiprocesamiento eficiente y una distribución de tareas tanto para aplicaciones cliente como servidor. Java hace que los hilos sean fáciles de usar porque su soporte está integrado en el lenguaje.
La concurrencia está bien, pero programar con hilos es mucho más que realizar varias tareas simultáneamente. En la mayoría de los casos, los hilos deben estar sincronizados (coordinados), lo que puede ser complicado sin un soporte explícito del lenguaje. Java admite la sincronización basada en el modelo de monitoreo, una especie de sistema de bloqueo y llave para acceder a los recursos. La palabra clave synchronized
designa métodos y bloques de código para un acceso seguro y serializado dentro de un objeto. También hay métodos simples y primitivos para la espera explícita y la señalización entre hilos interesados en el mismo objeto.
Java tiene un paquete de concurrencia de alto nivel que proporciona potentes utilidades que abordan patrones comunes en la programación multihilo , como agrupaciones de hilos, coordinación de tareas y bloqueos sofisticados. Con la adición del paquete de concurrencia y las utilidades relacionadas, Java proporciona algunas de las utilidades relacionadas con hilos más avanzadas de cualquier lenguaje. Y cuando necesites muchos, muchos hilos, puedes aprovechar el mundo de los hilos virtuales del Proyecto Telar, que comienza como una función de vista previa en Java 19.
Aunque es posible que algunos desarrolladores nunca tengan que escribir código multihilo, aprender a programar con hilos es una parte importante del dominio de la programación en Java y algo que todos los desarrolladores deberían comprender. Consulta el Capítulo 9 para profundizar en este tema. "Hilos vir tuales ", en particular, presenta los hilos virtuales y destaca algunas de sus mejoras de rendimiento.
Escalabilidad
Como hemos señalado antes, Los programas Java están formados principalmente por clases. Sobre las clases, Java proporciona paquetes, una capa de estructura que agrupa las clases en unidades funcionales. Los paquetes proporcionan una convención de nombres para organizar las clases y un segundo nivel de control organizativo sobre la visibilidad de variables y métodos en las aplicaciones Java.
Dentro de un paquete, una clase es visible públicamente o está protegida del acceso exterior. Los paquetes forman otro tipo de ámbito más cercano al nivel de aplicación. Esto se presta a construir componentes reutilizables que funcionen juntos en un sistema. Los paquetes también ayudan a diseñar una aplicación escalable que pueda crecer sin convertirse en un nido de pájaros de código estrechamente acoplado. En realidad, los problemas de reutilización y escalabilidad sólo se imponen con el sistema de módulos añadido en Java 9.6
Seguridad de aplicación
Una cosa es crear un lenguaje que evite que te dispares en el pie; otra muy distinta es crear uno que evite que otros te disparen en el pie.
La encapsulación es el concepto de ocultar los datos y el comportamiento dentro de una clase; es una parte importante del diseño orientado a objetos. Te ayuda a escribir software limpio y modular. En la mayoría de los lenguajes, sin embargo, la visibilidad de los elementos de datos es simplemente parte de la relación entre el programador y el compilador. Es una cuestión de semántica, no una afirmación sobre la seguridad real de los datos en el contexto del entorno del programa en ejecución .
Cuando Bjarne Stroustrup, el creador de C++, eligió la palabra clave private
para designar a los miembros ocultos de las clases en C++, probablemente estaba pensando en proteger a un desarrollador de los detalles sucios del código de otro desarrollador, no en proteger las clases y objetos de ese desarrollador del ataque de virus y caballos de Troya ajenos. El casting arbitrario y la aritmética de punteros en C o C++ hacen que sea trivial violar los permisos de acceso a las clases sin romper las reglas del lenguaje. Considera el siguiente código:
// C++ code class Finances { private: char creditCardNumber[16]; // ... }; main() { Finances finances; // Forge a pointer to peek inside the class char *cardno = (char *)&finances; printf("Card Number = %.16s\n", cardno); }
En este pequeño drama de C++, hemos escrito un código que viola la encapsulación de la clase Finances
y saca información secreta. Este tipo de travesura -abusar de un puntero no tipado- no es posible en Java. Si este ejemplo te parece poco realista, piensa en lo importante que es proteger las clases base (sistema) del entorno de ejecución de tipos de ataques similares. Si un código que no es de confianza puede corromper los componentes que proporcionan acceso a recursos reales como el sistema de archivos, la red o el sistema de ventanas, sin duda tiene posibilidades de robar los números de tu tarjeta de crédito.
Java creció con Internet y todas las fuentes no fiables que abundan en ella. Antes requería más seguridad que ahora, pero conserva un par de características de seguridad: un cargador de clases se encarga de cargar las clases desde el almacenamiento local o la red, y por debajo de eso, toda la seguridad del sistema descansa en última instancia en el verificador de Java, que garantiza la integridad de las clases entrantes.
El verificador del código de bytes de Java es un módulo especial y una parte fija del sistema de ejecución de Java. Los cargadores de clases, sin embargo, son componentes que pueden ser implementados de forma diferente por distintas aplicaciones, como servidores o navegadores web. Todas estas piezas deben funcionar correctamente para garantizar la seguridad en el entorno Java.
El Verificador
La primera línea de defensa de Java es el verificador de código de bytes. El verificador lee el código de bytes antes de que se ejecute y se asegura de que se comporta bien y obedece las reglas básicas de la especificación del código de bytes de Java. Un compilador Java de confianza no producirá código que haga lo contrario. Sin embargo, es posible que una persona malintencionada ensamble deliberadamente un bytecode Java malo. Es tarea del verificador detectar esto.
Una vez que el código ha sido verificado, se considera a salvo de ciertos errores inadvertidos o malintencionados. Por ejemplo, el código verificado no puede falsificar referencias ni violar los permisos de acceso a los objetos (como en nuestro ejemplo de la tarjeta de crédito). No puede realizar transformaciones ilegales ni utilizar objetos de forma no intencionada. Ni siquiera puede provocar ciertos tipos de errores internos, como el desbordamiento o subdesbordamiento de la pila interna. Estas garantías fundamentales son la base de toda la seguridad de Java.
Quizá te preguntes, ¿no está implícito este tipo de seguridad en muchos lenguajes interpretados? Bueno, aunque es cierto que no deberías poder corromper un intérprete BASIC con una línea falsa de código BASIC, recuerda que la protección en la mayoría de los lenguajes interpretados se produce a un nivel superior. Es probable que esos lenguajes tengan intérpretes pesados que realizan una gran cantidad de trabajo en tiempo de ejecución, por lo que son necesariamente más lentos y engorrosos.
En comparación, el bytecode de Java es un conjunto de instrucciones relativamente ligero y de bajo nivel. La capacidad de verificar estáticamente el bytecode de Java antes de la ejecución permite que el intérprete de Java funcione posteriormente a toda velocidad con total seguridad, sin costosas comprobaciones en tiempo de ejecución. Ésta fue una de las innovaciones fundamentales de Java.
El verificador es un tipo de "probador de teoremas" matemáticos. Recorre el código de bytes de Java y aplica reglas inductivas sencillas para determinar ciertos aspectos de cómo se comportará el código de bytes. Este tipo de análisis es posible porque el bytecode Java compilado contiene mucha más información de tipo que el código objeto de otros lenguajes de este tipo. El código de bytes también tiene que obedecer algunas reglas adicionales que simplifican su comportamiento. En primer lugar, la mayoría de las instrucciones del código de bytes sólo operan sobre tipos de datos individuales. Por ejemplo, en las operaciones de pila, hay instrucciones distintas para las referencias a objetos y para cada uno de los tipos numéricos de Java. Del mismo modo, hay una instrucción diferente para mover cada tipo de valor dentro y fuera de una variable local.
En segundo lugar, el tipo de objeto resultante de cualquier operación siempre se conoce de antemano. Ninguna operación del código byte consume valores y produce como salida más de un tipo posible de valor. Como resultado, siempre es posible mirar la siguiente instrucción y sus operandos y saber el tipo de valor que resultará.
Dado que una operación siempre produce un tipo conocido, es posible determinar los tipos de todos los elementos de la pila y de las variables locales en cualquier momento del futuro consultando el estado inicial. La colección de toda esta información de tipos en un momento dado se denomina estado de tipos de la pila. Esto es lo que Java intenta analizar antes de ejecutar una aplicación. Java no sabe nada sobre los valores reales de los elementos de la pila y de las variables en este momento; sólo sabe qué tipo de elementos son. Sin embargo, esta información es suficiente para aplicar las normas de seguridad y garantizar que los objetos no se manipulen ilegalmente.
Para que sea factible analizar el estado de tipos de la pila, Java impone una restricción adicional al modo en que se ejecutan sus instrucciones de código de bytes: todas las rutas al mismo punto del código deben llegar exactamente con el mismo estado de tipos.
Cargadores de clase
Java añade una segunda capa de seguridad con un cargador de clases. Un cargador de clases se encarga de introducir el código de bytes de las clases Java en el intérprete. Toda aplicación que cargue clases de la red debe utilizar un cargador de clases para gestionar esta tarea.
Una vez cargada una clase y pasada por el verificador, permanece asociada a su cargador de clases. Como resultado, las clases se dividen en espacios de nombres separados en función de su origen. Cuando una clase cargada hace referencia a otro nombre de clase, la ubicación de la nueva clase la proporciona el cargador de clases original. Esto significa que las clases recuperadas de una fuente específica pueden restringirse para interactuar sólo con otras clases recuperadas de esa misma ubicación. Por ejemplo, un navegador web habilitado para Java puede utilizar un cargador de clases para construir un espacio separado para todas las clases cargadas desde una URL determinada. También se puede implementar una seguridad sofisticada basada en clases firmadas criptográficamente utilizando cargadores de clases.
La búsqueda de clases en comienza siempre con las clases del sistema Java incorporadas. Estas clases se cargan desde las ubicaciones especificadas por el classpath del intérprete de Java (ver Capítulo 3). Las clases del classpath son cargadas por el sistema una sola vez y no pueden ser sustituidas. Esto significa que es imposible que una aplicación sustituya las clases fundamentales del sistema por versiones propias que cambien su funcionalidad.
Seguridad a nivel de aplicación y de usuario
Hay una delgada línea entre tener suficiente poder para hacer algo útil y tener todo el poder para hacer lo que quieras. Java proporciona la base para un entorno seguro en el que el código no fiable puede ponerse en cuarentena, gestionarse y ejecutarse con seguridad. Sin embargo, a menos que te conformes con mantener ese código en una cajita negra y ejecutarlo sólo para su propio beneficio, tendrás que concederle acceso al menos a algunos recursos del sistema para que pueda ser útil. Cada tipo de acceso conlleva ciertos riesgos y beneficios. Por ejemplo, en el entorno de los servicios en la nube, las ventajas de conceder a un código no fiable (desconocido) acceso al sistema de archivos del servidor en la nube son que puede encontrar y procesar archivos grandes más rápidamente de lo que tú podrías descargarlos y procesarlos localmente. Los riesgos asociados son que, en cambio, el código puede escabullirse por el servidor en nube y posiblemente descubrir información sensible que no debería ver.
En un extremo, el simple hecho de ejecutar una aplicación le proporciona un recurso -tiempo decomputación- que puede aprovechar o quemar frívolamente. Es difícil impedir que una aplicación no fiable te haga perder el tiempo o incluso que intente un ataque de "denegación de servicio". En el otro extremo, una aplicación potente y de confianza puede merecer justificadamente el acceso a todo tipo de recursos del sistema (como el sistema de archivos, la creación de procesos o las interfaces de red); una aplicación maliciosa podría causar estragos con estos recursos. El mensaje aquí es que debes abordar cuestiones de seguridad importantes y a veces complejas en tus programas.
En algunas situaciones, puede ser aceptable pedir simplemente al usuario que "dé el visto bueno" a las solicitudes. El lenguaje Java proporciona las herramientas para aplicar las políticas de seguridad que desees. Sin embargo, las políticas que elijas dependen en última instancia de si confías o no en la identidad e integridad del código en cuestión. Aquí es donde entran en juego las firmas digitales.
Las firmas digitales , junto con los certificados, son técnicas para verificar que los datos proceden realmente de la fuente de la que dicen proceder y no han sido modificados por el camino. Si el Banco de Boofa firma su aplicación de talonario de cheques, puedes verificar que la aplicación procede realmente del banco y no de un impostor y que no ha sido modificada. Por tanto, puedes decirle a tu sistema que confíe en el código que tenga la firma del Banco de Bufa .
Una hoja de ruta Java
Con las constantes actualizaciones de Java en , es difícil seguir la pista de las funciones disponibles ahora, las prometidas y las que existen desde hace tiempo. Las siguientes secciones constituyen una hoja de ruta que impone cierto orden en el pasado, presente y futuro de Java. En cuanto a las versiones de Java, las notas de publicación de Oracle contienen buenos resúmenes con enlaces a más detalles. Si utilizas versiones anteriores para trabajar, considera la posibilidad de leer los documentos de recursos tecnológicos de Oracle.
El pasado: Java 1.0-Java 20
Java 1.0 proporcionó el marco básico para el desarrollo Java: el lenguaje en sí, más paquetes que te permiten escribir applets y aplicaciones sencillas. Aunque la versión 1.0 está oficialmente obsoleta, todavía existen algunos applets que se ajustan a su API.
Java 1.1 sustituyó a la 1.0, incorporando importantes mejoras en el paquete Abstract Window Toolkit (AWT) (la función GUI original de Java), un nuevo patrón de eventos, nuevas funciones del lenguaje, como la reflexión y las clases internas, y muchas otras características fundamentales. Java 1.1 es la versión soportada de forma nativa por la mayoría de las versiones de Netscape y Microsoft Internet Explorer durante muchos años. Por diversas razones políticas, el mundo de los navegadores quedó congelado en esta condición durante mucho tiempo.
Java 1.2, apodado "Java 2" por Sun, fue un lanzamiento importante en diciembre de 1998. Aportó muchas mejoras y adiciones, principalmente en cuanto al conjunto de API que se incluían en las distribuciones estándar. Los añadidos más notables fueron la inclusión del paquete Swing GUI como API básica y una nueva API de dibujo 2D completa. Swing es el conjunto de herramientas de interfaz de usuario avanzado de Java, con capacidades muy superiores a las del antiguo AWT. (Swing, AWT y algunos otros paquetes han recibido diversos nombres, como JFC o Java Foundation Classes). Java 1.2 también añadió a Java una API de colecciones adecuada.
Java 1.3, publicada a principios de 2000, añadió características menores, pero se centró principalmente en el rendimiento. Con la versión 1.3, Java se hizo significativamente más rápido en muchas plataformas, y Swing recibió muchas correcciones de errores. En este periodo, también maduraron las API empresariales de Java, como Servlets y Enterprise JavaBeans.
Java 1.4, publicada en 2002, integró un nuevo e importante conjunto de API y muchas características largamente esperadas. Esto incluía aserciones del lenguaje, expresiones regulares, preferencias y API de registro, un nuevo sistema de E/S para aplicaciones de gran volumen, soporte estándar para XML, mejoras fundamentales en AWT y Swing, y una API Java Servlets muy madura para aplicaciones web.
Java 5, publicada en 2004, fue una versión importante que introdujo muchas mejoras en la sintaxis del lenguaje esperadas desde hacía tiempo, como los genéricos, las enumeraciones seguras, el bucle for mejorado, las listas de argumentos de variables, las importaciones estáticas, el autoboxing y unboxing de primitivas, así como metadatos avanzados en las clases. Una nueva API de concurrencia proporcionó potentes capacidades de subprocesamiento, y se añadieron API para impresión formateada y análisis sintáctico similares a las de C. También se revisó la Invocación Remota de Métodos (RMI) para eliminar la necesidad de compilar stubs y esqueletos. También se introdujeron importantes mejoras en las API XML estándar.
Java 6, publicado a finales de 2006, era una versión relativamente menor que no añadía nuevas características sintácticas al lenguaje Java, pero incluía nuevas API de extensión, como las de XML y servicios web.
Java 7, lanzado en 2011, representó una actualización bastante importante. En los cinco años transcurridos desde el lanzamiento de Java 6, se introdujeron varios pequeños retoques en el lenguaje, como permitir cadenas en las sentencias switch
(¡más adelante hablaremos de ambas cosas!), junto con adiciones importantes, como la nueva biblioteca de E/S java.nio
.
Java 8, lanzado en 2014, completó algunas de las características, como las lambdas y los métodos por defecto, que se habían eliminado de Java 7 al retrasarse una y otra vez la fecha de lanzamiento de esa versión. En esta versión también se trabajó en la compatibilidad con la fecha y la hora, incluida la posibilidad de crear objetos de fecha inmutables, útiles para usarlos en las lambdas ahora compatibles.
Java 9, lanzado tras varios retrasos en 2017, introdujo el Sistema de Módulos (Proyecto Jigsaw), así como un Bucle de Lectura-Evaluación-Impresión (REPL) para Java: jshell. Utilizaremos jshell para gran parte de nuestras exploraciones rápidas de muchas de las características de Java a lo largo del resto de este libro. Java 9 también eliminó JavaDB del JDK.
Java 10, publicado poco después de Java 9 a principios de 2018, actualizó la recolección de basura y aportó otras funciones, como los certificados raíz, a las versiones de OpenJDK. Se añadió compatibilidad con colecciones no modificables y se eliminó la compatibilidad con paquetes de aspecto antiguo (como Aqua de Apple).
Java 11, publicado a finales de 2018, añadió un cliente HTTP estándar y Transport Layer Security (TLS) 1.3. Se eliminaron los módulos JavaFX y Java EE. (JavaFX se rediseñó para seguir existiendo como biblioteca independiente.) También se eliminaron los applets de Java. Junto con Java 8, Java 11 forma parte del soporte a largo plazo (LTS) de Oracle. Algunas versiones -Java 8, Java 11, Java 17 y Java 21- se mantendrán durante periodos más largos. Oracle está intentando cambiar la forma en que los clientes y desarrolladores se comprometen con las nuevas versiones, pero siguen existiendo buenas razones para seguir con las versiones conocidas. Puedes leer más sobre las ideas y planes de Oracle para las versiones LTS y no LTS en la Hoja de ruta de soporte de Oracle Java SE de Oracle Technology Network.
Java 12, publicado a principios de 2019, añadió pequeñas mejoras en la sintaxis del lenguaje, como una vista previa de las expresiones de conmutación.
Java 13, que saldrá en septiembre de 2019, incluye más avances en características del lenguaje, como los bloques de texto, así como una gran reimplementación de la API de Sockets. Según los documentos oficiales de diseño, este impresionante esfuerzo proporciona "una implementación más sencilla y moderna que es fácil de mantener y depurar".
Java 14, publicado en marzo de 2020, añadió más previsualizaciones de mejora de la sintaxis del lenguaje, como los registros, actualizó la función de recogida de basura y eliminó las herramientas y la API Pack200. También sacó de su estado de previsualización la expresión de conmutación, prevista por primera vez en Java 12, y la incorporó al lenguaje estándar.
Java 15, publicado en septiembre de 2020, sacó de la vista previa la compatibilidad con bloques de texto (cadenas multilínea), y añadió clases ocultas y selladas que permiten nuevas formas de restringir el acceso a determinado código. (También se ha actualizado la codificación de texto a Unicode 13.0.
Java 16, publicado en marzo de 2021, mantuvo las clases selladas en la vista previa, pero sacó los registros de la vista previa. Se ampliaron las API de red para incluir los sockets de dominio Unix. También se añadió una opción de salida de lista a la API de flujos.
Java 17, publicado en septiembre de 2021 con LTS, actualizó las clases selladas para convertirlas en una característica habitual del lenguaje. Se añadió una vista previa de la concordancia de patrones para las sentencias switch
junto con varias mejoras en macOS. Ahora se pueden utilizar sockets de datagramas para unirse a grupos de multidifusión.
Java 18, publicado en marzo de 2022, hizo finalmente de UTF-8 el juego de caracteres por defecto para las API de Java SE. Introdujo un servidor web sencillo y estático apropiado para prototipos o pruebas, y amplió las opciones para la resolución de direcciones IP.
Java 19, publicado en septiembre de 2022, incluía hilos virtuales, concurrencia estructurada y patrones de registro. La compatibilidad con Unicode pasó a la versión 14.0, y se añadieron algunos formatos adicionales de fecha-hora.
Java 20, publicado en marzo de 2023, eliminó finalmente varias operaciones de subprocesamiento (detener/pausar/reanudar) que habían quedado obsoletas por inseguras más de 20 años antes en JDK 1.2. Se mejoró el análisis sintáctico de cadenas para admitir grafemas, como los símbolos emoji compuestos.
El presente: Java 21
Este libro incluye todas las últimas y mejores mejoras hasta el lanzamiento de Java 21 en septiembre de 2023. Con una cadencia de lanzamiento de seis meses, es casi seguro que habrá versiones más recientes del JDK cuando leas esto. Como ya se ha indicado, Oracle quiere que los desarrolladores traten estas versiones como actualizaciones de funciones. Con la excepción de los ejemplos que cubren los hilos virtuales, Java 17 es suficiente para trabajar con el código de este libro. En los raros casos en que utilicemos una característica más reciente, indicaremos la versión mínima requerida. No necesitarás "mantenerte al día" mientras lees, pero si utilizas Java para proyectos publicados, considera repasar la hoja de ruta oficial de Oracle para ver si mantenerte al día tiene sentido.
Resumen de funciones
He aquí un breve resumen de las características más importantes del núcleo actual de la API de Java que viven fuera de la biblioteca estándar:
- Conectividad con bases de datos Java (JDBC)
-
Una facilidad general para interactuar con bases de datos (introducida en Java 1.1).
- Invocación remota de métodos (RMI)
-
El sistema de objetos distribuidos de Java. RMI te permite llamar a métodos de objetos alojados en un servidor que se ejecuta en otro lugar de la red (introducido en Java 1.1).
- Seguridad Java
-
Una facilidad para controlar el acceso a los recursos del sistema, combinada con una interfaz uniforme para la criptografía. Java Security es la base de las clases firmadas.
- Escritorio Java
-
Un cajón de sastre para un gran número de funciones a partir de Java 9, incluidos los componentes de interfaz de usuario Swing; "aspecto y tacto enchufables", que te permiten adaptar y tematizar toda la propia interfaz de usuario; arrastrar y soltar; gráficos 2D; impresión; visualización, reproducción y manipulación de imágenes y sonido; y funciones de accesibilidad que pueden integrarse con software y hardware especiales para personas con deficiencias visuales o de otro tipo.
- Internacionalización
-
La capacidad de escribir programas que se adaptan al idioma y a la configuración regional que el usuario desea utilizar. El programa muestra automáticamente el texto en el idioma adecuado (introducido en Java 1.1).
- Interfaz Java de Nombres y Directorios (JNDI)
-
Un servicio general para buscar recursos. JNDI unifica el acceso a servicios de directorio, como LDAP, NDS de Novell y otros.
Las siguientes son API de "extensión estándar". Algunas, como las que permiten trabajar con XML y servicios web, se incluyen en la edición estándar de Java; otras deben descargarse por separado e implementarse con tu aplicación o servidor:
- JavaMail
-
Una API uniforme para escribir programas de correo electrónico.
- Marco Java Media
-
Otro cajón de sastre para coordinar la visualización de muchos tipos diferentes de medios, que incluye Java 2D, Java 3D, Java Speech (para reconocimiento y síntesis de voz), Java Sound (audio de alta calidad), Java TV (para televisión interactiva y aplicaciones similares), y otros.
- Servlets Java
-
Una herramienta que te permite escribir aplicaciones web del lado del servidor en Java.
- Criptografía Java
-
Implementaciones reales de algoritmos criptográficos. (Este paquete se separó de Java Security por motivos legales).
- Lenguaje de marcas extensible/Lenguaje de hojas de estilo extensible (XML/XSL)
-
Herramientas para crear y manipular documentos XML, validarlos, mapearlos hacia y desde objetos Java, y transformarlos con hojas de estilo.
Intentaremos tocar algunas de estas características. Desgraciadamente para nosotros (pero afortunadamente para los desarrolladores de software Java), el entorno Java se ha enriquecido tanto que es imposible abarcarlo todo en un solo libro. Indicaremos otros libros y recursos que sí cubren los temas que no podemos abordar en profundidad.
El futuro
Java no es, desde luego, el chico nuevo del barrio, pero sigue siendo una de las plataformas más populares para el desarrollo web y de aplicaciones. Esto es especialmente cierto en las áreas de servicios web, marcos de aplicaciones web y herramientas XML. Aunque Java no ha dominado las plataformas móviles como parecía destinado a hacerlo, puedes utilizar el lenguaje Java y las API básicas para programar para el sistema operativo móvil Android de Google, que se utiliza en miles de millones de dispositivos de todo el mundo. En el campo de Microsoft, el lenguaje C# derivado de Java se ha apoderado de gran parte del desarrollo .NET y ha llevado la sintaxis y los patrones centrales de Java a esas plataformas.
La propia JVM es también un área interesante de exploración y crecimiento. Están surgiendo nuevos lenguajes para aprovechar el conjunto de características y la ubicuidad de la JVM. Clojure es un robusto lenguaje funcional con una creciente base de seguidores que surgen en una amplia gama de trabajos, desde aficionados a las grandes tiendas. Y Kotlin es un lenguaje de propósito general que se está apoderando del desarrollo de Android con gusto. Está ganando tracción en nuevos entornos, al tiempo que conserva una buena interoperabilidad con Java.
Probablemente, las áreas de cambio más emocionantes en Java hoy en día se encuentran en las tendencias hacia marcos de trabajo más ligeros y sencillos para las empresas y hacia la integración de la plataforma Java con lenguajes dinámicos para el scripting de páginas web y extensiones. Hay mucho más trabajo interesante por hacer.
Tienes varias opciones de entornos de desarrollo Java y sistemas de ejecución. El kit de desarrollo Java de Oracle está disponible para macOS, Windows y Linux. Visita el sitio web Java de Oracle para obtener más información sobre cómo obtener el último JDK oficial.
Desde 2017, Oracle apoya oficialmente las actualizaciones del OpenJDK de código abierto. Los particulares y las pequeñas (o incluso medianas) empresas pueden encontrar suficiente esta versión gratuita. Las versiones van por detrás de la versión comercial del JDK y no incluyen el soporte técnico de Oracle, pero Oracle ha manifestado su firme compromiso de mantener el acceso libre y gratuito a Java. Todos los ejemplos de este libro se han escrito y probado con el OpenJDK. Puedes obtener más detalles directamente de la boca del caballo (¿de Oracle?) en el sitio del OpenJDK.
Para una instalación rápida de una versión gratuita de Java 19 (suficiente para casi todos los ejemplos de este libro, aunque observamos algunas características del lenguaje de versiones posteriores), Amazon ofrece su distribución Corretto en línea con instaladores amigables y familiares para las tres plataformas principales. El Capítulo 2 te guiará a través de la instalación básica de Corretto en Windows, macOS y Linux.
También hay una gran variedad de Entornos de Desarrollo Integrado (IDE) Java muy populares. Hablaremos de uno en este libro: la edición gratuita Community Edition de IntelliJ IDEA de JetBrains. Este entorno de desarrollo todo en uno te permite escribir, probar y empaquetar software con herramientas avanzadas al alcance de tu mano.
Ejercicios
Al final de cada capítulo, te proporcionaremos algunas preguntas y ejercicios de código para que los repases. Las respuestas a las preguntas se encuentran en el Apéndice B. Las soluciones a los ejercicios de código se incluyen con los demás ejemplos de código en GitHub.(El Apéndice A proporciona detalles sobre cómo descargar y utilizar el código de este libro). Te animamos a que respondas a las preguntas y pruebes los ejercicios. No te preocupes si tienes que volver a un capítulo y leer un poco más para encontrar una respuesta o buscar el nombre de algún método. De eso se trata. Aprender a utilizar este libro como referencia te resultará muy útil más adelante.
-
¿Qué empresa mantiene actualmente Java?
-
¿Cómo se llama el kit de desarrollo de código abierto para Java?
-
Nombra los dos componentes principales que intervienen en el enfoque de Java para ejecutar de forma segura el código de bytes.
1 La denominación Standard Edition (SE) apareció al principio de la historia de Java, cuando Sun lanzó la plataforma J2EE, o Java 2 Enterprise Edition. Ahora, la Enterprise Edition recibe el nombre de "Yakarta EE".
2 Si sientes curiosidad por Node.js, consulta Learning Node.js Development de Andrew Mead y Learning Node de Shelley Powers en el sitio web de O'Reilly.
3 Véase, por ejemplo, G. Phipps, "Comparing Observed Bug and Productivity Rates for Java and C++", Software-Practice & Experience, Volumen 29, 1999.
4 Las aserciones están fuera del alcance de este libro, pero son un tema que merece la pena explorar cuando te hayas afianzado más en Java. Encontrarás algunos detalles básicos en la Documentación de Oracle Java SE.
5 El mérito de la analogía del coche es de Marshall P. Cline, autor de C++ FAQ.
6 Los módulos están fuera del alcance de este libro, pero son el único tema de Java 9 Modularity, de Paul Bakker y Sander Mak (O'Reilly).
Get Aprender Java, 6ª Edición now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.