Java 8 for Android Development Stream API og dato og tidsbiblioteker

I denne tredelte serien har vi undersøkt alle de store Java 8-funksjonene som du kan begynne å bruke i Android-prosjekter dine i dag.

I Cleaner Code Med Lambda Expressions, fokuserte vi på å kutte kjeveplater fra prosjektene dine ved hjelp av lambda-uttrykk, og så i Default og Static Methods så vi hvordan disse lambda-uttrykkene ble mer konsise ved å kombinere dem med metodenes referanser. Vi dekket også gjentatte notater og hvordan deklarerer ikke-abstrakte metoder i grensesnittene dine ved hjelp av standard og statisk grensesnitt.

I dette siste innlegget skal vi se på annonseformater, funksjonelle grensesnitt og hvordan å ta en mer funksjonell tilnærming til databehandling med Java 8s nye Stream API.

Jeg vil også vise deg hvordan du får tilgang til noen ekstra Java 8-funksjoner som ikke støttes av Android-plattformen, ved hjelp av Joda-Time og ThreeTenABP-bibliotekene.

Skriv merknader

Annoteringer hjelper deg med å skrive kode som er mer robust og mindre feilaktig, ved å informere kodeinspeksjonsverktøy som Lint om feilene de burde se etter. Disse kontrollverktøyene vil da advare deg om et stykke kode ikke samsvarer med reglene som er angitt i disse merknadene.

Merknader er ikke en ny funksjon (faktisk går de tilbake til Java 5.0), men i tidligere versjoner av Java var det bare mulig å legge til merknader i deklarasjoner.

Med utgivelsen av Java 8 kan du nå bruke notater hvor som helst du har brukt en type, inkludert metodemottakere; Eksempler på skapelse av klasseeksempler; implementeringen av grensesnitt; generikk og arrays; spesifikasjonen av kaster og redskaper klausuler; og skriv avstøpning.

Frustrerende, selv om Java 8 gjør det mulig å bruke notater på flere steder enn noen gang før, gir det ikke noen merknader som er spesifikke for typer.

Android's Annotations Support Library gir tilgang til noen ekstra merknader, for eksempel @Nullable, @NonNull, og merknader for validering av ressurstyper som  @DrawableRes, @DimenRes, @ColorRes, og @StringRes. Du kan imidlertid også bruke et statisk analyseverktøy fra tredjepart, for eksempel Checker Framework, som ble samarbeids med JSR 308-spesifikasjonen (spesifikasjonen Annotations on Java Types). Dette rammeverket gir sitt eget sett med merknader som kan brukes til typer, pluss en rekke "brikker" (annoteringsprosessorer) som henger inn i kompileringsprosessen og utfører bestemte "sjekker" for hver type annotasjon som er inkludert i Checker Framework.

Siden Type Annotations ikke påvirker kjøretid operasjon, kan du bruke Java 8s Type Annotations i prosjektene dine mens du er tilbake bakover kompatibel med tidligere versjoner av Java.

Stream-API

Stream-API tilbyr et alternativt "rør-og-filter" -tilnærming til behandling av samlinger.

Før Java 8 manipulerte du samlinger manuelt, vanligvis ved å iterere over samlingen og operere på hvert element i sin tur. Denne eksplisitte loopingen krevde mye kjeleplater, pluss det er vanskelig å forstå for-loop-strukturen til du kommer til sløyfens kropp.

Stream-API gir deg muligheten til å behandle data mer effektivt, ved å utføre en enkelt kjøring over dataene, uavhengig av mengden data du behandler, eller om du utfører flere beregninger.

I Java 8, hver klasse som implementerer java.util.Collection har en strøm metode som kan konvertere sine forekomster til Strøm objekter. For eksempel, hvis du har en Array:

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

Deretter kan du konvertere det til en strøm med følgende:

Strøm myStream = Arrays.stream (myArray);

