¿Qué es JShell?

El Shell de Java o JSHell, es el REPL (read-evaluate-print-loop) oficial de Java que fue introducido en Java 9. Provee una terminal interactiva para que rápidamente podamos crear prototipos, depurar o aprender java y sus APIs sin la necesidad del proceso de crear una clase en un archivo y compilarlo para ejecutarlo. Ademas es mucho mas simple y mas practico usando la inferencia de tipos con el uso de var que conocemos de Java 10.

Uso Básico

En los ejemplos aquí expuestos usaremos Java 10 para tomar ventaja de la inferencia de tipos.

Para iniciar JShell basta con que escribamos jshell en una termina. Entonces se nos presenta un mensaje de bienvenida y estamos listos para comenzar a escribir y evaluar expresiones de Java validas.

$ jshell
|  Welcome to JShell -- Version 10.0.2
|  For an introduction type: /help intro

Ahora si queremos ejecutar un comando, simplemente lo escribimos y presionamos la tecla enter, y deberíamos ver que funciona así:

jshell> var saludo = "hola"
saludo ==> "hola"

Como se darán cuenta repite el valor de la variable saludo una vez que lo confirmamos. También hay que notar que no fue necesario colocar el punto y coma al final de la expresión. Basta con solo presionar enter y automáticamente delimitará la instrucción.

Ahora para completar vamos a declarar otra variable para saber a quien estamos saludando.

jshell> var audiencia ="mundo"
audiencia ==> "mundo"

Una característica interesante es que si presionamos la tecla TAB auto completara las palabras reservadas de Java así como también los nombres de las variables que hemos declarado en la sesión actual. De este modo para el saludo completo podemos hacer esto:

jshell> var decir = sa<tab> + aud<tab>
decir ==> "holamundo"

ademas el tabulador también nos mostrara los métodos de una clase para ahorrarnos escribirlos, así:

