De Java 8 a Java 11, ¿aún no te has migrado?

4
93180
  1. Introducción
  2. Entorno
  3. Filosofía
  4. Modularización
  5. Inferencia de tipos
  6. Nuevos métodos en las colecciones
  7. Nuevos métodos en las Streams
  8. Nuevos métodos en los Optional
  9. Métodos privados en interfaces
  10. Comando jshell del terminal
  11. Comando java del terminal
  12. Nuevos recolectores de basura
  13. Paquetes eliminados
  14. Docker
  15. Otras funcionalidades
  16. Conclusiones

Introducción

Hemos visto el importante cambio de filosofía en la liberación de versiones de Java, desde la versión 9 de java liberada en septiembre de 2017 se van a liberar versiones por calendario y no por funcionalidades, de esta forma cada 6 meses se liberará una versión de java, y esto se ha cumplido hasta la fecha. En marzo de 2018 se publicó Java 10 y en septiembre del mismo año Java 11.

También se ha llegado al acuerdo de que cada año y medio saldrá una versión LTS de Java que incluirá un soporte a largo plazo, es el caso de Java 11 y está previsto que el soporte sea de 8 años, hasta 2026.

Pero esto implica que ya no hay soporte de Java 9 y Java 10, por lo tanto, intentaremos ver todos los cambios juntos de Java 8 a Java 11. A esto se une que también se acababa el soporte de java 8 en enero de este año 2019.

Con esta nueva filosofía tanto los desarrolladores que quieran usar la nueva funcionalidad cuanto antes, como las empresas que necesitan tener un largo tiempo de soporte, pueden ir usando las versiones que les interese y siempre pueden anticiparse a los futuros cambios de versión sabiendo cuando va a salir la siguiente.

Entorno

  • Hardware: Portátil MacBook Pro 15′ (2.5 GHz Intel Core i7, 16GB DDR3)
  • Sistema Operativo: MacOS Mojave 10.14.2
  • OpenJDK version «11.0.1» 2018-10-16

Filosofía

Como ya hemos hablado en la introducción el cambio más importante ha sido el cambio de filosofía, en vez de sacar las siguientes versiones por funcionalidad, va a ser por calendario. Cada 6 meses tendremos una versión que solo tendrá soporte los siguientes 6 meses y cada año y medio Oracle sacará una versión LTS con soporte durante 8 años.

También hay que saber otro cambio de filosofía importante, la JDK de java va a ser de pago y por lo tanto si queremos el soporte tendremos que pagar a Oracle por ello. Aunque también vamos a tener una versión libre, la OpenJDK, mantenida por Oracle y que tiene gran apoyo de la comunidad, estas versiones tendrán soporte de 6 meses, por lo tanto, si optamos por esta solución tendremos que ir migrando nuestra versión de java cada 6 meses.

Vemos que la recomendación es pasar a Java 11 directamente desde Java 8 sin pasar por Java 9 ni por Java 10, pero hay que tener en cuenta alguna cosa, si cambiamos la versión de Java a la 11, puede que todo siga compilando, pero hay que cambiar todas nuestras frameworks o librerias como Hibernate a versiones que soporten Java 11 o podremos tener errores en runtime que antes no teníamos.

Este tutorial se ha hecho con la OpenJDK11 que se puede bajar precompilada de https://adoptopenjdk.net/?variant=openjdk11&jvmVariant=hotspot

Modularización

Los módulos permiten la encapsulación en tiempo de compilación, de esta forma podemos restringir el acceso a una serie de paquetes. Esta funcionalidad está disponible desde Java 9.

Para definir los módulos es necesario definir el fichero module-info.java en la raiz del código fuente. Este fichero puede tener una estructura parecida al siguiente:

module com.autentia.tutoriales.java11 {
    exports com.autentia.tutoriales.java11.exports;
    requires com.autentia.tutoriales.optional;
    requires com.autentia.tutoriales.interfaces;
}

Si alguien intenta usar otra clase de com.autentia.tutoriales.java11 que no estén dentro de com.autentia.tutoriales.java11.exports daría error de compilación. Además indico que el código de com.autentia.tutoriales.java11 requiere el código de los paquetes com.autentia.tutoriales.optional y com.autentia.tutoriales.interfaces para poder compilar.

Pero hay más funcionalidades que podemos usar:

open module, para tener el módulo accesible por reflexión.

open module com.autentia.tutoriales.java11 {
    exports com.autentia.tutoriales.java11.exports;
    requires com.autentia.tutoriales.optional;
    requires com.autentia.tutoriales.interfaces;
}

exports (package) to (package), para poder usar solo las clases de un paquete desde otro paquete concreto. Si además queremos exportar un paquete pero que solo se pueda usar dentro de otro paquete concreto, podríamos poner:

