¡Despidamos a Java 8!

share

Artículo desarrollado por Alejandro Perez.

Java 8 nos acompaña desde Marzo de 2014, y ya va siendo hora de que vayamos despidiendola para dar a lugar a las nuevas características de las recientes versiones Java 13 y Java 14.
¿Y las versiones entre medio? Bien, también enriquecieron al lenguaje y a la plataforma, pero ninguna fue tan disruptiva como lo fue Java 8.
En este artículo haremos un breve repaso de por qué esta versión es considerada un punto de quiebre en lo que es el lenguaje, y la forma de pensar el código al escribir en Java. 

Java decide apostar fuerte

Situándonos en el 2014, el estado del arte nos presentaban muchas opciones al momento de elegir un lenguaje de programación con el cual desarrollar.

Los cambios más notables de este año fueron un incremento del 300% en la popularidad de Objective-C, un aumento del 100% en C#, así como un aumento del 33% de Javascript; mientras PHP perdió un 55%, Perl cayó un 16% y Java se redujo un 14%. https://web.archive.org/web/20140224142555/http://blog.codeeval.com/2014 

En este contexto, los arquitectos responsables se dieron cuenta que el rumbo se encaminaba a la adopción de características de programación funcional, y que los desarrolladores (y con ellos la industria) estaba demandando cambios en la forma de entender el lenguaje. 

Para dar el puntapié inicial, decidieron incluir las siguientes (en ese entonces nuevas) características a Java: 

  • Clase Optional
  • Funciones Lambda
  • Streams

Clase Optional

Todo objeto en Java puede contener un valor o no, ciertamente esto no es una novedad. Antes de Java 8, no teníamos una “entidad” que nos represente este comportamiento por lo que por defecto tenemos el valor especial null para denotar la ausencia de un valor dentro de un objeto. 

Esta posibilidad de que un objeto sea potencialmente null nos llevaba (y nos sigue llevando) a caer en la famosa y odiada excepción NullPointerException. 

Para evitar esta situación, Java 8 agrega la nueva clase Optional que no es otra cosa que un contenedor diseñado para contener un valor o encontrarse “vacío”. Este enfoque puede parecer simple, pero nos facilita de sobremanera el chequeo de valores nulos en nuestro código. 

// Without optionals 
Computer myCommodore = retroStore.getCommodore(); 
if (myCommodore != null) { 
System.out.println(myCommodore) 
} else { 
System.out.println("Sorry, Commodore machines are out of stock!") 
} 
// With optionals 
Computer myCommodore = retroStore.getCommodore(); 
Optional<Computer> optionalComputer = Optional.ofNullable(myCommodore);

if (optionalComputer.isPresent()) { 
System.out.println(optionalComputer.get()) 
} else { 
System.out.println("Sorry, Commodore machines are out of stock!") 
}

Funciones Lambda

Las funciones Lambda no eran algo nuevo al momento de la creación de Java 8, pero esta fue la primera versión que las incorpora al lenguaje con nuevas estructuras y sintaxis. 

Básicamente, una función Lambda es una función anónima que no necesita una firma formal o definida dentro de una clase. La creamos al vuelo, sujeta al contexto donde es creada. 

Para crear funciones Lambda, Java 8 incorpora al lenguaje el operador “flecha” (->) que se usa así: 

(PARAMS) -> { FUNCTION_BODY }

Donde: 

  • Podemos tener 0, 1 o 2 parámetros. Si usamos 0 o 2 parámetros debemos usar paréntesis, si usamos uno solo los paréntesis son opcionales. 
  • En el cuerpo de la función, podemos omitir el uso de llaves en caso de tener solo una línea, en caso contrario debe llevar llaves y la palabra clave return si es que necesitamos retornar un valor. 

Otra forma de “pseudo” Lambda es la invocación de métodos directos, por medio del operador “::”. Esto genera una referencia al método, que en algunos casos puede directamente sustituir una función lambda en operaciones simples. 

ReferenceClass::ReferredMethod 

Dependiendo del origen del método que necesitamos usar, podemos clasificar estas referencias en tres categorías:

●  Acceso a métodos estáticos

(String msg) -> System.out.println(msg) 
// is the same that 
System.out::println 

●  Acceso a métodos de instancia

(Catalog catalog, int itemKey) -> 
catalog.getProduct(itemKey) // is the same that 
Catalog::getProduct 

 ● Acceso a métodos de instancia de un objeto existente

Catalog catalog -> catalog.getBestProduct() 
// is the same that 
this::getBestProduct 

Streams

Sin el agregado de este concepto al lenguaje, no hubiéramos podido aprovechar todo el potencial que nos ofrece el uso de los objetos Optional y las funciones Lambda. 

