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
- 2. Entorno
- 3. Instalación
- 4. Ejecución
- 5. Conclusiones
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:
- Vamos al sitio: https://jdk9.java.net/download/
- Selecionamos la version que queramos bajar, en mi caso con Ubuntu 15.10 elijo la de Linux 64bits.
- Esperamos pacientemente a la descarga.
- Descomprimios con:
1tar -xvf jre-9-ea+108_linux-x64_bin_tar.gz
- Lo movemos donde queramos:
1mv jdk-9 ~/dev/jdk-9
4. Ejecución
No puede ser más sencilla:
- Vamos al directorio del jdk. En mi caso:
1cd ~/dev/jdk9
- Accedemos al directorio bin:
1cd bin
- Ahí podemos ver todos los programas que incluye el Java Development Kit 9 en esta build 108:
1ls -la
Entre ellos se encuentra jshell - Lo ejecutamos como cualquier programa en Ubuntu:
1./jshell
ejecutamos el primer comando para ver la ayuda:
1/help
Para salir simplemente podemos escribir:
1 |
/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:
1 |
int width=5 |
El resultado es el siguiente
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:
1 |
/vars |
. El resultado es el siguiente:
Vamos a añadir ahora el alto mediante la instrucción:
1 |
int height=3 |
Ahora tendremos las dos variables:
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:
1 |
public int getArea(int a, int b){return a*b;} |
El resultado:
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:
1 |
/methods |
. Puedes escribir solamente /m y pulsar el tabulador :). El resultado es:
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:
1 |
getArea(width,height) |
Nos da:
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á:
1 |
/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:
1 |
/list |
Esto producirá:
No es mucho pero quizá nos sea útil para el futuro. Vamos a guardarlo en un fichero en nuestro disco:
1 |
/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:
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:
1 |
/reset |
El resultado del comando es una nota informativa:
Si compruebo las variables y métodos existentes, evidentemente no me devuelve valor alguno, lo que significa que nuestro reset ha tenido éxito.
Tranquilo, puedo recuperar el estado anterior mediante la carga del script que acabo de salvar en la consola:
1 |
. 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
Si pruebo a ver las variables, veremos que están cargadas como antes:
1 |
/var |
Sí, también se puede usar «/vars», es indiferente
Otra opción es salir de la consola a la bash del sistema:
1 |
/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 :).
1 |
./jshell ~/dev/jshell/rectangle.repl |
El resultado, acompañado de una consulta a las variables del JShell State no podría ser otro:
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í:
1 2 3 4 5 |
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:
4.3. Clases y referencias futuras
Vamos ahora a crear una clase básica Coche con el siguiente código:
1 2 3 4 5 6 7 |
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:
Y ahora comprobamos que esta clase está dada de alta en el JShell State:
1 |
/classes |
Como resultado tenemos:
Bien, ahora podemos hacer un par de instancias de coche como haríamos en Java:
1 2 |
Coche coche1 = new Coche("Toyota", "Auris"); Coche coche2 = new Coche("Lexus", "IS300h"); |
El resultado junto con la comprobación de variables es:
Y cómo no, podemos consultar los valores de los atributos de los coches con sus getters:
1 |
coche1.getMarca(); |
Da como resultado:
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:
1 2 3 4 5 6 7 8 9 |
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:
¿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:
1 |
/vars |
Da como resultado:
Efectivamente están desactivados los objetos coche1 y coche2 y no se puede hacer uso de llos 🙁
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:
1 |
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:
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:
1 |
double[] numbers = {12.3,1,3,6.2, 23.1, 7.8} |
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
1 |
~/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:
1 |
/classpath ~/dev/commons-math3-3.6/commons-math3-3.6.jar |
Y como resultado del comando tenemos:
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.
1 |
import org.apache.commons.math3.stat.StatUtils |
Si escribimos la clase estática StatUtils. y damos al tabulador, podremos ver qué tiene:
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»:
1 |
double media = StatUtils.mean(numbers) |
Y si consultamos el valor de la variable media, será 8.9:
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.