module com.autentia.tutoriales.java11 {
    exports com.autentia.tutoriales.java11.exports;
            to com.autentia.tutoriales.java12;
    requires com.autentia.tutoriales.optional;
    requires com.autentia.tutoriales.interfaces;
}

requires transitive equivale a hacer require de los módulos que requiere otro módulo. Si el módulo com.autentia.tutoriales.optional requiere com.autentia.tutoriales.interfaces podríamos haber puesto:

module com.autentia.tutoriales.java11 {
    exports com.autentia.tutoriales.java11.exports;
    requires transitive com.autentia.tutoriales.optional;
}

Inferencia de tipos

Este es el principal cambio de Java 10, a partir de este momento podremos utilizar var para crear objetos sin tener que definir el tipo. Aunque como sabemos, tampoco tenemos que volvernos locos poniendo var en todos los objetos y perdiendo la comprensión del código.

A partir de Java 10 podremos ver código como:

var list = List.of(1, 2, 3);
var example = "example";
var team = new Team();

Además, con Java 11, se ha añadido el uso de var en las lambdas permitiendo las anotaciones en estos parámetros, aunque no se puede mezclar el uso de var con tipos, ni de var y un tipo vacío.

Map<Integer, Integer> map = Map.of(1, 2, 3, 4, 5, 6);
map.forEach((x, y) -> LOGGER.info(x + y));
map.forEach((Integer x, Integer y) -> LOGGER.info(x + y));
map.forEach((var x, var y) -> LOGGER.info(x + y));
map.forEach((@NotNull var x, @NotNull var y) -> LOGGER.info(x + y));

map.forEach((x, var y) -> LOGGER.info(x + y)); // No compila
map.forEach((int x, var y) -> LOGGER.info(x + y)); // No compila

Nuevos métodos en las colecciones

Con la JDK de java 9, vamos a tener más facilidades para crear colecciones ya inicializadas. Podremos usar el método estático of() de List, Set, Stream o Map. Hay que recordar que estas colecciones son inmutables y si intentamos hacer un add tendremos una UnsupportedOperationException. Un ejemplo de cada una puede ser:

List<Integer> list = List.of(1, 2, 3);
Set<String> set = Set.of("a", "b", "c");
Stream<String> stream = Stream.of("a", "b", "c");
Map<String, String> map = Map.of("clave 1", "valor 1", "clave 2",  "valor 2");

LOGGER.info(list);
LOGGER.info(set);
LOGGER.info(stream.collect(Collectors.toList()));
LOGGER.info(map);

La salida por consola es:

[1, 2, 3]
[a, b, c]
[a, b, c]
{clave 2=valor 2, clave 1=valor 1}

Por otro lado, con Java 10, se han introducido los métodos copyOf() para crear copias inmutables de List, Set y Map. Al igual que con el método of(), tendremos una UnsupportedOperationException si intentamos añadir elementos. Añadiendo el código al ejemplo anterior vemos cómo hacer copias.

List<Integer> listCopyOf = List.copyOf(list);
Set<String> setCopyOf = Set.copyOf(set);
Map<String, String> mapCopyOf = Map.copyOf(map)

LOGGER.info(listCopyOf);
LOGGER.info(setCopyOf);
LOGGER.info(mapCopyOf);

La salida es:

[1, 2, 3]
[a, b, c]
{clave 2=valor 2, clave 1=valor 1}

Además, se han añadido los métodos toUnmodificableList(), toUnmodificableSet() y toUnmodificableMap() a la clase Collectors para hacer las colecciones inmutables. Los casos de uso serían:

List toUnmodifiableList = Stream.of("a", "b", "c").collect(toUnmodifiableList());
Set toUnmodifiableSet = Stream.of("g", "h", "i").collect(Collectors.toUnmodifiableSet());
Map<Integer, Integer> toUnmodifiableMap = Stream.of(1, 2, 3).collect(toUnmodifiableMap(
        num -> num,
        num -> num * 4));

LOGGER.info(toUnmodifiableList);
LOGGER.info(toUnmodifiableSet);
LOGGER.info(toUnmodifiableMap);

Esto imprimiria por consola:

[a, b, c]
[g, h, i]
{1=4, 2=8, 3=12}

Nuevos métodos en los Streams

Con Java 9, podemos usar los métodos takeWhile(), dropWhile(), iterate() y ofNullable() en los streams. Los dos primeros se usan para eliminar o escoger los primeros elementos mientras se cumple una condición, el método iterate() genera una iteración de valores y el método ofNullable() genera un Stream con un elemento si el elemento no es null o vacío. Veamos un ejemplo de uso:

Si ejecutamos:

LOGGER.info("Ejemplo takeWhile():");
List takeWhileResult = Stream.of(1, 2, 3, 4, 5).takeWhile(value -> value < 3).collect(Collectors.toList());
LOGGER.info(takeWhileResult);

LOGGER.info("Ejemplo dropWhile():");
List dropWhileResult = Stream.of(1, 2, 3, 4, 5).dropWhile(value -> value < 3).collect(Collectors.toList());
LOGGER.info(dropWhileResult);

LOGGER.info("Ejemplo iterate():");
List iterateResult = Stream.iterate(1L, n  ->  n  + 1).limit(10).collect(Collectors.toList());
LOGGER.info(iterateResult);

LOGGER.info("Ejemplo ofNullable():");
String example = "example";
List ofNullableResult = Stream.ofNullable(example).collect(Collectors.toList());
LOGGER.info(ofNullableResult);
String nullExample = null;
List ofNullableNullResult = Stream.ofNullable(nullExample).collect(Collectors.toList());
LOGGER.info(ofNullableNullResult);

Obtendremos:

Ejemplo takeWhile():
[1, 2]
Ejemplo dropWhile():
[3, 4, 5]
Ejemplo iterate():
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Ejemplo ofNullable():
[example]
[]

Nuevos métodos en los Optional

En java 9 se han añadido los métodos ifPresentOrElse(), or() y stream() para aportar funcionalidad extra a los ya conocidos métodos de Java 8. También vemos cómo los Optional avanzan con Java 10 y se añade el método orElseThrow().

Resulta bastante importante el método stream(), con este método podemos obtener un Stream con todos los Optional de una lista que tenían isPresent() a true. En la documentación pone que con stream() convertimos un Optional en un Stream de cero o un valor, pero vamos a hacer un test para ver cómo podemos usarlo. En este test convertimos un Stream de 3 Optional en un Stream de 2 Optional debido a que uno es vacío y después lo convertimos en una lista, por lo tanto, la lista tendrá tan solo 2 elementos.

    @Test
    public void streamTest() {
        Optional<String> optional1 = Optional.of("Texto 1");
        Optional<String> optional2 = Optional.empty();
        Optional<String> optional3 = Optional.of("Texto 3");

        List<String> textos = Stream.of(optional1, optional2, optional3)
                .flatMap(optional -> optional.stream()).collect(toList());

        assertEquals(2, textos.size());
        assertEquals("Texto 1", textos.get(0));
        assertEquals("Texto 3", textos.get(1));

    }

Con el método ifPresentOrElse() podemos añadir funcionalidad al ya existente ifPresent para ejecutar una función en caso de que el optional esté vacío. Lo vemos con el siguiente test.

    @Test
    public void ifPresentOrElseTest() {
        Optional<String> optionalConUnTexto = Optional.of("Un texto");
        Optional<String> optionalVacio = Optional.empty();

        optionalConUnTexto.ifPresentOrElse(
                texto ->{
                    System.out.printf("El optional con texto debería pasar por el present.n");
                    assertEquals("Un texto", texto);
                },
                () -> {
                    throw new RuntimeException("El optional con texto nunca debería pasar por el else");
                }
        );

        optionalVacio.ifPresentOrElse(
                texto -> {
                    throw new RuntimeException("El optional vacío nunca debería pasar por el present");
                },
                () -> System.out.printf("El optional vacío debería pasar por el else")
        );

    }

El primer ifPresentOrElse entra por el present al tener texto y el segundo ifPresentOrElse entra por el else al no tener texto. Si hubiera sido de otra forma nos hubiera saltado una excepción, pero lo que hace es imprimir por pantalla el siguiente texto y terminar satisfactoriamente.

El optional con texto debería pasar por el present.
El optional vacío debería pasar por el else.

Como último método añadido en Java 9 tenemos el método or(), con este método podemos devolver un Optional en caso de que no esté presente. Con un test vemos su uso.

    @Test
    public void orTest() {
        Optional<String> optionalConUnTexto = Optional.of("Un texto");
        Optional<String> optionalVacio = Optional.empty();

        Optional orOptionalConUnTexto = optionalConUnTexto.or(()->Optional.of("Optional vacío"));
        Optional orOptionalVacio = optionalVacio.or(()->Optional.of("Optional vacío"));

        assertEquals("Un texto", orOptionalConUnTexto.get());
        assertEquals("Optional vacío", orOptionalVacio.get());
    }

Para ver el último método, orElseThrow(), probamos cómo se lanza una excepción en caso de que el Optional sea vacío.

    @Test(expected = Exception.class)
    public void orElseThrowTest() throws Exception{
        Optional<String> optionalVacio = Optional.empty();

        optionalVacio.orElseThrow(()-> new Exception("El optional era nulo"));
    }

