Java 17 Programación Avanzada
()
Información de este libro electrónico
Este libro va dirigido a to
Relacionado con Java 17 Programación Avanzada
Libros electrónicos relacionados
Domine JavaScript (4ª Edición) Calificación: 0 de 5 estrellas0 calificacionesJava 17 Programación Avanzada Calificación: 0 de 5 estrellas0 calificacionesProgramación Paginas Web JavaScript y PHP Calificación: 0 de 5 estrellas0 calificacionesAdministración de Sistemas Gestores de Bases de Datos (2ª Edición) Calificación: 0 de 5 estrellas0 calificacionesJava 17 Calificación: 0 de 5 estrellas0 calificacionesCreación de componentes en JavaScript Curso practico Calificación: 0 de 5 estrellas0 calificacionesLa programación JavaScript Calificación: 0 de 5 estrellas0 calificacionesReversing. Ingeniería Inversa Calificación: 0 de 5 estrellas0 calificacionesMERN. Guía Práctica de Aplicaciones Web Calificación: 0 de 5 estrellas0 calificacionesHacking ético con herramientas Python Calificación: 0 de 5 estrellas0 calificacionesAprende a Programar con Ajax Calificación: 0 de 5 estrellas0 calificacionesColecciones de datos y algoritmos en Python: de cero al infinito Calificación: 0 de 5 estrellas0 calificacionesCurso de programación Bash Shell Calificación: 0 de 5 estrellas0 calificacionesAprender VueJS con 100 ejercicios prácticos Calificación: 0 de 5 estrellas0 calificacionesDesarrollo de aplicaciones web con Jakarta EE Calificación: 0 de 5 estrellas0 calificacionesJava Curso Práctico Calificación: 0 de 5 estrellas0 calificacionesJEE 7 a Fondo: Diseño y desarrollo de aplicaciones Java Enterprise Calificación: 0 de 5 estrellas0 calificacionesBackbone JS Calificación: 0 de 5 estrellas0 calificacionesProgramación con lenguajes de guión en páginas web. IFCD0110 Calificación: 0 de 5 estrellas0 calificacionesAprende a Programar Ajax y jQuery Calificación: 1 de 5 estrellas1/5Backbone JS. JavaScript Framework. 2ª Edición Calificación: 0 de 5 estrellas0 calificacionesAprender DREAMWEAVER CC con 100 ejercicios Calificación: 0 de 5 estrellas0 calificacionesProxmox. Curso Práctico Calificación: 0 de 5 estrellas0 calificacionesPHP Calificación: 0 de 5 estrellas0 calificacionesDominio de SQL Calificación: 0 de 5 estrellas0 calificacionesIntegración de Componentes Software en Páginas Web (MF0951_2): Ingeniería del Software Calificación: 0 de 5 estrellas0 calificacionesSistemas Operativos en Red (GRADO MEDIO) Calificación: 0 de 5 estrellas0 calificacionesProgramación en Pascal: Desde simples programas Pascal hasta aplicaciones de escritorio actuales con Base de Datos DEV-PASCAL, LAZARUS Y PASCAL N-IDE Calificación: 0 de 5 estrellas0 calificacionesAprende a Desarrollar con Spring Framework Calificación: 3 de 5 estrellas3/5AJAX en J2EE. 2ª Edición actualizada Calificación: 0 de 5 estrellas0 calificaciones
Programación para usted
Excel de la A a la Z: El Manual Práctico Paso a Paso de Microsoft Excel para Aprender Funciones Básicas y Avanzadas, Fórmulas y Gráficos con Ejemplos Fáciles y Claros Calificación: 0 de 5 estrellas0 calificacionesPython Paso a paso: PROGRAMACIÓN INFORMÁTICA/DESARROLLO DE SOFTWARE Calificación: 4 de 5 estrellas4/5GuíaBurros Microsoft Excel: Todo lo que necesitas saber sobre esta potente hoja de cálculo Calificación: 4 de 5 estrellas4/5Python a fondo Calificación: 5 de 5 estrellas5/5VBA Excel Guía Esencial Calificación: 5 de 5 estrellas5/5Aprender a programar con Excel VBA con 100 ejercicios práctico Calificación: 5 de 5 estrellas5/5Tablas dinámicas y Gráficas para Excel: Una guía visual paso a paso Calificación: 0 de 5 estrellas0 calificacionesPython para principiantes Calificación: 5 de 5 estrellas5/5JavaScript: Guía completa Calificación: 4 de 5 estrellas4/5Aprende programación Python: python, #1 Calificación: 0 de 5 estrellas0 calificacionesHTML para novatos Calificación: 5 de 5 estrellas5/5El gran libro de Python Calificación: 5 de 5 estrellas5/5Curso básico de Python: La guía para principiantes para una introducción en la programación con Python Calificación: 0 de 5 estrellas0 calificacionesFundamentos De Programación Calificación: 5 de 5 estrellas5/5Programación (GRADO SUPERIOR): PROGRAMACIÓN INFORMÁTICA/DESARROLLO DE SOFTWARE Calificación: 4 de 5 estrellas4/5Aprendizaje automático y profundo en python: Una mirada hacia la inteligencia artificial Calificación: 0 de 5 estrellas0 calificacionesProgramación orientada a objetos con C++, 5ª edición. Calificación: 5 de 5 estrellas5/5Linux Essentials: una guía para principiantes del sistema operativo Linux Calificación: 5 de 5 estrellas5/5Curso de Programación y Análisis de Software Calificación: 4 de 5 estrellas4/5Arduino. Trucos y secretos.: 120 ideas para resolver cualquier problema Calificación: 5 de 5 estrellas5/5Aprender PHP, MySQL y JavaScript Calificación: 5 de 5 estrellas5/5Todo el mundo miente: Lo que internet y el big data pueden decirnos sobre nosotros mismos Calificación: 4 de 5 estrellas4/5Tablas dinámicas para todos. Desde simples tablas hasta Power-Pivot: Guía útil para crear tablas dinámicas en Excel Calificación: 0 de 5 estrellas0 calificacionesPython Aplicaciones prácticas Calificación: 4 de 5 estrellas4/5Ortografía para todos: La tabla periódica de la ortografía Calificación: 5 de 5 estrellas5/5Controles PLC con Texto Estructurado (ST): IEC 61131-3 y la mejor práctica de programación ST Calificación: 3 de 5 estrellas3/5AngularJS: Conviértete en el profesional que las compañías de software necesitan. Calificación: 4 de 5 estrellas4/5115 Ejercicios resueltos de programación C++ Calificación: 3 de 5 estrellas3/5Aprende a Programar en C++ Calificación: 5 de 5 estrellas5/5
Comentarios para Java 17 Programación Avanzada
0 clasificaciones0 comentarios
Vista previa del libro
Java 17 Programación Avanzada - José María Vegas Gertrudix
Acerca del Autor
Con más de 15 años de experiencia, actualmente trabaja como Ingeniero de Software en QuEST Global Engineering España.
Ingeniero Técnico en Informática de Sistemas y Máster en Desarrollo de Videojuegos por la Facultad de Informática de la Universidad Complutense de Madrid.
A lo largo de su carrera, ha compaginado el ejercicio del magisterio privado personalizado para lograr que los alumnos alcancen sus metas con su labor en la empresa privada. Experto en Banca Electrónica y Comercio Electrónico, ha realizado aplicaciones para la inmensa mayoría de los grandes bancos mundiales.
Escritor de artículos:
En febrero de 2007, en la revista Sólo Programadores, titulado ¿Es viable una aplicación de escritorio con Java?
.
En Agosto de 2011, en la página web de javaHispano, titulado Multitarea en Swing
.
En Agosto de 2011, en la página web de javaHispano, titulado Tipos Abstractos de Datos y Diseño por Contrato
.
Autor de libros:
Java 17 Fundamentos prácticos de programación, Ed. Ra-Ma, 2021.
IFCD052PO Programación en Java, Ed. Ra-Ma, 2021.
Java Curso Práctico, Ed. Ra-Ma, 2020.
Introducción
El hardware es la parte física de un computador, mientras que el software es la parte lógica del mismo.
Un Ingeniero de Software es aquella persona que construye un sistema software correcto y eficiente para ser ejecutado por el hardware de un computador.
Construir software es divertido y gratificante. En cierto modo, nos convierte en creadores de mundos nuevos. Pero también es difícil, porque los computadores tienen la mala costumbre de hacer lo que les decimos que hagan, no lo que queremos que hagan.
El camino para aprender a construir software correcto y eficiente es duro, pero estoy seguro de que libros como el que estás leyendo ahora mismo pueden ayudarte a hacerlo más fácil.
El libro que estás leyendo está actualizado para Java 17 y JUnit 5. Se dirige a aquellos programadores que quieren poner a trabajar la tecnología Java en proyectos reales, para lo cual se estudian herramientas y técnicas metódicas que permiten el desarrollo de software fiable y eficiente.
El libro está pensado para ser leído secuencialmente, porque cada capítulo se construye sobre los conceptos aprendidos en los capítulos previos. No obstante, el lector que ya conozca determinados contenidos puede saltar directamente a los capítulos que le resulten desconocidos.
Es preciso tener en cuenta que lo normal no es entender todos los conceptos presentados en el libro simplemente leyendo el texto. Es importante estudiar el código, escribirlo, ejecutarlo e incluso puede que depurarlo para llegar a entenderlo.
Para los que comienzan en este entorno de programación, es aconsejable que inicialmente consulten el libro Java 17 Fundamentos prácticos de Programación para un aprendizaje secuencial del lenguaje Java.
El código fuente que aparece en el libro está disponible para descargar en
https://github.com/josemari/JavaCursoPracticoProjects
Y también en la web del libro en www.ra-ma.com
Incluye varios proyectos Maven que pueden ser importados en Eclipse. Cualquier versión de este entorno de desarrollo integrado debería servir, siempre y cuando soporte Java 17.
Me encantaría tener noticia de cualquier errata que exista en el libro o en el código. Para informar de una errata, está disponible el siguiente correo electrónico:
jomaveger@gmail.com
Al final de la presente obra, hay una sección dedicada a la bibliografía consultada para la elaboración de este manual. Quiero expresar mi más profundo agradecimiento tanto a los autores de dichas obras de consulta, como a las numerosas fuentes de internet que han ayudado a que este libro sea una realidad, tan amplias que son de difícil enumeración.
1
Interfaces Gráficas y Nuevas Características de Java
Java, lenguaje de Programación de Guiones
Al igual que otros lenguajes clásicos, como C# o C++, Java está sujeto al ciclo de desarrollo Edición - Compilación - Ejecución y suele emplearse un entorno de desarrollo integrado específico para trabajar con él como, por ejemplo, Eclipse.
Las últimas versiones de Java, sin embargo, han ido incorporando características que hacen posible su uso como un lenguaje de guiones más, al estilo de Python o el propio Bash. Los dos cambios más importantes se producen con Java 9, lanzado a finales de 2017, y con Java 11, disponible desde finales de 2018.
Hay que tener en cuenta que, desde 1995 a 2017 sólo han existido ocho versiones principales de Java, pero desde Java 9 se está lanzando una nueva versión cada 6 meses, lógicamente con menos novedades y cambios respecto a las versiones previas.
Desde su versión 9, el JDK de Java incluye una utilidad, denominada jshell, que funciona como una línea de comandos de Java que permite ejecutar cualquier sentencia Java.
La utilidad jshell está pensada para su uso de forma interactiva. Para arrancar la utilidad desde Eclipse, seleccionamos Run -> External Tools -> External Tools Configuration y creamos una nueva configuración de tipo Program llamada JShell con las siguientes características:
Para el apartado Working Directory, escribimos ${project_loc}
Para el apartado Arguments, escribimos --class-path ${project_classpath}
-v. El argumento -v permite ejecutar la utilidad jshell en modo verboso.
Para el apartado Location, hay que indicar la ruta de la utilidad jshell que viene incluida en el JDK. En el caso de MacOS, sería algo semejante a /Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/bin/jshell.
Hay un error en el funcionamiento de Eclipse por lo que, para que la herramienta externa recién definida arranque correctamente jshell, es necesario que alguno de los proyectos del espacio de trabajo esté seleccionado.
Para abandonar la herramienta jshell, escribimos el comando /exit:
jshell> /exit
/exit
| Goodbye
La herramienta JShell acepta declaraciones de importación de paquetes, expresiones, instrucciones y definiciones de variables, métodos y clases. Así, por ejemplo, se puede introducir una instrucción en la guía del intérprete de comandos de modo que cualquier efecto colateral tendrá lugar y se mostrará la salida al usuario:
jshell> System.out.println(Hello World!
);
System.out.println(Hello World!
);
Hello World!
jshell>
Por defecto, JShell proporciona información acerca del código que le hemos suministrado. En el siguiente ejemplo, definimos una variable:
jshell> int x = 45;
int x = 45;
x ==> 45
jshell>
En primer lugar, se muestra el resultado, a saber, que la variable x tiene el valor 45. Dado que estamos en modo verboso, se nos muestra en lenguaje natural una breve descripción de lo que ha ocurrido. En general, los mensajes informativos comienzan con una barra vertical. En este caso concreto, se indica el nombre y el tipo de la variable creada.
Es importante hacer notar que el punto y coma será automáticamente añadido por JShell al final del fragmento de código en caso de que esté ausente.
Cuando se define una expresión que no está referenciada por una variable con nombre, se crea una variable desde cero de modo que se pueda referenciar dicho valor más tarde:
jshell> 2 + 2
2 + 2
$3 ==> 4
| created scratch variable $3 : int
jshell> 4 + $3
4 + $3
$4 ==> 8
| created scratch variable $4 : int
jshell> String twice(String s) {
String twice(String s) {
| created variable x : int
...> return s + s;
return s + s;
...> }
}
| created method twice(String)
jshell> twice(Luke
)
twice(Luke
)
$6 ==> LukeLuke
| created scratch variable $6 : String
jshell>
Para cambiar la definición de una variable, método, o clase previamente definida, lo único que hay que hacer es introducir una nueva definición. Por ejemplo, podemos cambiar la definición del método twice() de la manera siguiente:
jshell> String twice(String s) {
String twice(String s) {
...> return Twice:
+ s;
return Twice:
+ s;
...> }
}
| modified method twice(String)
| update overwrote method twice(String)
jshell> twice(Luke
)
twice(Luke
)
$8 ==> Twice:Luke
| created scratch variable $8 : String
jshell>
Obsérvese que el intérprete de comandos afirma que el método ha sido modificado, no creado, lo que significa que la definición ha cambiado aunque tiene la misma signatura.
También es posible modificar definiciones de forma que la nueva definición sea incompatible con la antigua. Por ejemplo:
jshell> String x
String x
x ==> null
| replaced variable x : String
| update overwrote variable x : int
jshell>
Como podemos ver, hemos cambiado el tipo de la variable x.
Como ya dijimos, estamos ejecutando JShell en modo verboso. En cualquier momento, se puede configurar el nivel de retroalimentación de la herramienta mediante el comando /set feedback. Por ejemplo, en cualquier momento podemos limitar los mensajes que nos muestra cada vez que le pedimos ejecutar una instrucción con
jshell> /set feedback concise
/set feedback concise
jshell>
También, en cualquier momento podemos volver al modo verboso sin más que ejecutar:
jshell> /set feedback verbose
/set feedback verbose
| Feedback mode: verbose
jshell>
Vamos a ver otros comandos útiles.
El comando /vars muestra información de las variables actualmente definidas en JShell:
jshell> /vars
/vars
| int $3 = 4
| int $4 = 8
| String $6 = LukeLuke
| String $8 = Twice:Luke
| String x = null
jshell>
El comando /methods muestra información de los métodos actualmente definidos en JShell:
jshell> /methods
/methods
| String twice(String)
jshell>
El comando /list muestra una lista de todos los fragmentos de código introducidos en JShell:
jshell> /list
/list
1 : System.out.println(Hello World!
);
3 : 2 + 2
4 : 4 + $3
6 : twice(Luke
)
7 : String twice(String s) {
return Twice:
+ s;
}
8 : twice(Luke
)
9 : String x;
jshell>
JShell nos permite definir métodos cuyos cuerpos referencian a su vez métodos, variables o clases que todavía no han sido definidos. Digamos que deseamos definir un método para calcular el volumen de una esfera, para lo cual introducimos la fórmula correspondiente:
jshell> double volume(double radius) {
double volume(double radius) {
...> return 4.0 / 3.0 * PI * cube(radius);
return 4.0 / 3.0 * PI * cube(radius);
...> }
}
| created method volume(double), however, it cannot be invoked until variable PI, and method cube(double) are declared
jshell>
JShell permite la definición anterior pero nos avisa de qué elementos de la misma faltan por definir. También nos permite hacer referencia a esta definición pero, en el caso de intentar ejecutarla, fallará:
jshell> double PI = 3.1415926535
double PI = 3.1415926535
PI ==> 3.1415926535
| created variable PI : double
jshell> volume(2)
volume(2)
| attempted to call method volume(double) which cannot be invoked until method cube(double) is declared
jshell> double cube(double x) { return x * x * x; }
double cube(double x) { return x * x * x; }
| created method cube(double)
| update modified method volume(double)
jshell> volume(2)
volume(2)
$5 ==> 33.510321637333334
| created scratch variable $5 : double
jshell>
Como podemos comprobar, una vez se han completado todas las definiciones, la invocación al método volume() funciona.
Cuando se produce una excepción, la traza de la misma nos indica una ubicación en el código introducido en JShell mediante #id:linenumber, tal que:
El #id de fragmento de código es el número mostrado por el comando /list visto anteriormente.
El linenumber es el número de línea dentro del fragmento de código.
Así, en siguiente ejemplo, la excepción ocurre en el fragmento de código #6 que corresponde a divide() en la segunda línea de código de divide():
jshell> int divide(int x, int y) {
int divide(int x, int y) {
...> return x / y;
return x / y;
...> }
}
| created method divide(int,int)
jshell> divide(5, 0)
divide(5, 0)
| Exception java.lang.ArithmeticException: / by zero
| at divide (#6:2)
| at (#7:1)
jshell> /list
/list
1 : double volume(double radius) {
return 4.0 / 3.0 * PI * cube(radius);
}
2 : double PI = 3.1415926535;
3 : volume(2)
4 : double cube(double x) { return x * x * x; }
5 : volume(2)
6 : int divide(int x, int y) {
return x / y;
}
7 : divide(5, 0)
jshell>
Aunque JShell está pensado para su uso de forma interactiva, también permite ejecutar guiones que tengamos almacenados en un fichero. Para ello, no tenemos más que invocar a la herramienta facilitando el nombre de dicho fichero. Éste finalizará habitualmente con la orden /exit para devolver el control, en lugar de quedarse a la espera en la propia línea de comandos de JShell. Podemos convertir un guión Java en uno ejecutable desde la línea de comandos de Mac OS o desde Linux simplemente agregando una cabecera de una línea, tal y como se muestra en el siguiente ejemplo, que podemos almacenar en un fichero llamado, por ejemplo, hola.jshell, que almacenamos en la carpeta src/main/resources del proyecto BookExamples:
//usr/bin/env jshell --execution local $0
$@
; exit $?
System.out.println(Hola desde jshell
)
/exit
La secuencia de inicio // es análoga a #!, con la ventaja de que no interfiere en la posterior interpretación del fichero por parte de JShell. Con el comando env localizamos la ruta donde está instalado jshell, al que facilitamos como parámetros el propio nombre del fichero que contiene el guión, representado por $0
, y el vector con el resto de los parámetros facilitados desde la línea de comandos, representado por el parámetro $@
. Al ejecutar esa primera parte de la cabecera, jshell leerá el contenido del guión, lo ejecutará y, dado que el último comando es /exit, devolverá el control. Éste retornará a la primera línea del fichero, tras el punto y coma de separación, encontrándose con la instrucción exit. Su finalidad es terminar la ejecución sin que bash procese el resto del código. Tras dar permisos de ejecución a este fichero, a continuación podemos ver cómo se ejecuta directamente desde la línea de comandos como lo haría cualquier otro guión en Mac OS o en Linux:
jomaveger@MacBook-Air-de-Jose Documents % ls -lrt
-rw-r--r--@ 1 jomaveger staff 104 25 jul 13:08 hola.jshell
jomaveger@MacBook-Air-de-Jose Documents % cat hola.jshell
//usr/bin/env jshell --execution local $0
$@
; exit $?
System.out.println(Hola desde jshell
)
/exit%
jomaveger@MacBook-Air-de-Jose Documents % chmod 755 hola.jshell
jomaveger@MacBook-Air-de-Jose Documents % ./hola.jshell
Hola desde jshell
jomaveger@MacBook-Air-de-Jose Documents %
Las versiones 11 y posteriores de Java incorporan una funcionalidad nueva que permite ejecutar directamente un único fichero de código fuente Java sin necesidad de compilación; por tanto, no podemos ejecutar directamente una aplicación compleja que se componga de múltiples clases distribuidas por varios ficheros fuente. No obstante, de cara a hacer posible la programación mediante guiones usando Java eso es todo lo necesario.
En sistemas operativos derivados de Unix, como Linux y MacOS, es habitual usar la directiva #!
para ejecutar un fichero de guiones ejecutable. Por ejemplo, un guión del intérprete de comandos típicamente comenzará por:
#!/bin/sh
De este modo, podremos entonces ejecutar el guión de la manera siguiente:
$ ./some_script
Ahora, a partir de Java 11, es posible ejecutar programas de Java formados por un único fichero utilizando el mismo mecanismo, si añadimos la siguiente sentencia al comienzo de un fichero:
#!/path/to/java --source version
En el siguiente ejemplo, que podemos almacenar en un fichero llamado, por ejemplo, add.jshell, que almacenamos en la carpeta src/main/resources del proyecto BookExamples, incluimos el siguiente código:
#!/usr/bin/java --source 17
import java.util.Arrays;
public class Addition {
public static void main(String[] args) {
Integer sum = Arrays.stream(args)
.mapToInt(Integer::parseInt)
.sum();
System.out.println(sum);
}
}
Marcamos el fichero como ejecutable y, después, podemos ejecutar el fichero como si fuera un guión cualquiera:
jomaveger@MacBook-Air-de-Jose resources % chmod 755 add.jshell
jomaveger@MacBook-Air-de-Jose resources % ./add.jshell 1 2 3
6
jomaveger@MacBook-Air-de-Jose resources %
Podemos también explícitamente utilizar el lanzador del JDK, que es el comando java, para invocar el fichero:
jomaveger@MacBook-Air-de-Jose resources % java --source 17 add.jshell 1 2 3
6
La opción --source es obligatoria incluso aunque esté presente en el fichero. Empleando este método, la directiva #!
es ignorada y el fichero es tratado como un fichero de código fuente Java normal sin la extensión .java.
Un último tema a tener en cuenta sobre los ficheros de guiones ejecutables es que las directivas hacen que el fichero sea dependiente de la plataforma. Por tanto, el fichero no será utilizable en plataformas como Windows, que no las soportan nativamente.
Inferencia de Tipos para Variables Locales
Una de las características más notables introducidas en Java 10 es la inferencia de tipos para variables locales.
Hasta Java 9, era necesario mencionar explícitamente el tipo de datos de una variable local, y además asegurarse de que era compatible con el inicializador utilizado para asignarle un valor inicial:
String message = Hasta luego, Java 9
;
A partir de Java 10, podemos declarar una variable local de la siguiente forma:
var message = Hola, Java 10
;
No se proporciona el tipo de datos de la variable local message. En su lugar, se marca message como una variable local de tipo var, y el compilador infiere el tipo de datos a partir del inicializador presente en el lado derecho de la asignación. En este caso, el tipo inferido para message será String.
Obsérvese que esta característica está disponible únicamente para variables locales que presentan un inicializador. No se puede usar var con atributos, parámetros de métodos o tipos de retorno, por ejemplo, ya que el inicializador es obligatorio para que el compilador pueda deducir el tipo de datos de la variable.
Veamos otro ejemplo. La siguiente variable local:
Map
podría ser reescrita de la siguiente manera:
var idToNameMap = new HashMap
Otro asunto a tener en cuenta es que var no es una palabra clave del lenguaje Java, lo que garantiza la compatibilidad con programas que utilicen dicha palabra como nombre de variable o de función. En realidad, var es un nombre de tipo reservado, como ocurre con int.
Finalmente, podemos confirmar que el uso de la inferencia de tipos para variables locales no produce sobrecarga en el tiempo de ejecución ni tampoco convierte a Java en un lenguaje dinámicamente tipado, ya que el tipo de la variable se sigue deduciendo en tiempo de compilación y no se puede cambiar posteriormente.
Como hemos comentado antes, var no funciona si no existe el inicializador correspondiente:
var n; // error: no se puede usar 'var' con una variable sin inicializador
La inferencia de tipos para variables locales tampoco funciona si el inicializador tiene valor null:
var emptyList = null; // error: inicializador de la variable es 'null'
Tampoco funciona var para variables no locales, por ejemplo atributos:
public var s = hello
; // error: 'var' no se permite aquí
Los arrays necesitan indicar explícitamente el tipo de datos destino, por lo que var no se puede usar:
var arr = { 1, 2, 3 }; // error: inicializador de array necesita un tipo destino explícito
Hay situaciones en las que var puede usarse legalmente, pero quizá no sea una buena idea hacerlo.
Por ejemplo, es posible utilizar var con el operador diamante y en realidad funciona, pero probablemente no sea el resultado que buscamos. Así, sea el siguiente código:
var empList = new ArrayList<>();
El compilador inferirá de este código que el tipo de datos de empList es ArrayListy no List. Si queremos que el tipo sea ArrayList
var empList = new ArrayList
En general, es preferible utilizar un tipo de datos explícito a la izquierda con el operador diamante a la derecha, o bien usar var a la izquierda con un tipo de datos explícito a la derecha.
Otro ejemplo de situación en que quizá no sea buena idea usar var es cuando el código puede volverse menos legible:
var result = obj.process();
En este caso, aunque se trata de un uso legal de var, resulta difícil comprender el tipo retornado por el método process(), lo que hace el código fuente menos legible.
De todos modos, existen situaciones en las que hay que tener cuidado con la inferencia de tipos. Supongamos que tenemos una clase Car y una clase Bike que heredan de una clase Vehicle. Sea la siguiente declaración:
var v = new Car();
La pregunta que nos planteamos es si hemos declarado v de tipo Car o de tipo Vehicle. En este caso, la explicación es simple ya que el tipo del inicializador -es decir, new Car()- está perfectamente claro, y además var no puede utilizarse sin inicializador. Esto significa, no obstante, que una asignación posterior como la siguiente:
v = new Bike();
deja de funcionar. En otras palabras, el código polimórfico no funciona bien con var.
Java posee un cierto número de tipos no-nominables, es decir, tipos de datos que pueden existir en un programa pero para los cuales no existe forma de explícitamente escribir el nombre de dicho tipo de datos. Un buen ejemplo de un tipo no-nominable es una clase anónima, ya que es posible crearla y añadirle atributos y métodos, pero es imposible escribir el nombre de dicha clase anónima en el código Java. El operador diamante no se puede utilizar con clases anónimas pero var sí.
Mediante var, podemos referirnos a tipos que, de otro modo, sería imposible describir. Normalmente, si se crea una clase anónima, se añaden atributos a la misma, pero no es posible referirse a esos atributos en ningún otro lugar porque es necesario, antes que nada, que sea asignada a un tipo con nombre. Por ejemplo, el siguiente fragmento de código no compilará porque el tipo de productInfo es Object y no es posible acceder a los atributos name y total de un Object:
Object productInfo = new Object() {
String name = Apple
;
int total = 30;
};
System.out.println(name =
+ productInfo.name + , total =
+ productInfo.total);
Utilizando var, es posible superar esa limitación. Cuando se asigna una clase anónima a una variable local tipada con var, se infiere el tipo de la clase anónima, no el de su clase padre. Esto significa que es posible referirse mediante var a los atributos declarados en la clase anónima. De este modo, el siguiente fragmento de código sí funcionará:
var productInfo = new Object() {
String name = Apple
;
int total = 30;
};
System.out.println(name =
+ productInfo.name + , total =
+ productInfo.total);
Es importante recalcar lo que hemos dicho antes: Cuando se asigna una clase anónima a una variable local tipada con var, se infiere el tipo de la clase anónima, no el de su clase padre. Así, si partimos del siguiente código:
var obj = new Object() {};
Y ahora intentamos asignar otro Object a obj, obtendríamos un error de compilación:
obj = new Object(); // error: Object no se puede convertir a
Esto es debido a que el tipo inferido de obj no es Object, sino una clase anónima construida a partir de Object, por tanto, que hereda de Object.
En Java 10, las expresiones lambda necesitaban un tipo destino explícito, por lo que no era posible usar var con ellas. Sin embargo, a partir de Java 11 esa situación cambió.
Por ejemplo, sea la siguiente expresión lambda:
BiFunction
Podemos obviar los tipos de los parámetros y reescribir la expresión lambda de la siguiente manera, lo que es permitido en Java 8:
BiFunction
Lo lógico hubiera sido que Java 10 permitiera lo siguiente:
BiFunction
Sin embargo, no fue así, hasta que llegó Java 11 que sí dio soporte a la anterior sintaxis. De esta forma, la utilización de var es uniforme tanto en variables locales como en los parámetros lambda.
No obstante, hay que tener en cuenta una serie de elementos cuando se utiliza var con las expresiones lambda.
No se puede utilizar var para algunos parámetros de la expresión lambda y dejar de utilizarlo para otros. Así, la siguiente expresión lambda no compilaría:
BiFunction
De manera semejante, no se puede mezclar var con tipos explícitos; por tanto, la siguiente expresión lambda tampoco compila:
BiFunction
Finalmente, aunque podemos obviar los paréntesis cuando tenemos una expresión lambda de un único parámetro, no podemos obviarlos cuando utilizamos var. Por tanto, la siguiente expresión lambda es correcta:
Function
No obstante, la siguiente expresión lambda no compila:
Function
La expresión lambda anterior, para que compilara, debería reescribirse del siguiente modo:
Function
Registros. Inmutabilidad
Uno de los pilares del estilo de programación funcional es la inmutabilidad, que también tiene aplicación en la programación concurrente. Un objeto inmutable es un objeto cuyo estado interno permanece constante una vez que ha sido completamente creado.
Los objetos inmutables poseen una serie de ventajas importantes:
Un objeto inmutable puede ser compartido libremente entre otros objetos porque está libre de efectos colaterales.
Un objeto inmutable es también una opción perfecta para ser utilizado como elemento de un conjunto y como clave de un mapa, dado que tanto uno como otro no debe ser modificable.
Un objeto inmutable puede ser compartido de forma segura entre múltiples hilos de ejecución.
Para diseñar una clase inmutable, se fijan los siguientes criterios:
Se establecen todos los atributos como privados y finales.
No se proporciona método alguno que modifique el estado del objeto. Si tuviera que existir un método que modificara el estado de un objeto, deberemos en su lugar retornar un nuevo objeto.
Se establece la clase como final, de modo que no se pueda heredar de ella.
Se establece un acceso exclusivo a los atributos que sean mutables; es decir, no se debe proporcionar método alguno que retorne una referencia directa a un atributo que sea mutable. De ser necesario dicho método, creará y retornará una copia profunda del atributo mutable.
Si la clase es genérica, la única manera práctica de que sus objetos sean inmutables en tiempo de ejecución es, además de aplicar los criterios anteriores, utilizar un tipo de datos inmutable como parámetro genérico real a la hora de realizar una derivación genérica.
A partir de Java 14, se permite la creación de registros -en inglés records-, que facilitan la creación de objetos inmutables, aunque con ciertas limitaciones.
Antes de la existencia de los registros en Java, podíamos crear una clase inmutable Person que almacenara un atributo para el nombre de la persona, name, y otro atributo para la dirección postal de la persona, address, de la siguiente manera:
package org.jomaveger.bookexamples.chapter7;
import java.util.Objects;
public final class Person {
private final String name;
private final String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
@Override
public int hashCode() {
return Objects.hash(name, address);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (!(obj instanceof Person)) {
return false;
} else {
Person other = (Person) obj;
return Objects.equals(name, other.name) && Objects.equals(address, other.address);
}
}
@Override
public String toString() {
return Person [name=
+ name + , address=
+ address + ]
;
}
}
Sin duda alguna, logramos nuestro objetivo, pero podemos constatar que existe mucho código repetitivo que dificulta la comprensión del propósito de la clase, que no deja de ser representar una persona con un nombre y una dirección postal.
A partir de Java 14, podemos reemplazar algunas de nuestras clases inmutables por registros. Un registro es una clase inmutable que sólo requiere el tipo de datos y el nombre de sus atributos.
Para generar un registro Person, utilizamos la palabra clave record:
package org.jomaveger.bookexamples.chapter7.records;
public record Person (String name, String address) {
}
Ante la definición anterior, Java genera automáticamente de forma interna los siguientes elementos:
Un atributo privado por cada parámetro formal que aparece en la declaración del registro.
Un constructor público con dos parámetros formales, llamado constructor canónico, que asigna cada uno a su correspondiente atributo privado.
Un método getter público por cada atributo, donde el nombre del método coincide con el nombre del atributo. Es decir, si p es una persona Person, entonces se obtiene el nombre mediante p.name() y la dirección mediante p.address(), no mediante p.getName() ni tampoco p.getAddress().
Métodos públicos equals(), hashCode() y toString().
Por tanto, partiendo de la mencionada definición, podemos escribir las siguientes pruebas unitarias:
package org.jomaveger.bookexamples.chapter7.records;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class PersonTest {
@Test
public void checkConstructorAndGetterMethods() {
String name = John Doe
;
String address = 100 Linda Ln.
;
Person person = new Person(name, address);
assertEquals(name, person.name());
assertEquals(address, person.address());
}
@Test
public void checkEqualsMethod() {
String name = John Doe
;
String address = 100 Linda Ln.
;
Person person1 = new Person(name, address);
Person person2 = new Person(name, address);
assertTrue(person1.equals(person2));
}
@Test
public void checkHashCodeMethod() {
String name = John Doe
;
String address = 100 Linda Ln.
;
Person person1 = new Person(name, address);
Person person2 = new Person(name, address);
assertEquals(person1.hashCode(), person2.hashCode());
}
@Test
public void checkToStringMethod() {
String name = John Doe
;
String address = 100 Linda Ln.
;
Person person = new Person(name, address);
assertEquals(Person[name=John Doe, address=100 Linda Ln.]
, person.toString());
}
}
Por defecto, un registro contiene sólo un único constructor, el cual requiere todos los atributos del registro como parámetros. Puede ocurrir, no obstante, que algún parámetro sea opcional, de modo que, en su ausencia, se pueda emplear algún valor por defecto. Así, por ejemplo, podemos añadir al registro Person el siguiente constructor:
package org.jomaveger.bookexamples.chapter7.records;
public record Person(String name, String address) {
public static String UNKNOWN_ADDRESS = Unknown
;
public Person(String name) {
this(name, UNKNOWN_ADDRESS);
}
}
La primera sentencia de cualquier nuevo constructor que se añade a un registro debe invocar a otro constructor, de modo que al final el constructor canónico es invocado.
A menudo, es necesario realizar algún tipo de lógica personalizada en el constructor canónico, como por ejemplo la validación de la entrada. En este caso, es posible para el programador redefinir el constructor canónico y reemplazarlo. No obstante, esta acción requiere que cada atributo sea manualmente inicializado:
package org.jomaveger.bookexamples.chapter7.records;
import java.util.Objects;
public record Person(String name, String address) {
public static String UNKNOWN_ADDRESS = Unknown
;
public Person(String name, String address) {
Objects.requireNonNull(name);
Objects.requireNonNull(address);
this.name = name;
this.address = address;
}
public Person(String name) {
this(name, UNKNOWN_ADDRESS);
}
}
También es posible emplear lo que se denomina un constructor compacto, para el cual no se escriben los parámetros aunque siguen estando disponibles. La intención de declarar un constructor compacto es únicamente añadir código de validación al cuerpo del constructor canónico, de forma que el código de inicialización restante es proporcionado por el compilador.
Es importante tener en cuenta que no es posible declarar un constructor compacto y a la vez redefinir el constructor canónico. Por ello, en el siguiente código hemos comentado la redefinición que hicimos del constructor canónico y hemos dejado activo únicamente el código que declara el constructor compacto:
package org.jomaveger.bookexamples.chapter7.records;
import java.util.Objects;
public record Person(String name, String address) {
public static String UNKNOWN_ADDRESS = Unknown
;
public Person {
Objects.requireNonNull(name);
Objects.requireNonNull(address);
}
// public Person(String name, String address) {
// Objects.requireNonNull(name);
// Objects.requireNonNull(address);
// this.name = name;
// this.address = address;
// }
public Person(String name) {
this(name, UNKNOWN_ADDRESS);
}
}
Es posible incluir variables y métodos estáticos en un registro. Ya hemos visto el caso de una variable estática por medio de UNKNOWN_ADDRESS. También podemos definir en nuestro registro un método estático, como por ejemplo:
package org.jomaveger.bookexamples.chapter7.records;
import java.util.Objects;
public record Person(String name, String address) {
public static String UNKNOWN_ADDRESS = Unknown
;
public static String UNKNOWN_NAME = Unnamed
;
public Person {
Objects.requireNonNull(name);
Objects.requireNonNull(address);
}
// public Person(String name, String address) {
// Objects.requireNonNull(name);
// Objects.requireNonNull(address);
// this.name = name;
// this.address = address;
// }
public Person(String name) {
this(name, UNKNOWN_ADDRESS);
}
public static Person unnamed(String address) {
return new Person(UNKNOWN_NAME, address);
}
}
Como es de esperar, se pueden referenciar los elementos estáticos de un registro del siguiente modo:
Person.UNKNOWN_ADDRESS;
Persona.UNKNOWN_NAME;
Person.unnamed(100 Linda Ln.
);
Java proporciona serialización de manera automática a un registro siempre y cuando la definición del mismo implemente la interfaz java.io.Serializable.
Como es de esperar, una variable de tipo registro almacena una referencia a un objeto, exactamente de la misma forma que ocurre con una clase.
Dijimos antes que podemos reemplazar algunas de nuestras clases inmutables por registros. Se trata de algunas y no todas por el hecho de que los registros son en realidad tan inmutables como lo sean sus atributos. Es decir, nada nos impide definir el siguiente código:
record Employee(String name, double salary, java.util.Date hireDate) {
}
var harry = new Employee(Harry Hacker
, 100000, new Date(120, 0, 1));
Dado que el tipo java.util.Date es mutable, es posible, aunque estemos utilizando un registro, modificar el atributo hireDate de la siguiente manera:
harry.hireDate().setTime(...);
Un registro puede tener cualquier número de métodos de instancia, del mismo modo que puede tener métodos estáticos. No obstante, aunque cuando un registro puede tener variables estáticas, no puede tener más atributos que los incluidos en la declaración del mismo.
Si se desea, el programador puede proporcionar su propia implementación de los métodos equals(), hashCode() y toString().
Un registro puede implementar cualquier número de interfaces. Por ejemplo:
public record Point(int x, int y) implements Comparable
public int compareTo(Point other) {
int dx = Integer.compare(x, other.x);
return dx != 0 ? dx : Integer.compare(y, other.y);
}
}
También es posible definir registros genéricos:
public record Pair
}
Un registro no puede heredar de ninguna otra clase, ni siquiera de otro registro. La razón es que cualquier tipo registro extiende implícitamente al tipo java.lang.Record, de la misma manera que cualquier tipo enumerado implícitamente hereda de java.lang.Enum. Tampoco es posible heredar de un registro puesto que es implícitamente final.
Un registro que se define dentro de otra clase es automáticamente estático; por tanto, el registro no tiene una referencia a la clase que lo envuelve, puesto que esto supondría una variable de instancia adicional en el registro.
El constructor canónico de un registro no puede lanzar excepciones comprobadas.
Como norma general de diseño, podemos decir que la lógica de un registro debe ser sencilla; por tanto, cuanto más tentados estemos de enriquecer el código de un registro -por ejemplo, añadiendo bastantes métodos adicionales al mismo o implementando varias interfaces-, más probable es que deba usarse una clase en lugar de un registro.
Clases Selladas
En Java, una clase puede extender otra clase o implementar una o más interfaces, proceso que se denomina herencia. La clase que es extendida o la interfaz que es implementada se denomina supertipo o superclase, y la clase que hereda de otra o implementa una interfaz se denomina subtipo o subclase. La herencia tiene dos propósitos principales: La reutilización del código y el modelado del sistema de tipos.
De manera tradicional, Java se ha centrado en el propósito de la reutilización del código cuando se ha tratado de la herencia.
Así, cualquier clase pública, sea abstracta o no, es heredable por cualquier número de subclases, de manera que las opciones para controlar la herencia han sido muy limitadas. Básicamente, el control de la herencia se limitaba a dos posibles opciones:
Utilizar clases finales para impedir la herencia.
Utilizar clases que carecen de modificador de visibilidad y, por tanto, no son públicas. Se dice que la clase tiene la visibilidad por defecto o bien que es privada para el paquete en el que está definida. En este caso, dicha clase sólo puede ser heredada por subclases situadas en el mismo paquete. No obstante, el problema de utilizar clases que carecen de modificador de visibilidad es que no hay usuario que pueda acceder a una de dichas clases, sea abstracta o no, sin permitirle también heredar de ella.
La base del problema radica en que, de manera tradicional, en Java no ha sido posible que una clase decida quién puede heredar de ella.
A partir de Java 15, se introducen las interfaces y las clases selladas con el fin de permitir que una clase sea ampliamente accesible pero no ampliamente extensible.
Para sellar una interfaz, simplemente se aplica el modificador sealed a su declaración, delante de la palabra clave interface. A continuación, después de la cláusula extends si existiera, es necesario utilizar la cláusula permits para especificar las clases e interfaces que se permite que implementen y extiendan la interfaz sellada. Sea el siguiente ejemplo:
package org.jomaveger.bookexamples.chapter7.sealed;
public sealed interface Service permits Car, Truck {
int getMaxServiceIntervalInMonths();
default int getMaxDistanceBetweenServicesInKilometers() {
return 100000;
}
}
De forma semejante a las interfaces, se puede sellar una clase aplicando a su declaración el mismo modificador sealed, delante de la palabra clave class. A continuación, después de las cláusulas extends o implements que estén presentes, se requiere utilizar la cláusula permits para especificar las clases que se permite que extiendan la clase sellada -que puede ser o no abstracta.
Así, continuando con el ejemplo:
package org.jomaveger.bookexamples.chapter7.sealed;
public abstract sealed class Vehicle permits Car, Truck {
protected final String registrationNumber;
public Vehicle(String registrationNumber) {
this.registrationNumber = registrationNumber;
}
public String getRegistrationNumber() {
return registrationNumber;
}
}
Los tipos sellados y sus subtipos directos pueden ser genéricos, como era de esperar.
Cuando las subclases permitidas son pequeñas en tamaño y número, una opción factible es declararlas en el mismo fichero de código fuente en el que se encuentra la clase sellada, bien como clases auxiliares, bien como clases anidadas. En este caso, se puede omitir la cláusula permits puesto que el compilador infiere cuáles son las subclases permitidas a partir de las declaraciones en el código fuente. Un ejemplo sería el siguiente:
package org.jomaveger.bookexamples.chapter7.sealed;
public sealed class Figure {
}
final class Circle extends Figure {
float radius;
}
non-sealed class Square extends Figure {
float side;
}
sealed class Rectangle extends Figure {
float length, width;
}
final class FilledRectangle extends Rectangle {
int red, green, blue;
}
Si la clase sellada y sus subclases permitidas no se encuentran en el mismo fichero de código fuente, se debe cumplir que pertenecen al mismo paquete.
También se debe cumplir que toda subclase permitida hereda directamente de la clase sellada.
Además, toda subclase permitida debe usar un modificador para describir cómo propaga el sellado iniciado por su superclase, existiendo tres opciones:
Una subclase permitida se puede declarar final para impedir que su parte de la jerarquía de clases se siga extendiendo. Recordemos que todo registro está implícitamente declarado como final.
package org.jomaveger.bookexamples.chapter7.sealed;
public final class Truck extends Vehicle implements Service {
private final int loadCapacity;
public Truck(int loadCapacity, String registrationNumber) {
super(registrationNumber);
this.loadCapacity = loadCapacity;
}
public int getLoadCapacity() {
return loadCapacity;
}
@Override
public int getMaxServiceIntervalInMonths() {
return 18;
}
}
Una subclase permitida se puede declarar sellada para permitir que su parte de la jerarquía se extienda más allá de lo que ha previsto la superclase sellada, pero de forma restringida.
Una subclase permitida se puede declarar no-sellada, mediante la palabra clave non-sealed, si su parte de la jerarquía se considera de nuevo abierta para ser extendida por subclases desconocidas. Por supuesto, una clase sellada no puede impedir que sus subclases permitidas realicen esta acción.
package org.jomaveger.bookexamples.chapter7.sealed;
public non-sealed class Car extends Vehicle implements Service {
private final int numberOfSeats;
public Car(int numberOfSeats, String registrationNumber) {
super(registrationNumber);
this.numberOfSeats = numberOfSeats;
}
public int getNumberOfSeats() {
return numberOfSeats;
}
@Override
public int getMaxServiceIntervalInMonths() {
return 12;
}
}
Ni qué decir tiene que la utilización de las clases selladas requiere un conocimiento exhaustivo de la lógica de negocio de la aplicación que se está desarrollando, puesto que presupone que es conocida de antemano con gran precisión la jerarquía de clases.
Las clases selladas funcionan muy bien con los registros. Dado que los registros son implícitamente finales, como ya hemos recordado, cualquier jerarquía sellada se expresa de manera incluso más concisa empleando registros.
Sea el siguiente ejemplo que declara una interfaz sellada llamada Expr, donde sólo las clases ConstantExpr, PlusExpr, TimesExpr y NegExpr pueden implementarla:
package org.jomaveger.bookexamples.chapter7.sealed;
public class TestExpressions {
public static void main(String[] args) {
// (6 + 7) * -8
System.out.println(
new TimesExpr(new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)), new NegExpr(new ConstantExpr(8)))
.eval());
}
}
sealed interface Expr permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
public int eval();
}
final class ConstantExpr implements Expr {
int i;
ConstantExpr(int i) {
this.i = i;
}
public int eval() {
return i;
}