Java es una de los lenguajes de programación más extendidos y utilizados, no sólo del momento, sino desde la llegada de los llamados «Lenguajes de Alto Nivel». Es más, podríamos considerarlo como uno de los pilares fundamentales del mundo de la programación actual; ya que muchos frameworks que utilizamos diariamente en nuestro entorno laboral están construidos en éste lenguaje.

En este post se pretende dar una introducción de las nuevas características y herramientas que nos presenta Java 9, la cual ya está disponible para su descarga y utilización.

A grandes rasgos, en este artículo hablaremos de las novedades de Java 9 que enumeramos a continuación:

  • Pequeños cambios en el lenguaje, con actualizaciones en los lenguajes Lambda y Coin.
  • Java Platform Module System: Introduce un nuevo concepto de programación, el Módulo, el cual podemos entender como un conjunto auto-descriptivo de código y datos.
  • JShell: Java nos proporciona un CLI (Command Line Interface) para evaluar expresiones y declaraciones de Java como lenguaje de programación.
  • Mejoras G1 Garbage Collector para recolectar mayor memoria y optimizar resultados.
  • Integración en Cloud: Java 9 integra mejor con Sistemas de Contenedores, como Docker, debido a la optimización del entorno de construcción de la imagen, permitiendo la creación de imágenes Linux más pequeñas.
  • Mejoras en el API de Colecciones, haciendo más simple el manejo y creación de colecciones tales como List, Set y Map.
  • Cambio de filosofía en el lanzamiento de releases para JDK9: Oracle planea introducir nuevas releases cada seis meses, con soporte a largo plazo.

Como podemos observar, es una de las releases de Oracle más ambiciosas hasta la fecha, por lo que explicaremos, en una serie de artículos, paso por paso, cada una de las mejoras que aquí planteamos. En este primer artículo, explicaremos en profundidad los principales cambios en el lenguaje que nos aporta esta nueva release.

Pero antes de nada, refresquemos nuestros conocimientos:

¿Qué es Java?

Java, en sus inicios, fue una idea de James Gosling, Patrick Naughton, Crish Warth, Ed Frank y Mike Sheridan surgida en 1991 en una empresa llamada, por aquel entonces, Sun Microsystems. Inicialmente, el lenguaje se denominó Oak, pero en 1995 pasó a denominarse Java. Aunque parezca sorprendente, el objetivo inicial de Java no era Internet, sino la necesidad de un lenguaje independiente de las plataformas para crear software que controlara distintos dispositivos electrónicos como tostadoras, microondas y mandos a distancia.

Mientras se ultimaban los detalles de Java, surgió un segundo factor que desempeñaría un papel esencial en el futuro de Java: la Web. Si no hubiera aparecido en aquel momento, Java habría conservado su utilidad pero sería un lenguaje oscuro para programar productos electrónicos. Sin embargo, gracias a la Web, Java se impulsó a la vanguardia del diseño de lenguajes informáticos, ya que la Web también demanda programas portables (Internet es un mundo diverso y distribuido repleto de diferentes equipos, sistemas operativos y CPU).

Lo que antes era un problema secundario se convirtió en una necesidad imperante. En 1993, los miembros del equipo de diseño de Java comprendieron que los problemas de portabilidad que surgían al crear código para controladores incrustados también aparecían al intentar crear código para Internet. Por ello, Java se orientó hacia la programación para Internet. Así pues, aunque el deseo de un lenguaje de programación independiente de las plataformas fue la chispa inicial, en realidad fue Internet el responsable del éxito de Java.

Novedades en el lenguaje

Bloques Try with Resources más concisos

Un recurso (resource) es un objeto que debe ser cerrado una vez que el programa haya acabado su tratamiento. A partir de las versiones 7 y 8, Java introduce los bloques try-with-resources para gestionar el cierre de recursos de una manera más eficiente. Podríamos poner, como ejemplo de recurso, la conexión de una aplicación con una base de datos. Antes de Java 7, para cerrar estas conexiones, utilizábamos un bloque finally de la siguiente manera:

public void executeQuery(String query, String connectionUrl) {
   Connection connectionDb = null;
   PreparedStatement statement;
   ResultSet resultSet;

   try {
      connectionDb = DriverManager.getConnection(connectionUrl);
      statement = connectionDb.prepareStatement(query);
      resultSet = statement.executeQuery();

      while (resultSet.next()) {
         // do something
      }
   } catch (ClassNotFoundException e) {
      e.printStackTrace();
   } catch (SQLException e) {
      e.printStackTrace();
   } finally {
      try {
         connectionDb.close();
      } catch (SQLException e) {
         e.printStackTrace();
      }
   }
}