En el uso de Optional como todo en Java, hay que hacerlo con cabeza, no hay que usarlo de forma que cambiemos el antiguo

public void method(String value){
    if (value == null) return;
    ...

por un

public void method(Optional<String> value){
    if(!value.isPresent()) return;
    ...

En este apartado hemos visto cómo poco a poco Java nos va dando más funcionalidad de Optional para que nuestro código quede más legible y no este lleno de if por todos lados.

Métodos privados en interfaces

Como ya vimos con Java 8, es posible añadir métodos por defecto en interfaces, tenemos que intentar no hacerlo continuamente, pero es una posibilidad más que nos da el lenguaje. Después de permitir estos métodos, en Java 9 nos dan la posibilidad de tener métodos privados dentro de las interfaces para facilitar la legibilidad de código y que no se hagan métodos por defecto infinitos.

Comando jshell del terminal

En java 9 tenemos un nuevo comando disponible en nuestro terminal, el comando jshell. Con este comando podemos probar directamente por consola cualquier sentencia de java sin necesidad de un IDE. Como vemos en el siguiente ejemplo, podemos usar variables, realizar imports o cualquier sentencia que se nos ocurra. Además nos da detalles si encuentra algún error en el código.

Ejemplo de jshell

Comando java del terminal

Además del ya incluido jshell, con Java 11, podemos ejecutar ficheros java desde consola. Si creamos el fichero HolaMundo.java.

public class HolaMundo {
    public static void main(String[] args) {
        System.out.println("Hola mundo");
    }
}

Con el terminal entramos en la carpeta del fichero y ejecutamos:

java HolaMundo.java

Veremos en la salida por el terminal:

Hola mundo

Nuevos recolectores de basura

Aunque este cambio puede parecer transparente para los programadores, se ha cambiado el recolector de basura por defecto. A partir de Java 9, será el «G1 Garbage Collector», este recolector está optimizado para ofrecer un balance adecuado entre baja latencia y alto rendimiento. Por otro lado, con Java 11, se añade un recolector de basura que no reclama memoria, el recolector de basura Epsilon y de forma experimental el recolector de basura ZGC.

Paquetes eliminados

Con la versión de Java 11 se han eliminado una serie de paquetes.

  • java.corba (CORBA)
  • java.se.ee (Aggregator module for the six modules above)
  • java.transaction (JTA)
  • java.xml.ws (JAX-WS, plus the related technologies SAAJ and Web Services Metadata)
  • java.activation (JAF)
  • java.xml.bind (JAXB)
  • java.xml.ws.annotation (Common Annotations)
  • jdk.xml.bind (Tools for JAXB)
  • jdk.xml.ws (Tools for JAX-WS)

Docker

Hay varias funcionalidades que pueden afectar a Docker, en primer lugar, usando jlink de Java 9, podemos ensamblar y optimizar un conjunto de módulos y sus dependencias en una imagen personalizada en tiempo de ejecución generando así imagenes más pequeñas.

Además, con java 10 instalado sobre docker, podemos poner el número de cores que usa el programa java para ejecutar. Antes de esto, docker usaba todos los cores disponibles en la máquina.

Otras funcionalidades

Además de las ya citadas hay una serie extra de funcionalidades como:

  • Nuevos métodos  repeat, strip, stripLeading, stripTrailing, isBlank y lines en la clase String. (Java 11)
  • Unicode añade nuevos carácteres, emojis y símbolos. (Java 11)
  • Existen nuevos versionados de Jars con la posibilidad de multiversionado. (Java 9)
  • Hay nuevos métodos para identificar procesos pid(), sus hijos children() y sus descendientes descendants(). (Java 9)
  • Las nuevas clases Flow.Processor, Flow.Subscriber, Flow.Publisher Flow que permiten la programación reactiva de publicación-subscripción. (Java 9)

Se pueden ver todas las funcionalidades en:

Conclusiones

Durante el tutorial hemos visto las funcionalidades más destacables que se han incluido desde Java 8 hasta Java 11, pero nos damos cuenta de que lo más importante es el cambio de filosofía y hay que estar atentos a las nuevas versiones, especialmente si usamos la OpenJDK ya que hay que ir migrando de versión cada 6 meses para poder tener soporte. 

Es importante pasar a Java 11 debido a que ya no hay soporte oficial de Java 8, pero para poder migrar con seguridad es necesario que nuestro código tenga una buena red de test para asegurar que todo funciona correctamente en tiempo de ejecución.

4 COMENTARIOS

  1. En resumidas cuentas:
    – pasarnos a OpenJDK si no queremos pasar por caja.
    – pasarnos a OpenJDK 11 si no queremos estar desfasados y sin soporte.
    – Leer Adictos al Trabajo para estar actualizados.

    Muchas gracias por el artículo. 😉

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad