jShell: una consola REPL como novedad en Java 9

3
14152

En este tutorial vamos a ver una de las herramientas que incorporará por primera vez Java9 al JDK: una consola REPL llamada jShell.

0. Índice de contenidos.

1. Introducción

El mundo de los lenguajes de programación está cambiando a pasos agigantados en los últimos años:

  • La evolución de la potencia de los ordenadores ya no penalizan tanto a los lenguajes interpretados frente a los compilados.
  • Se retoman viejos paradigmas de programación, como la programación funcional.
  • Los lenguajes cada vez son de más alto nivel: basados en multitud de ĺibrerías.

… Y claro está, el mundo Java no ha sido ajeno a esta evolución: han aparecido lenguajes que son capaces de ejecutarse en la JVM, como Groovy o Scala por citar un par de ejemplos.

Estos nuevos lenguajes de la JVM son interpretados (al estilo Javascript) y tienen un tipado más relajados, lo que les permite un desarrollo más «informal» pero a la vez más productivo y creativo: experimentar con el código es más sencillo y no hay que cumplir las estrictas reglas de un lenguaje fuertemente tipado como Java (aunque luego sean menos mantenibles, a prori).

Y una herramienta básica para el desarrollo en estos lenguajes es la consola REPL, que permite experimentar con el código de forma directa, obteniendo una respuesta inmediata al ejecutar el código directamente y sin tener por qué incluir instrucciones específicas para la salida por la consola. Así, aprender nuevas funcionalidades o probar librerías es algo mucho más sencillo.

Muchos echamos de menos una funcionalidad de consolar REPL en Java, y aunque ha habido intentos, como por ejemplo el JSR-223 y la implementación BeanShell, hace ya unos cuantos años, nunca llegaron a cuajar. Así nos tenemos que conformar, cuando nos surje alguna duda que queremos solucionar con código, con hacer clases temporales con public void main; hacer un test; o probar alguna funcionalidad de nuetro IDE como por ejemplo los ScrapBooks en Eclipse.

Afortunamente dentro del JCP para Java 9 se ha retomado la idea de una consola REPL para Java, incluyendo el JEP 222: «jshell: The Java Shell (Read-Eval-Print-Loop)». Así que ya no nos tendremos que ir a Groovy para tocar un poco de la consola Java 🙂

Nota: En el momento de desarrollo de este tutorial todavía no se ha publicado la versión definitiva de Java9, pero sí que está disponible la versión JDK 9 Early Access Release, que incluye la consola jShell dentro de sus herramientas.

1.1 ¿Para qué sirve la consola JShell de Java9?

Para experimentar con el código principalmente. No tiene como objetivo crear un nuevo lenguaje de programación interpretado: solamente ejecutará código Java (del Java Language Specification – JLS) expresado en «Snippets», que son los comandos que se escriben en el prompt.

El objetivo primordial es eliminar el ciclo: crear una clase con el método public static void main -> escribir algo que imprima a consola con System.out.println -> compilar -> ejecutar para ver el resultado.

1.2 ¿Qué es una consola REPL?

Una consola es un programa que se ejecuta en la shell del sistema (modo texto) y que tiene un prompt en el cual escribimos comandos que se encarga de ejecutar.

En este caso concreto, jShell, se trata de una consola que sigue el sistema REPL: Read-Eval-Print-Loop. Es decir:

  • Read: lee lo que introducimos en el prompt de la consola.
  • Eval: evalúa o ejecuta lo que hemos escrito.
  • Print: muestra una salida por pantalla como consecuencia de lo evaluado, bien porque el comando implica de forma explícita sacar algo por pantalla o porque la propia consola nos ayuda con más información.
  • Loop: vuelve al primer estado, a la espera de que el usuario introduzca más comandos para ser leídos, evaluados y mostradas las consecuencias en la pantalla.

Quizá estés pensando en que esto tiene más que ver con un lenguaje interpretado que con uno compilado (aunque a ByteCode) como es Java. No te preocupes, se han definido una serie de peculiaridades en la consola de Java para que pueda funcionar.

1.3. Peculiaridades de la consola JShell y el JShell State

Efectivamente, no es lo mismo escribir comando a comando en la consola que hacerlo en un fichero por clase y que luego sea compilado. Hay algunas cosas que no nos cuadran con la concepción natural de Java y que varían un poco respecto a lo que estamos acostumbrados, pero ¡ojo! La consola jShell no crea un dialecto nuevo de Java.

Para ello hay que tener en cuenta que el código no se ejecuta dentro de un método public static void main(Strings… args), sino que se hace dentro de una clase llamada JShell que es el motor de ejecución de lo que escribimos en la consola y que le dota de algunas particularidades. Vamos a verlas:

  • Los fragmentos de código que se introducen son llamados snippets y corresponden a fragmentos del Java Language Specification (JLS).
  • Se pueden realizar estas operaciones:
    • Hacer imports de clases.
    • Declarar clases o interfaces.
    • Declarar métodos, variables y campos sin estar en niguna clase (estarán en una clase «sintética» en el JShell State como miembros estáticos).
    • Declarar sentencias y sentencias primarias del JLS
  • Mantiene el estado de todas las operaciones llevadas a cabo con anterioridad y por tanto podemos hacer referencias a variables, métodos, clases, imports…
  • Referencias futuras:
    • Por ejemplo, dentro de una clase se pueden hacer referencias a elementos que todavía no están resueltos porque vienen a continuación: quedan en estadounresolved.
    • Se pueden hacer cambios, por ejemplo,en clases que ya han sido instanciadas. Estos cambios deben ser compatibles.

Además tiene algunas funcionalidades de consola muy interesantes:

  • Se puede alterar el classpath en tiempo «de ejecución consola» para acceder a nuevas librerías .jar. Basta con hacer un import y ya estará disponible.
  • Tiene autocompletado (con el tabulador), como lo tiene cualquier IDE de Java, así tendremos más fácil saber los métodos de una clase.
  • Dispone de comandos, consultables con /help, para cargar, borrar o guardar el estado de la consola. Así, es posible cargar/salvar un fichero con instrucciones, o volver al estado inicial.

Finalmente señar que es posible usar el motor JShell desde código Java. Dispone de una API a través de la cual se instancia el JShell State y se pueden enviar comandos para la ejecución, incluso dispone del clásico método eval(String input) de cualquier lenguaje interpretado.

Pero vamos a dejarnos de teoría, lo mejor será que lo veamos en la práctica:

2. Entorno

Para realizar este tutorial se ha empleado el siguiente entorno de desarrollo:

  • Hardware: Acer Aspire One 753 (2 Ghz Intel Celeron, 4GB DDR2)
  • Sistema Operativo: Ubuntu 15.10
  • Java 9 – JDK 9 Early Access Release Build 108 de 64 bits.

3. Instalación

La aplicación de consola forma parte de las herramientas del JDK 9, asi que la instalación es muy sencilla.

Nos vamos a basar en la última revisión disponible de Java 9 en el momento de escribir estas línas: JDK 9 Early Release Build 108. La instalación es muy fácil:

  1. Vamos al sitio: https://jdk9.java.net/download/
  2. Selecionamos la version que queramos bajar, en mi caso con Ubuntu 15.10 elijo la de Linux 64bits.
  3. Esperamos pacientemente a la descarga.
  4. Descomprimios con:
    tar -xvf jre-9-ea+108_linux-x64_bin_tar.gz
  5. Lo movemos donde queramos:
    mv jdk-9 ~/dev/jdk-9

4. Ejecución

