Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Android SDK
Code

Java 8 para desarrollo en Android: la API Stream y las bibliotecas de fecha y hora

by
Difficulty:IntermediateLength:LongLanguages:
This post is part of a series called Java 8 for Android Development.
Java 8 for Android Development: Default and Static Methods

Spanish (Español) translation by Ana Paulina Figueroa Vazquez (you can also view the original English article)

En esta serie de tres partes hemos explorado todas las funciones principales de Java 8 que puedes comenzar a usar en tus proyectos de Android hoy en día.

En Código más limpio con expresiones lambda nos enfocamos en cortar la repetición de tus proyectos usando expresiones lambda, y luego en Métodos predeterminados y estáticos vimos cómo lograr que esas expresiones lambda sean más concisas combinándolas con referencias a métodos. También cubrimos anotaciones repetitivas y cómo declarar métodos no abstractos en tus interfaces usando métodos de interface predeterminados y estáticos.

En esta última publicación vamos a ver anotaciones de tipo, interfaces funcionales y cómo adoptar un enfoque más funcional en cuanto al procesamiento de datos con la nueva API Stream de Java 8.

También te mostraré cómo acceder a algunas características adicionales de Java 8 que actualmente no son compatibles con la plataforma Android usando las bibliotecas Joda-Time y ThreeTenABP.

Anotaciones de tipo

Las anotaciones te ayudan a escribir código más robusto y menos propenso a errores, ya que informan a herramientas de inspección de código, tales como Lint, acerca de los errores que deberían estar buscando. Estas herramientas de inspección a su vez te advertirán si una parte del código no cumple con las reglas establecidas por esas anotaciones.

Las anotaciones no son una característica nueva (de hecho se remontan a Java 5.0), pero en las versiones anteriores de Java solamente era posible aplicar anotaciones a declaraciones.

Con el lanzamiento de Java 8 ahora puedes usar anotaciones en cualquier lugar en el que hayas usado un tipo, incluyendo a los receptores de métodos, expresiones para la creación de instancias de clases, la implementación de interfaces, genéricos y arreglos, la especificación de cláusulas throws e implements, así como conversiones de tipo.

Algo frustrante es que, aunque Java 8 hace posible el uso de anotaciones en más ubicaciones que nunca, no proporciona ninguna anotación que sea específica para cada tipo.

La Biblioteca de Soporte de Anotaciones de Android proporciona acceso a algunas anotaciones adicionales, tales como @Nullable@NonNull, así como anotaciones para la validación de tipos de recursos como @DrawableRes@DimenRes@ColorRes y @StringRes. Sin embargo, es posible que también quieras usar una herramienta de análisis estático de terceros, como Checker Framework, que se desarrolló en conjunto con la especificación JSR 308 (las anotaciones en la especificación de tipos de Java). Este framework proporciona su propio conjunto de anotaciones que pueden aplicarse a tipos, además de una serie de "comprobadores" (procesadores de anotación) que se unen al proceso de compilación y realizan "comprobaciones" específicas para cada anotación de tipo que haya sido incluida en el Checker Framework.

Dado que las anotaciones de tipo no afectan las operaciones en tiempo de ejecución, puedes usar estas anotaciones de Java 8 en tus proyectos y al mismo tiempo conservar la compatibilidad con las versiones anteriores de Java.

La API Stream

La API Stream ofrece un enfoque alternativo de "tuberías y filtros" para el procesamiento de colecciones.

Antes de Java 8 debías manipular colecciones manualmente, por lo general iterando a través de la colección y usando operadores en cada elemento en turno. Estos bucles explícitos requerían muchas repeticiones, además de que es difícil entender la estructura del ciclo for hasta que uno llega al cuerpo del mismo.

La API Stream te proporciona una forma de procesar datos de manera más eficiente al realizar una única ejecución sobre ellos, independientemente de la cantidad de información que estés procesando o de si estás realizando múltiples cálculos.

En Java 8, cada clase que implementa java.util.Collection tiene un método stream que puede convertir a sus instancias en objetos Stream. Por ejemplo, si tienes un arreglo (Array):

A continuación puedes convertirlo a un objeto Stream con lo siguiente:

La API Stream procesa datos transportando valores a partir de una fuente mediante una serie de pasos computaciones conocidos como canalización de flujo. Una canalización de flujo está compuesta por lo siguiente:

  • Una fuente, por ejemplo una colección (Collection), un arreglo o una función generadora.
  • Cero o más operaciones "flojas" intermedias. Las operaciones intermedias no comienzan a procesar elementos sino hasta que invocas una operación terminal, por esta razón se consideran flojas. Por ejemplo, invocar a Stream.filter() en una fuente de datos simplemente configura la canalización de flujo; no se produce ningún filtrado sino hasta que invoques a la operación terminal. Esto permite encadenar múltiples operaciones entre sí y luego realizar todos estos cálculos en una sola pasada por los datos. Las operaciones intermedias producen un nuevo flujo (por ejemplo, filter generará un flujo que contendrá a los elementos filtrados) sin modificar la fuente de los datos, por lo que tienes la libertad de utilizar los datos originales en cualquier parte de tu proyecto o crear múltiples flujos a partir de la misma fuente.
  • Una operación terminal, tal como Stream.forEach. Cuando invoques la operación terminal, todas tus operaciones intermedias se ejecutarán y producirán un nuevo flujo. Un flujo no es capaz de almacenar elementos, así que tan pronto como invoques una operación terminal ese flujo se considera "consumido" y ya no podrá usarse. Si quieres volver a visitar los elementos de un flujo, entonces deberás generar un nuevo flujo a partir de la fuente original de los datos.

Cómo crear un flujo

Existen varias maneras de obtener un flujo a partir de una fuente de datos, entre ellas:

  • Stream.of() Crea un flujo a partir de valores individuales:

  • IntStream.range() Crea un flujo a partir de un rango de números:

  • Stream.iterate() Crea un flujo al aplicar repetidamente un operador a cada elemento. Por ejemplo, aquí estamos creando un flujo en el que cada elemento incrementa su valor en uno:

Cómo transformar un flujo con operaciones

Hay muchas operaciones que puedes usar para llevar a cabo cálculos de estilo funcional en tus flujos. En esta sección cubriré solamente algunas de las operaciones de flujos que se usan más comúnmente.

Map

La operación map() toma una expresión lambda como su único argumento y la usa para transformar el valor o el tipo de cada elemento del flujo. Por ejemplo, la siguiente instrucción nos proporciona un nuevo flujo en el que cada String (Cadena) ha sido convertida a mayúsculas:

Limit

Esta operación establece un límite para el tamaño de un flujo. Por ejemplo, si quisieras crear un nuevo flujo que contenga un máximo de cinco valores, entonces deberías usar la siguiente instrucción:

Filter

La operación filter(Predicate<T>) te permite definir criterios de filtrado usando una expresión lambda. Esta expresión lambda debe devolver un valor booleano que determina si cada elemento debe ser incluido en el flujo resultante. Por ejemplo, si tuvieras un arreglo de cadenas y quisieras filtrar cualquier cadena que contuviera menos de tres caracteres, deberías usar lo siguiente:

Sorted

Esta operación ordena los elementos de un flujo. Por ejemplo, lo siguiente devuelve un flujo de números acomodados en orden ascendente:

Procesamiento paralelo

Todas las operaciones de flujos pueden ejecutarse en serie o en paralelo, aunque los flujos son secuenciales a menos que especifiques lo contrario explícitamente. Por ejemplo, la siguiente instrucción procesará cada elemento uno por uno:

Para ejecutar un flujo en paralelo necesitas marcar explícitamente dicho flujo como paralelo usando el método parallel():

A bajo nivel, los flujos paralelos usan el Framework Fork/Join (Bifurcación/Unión), por lo que el número de hilos disponibles siempre es igual a la cantidad de núcleos disponibles en la CPU.

La desventaja de los flujos en paralelo es que cada vez que el código se ejecuta hay diferentes núcleos involucrados, por lo que normalmente obtendrás un resultado diferente con cada ejecución. Por lo tanto solamente debes usar flujos en paralelo cuando el orden de procesamiento no es relevante, además de evitar flujos en paralelo al llevar a cabo operaciones basadas en un orden, tales como findFirst().

Operaciones terminales

Puedes recolectar los resultados de un flujo usando una operación terminal, que siempre es el último elemento en una cadena de métodos de flujo y siempre devuelve algo que no es un flujo.

Existen algunos tipos diferentes de operaciones terminales que devuelven varios tipos de datos, pero en esta sección vamos a ver dos de las operaciones terminales más comúnmente usadas.

Collect

La operación Collect reúne todos los elementos procesados en un contenedor, tal como List o Set. Java 8 proporciona la clase de utilidad Collectors, por lo que no tienes que preocuparte por implementar la interfaz Collectors, además de fábricas para muchos recolectores comunes, incluyendo a toList()toSet() y toCollection().

El siguiente código producirá una List que contendrá solamente figuras rojas:

Alternativamente puedes recolectar estos elementos filtrados en un Set:

forEach

La operación forEach() lleva a cabo alguna acción en cada elemento del flujo, lo que la vuelve equivalente a una instrucción for-each, pero perteneciente a la API Stream.

Si tuvieras una lista items podrías usar forEach para imprimir todos los elementos incluidos en este objeto List:

En el ejemplo anterior estamos usando una expresión lambda, por lo que es posible llevar a cabo la misma tarea con menos código usando una referencia a un método:

Interfaces funcionales

Una interface funcional es aquella que contiene exactamente un método abstracto, conocido como método funcional.

El concepto de interfaces de un solo método no es nuevo. RunnableComparatorCallable y OnClickListener son ejemplos de este tipo de interface, aunque en versiones anteriores de Java eran conocidas como Interfaces de Método Abstracto Único (Interfaces SAM).

Esto es más que un simple cambio de nombre, ya que hay algunas diferencias notables en la forma de trabajar con interfaces funcionales (o SAM) en Java 8 en comparación con las versiones anteriores.

Antes de Java 8 por lo general creabas la instancia de una interface funcional usando una voluminosa implementación de una clase anónima. Por ejemplo, aquí estamos creando una instancia de Runnable usando una clase anónima:

Como vimos en la primera parte, cuando tienes una interface de método único puedes crear una instancia de esa interface usando una expresión lambda en vez de usar una clase anónima. Ahora podemos actualizar esta regla: puedes crear instancias de interfaces funcionales usando una expresión lambda. Por ejemplo:

Java 8 también introduce una anotación @FunctionalInterface que te permite marcar una interface como funcional:

La anotación @FunctionalInterface es opcional para garantizar la compatibilidad con versiones anteriores de Java; sin embargo es recomendada para ayudar a asegurarte de que estés implementando tus interfaces funcionales correctamente.

Si intentas implementar dos o más métodos en una interface marcada como @FunctionalInterface, el compilador mostrará un error diciendo que ha descubierto múltiples métodos abstractos no superpuestos. Por ejemplo, lo siguiente no podrá compilarse:

Y si intentas compilar una interface @FunctionInterface que no contenga métodos, entonces te vas a encontrar con el error No target method found (No se encontró el método destino).

Las interfaces funcionales deben contener exactamente un método abstracto, pero dado que los métodos predeterminados y estáticos no tienen un cuerpo, estos son considerados como no abstractos. Esto significa que puedes incluir múltiples métodos predeterminados y estáticos en una interface, marcarlos como @FunctionalInterface y el programa aún podrá compilarse correctamente.

Java 8 también agregó un paquete java.util.function que contiene muchas interfaces funcionales. Vale la pena que te tomes el tiempo para familiarizarte con todas estas nuevas interfaces funcionales para así saber exactamente qué hay disponible de manera inmediata.

JSR-310: La nueva API de fecha y hora de Java

Trabajar con fechas y horas en Java nunca ha sido particularmente sencillo, al tener muchas APIs que omiten funciones importantes, por ejemplo la información de la zona horaria.

Java 8 introdujo una nueva API de fecha y hora (JSR-310) que tiene como objetivo resolver estos problemas, pero desafortunadamente al momento de escribir este tutorial la API no es compatible con la plataforma Android. Sin embargo, hoy en día puedes usar algunas de las nuevas funciones de fecha y hora en tus proyectos de Android usando una biblioteca de terceros.

En esta última sección, voy a mostrarte cómo configurar y usar dos populares bibliotecas de terceros que permiten usar la API de fecha y hora de Java 8 en Android.

ThreeTen Android Backport

ThreeTen Android Backport (también conocido como ThreeTenABP) es una adaptación del popular proyecto ThreeTen backport, que proporciona una implementación de JSR-310 para Java 6.0 y Java 7.0. ThreeTenABP está diseñado para brindar acceso a todas las clases de la API de fecha y hora (aunque con un nombre de paquete diferente) sin añadir un gran número de métodos a tus proyectos Android.

Para comenzar a usar esta biblioteca, abre tu archivo a nivel de módulo build.gradle y añade ThreeTenABP como una dependencia del proyecto:

A continuación debes agregar la instrucción import para ThreeTenABP.

Además inicializa la información de zona horaria en tu método Application.onCreate().

ThreeTenABP contiene dos clases que muestran dos "tipos" de información de fecha y hora:

  • LocalDateTime, que almacena una fecha y hora en el formato 2017-10-16T13:17:57.138
  • ZonedDateTime, que tiene conocimiento sobre la zona horaria y almacena información de fecha y hora en el siguiente formato: 2011-12-03T10:15:30+01:00[Europa/París]

Para darte una idea de cómo podrías usar esta biblioteca para obtener información de fecha y hora, vamos a usar la clase LocalDateTime para mostrar la fecha y hora actuales:

Display the date and time using the ThreeTen Android Backport library