Básicamente un Stream es una secuencia de elementos que soporta operaciones secuenciales y en paralelo. Un Stream no almacena datos, trabaja sobre la estructura de datos origen (Collections, arreglos por ejemplo) y produce datos canalizados que pueden ser procesados luego. 

Comparemos una iteración clásica sobre una Collection, sin uso de Streams:

private static int sumIterator(List<Integer> list) { 
Iterator<Integer> it = list.iterator(); 
int sum = 0; 
while (it.hasNext()) { 
int num = it.next(); 
if (num % 10 == 0) { 
sum += num; 
} 
} 
return sum; 
} 

Tenemos un enfoque clásico que crea un Iterator desde nuestra lista y trabaja con ella mientras exista un elemento por procesar en el Iterator. (método hasNext()

  • Como desarrolladores, solo queremos obtener la suma de los números, pero en este caso necesitamos proveer detalles de cómo iterar los elementos. Esto recibe el nombre de “iteración externa” porque el desarrollador es quien controla el algoritmo de iteración. Esto incluye el uso de estructuras como for, foreach, while por ejemplo. 
  • El algoritmo es secuencial por naturaleza, no tenemos una forma fácil de paralelizar. 
  • Es bastante código en contraste con lo que queremos hacer, ¿no? 

Los arquitectos de Oracle tomaron nota de estas situaciones, he incluyeron en la API de los Streams métodos con la habilidad de iterar a través de los elementos, sin la necesidad de proveer detalles de cómo debería hacerse esa iteración. Además, la API provee nuevas formas de acceso a los elementos. 

Podemos reescribir el ejemplo anterior de la siguiente forma: 

private static int sumStream(List<Integer> list) { 
return list.stream() 
.filter(i -> (i % 10 == 0)) 
.mapToInt(i -> i) 
.sum(); 
} 

Donde: 

  • stream(): Crea un Stream a partir de una lista de enteros. 
  • Stream<T> filter( Predicate<T> p ): Retorna un Stream con elementos que aplican al predicado “p”. 
  • IntStream mapToInt( function_lambda ):Toma uno a uno los elementos del Stream y los procesa con la funcion Lambda. Este retorna un Stream con enteros de la clase IntStream
  • sum(): Suma cada elemento del Stream y retorna un resultado.

Se ve mucho más simple, ¿verdad? No solo eso, los Streams son una vía con la cual iterar elementos que ya están alojados en memoria, sin un consumo extra de memoria notable en contraste con la cantidad de elementos que iteramos. 

Esto le permite generar cantidades infinitas de elementos, cuya creación puede ser controlada por el desarrollador. También nos permite procesar archivos de gran tamaño con un impacto mínimo, permitiéndonos centrarnos en la lógica a implementar y no en la iteración de los datos. 

Conclusiones

Los Streams y su API fueron diseñados con el concepto de funciones Lambda y Optional en mente desde el inicio. Estos a su vez fueron pensados para cubrir una necesidad creciente de herramientas que nos ofrecen aspectos de programación funcional y programación dinámica. 

Java 8 ha sido un punto de quiebre que ha evolucionado el lenguaje, permitiendo crear muchas técnicas de programación avanzadas que fueron incluidas posteriormente. Con el conocimiento de estas características podemos entender por qué son importantes, y cómo nos ha ayudado a codear de una manera más eficiente y clara. 

Este artículo ha tomado solo una pequeña parte de los cambios introducidos por Java 8, ya que esta versión ha marcado un antes y un después no solo para el lenguaje, sino para todas las aplicaciones, sistemas y tecnologías basadas en Java. 

Gracias por todo Java 8, ya puedes descansar, es hora de seguir adelante. Tu legado sigue vigente y activo al día de hoy, incluso en Java 14.

Links útiles

Para profundizar en estos temas, les recomiendo las siguientes páginas:

  • [Oracle] What’s New in JDK 8
  • [Oracle] Processing Data with Java SE 8 Streams, Part 1
  • [Oracle] Part 2: Processing Data with Java SE 8 Streams
  • [Oracle] Stream (Java Platform SE 8 )
  • [Oracle] Lambda Expressions
  • [Oracle] Lambdas and Streams in JDK 8
  • [journaldev.com] Java 8 Features with Examples
  • [softwaretestinghelp.com] Prominent Java 8 Features With Code Examples
  • [Baeldung] https://www.baeldung.com/java-optional
  • [Stackify] https://stackify.com/optional-java/

Thank You.

The white paper will open in a new window.

If you experience issues with accessing or downloading the white paper, please contact info@globallogic.com.

click here to go back to the Insights page.