shell> decir.to<tab>
toCharArray()   toLowerCase( toString()      toUpperCase(

Los métodos con argumentos se mostrarán solo con un paréntesis abierto y los métodos que no tienen argumentos se mostrarán con los paréntesis cerrados.

Errores

Si comentemos errores mientras escribimos en el JShell ya sea que ingresamos una expresion invalida, metodo o comando, este provee una forma inmediata de retroalimentación en la cual nos despliega el problema.

jshell> decir.subString(0,1)
|  Error:
|  cannot find symbol
|    symbol:   method subString(int,int)
|  decir.subString(0,1)
|  ^--------------^

Si terminamos de llamar al método toUpperCase y presionamos tab una vez mas, entonces se nos presenta una lista de todas las firmas posibles para ese método:

jshell> decir.toUpperCase(
Signatures:
String String.toUpperCase(Locale locale)
String String.toUpperCase()

<press tab again to see documentation>

Y como el mensaje nos dice, si presionamos tab una tercera vez, nos mostrará la documentación correspondiente al método:

jshell> saying.toUpperCase(
String String.toUpperCase(Locale locale)
Converts all of the characters in this String to upper case ... (shortened for brevity)

Y si seguimos presionando tab, iterará en la documentación de todas las firmas disponibles para un solo método.

Paquetes

Ahora bien, en lugar de un simple hola mundo, vamos a expandir la audiencia del mensaje a otras audiencias como el universo o la galaxia. Creando una lista llamada audiencias con tres diferentes audiencias: “mundo”, “universo” y “galaxia”. Para ello podemos hacer uso del método List.of que fue introducido en Java 9.

jshell> var audiencias = new ArrayList<>(List.of("mundo", "universo", "galaxia"))
audiencias ==> [mundo, universo, galaxia]

Ahora notemos que para referenciar la clase ArrayList exitosamente, no necesitamos incluir el nombre del paquete o mejor dicho FQCN (Fully Qualified Name) o importar java.util, eso es porque por defecto JShell importa una serie de paquetes para que los usemos sin preocuparnos. Estos paquetes son:

  • java.io.*
  • java.math.*
    java.net.*
  • java.nio.file.*
    java.util.*
  • java.util.concurrent.*
    java.util.function.*
  • java.util.prefs.*
  • java.util.regex.*
  • java.util.stream.*

Como esperaríamos también podemos definir nuestros propios paquetes simplemente especificando import <nombre_del_paquete> donde <nombre_del_paquete> es un classpath valido

Métodos

Ahora podemos definir un método que llamaremos obtenerAudiencia y obtener una de las audiencias de forma aleatoria.  El método tomara un List<String> de audiencias y regresara una referencia aleatoria de esa lista.

Para definir un método simplemente escribimos la firma del método como si lo estuviéramos escribiendo dentro de una clase, excepto que no hay que definir la clase!

jshell> public String obtenerAudiencia(List<String> audiencias) {
  ...> return audiencias.get(new Random().nextInt(audiencias.size()));
  ...> }
|  created method obtenerAudiencia(List<String>)

Inmediatamente es posible llamar al método:

jshell> obtenerAudiencia(audiencias)
$7 ==> "mundo"

jshell> obtenerAudiencia(audiencias)
$8 ==> "universo"

jshell> obtenerAudiencia(audiencias)
$9 ==> "galaxia"

Algo interesante es que adentro del método es posible hacer referencia a cualquier variable anteriormente definida.

Ahora creamos otro sabor del método obtenerAudiencia, pero esta vez hacemos que no tome ningún argumento y use las audiencias que definimos anteriormente.

jshell> public String obtenerAudiencia() {
  ...> return audiencias.get(new Random().nextInt(audiencias.size()));
  ...> }
|  created method obtenerAudiencia()

Y si probamos de nuevo:

jshell> obtenerAudiencia()
$10 ==> "galaxia"

jshell> obtenerAudiencia()
$11 ==> "mundo"

jshell> obtenerAudiencia()
$12 ==> "galaxia"

En los métodos también se puede hacer referencia a variables que aun no han sido definidas. Por ejemplo, creemos un método que se llame obtenerSeparador que regresa un valor que pueda separar las dos palabras de nuestro saludo. Y adentro hagamos una referencia a una variable que no ha sido definida que se llame separador

jshell> public String obtenerSeparador() {
  ...> return separador;
  ...> }
|  created method obtenerSeparador(), however, it cannot be invoked until variable wordSeparator is declared

Como vemos JShell felizmente no tiene problemas en crear el método. Pero no obstante no nos dejará usarlo hasta que declaremos la variable separador!

Por lo tanto si llamamos al método nos devuelve un error de invocación

jshell> obtenerSeparador()
|  attempted to call method obtenerSeparador() which cannot be invoked until variable wordSeparator is declared

Pues bien, simplemente definimos la variable separador y hacemos uso del método:

jshell> var separador = " "
separador ==> " "

jshell> obtenerSeparador()
$13 ==> " "

Una cosa importante que hay que notar es que no se pueden crear métodos estáticos. Si lo intentamos obtenemos una advertencia de que la palabra reservada static ha sido ignorada.

jshell> public static String foobar(String arg) {
  ...> return arg;
  ...> }
|  Warning:
|  Modifier 'static'  not permitted in top-level declarations, ignored
|  public static void foobar(String arg) {
|  ^-----------^

Variables Temporales

Otra cosa interesante que hay que notar, es que cada vez que JShell evalúa una expresión el resultado es guardado en una variable temporal, la cual podemos usar sin ningun problema.

jshell> obtenerAudiencia()
$14 ==> "galaxia"

jshell> System.out.println($14)
galaxia

Clases

De manera similar a como creamos los métodos, también podemos crear clases:

jshell> public class Foo {
  ...> private String bar;
  ...> public String getBar() {
  ...> return this.bar;
  ...> }
  ...> }
|  created class Foo

Crear clases y métodos en JShell no resulta lo mas práctico. No hay formato ni se marcan los errores conforme vamos escribiendo. Esto puede resultar un poco frustrante ya que si comentemos un error no lo sabremos hasta que terminemos de escribir la clase. Para una mejor manera podemos hacer uso del comando /open que examinamos mas adelante en este post.

Librerías Externas

Si queremos cargar una librería externa como apache commons, es necesario iniciar el JShell con una bandera como argumento:

$ jshell --class-path /path/to/foo.jar

 Comandos

JShell tiene una serie de comandos que muy útiles que pueden simplificar el uso del mismo:

  • /help Despliega un texto de ayuda general. Si le pasamos otro comando, despliega ayuda de ese comando.
  • /list Despliega una lista de todos los fragmentos de código que hemos hecho en esta sesión
  • /vars Despliega una lista de todas las variables que se han declarado
  • /methods Despliega una lista de todos los métodos que se han declarado
  • /types Despliega una lista de todos los tipos (clases) que se han declarado
  • /imports Despliega una lista de todos los paquetes importados hasta el momento
  • /reset borra la sesión actual, se van todas las declaraciones que ya hicimos.
  • /edit Sirve para editar un fragmento de código, acepta un segundo argumento (/list 3) que corresponde al numero de fragmento, el cual podemos ver en lo que muestra el comando /list
  • /drop Parecido a /edit solo que para borrar el fragmento de código
  • /save Recibe como segundo argumento el nombre de un archivo y guarda la sesión actual a un archivo.
  • /open Igual que /save solo que este abre el archivo previamente guardado.

Atajos del teclado

Finalmente los dejo con una lista de atajos del teclado que les pueden ser útiles en algún momento.

  • CTRL-A – Mueve el cursor al inicio de la linea
  • CTRL-E – Mueve el cursor al final del la linea
  • ALT-F – Mueve el cursor una palabra hacia adelante
  • ALT-B – Mueve el cursor una palabra hacia atrás
  • CTRL-K – Corta hasta el final de la linea
  • CTRL-U – Corta hacia el inicio de la linea
  • CTRL-W – Corta la palabra anterior al cursor
  • CTRL-Y – Pega el contenido del porta papeles
  • CTRL-R – Buscar en el historial hacia atrás
  • CTRL-S -Buscar en el historial hacia adelante.

Java se tardo mucho en tener un REPL… pero vaya! excedieron mis expectativas 🙂

Categorized in:

Tagged in:

, ,