Stream-API behandler data ved å bære verdier fra en kilde, gjennom en rekke beregningssteg, kjent som a strømrørledning. En strømrørledning består av følgende:

  • En kilde, for eksempel a Samling, array eller generatorfunksjon.
  • Null eller flere mellomliggende "dovne" operasjoner. Mellomliggende operasjoner starter ikke behandlingselementer før du anvender a terminal operasjon-Derfor er de betraktet lat.For eksempel ringer Stream.filter () På en datakilde er det bare å sette opp strømrørledningen. ingen filtrering skjer faktisk før du ringer til terminaloperasjonen. Dette gjør det mulig å strengere flere operasjoner sammen, og deretter utføre alle disse beregningene i ett enkelt pass av dataene. Mellomliggende operasjoner produserer en ny strøm (for eksempel, filter vil produsere en strøm som inneholder de filtrerte elementene) uten endre datakilden, slik at du er fri til å bruke originaldataene andre steder i prosjektet ditt, eller opprette flere strømmer fra samme kilde.
  • En terminaloperasjon, for eksempel Stream.forEach. Når du påkaller terminaloperasjonen, kjører alle dine mellomliggende operasjoner og produserer en ny strøm. En strøm er ikke i stand til å lagre elementer, så så snart du påberoper en terminaloperasjon, anses denne strømmen som "forbrukes" og er ikke lenger brukbar. Hvis du vil gjenopprette elementene i en strøm, må du generere en ny strøm fra den opprinnelige datakilden.

Opprette en strøm

Det finnes ulike måter å skaffe en strøm fra en datakilde, inkludert:

  • Stream.of ()Oppretter en strøm fra individuelle verdier:

Strøm stream = Stream.of ("A", "B", "C");
  • IntStream.range () Oppretter en strøm fra en rekke tall:

IntStream i = IntStream.range (0, 20);
  • Stream.iterate () Oppretter en strøm ved gjentatte ganger å bruke en operatør til hvert element. For eksempel lager vi en strøm der hvert element øker i verdi for en:

Strøm s = Stream.iterate (0, n -> n + 1);

Transformere en strøm med operasjoner

Det er massevis av operasjoner som du kan bruke til å utføre funksjonell stilberegning på dine strømmer. I denne delen skal jeg dekke noen få av de mest brukte stream operasjonene.

Kart

De kart() operasjonen tar et lambda-uttrykk som det eneste argumentet, og bruker dette uttrykket for å transformere verdien eller typen av hvert element i strømmen. For eksempel gir følgende oss en ny strøm, hvor hver string har blitt konvertert til store versjoner:

Strøm myNewStream = myStream.map (s -> s.toUpperCase ());

Grense

Denne operasjonen angir en grense for størrelsen på en strøm. Hvis du for eksempel vil opprette en ny strøm som inneholder maksimalt fem verdier, bruker du følgende:

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

Filter

De filter (Predicate) operasjon lar deg definere filtreringskriterier ved hjelp av et lambda-uttrykk. Dette lambda-uttrykket returnere en boolsk verdi som bestemmer om hvert element skal inkluderes i den resulterende strømmen. Hvis du for eksempel hadde en rekke strenger og ønsket å filtrere ut noen strenger som inneholdt mindre enn tre tegn, ville du bruke følgende:  

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

sortert

Denne operasjonen sorterer elementene i en strøm. For eksempel returnerer følgende en strøm av tall som er arrangert i stigende rekkefølge:

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

Parallell prosessering

Alle stream-operasjoner kan utføres i seriell eller parallell, selv om strømmer er sekvensielle med mindre du spesifikt angir noe annet. For eksempel vil følgende behandle hvert element en etter en:

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

For å utføre en strøm parallelt, må du eksplisitt markere denne strømmen som parallell, ved hjelp av parallell() metode:

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

Under hetten bruker parallellstrømmer Fork / Join Framework, slik at antall tilgjengelige tråder alltid tilsvarer antall tilgjengelige kjerner i CPU.

Ulempen ved parallelle strømmer er at forskjellige kjerner kan være involvert hver gang koden er utført, slik at du vanligvis får en annen utgang med hver utførelse. Derfor bør du bare bruke parallelle strømmer når behandlingsordren er ubetydelig, og unngå parallelle strømmer når du utfører bestillingsbaserte operasjoner, for eksempel findFirst ().

Terminaloperasjoner

Du samler resultatene fra en strøm ved hjelp av en terminaloperasjon, som er alltid Det siste elementet i en kjede av strømmetoder, og returnerer alltid noe annet enn en strøm.

Det finnes noen forskjellige typer terminaloperasjoner som returnerer ulike typer data, men i dette avsnittet skal vi se på to av de mest brukte terminaloperasjonene.

Samle inn

De Samle inn Operasjonen samler alle behandlede elementer inn i en beholder, for eksempel a Liste eller Sett. Java 8 gir en samlere verktøysklasse, så du trenger ikke å bekymre deg for å implementere samlere grensesnitt, pluss fabrikker for mange vanlige samlere, inkludert ramse opp(), å sette(), og toCollection ().

Følgende kode vil produsere en Liste inneholder bare røde figurer:

shapes.stream () .filter (s -> s.getColor () .lign ("rød")) .collect (Collectors.toList ());

Alternativt kan du samle disse filtrerte elementene inn i en Sett:

 .samle (Collectors.toSet ());

for hver

De for hver() operasjonen utfører en viss handling på hvert element i strømmen, noe som gjør at det er Stream APIs ekvivalent av en for hver setning.

Hvis du hadde en elementer liste, så kan du bruke for hver å skrive ut alle elementene som er inkludert i dette Liste:

items.forEach (element-> System.out.println (pos));

I eksemplet ovenfor bruker vi et lambda-uttrykk, så det er mulig å utføre det samme arbeidet i mindre kode, ved hjelp av en metodereferanse:

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

Funksjonelle grensesnitt

Et funksjonelt grensesnitt er et grensesnitt som inneholder nøyaktig en abstrakt metode, kjent som funksjonell metode.

Begrepet enkeltmetodegrensesnitt er ikke nytt-kjørbart, komparator, Callable, og OnClickListener er alle eksempler på denne typen grensesnitt, men i tidligere versjoner av Java var de kjent som Single Abstract Method Interfaces (SAM-grensesnitt).  

Dette er mer enn en enkel navneendring, da det er noen bemerkelsesverdige forskjeller i hvordan du jobber med funksjonelle (eller SAM) grensesnitt i Java 8, sammenlignet med tidligere versjoner.

Før Java 8 opprettede du vanligvis et funksjonelt grensesnitt ved hjelp av en omfangsrik anonym klasse implementering. For eksempel, her skaper vi en forekomst av kjørbart bruker en anonym klasse:

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

Som vi så tilbake i del ett, når du har en enkeltmetode grensesnitt, kan du instantiere det grensesnittet ved hjelp av et lambda-uttrykk, i stedet for en anonym klasse. Nå kan vi oppdatere denne regelen: du kan instantiere funksjonelle grensesnitt, bruker et lambda-uttrykk. For eksempel:

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

Java 8 introduserer også a @FunctionalInterface annotasjon som lar deg merke et grensesnitt som et funksjonelt grensesnitt:

@FunctionalInterface offentlige grensesnitt MyFuncInterface public void doSomething (); 

For å sikre kompatibilitet bakover med tidligere versjoner av Java, @FunctionalInterface merknad er valgfri; Det anbefales imidlertid å sikre at du implementerer dine funksjonsgrensesnitt på riktig måte.

Hvis du prøver å implementere to eller flere metoder i et grensesnitt som er merket som @FunctionalInterface, så vil kompilatoren klage over at det er oppdaget flere ikke-overordnede abstrakte metoder. For eksempel vil ikke følgende kompilere:

@FunctionalInterface offentlig grensesnitt MyFuncInterface void doSomething (); // Definer en annen abstrakt metode // void doSomethingElse ();  

Og hvis du prøver å kompilere en @FunctionInterface grensesnitt som inneholder null metoder, så kommer du til å møte en Ingen målmetode funnet feil.

Funksjonelle grensesnitt må inneholde nøyaktig en abstrakt metode, men siden standard og statiske metoder ikke har en kropp, anses de som ikke-abstrakte. Dette betyr at du kan inkludere flere standard og statiske metoder i et grensesnitt, merk det som @FunctionalInterface, og det vil det fortsatt kompilere.

Java 8 har også lagt til en java.util.function-pakke som inneholder mange funksjonelle grensesnitt. Det er vel verdt å ta deg tid til å bli kjent med alle disse nye funksjonelle grensesnittene, bare slik at du vet nøyaktig hva som er tilgjengelig ute av esken.

JSR-310: Java's New Date and Time API

Arbeide med dato og klokkeslett i Java har aldri vært spesielt grei, med mange APIer som utelater viktig funksjonalitet, for eksempel tidssoneinformasjon.

Java 8 introduserte en ny Date and Time API (JSR-310) som tar sikte på å løse disse problemene, men dessverre støttes ikke API-en på Android-plattformen dessverre. Du kan imidlertid bruke noen av de nye dato- og klokkeslettfunksjonene i Android-prosjekter i dag, ved hjelp av et tredjepartsbibliotek.

I denne siste delen skal jeg vise deg hvordan du konfigurerer og bruker to populære tredjepartsbiblioteker som gjør det mulig å bruke Java 8s dato og tid API på Android.

ThreeTen Android Backport

ThreeTen Android Backport (også kjent som ThreeTenABP) er en tilpasning av det populære ThreeTen backport-prosjektet, som gir en implementering av JSR-310 for Java 6.0 og Java 7.0. ThreeTenABP er designet for å gi tilgang til alle dato og tid API-klasser (om enn med et annet pakkenavn) uten å legge til et stort antall metoder for Android-prosjektene dine.

For å begynne å bruke dette biblioteket, åpne modulnivået ditt build.gradle fil og legg til ThreeTenABP som prosjektavhengighet:

avhengigheter // Legg til følgende linje // kompilere 'com.jakewharton.threetenabp: threetenabp: 1.0.5'

Du må da legge til treTenABP import setningen:

importer com.jakewharton.threetenabp.AndroidThreeTen;

Og initialiser tidssoneinformasjonen i din Application.onCreate () metode:

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

ThreeTenABP inneholder to klasser som viser to "typer" av tid og dato informasjon:

  • LocalDateTime, som lagrer en tid og en dato i formatet 2017-10-16T13: 17: 57,138
  • ZonedDateTime, som er tidszonen oppmerksom og lagrer dato og klokkeslettinformasjon i følgende format: 2011-12-03T10: 15: 30 + 01: 00 [Europa / Paris]

For å gi deg en ide om hvordan du vil bruke dette biblioteket til å hente dato og tid informasjon, la oss bruke LocalDateTime klasse for å vise gjeldende dato og klokkeslett:

importer android.support.v7.app.AppCompatActivity; importere android.os.Bundle; importer com.jakewharton.threetenabp.AndroidThreeTen; importer android.widget.TextView; importer org.threeten.bp.LocalDateTime; offentlig klasse MainActivity utvider AppCompatActivity @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); AndroidThreeTen.init (getApplication ()); setContentView (R.layout.activity_main); TextView textView = ny TextView (dette); textView.setText ("Tid:" + LocalDateTime.now ()); setContentView (textView); 

Dette er ikke den mest brukervennlige måten å vise dato og klokkeslett på! For å analysere disse rådataene i noe mer menneskelig lesbar, kan du bruke DateTimeFormatter klasse og sett den til en av følgende verdier:

  • BASIC_ISO_DATE. Formater datoen som 2017-1016 + 01,00
  • ISO_LOCAL_DATE. Formater datoen som 2017-10-16
  • ISO_LOCAL_TIME. Formaterer tiden som 14: 58: 43,242
  • ISO_LOCAL_DATE_TIME. Formaterer dato og klokkeslett som 2017-10-16T14: 58: 09,616
  • ISO_OFFSET_DATE. Formater datoen som 2017-10-16 + 01,00
  • ISO_OFFSET_TIME.  Formaterer tiden som 14: 58: 56,218 + 01: 00
  • ISO_OFFSET_DATE_TIME. Formaterer dato og klokkeslett som 2017-10-16T14: 5836,758 + 01: 00
  • ISO_ZONED_DATE_TIME. Formaterer dato og klokkeslett som 2017-10-16T14: 58: 51,324 + 01: 00 (Europe / London)
  • ISO_INSTANT. Formaterer dato og klokkeslett som 2017-10-16T13: 52: 45.246Z
  • ISO_DATE. Formater datoen som 2017-10-16 + 01: 00
  • ISO_TIME. Formaterer tiden som 14: 58: 40,945 + 01: 00
  • ISO_DATE_TIME. Formaterer dato og klokkeslett som 2017-10-16T14: 55: 32,263 + 01: 00 (Europe / London)
  • ISO_ORDINAL_DATE. Formater datoen som 2017-289 + 01: 00
  • ISO_WEEK_DATE. Formater datoen som 2017-W42-1 + 01: 00
  • RFC_1123_DATE_TIME. Formaterer dato og klokkeslett som Mandag, 16 oktober 2017 14: 58: 43 + 01: 00

Her oppdaterer vi appen vår for å vise dato og klokkeslett med DateTimeFormatter.ISO_DATE formatering:

importer android.support.v7.app.AppCompatActivity; importere android.os.Bundle; importer com.jakewharton.threetenabp.AndroidThreeTen; importer android.widget.TextView; // Legg til DateTimeFormatter import setning // importer org.threeten.bp.format.DateTimeFormatter; importer org.threeten.bp.ZonedDateTime; offentlig klasse MainActivity utvider AppCompatActivity @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); AndroidThreeTen.init (getApplication ()); setContentView (R.layout.activity_main); TextView textView = ny TextView (dette); DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE; String formattedZonedDate = formatter.format (ZonedDateTime.now ()); textView.setText ("Time:" + formattedZonedDate); setContentView (textView); 

Hvis du vil vise denne informasjonen i et annet format, kan du bare erstatte DateTimeFormatter.ISO_DATE for en annen verdi. For eksempel:

DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

Joda-Time

Før Java 8 ble Joda-Time-biblioteket betraktet som standardbiblioteket for håndtering av dato og klokkeslett i Java, til det punkt der Java 8s nye dato og klokkeslett API faktisk trekker "tungt på erfaring oppnådd fra Joda-Time-prosjektet."

Mens Joda-Time-nettstedet anbefaler at brukerne migrerer til Java 8, dato og klokkeslett så snart som mulig, siden Android for øyeblikket ikke støtter denne API-en, er Joda-Time fortsatt et levedyktig alternativ for Android-utvikling. Vær imidlertid oppmerksom på at Joda-Time har en stor API og laster inn tidssoneinformasjon ved hjelp av en JAR-ressurs, som begge kan påvirke appens ytelse.

For å begynne å jobbe med Joda-Time-biblioteket, åpne modulnivået build.gradle fil og legg til følgende:

avhengigheter compile 'joda-time: joda-time: 2.9.9' ... 

Joda-Time-biblioteket har seks store dato- og tidsklasser:

  • Umiddelbar: Representerer et punkt i tidslinjen; Du kan for eksempel få dagens dato og klokkeslett ved å ringe Instant.now ().
  • Dato tid: En generell erstatning for JDK Kalender klasse.
  • LOCALDATE: En dato uten tid, eller en henvisning til en tidssone.
  • Lokal tid: En tid uten en dato eller en henvisning til en tidssone, for eksempel 14:00:00.
  • LocalDateTime: En lokal dato og klokkeslett, fortsatt uten tidszoneinformasjon.
  • ZonedDateTime: En dato og klokkeslett med en tidssone.

La oss se på hvordan du vil skrive ut dato og klokkeslett ved hjelp av Joda-Time. I det følgende eksemplet bruker jeg koden fra vårt ThreeTenABP-eksempel, for å gjøre ting mer interessant, bruker jeg også withZone å konvertere dato og klokkeslett til a ZonedDateTime verdi.

importer android.support.v7.app.AppCompatActivity; importere android.os.Bundle; importer android.widget.TextView; importer org.joda.time.DateTime; importer org.joda.time.DateTimeZone; offentlig klasse MainActivity utvider AppCompatActivity @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); DateTime today = ny DateTime (); // Returner en ny formatter (bruker withZone) og angi tidszonen, ved hjelp av ZoneId // DateTime todayNy = today.withZone (DateTimeZone.forID ("America / New_York")); TextView textView = ny TextView (dette); textView.setText ("Tid:" + i dagNy); setContentView (textView); 

Du finner en fullstendig liste over støttede tidssoner i de offisielle Joda-Time-dokumentene.

Konklusjon

I dette innlegget så vi på hvordan du oppretter mer robust kode ved hjelp av typebeskrivelser, og utforsket "pipes and filters" tilnærmingen til databehandling med Java 8s nye Stream API.

Vi så også på hvordan grensesnittene har utviklet seg i Java 8 og hvordan de skal brukes i kombinasjon med andre funksjoner vi har utforsket gjennom denne serien, inkludert lambda-uttrykk og statiske grensesnittmetoder.

For å pakke opp ting, viste jeg deg hvordan du får tilgang til noen ekstra Java 8-funksjoner som Android for øyeblikket ikke støtter som standard, ved hjelp av Joda-Time og ThreeTenABP-prosjekter.

Du kan lære mer om Java 8-utgivelsen på Oracles nettsted.

Og mens du er her, sjekk ut noen av våre andre innlegg om Java 8 og Android utvikling!