Java 8 per lo sviluppo Android API Stream e librerie di data e ora

In questa serie in tre parti, abbiamo esplorato tutte le principali funzionalità di Java 8 che puoi iniziare a utilizzare nei tuoi progetti Android oggi.

Nel codice Cleaner con Lambda Expressions, ci siamo concentrati sul taglio delle lastre dai vostri progetti usando espressioni lambda, e poi in Default e Static Methods, abbiamo visto come rendere queste espressioni lambda più concise combinandole con riferimenti di metodo. Abbiamo anche trattato le Annotazioni ripetute e come dichiarare i metodi non astratti nelle interfacce utilizzando i metodi di interfaccia statici e predefiniti.

In questo post finale, esamineremo annotazioni di tipo, interfacce funzionali e come adottare un approccio più funzionale all'elaborazione dei dati con la nuova API Stream di Java 8.

Ti mostrerò anche come accedere ad alcune funzionalità aggiuntive di Java 8 che non sono attualmente supportate dalla piattaforma Android, utilizzando le librerie Joda-Time e ThreeTenABP.

Digita annotazioni

Le annotazioni aiutano a scrivere codice più solido e meno soggetto a errori, informando gli strumenti di ispezione del codice, come Lint, sugli errori che dovrebbero essere tenuti alla larga. Questi strumenti di ispezione ti avviseranno quindi se un pezzo di codice non è conforme alle regole stabilite da queste annotazioni.

Le annotazioni non sono una nuova funzionalità (infatti risalgono a Java 5.0), ma nelle versioni precedenti di Java era possibile applicare solo annotazioni alle dichiarazioni.

Con il rilascio di Java 8, ora puoi usare le annotazioni ovunque tu abbia usato un tipo, compresi i ricevitori di metodi; espressioni di creazione di istanze di classe; l'implementazione di interfacce; generici e matrici; la specifica di getta e attrezzi clausole; e digitare casting.

Frustrante, anche se Java 8 rende possibile l'uso di annotazioni in più posizioni che mai, non fornisce alcuna annotazione specifica per i tipi.

La libreria di supporto Annotazioni di Android fornisce accesso ad alcune annotazioni aggiuntive, come ad esempio @Nullable, @NonNull, e annotazioni per la convalida di tipi di risorse come  @DrawableRes, @DimenRes, @ColorRes, e @StringRes. Tuttavia, potresti anche voler utilizzare uno strumento di analisi statica di terze parti, come il Checker Framework, che è stato sviluppato in collaborazione con la specifica JSR 308 (la specifica Annotations on Java Types). Questo framework fornisce il proprio set di annotazioni che possono essere applicate ai tipi, oltre a un numero di "controllori" (processori di annotazione) che si agganciano al processo di compilazione ed eseguono specifici "controlli" per ogni annotazione di tipo inclusa nel Checker Framework.

Poiché le annotazioni di tipo non influenzano le operazioni di runtime, è possibile utilizzare le annotazioni di tipo di Java 8 nei progetti rimanendo al contrario compatibili con le versioni precedenti di Java.

Stream API

L'API Stream offre un approccio alternativo, "pipe-and-filters", all'elaborazione delle collezioni.

Prima di Java 8, hai manipolato manualmente le raccolte, tipicamente eseguendo il iterazione sulla raccolta e operando a turno su ciascun elemento. Questo ciclo esplicito richiedeva molto standard, inoltre è difficile afferrare la struttura del ciclo finché non si raggiunge il corpo del loop.

L'API Stream ti offre un modo per elaborare i dati in modo più efficiente, eseguendo una singola esecuzione su quei dati, indipendentemente dalla quantità di dati che stai elaborando, o se stai eseguendo più calcoli.

In Java 8, ogni classe che implementa java.util.Collection ha un ruscello metodo in grado di convertire le sue istanze in ruscello oggetti. Ad esempio, se hai un schieramento:

String [] myArray = new String [] "A", "B", "C", "D";

Quindi puoi convertirlo in uno Stream con il seguente:

ruscello myStream = Arrays.stream (myArray);

L'API Stream elabora i dati trasportando valori da un'origine, attraverso una serie di passaggi computazionali, noti come a flusso di gasdotto. Una pipeline di flusso è composta da quanto segue:

  • Una fonte, come a Collezione, array, o funzione del generatore.
  • Zero o più operazioni intermedie "pigre". Le operazioni intermedie non iniziano l'elaborazione degli elementi finché non si richiama a operazione terminale-ed è per questo che sono considerati pigri.Ad esempio, chiamando Stream.filter () su un'origine dati imposta semplicemente la pipeline del flusso; nessun filtro si verifica effettivamente fino a quando non si chiama l'operazione del terminale. Ciò rende possibile stringere insieme più operazioni e quindi eseguire tutti questi calcoli in un'unica passata di dati. Le operazioni intermedie generano un nuovo flusso (ad esempio, filtro produrrà un flusso contenente gli elementi filtrati) senza modifica dell'origine dati, quindi sei libero di utilizzare i dati originali altrove nel tuo progetto o creare più stream dalla stessa origine.
  • Un'operazione terminale, come Stream.forEach. Quando invochi l'operazione del terminale, tutte le tue operazioni intermedie verranno eseguite e produrranno un nuovo flusso. Un flusso non è in grado di memorizzare elementi, quindi non appena si richiama un'operazione terminale, quel flusso viene considerato "consumato" e non più utilizzabile. Se si desidera rivisitare gli elementi di uno stream, è necessario generare un nuovo flusso dall'origine dati originale.

Creazione di un flusso

Esistono vari modi per ottenere uno stream da un'origine dati, tra cui:

  • Stream.of ()Crea un flusso da singoli valori:

ruscello stream = Stream.of ("A", "B", "C");
  • IntStream.range () Crea un flusso da un intervallo di numeri:

IntStream i = IntStream.range (0, 20);
  • Stream.iterate () Crea un flusso applicando ripetutamente un operatore a ciascun elemento. Ad esempio, qui stiamo creando uno stream in cui ogni elemento aumenta di valore di uno:

ruscello s = Stream.iterate (0, n -> n + 1);

Trasformare un flusso con le operazioni

Ci sono un sacco di operazioni che puoi usare per eseguire calcoli in stile funzionale sui tuoi stream. In questa sezione, tratterò solo alcune delle operazioni di streaming più comunemente utilizzate.

Carta geografica

Il carta geografica() operazione prende un'espressione lambda come unico argomento e usa questa espressione per trasformare il valore o il tipo di ogni elemento nel flusso. Ad esempio, il seguente ci dà un nuovo flusso, in cui ogni Stringa è stato convertito in maiuscolo:

ruscello myNewStream = myStream.map (s -> s.toUpperCase ());

Limite

Questa operazione imposta un limite sulla dimensione di un flusso. Ad esempio, se desideri creare un nuovo stream contenente un massimo di cinque valori, devi utilizzare quanto segue:

Elenco number_string = numbers.stream () .limit (5)

Filtro

Il filtro (predicato) operazione consente di definire i criteri di filtraggio utilizzando un'espressione lambda. Questa espressione lambda dovere restituisce un valore booleano che determina se ogni elemento deve essere incluso nel flusso risultante. Ad esempio, se disponevi di una serie di stringhe e desideri filtrare qualsiasi stringa contenente meno di tre caratteri, devi utilizzare quanto segue:  

Arrays.stream (myArray) .filter (s -> s.length ()> 3) .forEach (System.out :: println); 

smistato

Questa operazione ordina gli elementi di un flusso. Ad esempio, il seguente comando restituisce un flusso di numeri disposti in ordine crescente:

Elenco list = Arrays.asList (10, 11, 8, 9, 22); list.stream () .sorted () .for Any (System.out :: println);

Elaborazione parallela

Tutte le operazioni di streaming possono essere eseguite in serie o in parallelo, sebbene gli stream siano sequenziali a meno che non si specifichi esplicitamente il contrario. Ad esempio, il seguente elaborerà ogni elemento uno per uno:

Stream.of ("a", "b", "c", "d", "e"). ForEach (System.out :: print);

Per eseguire uno stream in parallelo, è necessario contrassegnare esplicitamente tale stream come parallelo, utilizzando il comando parallelo() metodo:

Stream.of ("a", "b", "c", "d", "e") .parallel () .forEach (System.out :: print);

Sotto il cofano, i flussi paralleli usano il Fork / Join Framework, quindi il numero di thread disponibili è sempre uguale al numero di core disponibili nella CPU.

Lo svantaggio dei flussi paralleli è che possono essere coinvolti diversi core ogni volta che viene eseguito il codice, quindi in genere si ottiene un output diverso ad ogni esecuzione. Pertanto, è necessario utilizzare flussi paralleli solo quando l'ordine di elaborazione non è importante ed evitare flussi paralleli durante l'esecuzione di operazioni basate sugli ordini, ad esempio FindFirst ().

Operazioni terminalistiche

