Por qué necesita actualizar a Java 8 ya

Publicado: 2022-03-11

La versión más reciente de la plataforma Java, Java 8, se lanzó hace más de un año. Muchas empresas y desarrolladores todavía están trabajando con versiones anteriores, lo cual es comprensible, ya que hay muchos problemas al migrar de una versión de plataforma a otra. Aun así, muchos desarrolladores aún están iniciando nuevas aplicaciones con versiones antiguas de Java. Hay muy pocas buenas razones para hacer esto, porque Java 8 ha traído algunas mejoras importantes al lenguaje.

Hay muchas características nuevas en Java 8. Te mostraré algunas de las más útiles e interesantes:

  • expresiones lambda
  • Stream API para trabajar con Colecciones
  • Encadenamiento de tareas asincrónicas con CompletableFuture
  • Nueva API de tiempo

Expresiones Lambda

Una lambda es un bloque de código al que se puede hacer referencia y pasar a otra pieza de código para su ejecución futura una o más veces. Por ejemplo, las funciones anónimas en otros lenguajes son lambdas. Al igual que las funciones, a las lambdas se les pueden pasar argumentos en el momento de su ejecución, modificando sus resultados. Java 8 introdujo expresiones lambda , que ofrecen una sintaxis simple para crear y usar lambdas.

Veamos un ejemplo de cómo esto puede mejorar nuestro código. Aquí tenemos un comparador simple que compara dos valores Integer por su módulo 2:

 class BinaryComparator implements Comparator<Integer>{ @Override public int compare(Integer i1, Integer i2) { return i1 % 2 - i2 % 2; } }

Una instancia de esta clase puede llamarse, en el futuro, en el código donde se necesita este comparador, así:

 ... List<Integer> list = ...; Comparator<Integer> comparator = new BinaryComparator(); Collections.sort(list, comparator); ...