El código es bastante duro para únicamente realizar operaciones con base de datos. Gracias a Java 7 y, con ello, la aparición de los try-with-resources, podemos reducir el código de la siguiente manera:

public void executeQuery(String query, String connectionUrl) {
   try (Connection connectionDb = DriverManager.getConnection(connectionUrl);
      PreparedStatement statement = connectionDb.prepareStatement(query);
      ResultSet resultSet = statement.executeQuery()) {

      while (resultSet.next()) {
         // do something
      }
   } catch (SQLException e) {
      e.printStackTrace();
   }
}

Pero, ¿donde se está cerrando la base de datos? Esta nueva modificación se puede utilizar con todos aquellos objetos que implementan la interface AutoCloseable:

public interface AutoCloseable {
   void close() throws Exception;
}

El objeto ResulSet tiene, como una de sus implementaciones, la interface AutoCloseable. Así que el core de Java aprovecha ésta implementación para cerrar la conexión con la base de datos cuando se termina el bloque try-with-resources.

Si observamos bien la implementación anterior, debemos declarar una nueva variable a la entrada del bloque try-wit-resources. Con Java 9 podemos utilizar esta funcionalidad sin tener que declarar ninguna nueva variable a la entrada del bloque, simplemente podemos utilizarla si la hemos declarado previamente como final o «effectively final», la cual establece que la variable no puede ser modificada después de ser inicializada:

public void executeQuery(String query, String connectionUrl) {
       Connection connectionDb;
       PreparedStatement statement;

       try {
           connectionDb = DriverManager.getConnection(connectionUrl);
           statement = connectionDb.prepareStatement(query);
           ResultSet resultSet = statement.executeQuery();

           try (resultSet) {
               while (resultSet.next()) {
                       // do something
               }
           }
       } catch (SQLException e) {
           e.printStackTrace();
       }
}

En este caso, al ser la variable resultSet «effectively final», no puede ser modificada dentro del bloque try-with-resources.

Si quieres sabe más sobre el bloque try-with-resources, puedes visitar éste enlace para Java 8 y versiones anteriores.

Permitir @SafeVarargs en métodos privados

Imagina que tienes el siguiente código en tu programa:

List<List<String>> monthInTwoLanguages =
           Arrays.asList(Arrays.asList("January", "February"),
                   Arrays.asList("Enero", "Febrero"));

Como podemos observar, no hay nada erróneo, pero sí que obtenemos un warning del compilador en nuestro IDE comentando que estamos usando operaciones no seguras o no chequeadas. Para resolver ese warning, Java 7 nos provee de la anotación @SaveVarargs, la cual nos permite remover éste tipo de mensajes en nuestro entorno de trabajo.

Hasta Java 9, la anotación @SafeVarargs sólo podía ser aplicada a métodos que no podían ser sobre escritos (override). Esto incluye los siguientes:

  • métodos final
  • métodos static
  • Constructores
  • A partir de ahora, métodos privados

Inferencia de tipos con el operador Diamante en clases anónimas

A partir de Java 5, los tipos de datos Genéricos fueron introducidos. Los cuales nos permiten parametrizar una clase en función de el tipo de dato que queramos utilizar, únicamente especificando este último a la hora de instanciar el objeto.

Al utilizar Genéricos en nuestras definiciones, muchas de las veces las expresiones no eran fácilmente legibles y el código empezaba a ser demasiado tedioso:

Map<String, List<Map<String, Map<String, Integer>>>> cars
              = new HashMap<String, List<Map<String, Map<String, Integer>>>>();

A partir de Java 7 se introduce el operador Diamante. El cual añade el tipo inferido y reduce verbosidad a nuestras expresiones, dejando al compilador la tarea de determinar el tipo de dato adecuado que coincida con la invocación:

List<String> cars = new ArrayList<>();

A partir de aquí, podemos dividir los tipos de datos en dos grandes grupos:

  • Aquellos tipos de datos que podemos usar de forma explícita en un programa Java, tal como int o String, son denominados tipos denotables.
  • Aquellos tipos que el compilador utiliza internamente que no pueden ser usados explícitamente, se denominan tipos no denotables.