No puede ser más sencilla:

  1. Vamos al directorio del jdk. En mi caso:
    cd ~/dev/jdk9
  2. Accedemos al directorio bin:
    cd bin
  3. Ahí podemos ver todos los programas que incluye el Java Development Kit 9 en esta build 108:
    ls -la

    Entre ellos se encuentra jshell

    jshell_ls

  4. Lo ejecutamos como cualquier programa en Ubuntu:
    ./jshell

    ejecutamos el primer comando para ver la ayuda:

    /help

    y el resultado es el siguiente:

    jshell_completo

  5. Para salir simplemente podemos escribir:

    /exit

    4.1. Ejemplo básico: área de un rectángulo

    Comenzaremos con un ejemplo básico de declaración de variables y métodos. Un ejemplo sencillo:

    int width=5

    El resultado es el siguiente

    jshell_int5

    Seguro que te llama la atención que no hemos puesto el símbolo «;» de final de instrucción. Es otra de las características que tiene JShell a la hora de introducir comandos para hacer las cosas más sencillas. No te preocupes, lo autocompleta él mismo.

    Otra cosa que te debería haber llamado la atención es la respueta de la consola indicándonos el resultado de la operación. Puede ser trivial porque estés acostumbrado a las consolas, pero esto es un elemento disonante respecto a Java: ¿acaso no tienes que completar el código con System.out.println(width) para saber el valor de la variable?

    ¿Dónde se ha establecido esta variable? ¿En qué clase? Pues en una clase virtual del JShell State… por tanto no es una clase normal de Java. Es una clase para poder interactuar con la Shell.

    Podemos listar las variables que hay en el JShell State mediante el comando:

    /vars

    . El resultado es el siguiente:

    jshell_var

    Vamos a añadir ahora el alto mediante la instrucción:

    int height=3

    Ahora tendremos las dos variables:

    jshell_2var

    Es el momento ahora de declarar un método. De igual modo que las variables, los métodos o funciones que declaremos se ubicarán en esta clase virtual que mantiene JShell State, de modo que pasará a ser un método estático. Usamos esta instrucción:

    public int getArea(int a, int b){return a*b;}

    El resultado:

    jshell_getArea

    Te habrá llamado la atención el «warning». Efectivamente, el modificador «public» no tiene sentido en este tipo de métodos o variables que él llama «top-level». Lo elimina de forma automática.

    Como sucedía con las variables, hay un comando para ver los métodos que tiene declarada la clase virtual del JShell State:

    /methods

    . Puedes escribir solamente /m y pulsar el tabulador :). El resultado es:

    jshell_methods

    Vemos que hay dos métodos, uno printf, que ya viene con el sistema de la consola, y otro que hemos introducido nosotros… ¿lo ejecutamos?

    Como puedes suponer, ejecutarlo es muy fácil. Atiende a que ahora no hemos asignado el resultado a ninguna variable, ni tampoco usamos «;» … veamos qué sucede:

    getArea(width,height)

    Nos da:

    jshell_areares

    Como puedes leer en el mensaje, al no haber asignado el resultado de la función a ninguna variable, nos ha creado una llamada «$4» en la que reside el valor. Si volvemos a ver las variables de la shell, aparecerá:

    /vars

    jshell_vars

    Esa variable puede ser tratada como una variable más, como otra cualquiera.

    4.2. Gestionando los Scripts

    Vamos a continuar con el ejemplo de cálculo de área para ver unas herramientas sencillas para tratar con el script.

    Imagina que esto es una prueba seria y lo que hemos hecho nos es de utilidad… ¿Qué podemos hacer? Pues lo podemos listar y salvar a fichero para reutilizarlo en el futuro. Vamos con ello.

    Para listar los snippers que han resultado útiles podemos hacerlo con:

    /list

    Esto producirá:

    jshell_list

    No es mucho pero quizá nos sea útil para el futuro. Vamos a guardarlo en un fichero en nuestro disco:

    /save ~/dev/jshell/rectangle.repl

    He señalado un fichero cualquiera en disco. Si compruebo desde la shell de mi Ubuntu el contenido, obtendré algo similar a esto:

    jshell_cat

    Una vez que he salvado el trabajo, podría pensar en que necesito empezar de cero otro experimento de código, así que voy a reiniciar el entorno:

    /reset

    El resultado del comando es una nota informativa:

    jshell_reset

    Si compruebo las variables y métodos existentes, evidentemente no me devuelve valor alguno, lo que significa que nuestro reset ha tenido éxito.

    jshell_emptySpace

    Tranquilo, puedo recuperar el estado anterior mediante la carga del script que acabo de salvar en la consola:

    
    

    . Afortunadamente con la tecla «tab» podemos completar la ruta fácilmente.. El resultado es el esperado, incluyendo mensaje de warning por el modificador «public» del método

    jshell_open

    Si pruebo a ver las variables, veremos que están cargadas como antes:

    /var

    Sí, también se puede usar «/vars», es indiferente

    jshel_vars2

    Otra opción es salir de la consola a la bash del sistema:

    /exit

    y hacer la carga del fichero como parámetro de la consola. Esto nos dará la posibilidad de hacer scripts en Java y ejecutarlos como sucede en muchos otros lenguajes de scripts, teniendo la potencia de acceder a las clases de Java :).

    ./jshell ~/dev/jshell/rectangle.repl

    El resultado, acompañado de una consulta a las variables del JShell State no podría ser otro:

    jshell_open_ext

    Y ya si en nuestro script pondemos un /exit al final, quedará como la ejecución de un programa en la consola del sistema, devolviéndonos el control al bash. Como Java tiene multitud de librerías, podríamos programa un script que hiciera uso de estas librerías para multitud de fines.

    Por ejemplo, si añado una instrucción para imprimir por pantalla el resultado del área (el 15) y además un /exit al final y elimino el modificador public para evitar el warning, tendríamos un código así:

    int width=5;
    int height=3;
    int getArea(int a, int b){return a*b;}
    System.out.println("El área es:" + getArea(width,height))
    /exit
    

    Y la ejecución nos pintará el valor del área y devolverá el control al bash:

    jshell_bash

    4.3. Clases y referencias futuras

    Vamos ahora a crear una clase básica Coche con el siguiente código:

    class Coche{
    private String marca;
    private String modelo;
    Coche(String marca,String modelo){this.marca = marca;this.modelo=modelo;}
    public String getMarca(){return this.marca;}
    public String getModelo(){return this.modelo;}
    }
    

    Si vamos presionando enter al final de cada línea, jShell sabe que aún no ha acabado porque analiza las llaves para ver que aún queda información. El resultado es el siguiente:

    jshell_classCoche

    Y ahora comprobamos que esta clase está dada de alta en el JShell State:

    /classes

    Como resultado tenemos:

    jshell_cocheVar

    Bien, ahora podemos hacer un par de instancias de coche como haríamos en Java:

    Coche coche1 = new Coche("Toyota", "Auris");
    Coche coche2 = new Coche("Lexus", "IS300h");
    

    El resultado junto con la comprobación de variables es:

    jshell_coches

    Y cómo no, podemos consultar los valores de los atributos de los coches con sus getters:

    coche1.getMarca();

    Da como resultado:

    jshell_toyo

    Como ha sucedido antes, si una operación da un resultado y no es asignado a ninguna variable, el propio JShell crea una y lo asigna. De nuevo le da el nombre $4 y le asigna el valor «Toyota».

    Vamos ahora a hacer una cosa que igual te rompe un poco tu esquema Java… Estamos en un entorno de scripting… ¿Por qué no redefinimos la clase Coche para meter el campo potencia?. Veamos a ver qué sucede:

    Ampliamos la declaración de coche anterior para sobreescribirla con una nueva:

    class Coche{
    private String marca;
    private String modelo;
    private int potencia;
    Coche(String marca,String modelo, int potencia){this.marca = marca;this.modelo=modelo;}
    public String getMarca(){return this.marca;}
    public String getModelo(){return this.modelo;}
    public int getPotencia(){return this.potencia;}
    }
    

    El tema se pone un poco más serio:

    jshell_coche2

    ¿Qué ha pasado? Pues claramente, ya teníamos creadas dos clases, coche1 y coche2 de la clase coche original, con su constructor con dos parámetros (marca y modelo). Ahora tenemos una redefinición de la clase Coche con un tercer elemento, la potencia, que se ha añadido al constructor, por lo que los objetos coche1 y coche2 están en serio peligro. Veamos qué ha pasado:

    /vars

    Da como resultado:

    json_coches2

    Efectivamente están desactivados los objetos coche1 y coche2 y no se puede hacer uso de llos 🙁

    jshell_coche1

    Otro caso curioso es el de las referencias futuras. Dentro de una clase se puede hacer referencias a métodos o variables que todavía no están declaradas. Por supuesto no se puede hacer uso de la clase hasta que este método/variable no esté declarado. Por ejemplo tenemos:

     class Moto{String marca;public String getMarca(){return cleanSpaces(this.marca);}}
    

    Donde «cleanSpaces», por sintaxis es un método de esa clase «Moto» pero que no está declarado. En el feedback al respecto tenemos este resultado:

    jshell_moto

    4.4. Invocando librerías

    Para concluir con este tutorial vamos a ver cómo podemos hacer uso de librerías .jar a través de jShell. Seguro que nos facilitará la vida a la hora de poder experimentar y aprender sobre las nuevas funciones, o incluso crear scripts de JShell que hagan operaciones interesantes.

    Como prueba de concepto voy a calcular los estadísticos básicos de un array de doubles haciendo uso de la librería de Apache Commons Math, que deberías descargar para hacer este ejemplo

    Comenzaremos declarando la variable del array de doubles:

    double[] numbers = {12.3,1,3,6.2, 23.1, 7.8}

    jshell_numbers

    Para continuar nos descargamos la librería Apache Commons Math y la descomprimimos para tener acceso al fichero .jar. En mi caso está ubicado en

    ~/dev/commons-math3-3.6/commons-math3-3.6.jar

    Si continuamos con la consola JShell podemos alterar el ClassPath para tener acceso a las clases del fichero .jar:

    /classpath ~/dev/commons-math3-3.6/commons-math3-3.6.jar

    Y como resultado del comando tenemos:

    jshell_cp

    Esto nos da la oportunidad de hacer un import de la clase de estadística básica que hay en esta librería. Gracias al autocomplete de Jshell es fácil si vamos dando al tabulador, tal y como hacemos en nuestro IDE favorito.

    import org.apache.commons.math3.stat.StatUtils

    Si escribimos la clase estática StatUtils. y damos al tabulador, podremos ver qué tiene:

    jshell_staty

    Desafortunadamente esta versión no muestra el signature de los métodos, sólo el nombre. Esperemos que en el futuro subsanen este problema y salga toda la información de versión…

    Como ya he mirado la documentación de Apache Commons Math, sé que hay un método estático de esa clase llamado «mean», que admite un array de doubles por parámetro. Lo aplicamos con jShell y lo asignamos a la variable «media»:

    double media = StatUtils.mean(numbers)

    jshell_media

    Y si consultamos el valor de la variable media, será 8.9:

    jshell_mediaRes

    5. Conclusiones

    Parece que Java está acelerando la máquina para adaptarse a los nuevos tiempos y luchar contra la competencia que suponen los lenguajes de nueva generación, que se aprovechan de ventajas como el disponer de una consola REPL para trabajar.

    El modo en el que se trabaja con una consola REPL para ciertas tareas de aprendizaje, pruebas y prototipado parece más creativo o directo que tener que hacerlo con un lenguaje compilado y fuertemente tipado como es Java.

    Con la adopción de un entorno de consola REPL en Java puede que comience una nueva senda en las formas de desarrollar, enriqueciendo no sólo los métodos básicos de aprendizaje para los nuevos programadores, sino que también habre nuevas posibilidades como el uso de Java y sus librerías como sistema de scripting para tareas que hasta ahora se dejaban en mano de lenguajes menos formales.

    Sea como fuere, es uno de los cambios que vendrán incluidos en Java9, y que promete continuar la senda revolucionaria que comenzó con Java 8. Sólo el tiempo y los programadores dictarán sentencia sobre la utilidad de esta funcionalidad.

3 COMENTARIOS

  1. […] Administrador de procesos: además de los procesos propios de la ejecución de la aplicación, existirán procesos de tareas de administración y mantenimiento que suelen ejecutarse una única vez. Estos procesos, aunque puedan parecer secundarios, se ejecutarán con igual importancia que el resto de los procesos de la aplicación. Además, se recomienda el uso de consolas REPL si el entorno dispone de ellas (en Java 9 habrá una). […]

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