La nueva sintaxis lambda nos permite hacer esto de manera más simple. Aquí hay una expresión lambda simple que hace lo mismo que el método de compare de BinaryComparator :

 (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

La estructura tiene muchas similitudes con una función. Entre paréntesis, configuramos una lista de argumentos. La sintaxis -> muestra que se trata de una lambda. Y en la parte derecha de esta expresión, establecemos el comportamiento de nuestra lambda.

EXPRESIÓN JAVA 8 LAMBDA

Ahora podemos mejorar nuestro ejemplo anterior:

 ... List<Integer> list = ...; Collections.sort(list, (Integer i1, Integer i2) -> i1 % 2 - i2 % 2); ...

Podemos definir una variable con este objeto. Veamos cómo se ve:

 Comparator<Integer> comparator = (Integer i1, Integer i2) -> i1 % 2 - i2 % 2;

Ahora podemos reutilizar esta funcionalidad, así:

 ... List<Integer> list1 = ...; List<Integer> list2 = ...; Collections.sort(list1, comparator); Collections.sort(list2, comparator); ...

Tenga en cuenta que en estos ejemplos, la lambda se pasa al método sort() de la misma manera que se pasa la instancia de BinaryComparator en el ejemplo anterior. ¿Cómo sabe la JVM que debe interpretar la lambda correctamente?

Para permitir que las funciones tomen lambdas como argumentos, Java 8 introduce un nuevo concepto: interfaz funcional . Una interfaz funcional es una interfaz que tiene solo un método abstracto. De hecho, Java 8 trata las expresiones lambda como una implementación especial de una interfaz funcional. Esto significa que, para recibir una lambda como argumento de método, el tipo declarado de ese argumento solo necesita ser una interfaz funcional.

Cuando declaramos una interfaz funcional, podemos agregar la notación @FunctionalInterface para mostrar a los desarrolladores qué es:

 @FunctionalInterface private interface DTOSender { void send(String accountId, DTO dto); } void sendDTO(BisnessModel object, DTOSender dtoSender) { //some logic for sending... ... dtoSender.send(id, dto); ... }

Ahora, podemos llamar al método sendDTO , pasando diferentes lambdas para lograr un comportamiento diferente, como este:

 sendDTO(object, ((accountId, dto) -> sendToAndroid(accountId, dto))); sendDTO(object, ((accountId, dto) -> sendToIos(accountId, dto)));

Referencias de métodos

Los argumentos lambda nos permiten modificar el comportamiento de una función o método. Como podemos ver en el último ejemplo, en ocasiones la lambda solo sirve para llamar a otro método ( sendToAndroid o sendToIos ). Para este caso especial, Java 8 presenta una abreviatura conveniente: referencias a métodos . Esta sintaxis abreviada representa una lambda que llama a un método y tiene la forma objectName::methodName . Esto nos permite hacer que el ejemplo anterior sea aún más conciso y legible:

 sendDTO(object, this::sendToAndroid); sendDTO(object, this::sendToIos);

En este caso, los métodos sendToAndroid y sendToIos se implementan en this clase. También podemos hacer referencia a los métodos de otro objeto o clase.

API de transmisión

Java 8 trae nuevas capacidades para trabajar con Collections , en forma de una nueva API Stream. Esta nueva funcionalidad la proporciona el paquete java.util.stream y tiene como objetivo habilitar un enfoque más funcional para la programación con colecciones. Como veremos, esto es posible en gran medida gracias a la nueva sintaxis lambda que acabamos de analizar.

La API Stream ofrece filtrado, conteo y mapeo sencillos de las colecciones, así como diferentes formas de obtener porciones y subconjuntos de información de ellas. Gracias a la sintaxis de estilo funcional, Stream API permite un código más corto y elegante para trabajar con colecciones.

Comencemos con un breve ejemplo. Usaremos este modelo de datos en todos los ejemplos:

 class Author { String name; int countOfBooks; } class Book { String name; int year; Author author; }

Imaginemos que necesitamos imprimir todos los autores en una colección de books que escribieron un libro después de 2005. ¿Cómo lo haríamos en Java 7?

 for (Book book : books) { if (book.author != null && book.year > 2005){ System.out.println(book.author.name); } }

¿Y cómo lo haríamos en Java 8?

 books.stream() .filter(book -> book.year > 2005) // filter out books published in or before 2005 .map(Book::getAuthor) // get the list of authors for the remaining books .filter(Objects::nonNull) // remove null authors from the list .map(Author::getName) // get the list of names for the remaining authors .forEach(System.out::println); // print the value of each remaining element

¡Es sólo una expresión! Llamar al método stream() en cualquier Collection devuelve un objeto Stream que encapsula todos los elementos de esa colección. Esto se puede manipular con diferentes modificadores de Stream API, como filter() y map() . Cada modificador devuelve un nuevo objeto Stream con los resultados de la modificación, que se puede manipular aún más. El método .forEach() nos permite realizar alguna acción para cada instancia del flujo resultante.

Este ejemplo también demuestra la estrecha relación entre la programación funcional y las expresiones lambda. Tenga en cuenta que el argumento pasado a cada método en la transmisión es una lambda personalizada o una referencia de método. Técnicamente, cada modificador puede recibir cualquier interfaz funcional, como se describe en la sección anterior.

La API Stream ayuda a los desarrolladores a ver las colecciones de Java desde un nuevo ángulo. Imagina ahora que necesitamos obtener un Map de idiomas disponibles en cada país. ¿Cómo se implementaría esto en Java 7?

 Map<String, Set<String>> countryToSetOfLanguages = new HashMap<>(); for (Locale locale : Locale.getAvailableLocales()){ String country = locale.getDisplayCountry(); if (!countryToSetOfLanguages.containsKey(country)){ countryToSetOfLanguages.put(country, new HashSet<>()); } countryToSetOfLanguages.get(country).add(locale.getDisplayLanguage()); }

En Java 8, las cosas son un poco más claras:

 import java.util.stream.*; import static java.util.stream.Collectors.*; ... Map<String, Set<String>> countryToSetOfLanguages = Stream.of(Locale.getAvailableLocales()) .collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));

El método collect() nos permite recopilar los resultados de un flujo de diferentes formas. Aquí, podemos ver que primero agrupa por país y luego mapea cada grupo por idioma. ( groupingBy() y toSet() son métodos estáticos de la clase Collectors ).

API JAVA 8 FLUJO

Hay muchas otras capacidades de Stream API. La documentación completa se puede encontrar aquí. Recomiendo leer más para obtener una comprensión más profunda de todas las poderosas herramientas que este paquete tiene para ofrecer.

Encadenamiento de tareas asincrónicas con CompletableFuture

En el paquete java.util.concurrent de Java 7, hay una interfaz Future<T> , que nos permite obtener el estado o resultado de alguna tarea asíncrona en el futuro. Para usar esta funcionalidad, debemos:

  1. Cree un ExecutorService , que gestione la ejecución de tareas asincrónicas y pueda generar objetos Future para realizar un seguimiento de su progreso.
  2. Cree una tarea Runnable de forma asíncrona.
  3. Ejecute la tarea en ExecutorService , que proporcionará un Future que dará acceso al estado o los resultados.

Para hacer uso de los resultados de una tarea asincrónica, es necesario monitorear su progreso desde el exterior, utilizando los métodos de la interfaz Future , y cuando esté listo, recuperar explícitamente los resultados y realizar más acciones con ellos. Esto puede ser bastante complejo de implementar sin errores, especialmente en aplicaciones con un gran número de tareas simultáneas.

En Java 8, sin embargo, el concepto Future se lleva más allá, con la interfaz CompletableFuture<T> , que permite la creación y ejecución de cadenas de tareas asincrónicas. Es un mecanismo poderoso para crear aplicaciones asincrónicas en Java 8, porque nos permite procesar automáticamente los resultados de cada tarea al finalizar.

Veamos un ejemplo:

 import java.util.concurrent.CompletableFuture; ... CompletableFuture<Void> voidCompletableFuture = CompletableFuture.supplyAsync(() -> blockingReadPage()) .thenApply(this::getLinks) .thenAccept(System.out::println);

El método CompletableFuture.supplyAsync crea una nueva tarea asíncrona en el Executor predeterminado (normalmente ForkJoinPool ). Cuando finaliza la tarea, sus resultados se proporcionarán automáticamente como argumentos a la función this::getLinks , que también se ejecuta en una nueva tarea asíncrona. Finalmente, los resultados de esta segunda etapa se imprimen automáticamente en System.out . thenApply() y thenAccept() son solo dos de varios métodos útiles disponibles para ayudarlo a crear tareas simultáneas sin usar Executors manualmente.