Raccogli i risultati da un flusso usando un'operazione terminale, che è sempre l'ultimo elemento in una catena di metodi stream e restituisce sempre qualcosa di diverso da un flusso.

Esistono diversi tipi di operazioni di terminale che restituiscono vari tipi di dati, ma in questa sezione esamineremo due delle operazioni di terminale più utilizzate.

Raccogliere

Il Raccogliere operazione raccoglie tutti gli elementi elaborati in un contenitore, come a Elenco o Impostato. Java 8 fornisce a Collezionisti classe di utilità, quindi non è necessario preoccuparsi di implementare il Collezionisti interfaccia, oltre a fabbriche per molti collezionisti comuni, tra cui elencare(), impostare(), e toCollection ().

Il seguente codice produrrà a Elenco contenente solo forme rosse:

shapes.stream () .filter (s -> s.getColor (). equals ("red")) .collect (Collectors.toList ());

In alternativa, puoi raccogliere questi elementi filtrati in a Impostato:

 .raccogliere (Collectors.toSet ());

per ciascuno

Il per ciascuno() operazione esegue alcune azioni su ogni elemento dello stream, rendendolo equivalente a una dichiarazione for-each dell'API Stream.

Se tu avessi un elementi elenco, quindi è possibile utilizzare per ciascuno per stampare tutti gli elementi inclusi in questo Elenco:

items.forEach (item-> System.out.println (voce));

Nell'esempio sopra stiamo usando un'espressione lambda, quindi è possibile eseguire lo stesso lavoro con meno codice, usando un riferimento al metodo:

items.forEach (System.out :: println);

Interfacce funzionali

Un'interfaccia funzionale è un'interfaccia che contiene esattamente un metodo astratto, noto come metodo funzionale.

Il concetto di interfacce a metodo singolo non è nuovo-Runnable, comparatore, callable, e OnClickListener sono tutti esempi di questo tipo di interfaccia, anche se nelle versioni precedenti di Java erano conosciute come Interfacce a metodo astratto (interfacce SAM).  

Questo è più di un semplice cambio di nome, poiché ci sono alcune notevoli differenze nel modo di lavorare con le interfacce funzionali (o SAM) in Java 8, rispetto alle versioni precedenti.

Prima di Java 8, in genere è stata creata un'istanza di un'interfaccia funzionale utilizzando un'implementazione ingombrante di classe anonima. Ad esempio, qui stiamo creando un'istanza di Runnable usando una classe anonima:

Runnable r = new Runnable () @Override public void run () System.out.println ("My Runnable"); ;

Come abbiamo visto nella prima parte, quando hai un'interfaccia a metodo singolo, puoi istanziare quell'interfaccia usando un'espressione lambda, piuttosto che una classe anonima. Ora possiamo aggiornare questa regola: puoi creare un'istanza interfacce funzionali, usando un'espressione lambda. Per esempio:

Runnable r = () -> System.out.println ("My Runnable");

Java 8 introduce anche a @FunctionalInterface annotazione che consente di contrassegnare un'interfaccia come un'interfaccia funzionale:

@FunctionalInterface interfaccia pubblica MyFuncInterface public void doSomething (); 

Per garantire la retrocompatibilità con le versioni precedenti di Java, il @FunctionalInterface l'annotazione è facoltativa; tuttavia, si consiglia di garantire l'implementazione corretta delle interfacce funzionali.

Se si tenta di implementare due o più metodi in un'interfaccia contrassegnata come @FunctionalInterface, allora il compilatore si lamenterà di aver scoperto molteplici metodi astratti non sovrascriventi. Ad esempio, il seguente non verrà compilato:

@FunctionalInterface interfaccia pubblica MyFuncInterface void doSomething (); // Definire un secondo metodo astratto // void doSomethingElse ();  

E, se provi a compilare a @FunctionInterface un'interfaccia che contiene metodi zero, quindi incontrerai un Nessun metodo di destinazione trovato errore.

Le interfacce funzionali devono contenere esattamente un metodo astratto, ma poiché i metodi predefiniti e quelli statici non hanno un corpo, sono considerati non astratti. Ciò significa che è possibile includere più metodi predefiniti e statici in un'interfaccia, contrassegnarlo come @FunctionalInterface, e lo sarà ancora compilare.

Java 8 ha anche aggiunto un pacchetto java.util.function che contiene molte interfacce funzionali. Vale la pena dedicare del tempo a familiarizzare con tutte queste nuove interfacce funzionali, solo così sai esattamente cosa è disponibile fuori dalla scatola.

