11 características esenciales de Java para ayudar a modernizar su código

Dado que muchos desarrolladores aprendieron Java hace años, te mostramos algunas de las características, introducidas en los últimos años, que pueden ayudarlo a modernizar y mejorar su código. Piense en estas mejoras de Java como una fruta sabrosa que está a su alcance: si no está utilizando estas 11 funciones en su software, debería hacerlo. Como mínimo, debes probarlos.

No son todas características nuevas . De hecho, este artículo se centra en algunas funciones antiguas que es posible que usted y su equipo de desarrolladores no estén utilizando.

La conclusión principal es que los cambios de gran alcance siempre han sido una realidad para los desarrolladores de Java (como en muchos lenguajes de programación), y el ejercicio de mantenerse al tanto de estos cambios puede ayudar a mantener su código flexible. Si no está utilizando estas 11 mejoras de idioma, tal vez sea el momento de comenzar.

Paquete de fecha / hora (introducido con Java 8)

Uno de mis beneficios favoritos, incluido con Java 8, es el 

java.time
paquete, originalmente conocido como Joda-Time . (Antes de Java 8, los desarrolladores podían usar explícitamente la biblioteca Joda-Time).

Muchos de los primeros en adoptarlo lucharon con la 

Date
clase de JDK 1.0 , con su peculiar sesgo de años desde 1970 y capacidades de cálculo limitadas. El 
Calendar
(agregado en Java 1.1) ayudó un poco, pero no lo suficiente. Las mejoras JSR 310 del paquete Java 8 agregaron muchas clases de fecha y hora y pueden parecer abrumadoras.

Las principales clases de la mayoría de los desarrolladores trabajan son 

LocalDate
LocalDateTime
ZonedDate
, y 
ZonedDateTime
. Como implican los nombres, dos de estos son para representación de solo día, y dos son para visualización de fecha y hora; dos no tienen zona horaria asociada y dos sí. Todos tienen valores predeterminados sensibles y métodos de construcción simples, y todos tienen un grado asombroso de métodos de cálculo integrados, como 
plusDays()
between()
.

Por ejemplo, para usar la fecha de hoy, simplemente haga referencia 

LocalDate.now()
. Para usar una fecha fija, como el día de Navidad de 2025, use 
LocalDate.of(2025, 12, 25)
. El orden de los campos es inequívoco y sigue la especificación ISO para fechas: año, mes y día.


LocalDate d = LocalDate.now();
System.out.println("Today is " + d);
Today is 2027-03-12

Puede imprimir fechas y fechas y horas en cualquier formato que desee utilizando un 

DateTimeFormatter
(del 
java.time.format
subpaquete).


DateTimeFormatter df = DateTimeFormatter.ofPattern("yyyy/LL/dd");
System.out.println(df.format(LocalDate.now()));
2027/03/12

Debido a que no todo el código se puede convertir a la nueva API de fecha / hora de la noche a la mañana, puede convertir entre la API anterior y la nueva utilizando métodos integrados en ambas. 

Date
ofrece 
toInstance()
, que devuelve un 
Instant
(un punto en el tiempo) sin ninguna zona horaria (fiel a la 
Date
clase original ). Para obtener una 
LocalDateTime
instancia, pase el 
Instant
objeto junto con una zona horaria en la que convertirlo, comúnmente el predeterminado del sistema, de la siguiente manera:


Date oldDate = new Date();
System.out.println(oldDate.toInstant());
LocalDateTime newDate = LocalDateTime.ofInstant(oldDate.toInstant(), ZoneId.systemDefault());
System.out.println(newDate);

Hacer eso imprime lo siguiente:


2027-05-12T19:23:19.695Z
2027-05-12T15:23:19.695

Más interesante es la funcionalidad de cálculo incorporada en la nueva API. Por ejemplo, al administrar una pequeña empresa, debe saber cuándo pagar a las personas. Suponga que hay una combinación de empleados asalariados mensuales y empleados por hora pagados semanalmente. ¿Cuándo se les pagará a los trabajadores de cada tipo? El 

TemporalAdjusters
(en 
java.time.temporal
) ayudará.


LocalDateTime now = LocalDateTime.now();
LocalDateTime weeklyPayDay =
    now.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
System.out.println("Weekly employees' payday is Friday " +
    weeklyPayDay.getMonth() + " " +
    weeklyPayDay.getDayOfMonth());
LocalDateTime monthlyPayDay = now.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY));
System.out.println("Monthly employees are paid on " + monthlyPayDay.toLocalDate());

El código anterior imprime lo siguiente:


Weekly employees' payday is Friday MAY 14
Monthly employees are paid on 2027-05-28

Con la ayuda de la gran variedad de métodos para crear una fecha y hora modificadas, todos los objetos de la nueva API son inmutables. La inmutabilidad es útil para escribir código seguro para subprocesos. Existe una tendencia hacia los objetos inmutables, como se verá en la sección posterior sobre registros.

Hay más en esta API. Estos ejemplos (con importaciones) están en mi javasrc en GitHub en la carpeta 

main/src/main/java/datetime
.

Lambdas (introducido con Java 8)

Lambdas son funciones anónimas y le dieron a Java el principio de programación funcional de “código como datos”. En cierto sentido, esa capacidad siempre había estado ahí: se podía definir una variable de un tipo de objeto dado y pasarla a un método. Con lambdas, sin embargo, el proceso es mucho más sencillo y claro.

Las lambdas no son nuevas. El término fue utilizado por primera vez por Alonzo Church (de la fama de Church y Turing) en matemáticas en la década de 1930 . Para ver cómo las lambdas son implementadas por Java, consulte el artículo de Ben Evans “ Detrás de escena: ¿Cómo funcionan realmente las expresiones lambda? 

¿Cómo usas las lambdas? Considere el caso una vez común de agregar un 

ActionListener
a un 
JButton
.


JButton qb = new JButton("Quit");
class QuitListener implements ActionListener {
  public void actionPerformed(ActionEvent e) {
    System.exit(0);
  }
};
ActionListener al = new QuitListener();
qb.addActionListener(al);

El código anterior crea una clase que se usa solo una vez. Puede acortar el código utilizando una clase interna anónima, de la siguiente manera:


JButton qb = new JButton("Quit");
ActionListener al = new ActionListener {
  public void actionPerformed(ActionEvent e) {
    System.exit(0);
  }
};
qb.addActionListener(al);

Este código permite al compilador elegir un nombre para la clase, pero aún crea una única instancia que se usa solo una vez. Puede acortar un poco más el código si no obtiene una referencia a la clase interna anónima.


qb.addActionListener(new ActionListener {
  public void actionPerformed(ActionEvent e) {
    System.exit(0);
  }
};

Pero el código sigue repitiendo mucha información que el compilador puede inferir sin ambigüedades. Las lambdas aprovechan al máximo la inferencia del compilador.


qb.addActionListener(e -> System.exit(0));

¡Auge! Se guardó tanta escritura redundante, mucho menos código para revisar y mucho menos código para leer y releer al mantener el código.

La sintaxis de lambdas es 

param-list -> statement
.

El 

param-list
puede ser un único parámetro por sí mismo o una lista posiblemente vacía de parámetros entre paréntesis, como con cualquier otra llamada al método. Una lambda tiene que implementar una interfaz funcional , es decir, una interfaz Java con un solo método abstracto. 
ActionListener
es uno de esos métodos, pero el 
WindowListener
método relacionado no lo es; 
WindowListener
tiene siete métodos para implementar. Se utiliza una amplia inferencia del compilador para inferir toda la demás información necesaria.

Para evitar que tenga que crear sus propias interfaces funcionales, puede encontrar una serie de interfaces predefinidas en el 

java.util.function
subpaquete de Java 8 . El más simple es 
Consumer
, que tiene un método abstracto, 
accept(T)
. El 
T
es un 
type
parámetro introducido en Java 5 y, con suerte, le resulta familiar. 


$ javap java.util.function.Consumer
Compiled from "Consumer.java"
public interface java.util.function.Consumer<T> {
  public abstract void accept(T);
  public default java.util.function.Consumer<T> andThen(java.util.function.Consumer<? super T>);
}
$

Hay un ejemplo de uso 

Consumer
en la siguiente sección.

Métodos predeterminados y List.forEach (introducido con Java 8)

Java 8 agregó la noción de métodos predeterminados a las interfaces con, como su nombre lo indica, cuerpos de métodos predeterminados definidos en la interfaz. Esto permitió cambios compatibles con versiones anteriores. Mi ejemplo favorito de eso es 

List.forEach()
. En realidad 
Iterable.forEach()
, es , pero esa es la super-super-interfaz 
List
List
es donde la gente tiende a usarla. La 
Iterable
interfaz ahora tiene el siguiente aspecto:


public interface java.lang.Iterable<T> {
  public abstract java.util.Iterator<T> iterator();
  public default void forEach(java.util.function.Consumer<? super T>);
  public default java.util.Spliterator<T> spliterator();
}

¿Y eso qué 

Consumer
? Es de 
java.util.function
, discutido anteriormente en la sección de lambdas.

Si bien el código Java antiguo no tenía el 

forEach
método, la adición de ese método no interfiere con el código existente; si tiene su propia 
List
implementación, por ejemplo, no tiene que hacer nada. El método predeterminado simplemente funcionará.


List<String> names = List.of("Robin", "Toni", "JJ");
names.forEach(System.out::println);

La última pieza del código anterior 

System.out::println
, es una expresión de método para el 
println
método. El 
forEach()
método requiere a 
Consumer
, que es una interfaz funcional a cuyo 
accept()
método abstracto (un método que acepta un parámetro y lo devuelve 
void
) se puede aplicar 
System.out.println(String s)
, por lo que puede usarlo aquí.

La 

::
sintaxis genera una referencia de método , también nueva en Java 8, que permite crear una referencia lambda a una función existente. ¿Cómo? Mediante el uso de una definición estática (clase) o de instancia, el 
::
operador y el método al que se hace referencia.

Streams (introducido con Java 8)

Una secuencia es un objeto (definido en 

java.util.stream
) que proporciona un flujo de objetos de datos. Los flujos pueden ser finitos o infinitos, de un solo subproceso o paralelos. Las secuencias pueden provenir de una 
List
colección u otra o de un archivo, generarse dinámicamente, etc. Un proceso de flujo típico contiene un generador de flujo, cero o más operaciones de flujo y una operación de terminal.

Considere el problema de contar el número de líneas únicas en un archivo. Sin flujos, puede leer las líneas en una matriz, ordenar la matriz y recorrerla comparando cada línea con la anterior. Otros enfoques son utilizar una tabla o un árbol de hashing. Las transmisiones son más simples y el código es más legible. A continuación, el 

Files.lines()
método ( 
java.nio.file
desde Java 7) proporciona un flujo finito de todas las líneas de texto de un archivo. El código ordena la secuencia, elimina los duplicados y cuenta el número de líneas distintas.


long numberLines = Files.lines(Path.of(("lines.txt")))
  .sorted()
  .distinct()
  .count();
System.out.printf("lines.txt contains " + numberLines + " unique lines.");

He aquí otro ejemplo. Un método útil 

Random.doubles()
, devuelve un flujo infinito de números aleatorios generados sobre la marcha. La secuencia infinita se puede detener llamando al método de secuencia 
limit(int num)
; todos los elementos del flujo hasta 
num
se pasarán, en este caso, a la 
average()
función. Para un valor grande de 
num
, el resultado debería ser aproximadamente 0,5; por ejemplo, un gran número de lanzamientos de una moneda sin modificar debería ser 0,5 caras y 0,5 cruces.

La 

average()
función devuelve un 
OptionalDouble
. Llame al 
getAsDouble()
método (in 
OptionalDouble
) para recuperar el valor (que no puede ser el valor 
empty
, dado lo que sabe sobre la entrada). Para obtener más información al respecto 
Optional
, consulte “ 12 recetas para usar la clase opcional como debe usarse ” por Mohamed Taman.


System.out.println(new Random().doubles().limit(10).average().getAsDouble());
0.5384002737224926
System.out.println(new Random().doubles().limit(10).average().getAsDouble());
0.633140919801152
System.out.println(new Random().doubles().limit(100).average().getAsDouble());
0.514099594406035
System.out.println(new Random().doubles().limit(1000).average().getAsDouble());
0.48159324772449064
System.out.println(new Random().doubles().limit(10000).average().getAsDouble());
0.5059246586725906

Como puede ver, con valores mayores de 

num
, el promedio se acerca a 0,5.

Módulos (introducidos con Java 9)

Hace mucho tiempo, los responsables del JDK se dieron cuenta de que su código fuente había crecido demasiado para administrarlo. Se puso en marcha un plan para modularizar el JDK, tanto para facilitar el mantenimiento como para proporcionar un mejor aislamiento entre las partes del JDK y entre el JDK y las aplicaciones, un proceso que continúa incluso hoy, como explica Ben Evans en “ Un vistazo a Java 17: Continuación de la unidad para encapsular los componentes internos del tiempo de ejecución de Java “.

Esta iniciativa, originalmente llamada Java Platform Module System, logró simplificar y segregar el código del JDK. Se decidió hacer que este mismo mecanismo fuera parte del desarrollo normal de aplicaciones Java. Si bien esto causó bastante abandono ya que las herramientas y bibliotecas de terceros se movieron más lentamente que otras para admitir la modularidad, ahora está bastante sólidamente establecido.

Lo mejor de todo es que el sistema de módulos es clave para ayudarlo a crear y mantener sus propias aplicaciones grandes.

El sistema de módulos es importante para los desarrolladores. Proporciona, por ejemplo, una declaración clara de qué partes de su código son API públicas, cuáles son implementación y cuáles son (posiblemente múltiples) implementaciones de una interfaz pública. Debería migrar a código modular si aún no lo ha hecho.

Si aún no está utilizando módulos, consulte Java 9 Modularity de Sander Mak y Paul Bakker .

JShell (introducido con Java 9)

Uno de los obstáculos que muchas personas tienen mientras aprenden Java es tener que escribir la clase pública 

Foo
(que debe coincidir con el nombre del archivo en el que se encuentra la clase) seguida 
public static void main(String[] args) {...}
simplemente para poder agregar 2 + 2.

JShell es la respuesta a esta excesiva complejidad. JShell es un shell interactivo, también conocido como REPL (bucle de lectura-evaluación-impresión), donde la sintaxis de Java es un poco relajada y no tienes que poner 

class
main
rodear declaraciones.

El argumento 

PRINTING
le dice a JShell que permita 
println()
como atajo para 
System.out.println
. Los valores que no se almacenan en variables se emiten inmediatamente; esa es la parte P de REPL .


$ jshell PRINTING
|  Welcome to JShell -- Version 16
|  For an introduction type: /help intro
jshell> println(2+2)
4
jshell> 2+2
$23 ==> 4
jshell> import java.time.*;
jshell> var d = LocalDate.of(2027,12,5);
d ==> 2027-12-05
jshell> /exit
|  Goodbye
$

Esta herramienta no es solo para principiantes, por supuesto: uso mucho JShell cuando exploro API. Como siempre, hay más en el tema. Pruebe 

jshell –help
/intro
en JShell para obtener más información. La documentación de Java SE tiene una buena introducción a JShell .

Ejecución de un solo archivo (introducido con Java 11)

Java 11 le permite ejecutar un programa Java autónomo sin compilarlo primero. Por ejemplo


$ java HelloWorld.java
Hello, world
$

Debe agregar la extensión 

.java
al nombre del archivo, para que el comando no crea que se está refiriendo a un archivo 
HelloWorld.class
. Puede agregar argumentos como 
-classpath
, pero no puede pedirle a esta función que compile más de un archivo. Poder ejecutar un solo archivo puede ahorrarle tiempo cuando explora las API, aunque para ese uso, JShell puede ser más conveniente. Esta capacidad suele ser útil para asegurarse de que un solo archivo se compile y haga algo útil.

Bloques de texto (introducidos con Java 13)

Considere el siguiente código:


String paraV1 =
  "Creating long strings over multiple lines is tedious and error-prone.\n" +
  "The developer could forget the '+' at the end of a line, or more\n" +
  "commonly forget the space or the '\\n' at the end of a line.";

Los bloques de texto, también conocidos como cadenas de varias líneas, facilitan esta tarea. Con bloques de texto (previsualizados en Java 13 pero ahora estándar), el ejemplo anterior se convertiría en el siguiente:


String paraV2 = """
  Creating long strings over multiple lines is tedious and error-prone.
  The developer could forget the '+' at the end of a line, or more
  commonly, forget the space or the '\\n' at the end of a line.""";

El bloque de texto requiere un carácter de nueva línea después de la cita triple de apertura inicial, descarta la sangría inicial y procesa las nuevas líneas sin problemas.

En este ejemplo, 

paraV1.equals(paraV2);
devuelve 
true
.

Los bloques de texto son un gran ahorro de tiempo y de escritura cuando estás escribiendo mensajes útiles para el usuario o cualquier otra cadena larga. Obtenga más información en el artículo de Mala Gupta ” Los bloques de texto llegan a Java “.

Registros (introducido con Java 14)

Los desarrolladores de Java han dedicado innumerables horas a escribir y ajustar lo que son esencialmente clases de datos de objetos Java antiguos (POJO). Ya conoce el ejercicio: cree una clase, agregue algunos campos, agregue un constructor para asegurarse de que los campos estén inicializados, genere getters y setters, genere 

equals()
hashcode()
, escriba a 
toString()
, y así sucesivamente.

Los registros automatizan este proceso. Una 

record
clase amplía la nueva clase 
java.lang.Record
con una lista de campos y todos los métodos repetitivos enumerados anteriormente, todos generados automáticamente. Aquí hay un 
Person
registro muy simple , que contiene el nombre y la dirección de correo electrónico de una persona.


public record Person(String name, String email) { }

La sintaxis parece un poco extraña al principio y parece una fusión entre una definición de clase y una definición de método. Eso es porque eso es básicamente lo que es. Pasas los argumentos para el constructor y todos se convierten en argumentos, campos y descriptores de acceso del constructor. El compilador generará todas las demás piezas necesarias.

Esto es lo que se genera realmente como se ve por el 

javap
desensamblador en el compilado 
Person
registro y en su clase padre, 
java.lang.Record
.


$ javap 'structure.RecordDemoPerson$Person'
Compiled from "RecordDemoPerson.java"
public final class structure.RecordDemoPerson$Person extends java.lang.Record {
  public structure.RecordDemoPerson$Person(java.lang.String, java.lang.String);
  public final java.lang.String toString();
  public final int hashCode();
  public final boolean equals(java.lang.Object);
  public java.lang.String name();
  public java.lang.String email();
}
$ javap java.lang.Record
Compiled from "Record.java"
public abstract class java.lang.Record {
  protected java.lang.Record();
  public abstract boolean equals(java.lang.Object);
  public abstract int hashCode();
  public abstract java.lang.String toString();
}
$

En la siguiente demostración simple, los argumentos del constructor están en el mismo orden que en la definición del registro. Los métodos getter no siguen el 

getName
patrón; simplemente solicita el campo o lo invoca como nombre de método.


public static void main(String[] args) {
  Person p = new Person("Covington Roderick Smythe III", "roddy3@smythe.tld");
  System.out.println(p);
  System.out.println(p.name);
  System.out.println(p.name());
}

La ejecución de ese código muestra lo siguiente:


$ java RecordDemoPerson.java
Person[name=Covington Roderick Smythe III, email=roddy3@smythe.tld]
Covington Roderick Smythe III
Covington Roderick Smythe III
$

La demostración recupera el nombre como campo y como método para mostrar que puede usar cualquiera, por lo que el nombre se imprime dos veces en la salida.

Puede agregar métodos adicionales, pero todos los campos son inmutables. Una buena pista es que no se proporcionan métodos de establecimiento. Si necesita cambiar un registro, simplemente cree uno nuevo; son muy ligeros.

Para obtener más información sobre los registros, consulte “Los registros llegan a Java ” de Ben Evans. La inmutabilidad y la falta de nombres de métodos setter y getter pueden ser un problema con algunos frameworks; consulte “ Buceo en los registros de Java: serialización, cálculo de referencias y validación del estado del bean ” de Frank Kiwy para obtener ideas sobre cómo hacer frente a eso.

Las clases de discos son breves, sencillas y dulces. Deberías usarlos; le ahorrarán mucho escribir y reducirán significativamente la cantidad de código que tiene que leer (y mantener).

jpackage (introducido con Java 14)

jpackage
produce instaladores para cargar su software en computadoras de escritorio. Estos instaladores incluyen un JDK minimizado que contiene los módulos necesarios para ejecutar su aplicación en Linux, macOS y Windows.

En cada plataforma, 

jpackage
impulsa otra herramienta para realizar la creación real del instalador. En los sistemas comúnmente utilizados por los desarrolladores, hay herramientas de creación de paquetes integradas, como las herramientas 
pkg
dmg
en macOS o 
rpm
deb
en Linux. En Windows, 
jpackage
requiere un par de herramientas adicionales. Debe ejecutar 
jpackage
en una plataforma determinada para generar un instalador para esa plataforma.

Dada la variedad de funciones que la gente espera de un instalador, no es sorprendente que haya muchas opciones de línea de comandos para 

jpackage
. Por ejemplo, utilizo 
jpackage
para crear un instalador para mi herramienta de presentación en PDF, pdfshow, en GitHub . En lugar de intentar recordar todas las opciones, escribí un script que usa el tipo de sistema operativo para proporcionar los indicadores deseados. El guión es demasiado largo y tangencial para incluirlo aquí; lo puedes ver en el repositorio de GitHub bajo el nombre 
mkinstaller
. El corazón es la llamada a 
jpackage
, que se parece a lo siguiente (se han establecido varias variables anteriormente en el script):


jpackage \
  --name PDFShow \
  --app-version ${RELEASE_VERSION} \
  --license-file LICENSE.txt \
  --vendor "${COMPANY_NAME}" \
  --type "${inst_format}" \
  --icon src/main/resources/images/logo.${icon_format} \
  --input target \
  --main-jar pdfshow-${RELEASE_VERSION}-jar-with-dependencies.jar \
  ${OS_SPECIFIC}

Los instaladores resultantes se han utilizado para instalar 

pdfshow
en las tres plataformas principales. Si desea probar uno de los instaladores, consulte la 
Releases
pestaña en la página de GitHub.

Tipos sellados (introducido con Java 15)

“La subclasificación ilimitada es la raíz de todos los males”, dijo alguien una vez en una discusión sobre la arquitectura del software. Los tipos sellados, previsualizados en Java 15 y 16, proporcionan al autor de una clase control sobre sus subclases. Por ejemplo, en el siguiente


public abstract sealed class Person
  permits Customer, SalesRep, Manager {...}

solo las tres clases nombradas (en el mismo paquete) pueden realizar subclases directamente desde 

Person
. Sin embargo, el mecanismo de tipos sellados se puede utilizar para abrir subclases un nivel por debajo de la clase sellada, de la siguiente manera:


sealed class A permits B {
  ...
}
non-sealed class B extends A {
  ...
}
final class C extends B { // "extends A" here would not compile!
  ...
}

FUENTE: https://blogs.oracle.com/javamagazine/java-modernization-streams-records-lambdas-sealedclasses