CompletableFuture facilita la gestión de la secuenciación de operaciones asincrónicas complejas. Digamos que necesitamos crear una operación matemática de varios pasos con tres tareas. La tarea 1 y la tarea 2 usan diferentes algoritmos para encontrar un resultado para el primer paso, y sabemos que solo uno de ellos funcionará mientras que el otro fallará. Sin embargo, cuál funciona depende de los datos de entrada, que no sabemos de antemano. El resultado de estas tareas debe sumarse con el resultado de la tarea 3 . Por lo tanto, necesitamos encontrar el resultado de la tarea 1 o la tarea 2 y el resultado de la tarea 3 . Para lograr esto, podemos escribir algo como esto:

 import static java.util.concurrent.CompletableFuture.*; ... Supplier<Integer> task1 = (...) -> { ... // some complex calculation return 1; // example result }; Supplier<Integer> task2 = (...) -> { ... // some complex calculation throw new RuntimeException(); // example exception }; Supplier<Integer> task3 = (...) -> { ... // some complex calculation return 3; // example result }; supplyAsync(task1) // run task1 .applyToEither( // use whichever result is ready first, result of task1 or supplyAsync(task2), // result of task2 (Integer i) -> i) // return result as-is .thenCombine( // combine result supplyAsync(task3), // with result of task3 Integer::sum) // using summation .thenAccept(System.out::println); // print final result after execution

Si examinamos cómo Java 8 maneja esto, veremos que las tres tareas se ejecutarán al mismo tiempo, de forma asíncrona. A pesar de que la tarea 2 falló con una excepción, el resultado final se calculará e imprimirá correctamente.

PROGRAMACIÓN ASINCRÓNICA JAVA 8 CON CompletableFuture

CompletableFuture facilita mucho la creación de tareas asincrónicas con varias etapas y nos brinda una interfaz sencilla para definir exactamente qué acciones se deben tomar al finalizar cada etapa.

API de fecha y hora de Java

Como se indica en la propia admisión de Java:

Antes del lanzamiento de Java SE 8, el mecanismo de fecha y hora de Java lo proporcionaban las clases java.util.Date , java.util.Calendar y java.util.TimeZone , así como sus subclases, como java.util.GregorianCalendar . Estas clases tenían varios inconvenientes, incluyendo

  • La clase Calendar no era segura.
  • Debido a que las clases eran mutables, no se podían usar en aplicaciones de subprocesos múltiples.
  • Los errores en el código de la aplicación eran comunes debido a la numeración inusual de los meses y la falta de seguridad de tipos”.

Java 8 finalmente resuelve estos problemas de larga data, con el nuevo paquete java.time , que contiene clases para trabajar con fecha y hora. Todos ellos son inmutables y tienen API similares al popular framework Joda-Time, que casi todos los desarrolladores de Java usan en sus aplicaciones en lugar de los nativos Date , Calendar y TimeZone .

Estas son algunas de las clases útiles en este paquete:

  • Clock : un reloj para indicar la hora actual, incluido el instante actual, la fecha y la hora con la zona horaria.
  • Duration y Period : una cantidad de tiempo. La Duration utiliza valores basados ​​en el tiempo, como "76,8 segundos, y el Period , basado en fechas, como "4 años, 6 meses y 12 días".
  • Instant : un punto instantáneo en el tiempo, en varios formatos.
  • LocalDate , LocalDateTime , LocalTime , Year , YearMonth : una fecha, hora, año, mes o alguna combinación de los mismos, sin zona horaria en el sistema de calendario ISO-8601.
  • OffsetDateTime , OffsetTime : una fecha y hora con un desplazamiento de UTC/Greenwich en el sistema de calendario ISO-8601, como "2015-08-29T14:15:30+01:00".
  • ZonedDateTime : una fecha y hora con una zona horaria asociada en el sistema de calendario ISO-8601, como "1986-08-29T10:15:30+01:00 Europa/París".

API JAVA 8 TIEMPO

A veces, necesitamos encontrar alguna fecha relativa como "primer martes del mes". Para estos casos, java.time proporciona una clase especial TemporalAdjuster . La clase TemporalAdjuster contiene un conjunto estándar de ajustadores, disponibles como métodos estáticos. Estos nos permiten:

  • Encuentra el primer o último día del mes.
  • Encuentra el primer o último día del mes siguiente o anterior.
  • Encuentra el primer o último día del año.
  • Encuentra el primer o último día del año siguiente o anterior.
  • Encuentre el primer o último día de la semana dentro de un mes, como "primer miércoles de junio".
  • Encuentre el día de la semana siguiente o anterior, como "el próximo jueves".

Aquí hay un breve ejemplo de cómo obtener el primer martes del mes:

 LocalDate getFirstTuesday(int year, int month) { return LocalDate.of(year, month, 1) .with(TemporalAdjusters.nextOrSame(DayOfWeek.TUESDAY)); }
¿Sigues usando Java 7? ¡Consiga con el programa! #Java8
Pío

Java 8 en Resumen

Como podemos ver, Java 8 es un lanzamiento de época de la plataforma Java. Hay muchos cambios de lenguaje, particularmente con la introducción de lambdas, lo que representa un movimiento para traer capacidades de programación más funcionales a Java. Stream API es un buen ejemplo de cómo las lambdas pueden cambiar la forma en que trabajamos con las herramientas estándar de Java a las que ya estamos acostumbrados.

Además, Java 8 trae algunas características nuevas para trabajar con programación asincrónica y una revisión muy necesaria de sus herramientas de fecha y hora.

Juntos, estos cambios representan un gran paso adelante para el lenguaje Java, haciendo que el desarrollo de Java sea más interesante y eficiente.