JSR-310: nuova API di data e ora di Java

Lavorare con data e ora in Java non è mai stato particolarmente semplice, con molte API che omettono funzionalità importanti, come le informazioni sul fuso orario.

Java 8 ha introdotto una nuova API Date and Time (JSR-310) che ha lo scopo di risolvere questi problemi, ma sfortunatamente al momento della stesura di questa API non è supportata sulla piattaforma Android. Tuttavia, puoi utilizzare alcune delle nuove funzionalità di data e ora nei tuoi progetti Android oggi, utilizzando una libreria di terze parti.

In questa sezione finale, ti mostrerò come configurare e utilizzare due popolari librerie di terze parti che rendono possibile l'utilizzo dell'API Date and Time di Java 8 su Android.

Threeen Android Backport

ThreeTen Android Backport (noto anche come ThreeTenABP) è un adattamento del popolare progetto di backport ThreeTen, che fornisce un'implementazione di JSR-310 per Java 6.0 e Java 7.0. ThreeTenABP è progettato per fornire accesso a tutte le classi di API Date e Time (anche se con un nome di pacchetto diverso) senza aggiungere un gran numero di metodi ai tuoi progetti Android.

Per iniziare a utilizzare questa libreria, apri il tuo livello di modulo build.gradle file e aggiungi ThreeTenABP come dipendenza del progetto:

dipendenze // Aggiungi la seguente riga // compila 'com.jakewharton.threetenabp: threetenabp: 1.0.5'

È quindi necessario aggiungere l'istruzione di importazione ThreeTenABP:

import com.jakewharton.threetenabp.AndroidThreeTen;

E inizializza le informazioni sul fuso orario nel tuo Application.onCreate () metodo:

@Override public void onCreate () super.onCreate (); AndroidThreeTen.init (questo); 

ThreeTenABP contiene due classi che visualizzano due "tipi" di informazioni su data e ora:

  • LocalDateTime, che memorizza un orario e una data nel formato 2017-10-16T13: 17: 57,138
  • ZonedDateTime, che è a conoscenza di fuso orario e memorizza le informazioni su data e ora nel seguente formato: 2011-12-03T10: 15: 30 + 01: 00 [Europe / Paris]

Per darti un'idea di come useresti questa libreria per recuperare informazioni su data e ora, usiamo il LocalDateTime classe per visualizzare la data e l'ora correnti:

import android.support.v7.app.AppCompatActivity; importare android.os.Bundle; import com.jakewharton.threetenabp.AndroidThreeTen; import android.widget.TextView; import org.threeten.bp.LocalDateTime; public class MainActivity estende AppCompatActivity @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); AndroidThreeTen.init (getApplication ()); setContentView (R.layout.activity_main); TextView textView = new TextView (this); textView.setText ("Time:" + LocalDateTime.now ()); setContentView (textView); 

Questo non è il modo più user-friendly di visualizzare la data e l'ora! Per analizzare questi dati grezzi in qualcosa di più leggibile, puoi usare il DateTimeFormatter classe e impostarlo su uno dei seguenti valori:

  • BASIC_ISO_DATE. Formatta la data come 2017-1016 + 01.00
  • ISO_LOCAL_DATE. Formatta la data come 2017/10/16
  • ISO_LOCAL_TIME. Formatta il tempo come 14: 58: 43,242
  • ISO_LOCAL_DATE_TIME. Formatta la data e l'ora come 2017-10-16T14: 58: 09,616
  • ISO_OFFSET_DATE. Formatta la data come 2017/10/16 + 01.00
  • ISO_OFFSET_TIME.  Formatta il tempo come 14: 58: 56,218 + 01: 00
  • ISO_OFFSET_DATE_TIME. Formatta la data e l'ora come 2017-10-16T14: 5.836,758 + 01: 00
  • ISO_ZONED_DATE_TIME. Formatta la data e l'ora come 2017-10-16T14: 58: 51,324 + 01: 00 (Europe / London)
  • ISO_INSTANT. Formatta la data e l'ora come 2017-10-16T13: 52: 45.246Z
  • ISO_DATE. Formatta la data come 2017/10/16 + 01: 00
  • ISO_TIME. Formatta il tempo come 14: 58: 40,945 + 01: 00
  • ISO_DATE_TIME. Formatta la data e l'ora come 2017-10-16T14: 55: 32,263 + 01: 00 (Europe / London)
  • ISO_ORDINAL_DATE. Formatta la data come 2017-289 + 01: 00
  • ISO_WEEK_DATE. Formatta la data come 2017-W42-1 + 01: 00
  • RFC_1123_DATE_TIME. Formatta la data e l'ora come Lun, 16 OTT 2017 14: 58: 43 + 01: 00