¡Esta no es la forma más sencilla de mostrar la fecha y la hora al usuario! para convertir estos datos sin procesar a algo más legible para el ser humano, puedes usar la clase DateTimeFormatter y asignarle uno de los siguientes valores:

  • BASIC_ISO_DATE. Le da formato a la fecha de esta manera: 2017-1016+01.00
  • ISO_LOCAL_DATE. Le da formato a la fecha de esta manera: 2017-10-16
  • ISO_LOCAL_TIME. Le da formato a la hora de esta manera: 14:58:43.242
  • ISO_LOCAL_DATE_TIME. Le da formato a la fecha y a la hora de esta manera: 2017-10-16T14:58:09.616
  • ISO_OFFSET_DATE. Le da formato a la fecha de esta manera: 2017-10-16+01.00
  • ISO_OFFSET_TIME. Le da formato a la hora de esta manera: 14:58:56.218+01:00
  • ISO_OFFSET_DATE_TIME. Le da formato a la fecha y a la hora de esta manera: 2017-10-16T14:5836.758+01:00
  • ISO_ZONED_DATE_TIME. Le da formato a la fecha y a la hora de esta manera: 2017-10-16T14:58:51.324+01:00(Europa/Londres)
  • ISO_INSTANT. Le da formato a la fecha y a la hora de esta manera: 2017-10-16T13:52:45.246Z
  • ISO_DATE. Le da formato a la fecha de esta manera: 2017-10-16+01:00
  • ISO_TIME. Le da formato a la hora de esta manera: 14:58:40.945+01:00
  • ISO_DATE_TIME. Le da formato a la fecha y a la hora de esta manera: 2017-10-16T14:55:32.263+01:00(Europa/Londres)
  • ISO_ORDINAL_DATE. Le da formato a la fecha de esta manera: 2017-289+01:00
  • ISO_WEEK_DATE. Le da formato a la fecha de esta manera: 2017-W42-1+01:00
  • RFC_1123_DATE_TIME. Le da formato a la fecha y a la hora de esta manera: Mon, 16 OCT 2017 14:58:43+01:00

Aquí estamos actualizando nuestra aplicación para mostrar la fecha y la hora con el formato DateTimeFormatter.ISO_DATE:

Para mostrar esta información en un formato diferente, simplemente sustituye DateTimeFormatter.ISO_DATE por otro valor. Por ejemplo:

Joda-Time

Antes de Java 8, la biblioteca Joda-Time era considerada la biblioteca estándar para el manejo de la fecha y la hora en Java, hasta el punto en el que la nueva API de fecha y hora de Java 8 verdaderamente está basada "en gran medida en la experiencia obtenida durante el proyecto Joda-Time".

Si bien el sitio web de Joda-Time recomienda que los usuarios migren a la API de fecha y hora de Java 8 tan pronto como puedan, dado que Android actualmente no es compatible con dicha API, Joda-Time aún es una opción viable para desarrollar en Android. Sin embargo, toma en cuenta que Joda-Time tiene una API grande y carga la información de la zona horaria usando un recurso JAR. Ambos factores pueden afectar el rendimiento de tu aplicación.

Para comenzar a trabajar con la biblioteca Joda-Time, abre tu archivo a nivel de módulo build.gradle y agrega lo siguiente:

La biblioteca Joda-Time tiene seis clases principales para fechas y horas:

  • Instant: Representa un punto en la línea de tiempo; por ejemplo, puedes obtener la fecha y hora actuales invocando a la función Instant.now().
  • DateTime: Un reemplazo de propósito general para la clase Calendar de JDK.
  • LocalDate: Una fecha sin hora ni referencia a una zona horaria.
  • LocalTime: Una hora sin fecha ni referencia a una zona horaria, por ejemplo 14:00:00.
  • LocalDateTime: Una fecha y hora locales, aún sin información de zona horaria.
  • ZonedDateTime: Una fecha y hora con zona horaria.

Veamos cómo podrías mostrar la fecha y la hora usando Joda-Time. En el siguiente ejemplo estoy reutilizando código de nuestro ejemplo ThreeTenABP, y para hacer las cosas más interesantes también estoy usando withZone para convertir la fecha y la hora a un valor ZonedDateTime.

Display the date and time using the Joda-Time library

Puedes encontrar una lista completa de las zonas horarias soportadas en los documentos oficiales de Joda-Time.

Conclusión

En esta publicación vimos cómo crear código más robusto usando anotaciones de tipo y exploramos el enfoque de "tuberías y filtros" para el procesamiento de datos con la nueva API Stream de Java 8.

También analizamos cómo han evolucionado las interfaces en Java 8 y cómo usarlas en combinación con otras funciones que hemos explorado a lo largo de esta serie, incluyendo expresiones lambda y métodos de interface estáticos.

Para concluir, te mostré cómo acceder a algunas características adicionales de Java 8 que Android no admite actualmente de forma predeterminada, al usar los proyectos Joda-Time y ThreeTenABP.

Puedes obtener más información sobre la versión Java 8 en el sitio web de Oracle.

Y mientras estás aquí ¡revisa algunas de nuestras otras publicaciones sobre desarrollo en Java 8 y Android!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.