Estos últimos, pueden ocurrir como el resultado de la inferencia del operador diamante a la hora de establecer el tipo adecuado para una expresión. Debido a que el tipo inferido (deducido) por el compilador que usa el diamante con un constructor anónimo podría estar fuera del conjunto de tipos soportados por el atributo de firma en una clase. En Java 7 no se permite usar el operador diamante con clases anónimas.

Es decir, si tenemos la siguiente clase abstracta:

public abstract class GenQueue<T> {

    List<T> elements;

    public GenQueue(List<T> elements) {
        this.elements = elements;
    }

    // Adds an element
    public void put(T element) {
        if(this.elements == null) {
            this.elements = new ArrayList<T>();
        }

        this.elements.add(element);
    }

    // Gets the first element
    abstract T getFirst();
}

Si creamos una instancia de esta clase, en Java 7 no podremos utilizar el operador diamante, como ilustra el siguiente código:

GenQueue<Integer> integerQueue = new GenQueue<Integer>(new ArrayList<Integer>(1)) {

    @Override
    Integer getFirst() {
        // TODO
        return null;
    }
}

Mientras que en Java 9 se podrá utilizar el operador diamante siempre que el tipo a utilizar sea un tipo denotable, como en el mismo ejemplo que el anterior, pero ya adaptado:

GenQueue<Integer> integerQueue = new GenQueue<>(new ArrayList<>(1)) {

    @Override
    Integer getFirst() {
        // TODO
        return null;
    }
};

El carácter underscore (_) ya no es un nombre válido

En Java podemos crear identificadores que comienzan con una letra, el signo de dólar ($) o el carácter underscore («_»), pero no pueden comenzar con un número.

Principalmente, podemos utilizar el carácter underscore en identificadores de las maneras siguientes:

  • Conectar dos palabras diferentes, en el estilo conocido como snake case.
  • Definir constantes.
  • Describir identificadores de forma útil, sobre todo en test unitarios.
  • Definir variables privadas, métodos, etc.

Antes de Java 8, podíamos utilizar el carácter underscore solo en identificadores. No era la mejor práctica, pero funcionaba sin ningún tipo de problemas. A partir de Java 8, se introdujo un mensaje warning en el compilador avisándonos de que utilizar underscore como identificador no es una práctica recomendada y se desaconseja su uso.

En Java 9, el identificado underscore está deshabilitado, por lo que, al utilizarlo, tendremos un error en tiempo de compilación.

Definición de métodos privados en interfaces

En Java 8, como parte del proyecto Lambda, apareció el concepto de las implementaciones de métodos de interfaces por defecto, los cuales no eran abstractos y, por tanto, tenían el cuerpo de la implementación en la misma definición de la interface. Las nuevas novedades de Java 8 incluían métodos static y public default en las interfaces:

public interface Queue<T> {

    void put(T element);

    T getFirst();

    default void printFirstElement(T element) {
        System.out.println("This is the element: " + getFirst().toString());
    }
}

Al nivel de la Máquina Virtual de Java (JVM), podíamos tener métodos privados que ayudan a la implementación de las expresiones lambda, así como otras muchas tareas. Sin embargo, no podíamos definirlos como código fuente en una interface, ya que por la propia naturaleza de una interface, un método privado no estaba permitido. En el siguiente ejemplo, ya podemos observar cómo se puede definir un método privado dentro de una interface:

public interface Queue<T> {

    void put(T element);

    T getFirst();

    default void printFirstElement(T element) {
        System.out.println("This is the element: " + getFirst().toString());
    }

    private List<T> createQueue() {
        return new LinkedList<T>();
    }
}

El objetivo de este tipo de métodos es mejorar la reutilización de código entre interfaces. Por ejemplo, si tenemos dos métodos default definidos en una interface, los cuales comparten una parte común, podemos utilizar un método privado dentro de esa interface para compartir esa parte común y no tener que exponer ese método en una clase implementada.

El uso de métodos privados en las interfaces tiene cuatro reglas:

  • No puede ser abstract.
  • Se puede usar solo dentro de la interface.
  • No se puede utilizar dentro de métodos static private.
  • Y si además es static se puede utilizar dentro de otros métodos de interface static y non-static.

Conclusiones

Como vemos, los cambios en el lenguaje siguen una progresión que inició Java 8 y que en su versión 9, Java trata de dar continuidad con un solo objetivo: hacer el lenguaje completamente modularizable, adaptándolo a las nuevas especificaciones y tecnologías existentes en el lenguaje.

En los siguientes capítulos hablaremos de la siguiente nueva característica de Java 9, la nuevo Java Platform Module System.