Qui, stiamo aggiornando la nostra app per visualizzare la data e l'ora DateTimeFormatter.ISO_DATE formattazione:

import android.support.v7.app.AppCompatActivity; importare android.os.Bundle; import com.jakewharton.threetenabp.AndroidThreeTen; import android.widget.TextView; // Aggiungi la dichiarazione di importazione DateTimeFormatter // import org.threeten.bp.format.DateTimeFormatter; import org.threeten.bp.ZonedDateTime; public class MainActivity estende AppCompatActivity @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); AndroidThreeTen.init (getApplication ()); setContentView (R.layout.activity_main); TextView textView = new TextView (this); DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE; String formattedZonedDate = formatter.format (ZonedDateTime.now ()); textView.setText ("Time:" + formattedZonedDate); setContentView (textView); 

Per visualizzare queste informazioni in un formato diverso, è sufficiente sostituire DateTimeFormatter.ISO_DATE per un altro valore. Per esempio:

DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

Joda-Time

Prima di Java 8, la libreria Joda-Time era considerata la libreria standard per la gestione di data e ora in Java, al punto in cui la nuova API Date e Time di Java 8 attingeva "pesantemente all'esperienza acquisita dal progetto Joda-Time."

Mentre il sito web Joda-Time consiglia agli utenti di migrare a Data e ora Java 8 il più presto possibile, dal momento che Android non supporta attualmente questa API, Joda-Time è ancora un'opzione valida per lo sviluppo Android. Tuttavia, nota che Joda-Time ha un'API di grandi dimensioni e carica le informazioni sul fuso orario utilizzando una risorsa JAR, entrambe le quali possono influire sul rendimento della tua app.

Per iniziare a lavorare con la libreria Joda-Time, apri il tuo livello di modulo build.gradle file e aggiungere il seguente:

dipendenze compila 'joda-time: joda-time: 2.9.9' ... 

La libreria Joda-Time ha sei principali classi di data e ora:

  • Immediato: Rappresenta un punto nella timeline; ad esempio, è possibile ottenere la data e l'ora correnti chiamando Instant.now ().
  • Appuntamento: Un sostituto generale per JDK Calendario classe.
  • LocalDate: Una data senza un'ora o qualsiasi riferimento a un fuso orario.
  • Ora locale: Un'ora senza una data o un riferimento a un fuso orario, ad esempio 14:00:00.
  • LocalDateTime: Una data e un'ora locali, ancora senza informazioni sul fuso orario.
  • ZonedDateTime: Una data e ora con un fuso orario.

Diamo un'occhiata a come stamperesti data e ora usando Joda-Time. Nell'esempio seguente sto riutilizzando il codice del nostro esempio ThreeTenABP, così da rendere le cose più interessanti che sto usando anche io withZone convertire la data e l'ora in a ZonedDateTime valore.

import android.support.v7.app.AppCompatActivity; importare android.os.Bundle; import android.widget.TextView; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; public class MainActivity estende AppCompatActivity @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); DateTime today = new DateTime (); // Restituisce un nuovo formattatore (usando withZone) e specifica il fuso orario, usando ZoneId // DateTime todayNy = today.withZone (DateTimeZone.forID ("America / New_York")); TextView textView = new TextView (this); textView.setText ("Time:" + todayNy); setContentView (textView); 

Troverai un elenco completo dei fusi orari supportati nei documenti ufficiali di Joda-Time.

Conclusione

In questo post, abbiamo esaminato come creare codice più robusto utilizzando le annotazioni sui tipi ed esplorato l'approccio "pipe and filter" all'elaborazione dei dati con la nuova API Stream di Java 8.

Abbiamo anche esaminato come le interfacce si sono evolute in Java 8 e come usarle in combinazione con altre funzionalità che abbiamo esplorato in questa serie, comprese le espressioni lambda e i metodi di interfaccia statica.

Per concludere, ti ho mostrato come accedere ad alcune funzionalità aggiuntive di Java 8 che Android attualmente non supporta per impostazione predefinita, utilizzando i progetti Joda-Time e ThreeTenABP.

Puoi saperne di più sulla versione di Java 8 sul sito Web di Oracle.

E mentre sei qui, dai uno sguardo ad alcuni dei nostri altri post su Java 8 e lo sviluppo di Android!