Java 8 för Android Development Stream API och Date & Time-bibliotek

I den här tredelade serien har vi undersökt alla större Java 8-funktioner som du kan börja använda i dina Android-projekt idag.

I Cleaner Code With Lambda Expressions fokuserade vi på att skära panna från dina projekt med hjälp av lambda-uttryck, och sedan i Default och Static Methods såg vi hur man gör dessa lambda-uttryck mer koncisa genom att kombinera dem med metodreferenser. Vi omfattade också Repeating Annotations och hur man förklarar icke-abstrakta metoder i dina gränssnitt med standard- och statiska gränssnittsmetoder.

I det här sista inlägget ska vi titta på typannonser, funktionella gränssnitt och hur man tar ett mer funktionellt förhållningssätt till databehandling med Java 8: s nya Stream API.

Jag visar dig också hur du får tillgång till ytterligare Java 8-funktioner som inte stöds av Android-plattformen, med hjälp av Joda-Time och ThreeTenABP-biblioteken.

Skriv anteckningar

Annotationer hjälper dig att skriva kod som är mer robust och mindre felaktig, genom att informera kodinspektionsverktyg som Lint om de fel de borde leta efter. Dessa inspektionsverktyg kommer då att varna dig om ett kodnummer inte överensstämmer med de regler som anges i dessa anmärkningar.

Anteckningar är inte en ny funktion (i själva verket går de tillbaka till Java 5.0), men i tidigare versioner av Java var det bara möjligt att ange anteckningar till deklarationer.

Med utgåvan av Java 8 kan du nu använda anteckningar överallt du har använt en typ, inklusive metodmottagare; expres skapande uttryck i klass genomförandet av gränssnitt generika och arrays; specifikationen av kastar och redskap klausuler; och typgjutning.

Frustrerande, även om Java 8 gör det möjligt att använda anteckningar på fler platser än någonsin tidigare, ger det inga kommentarer som är specifika för typer.

Androids annotationssupportbibliotek ger tillgång till några ytterligare kommentarer, till exempel @Nullable, @NonNull, och anteckningar för att validera resurs typer som  @DrawableRes, @DimenRes, @ColorRes, och @StringRes. Du kan dock också använda ett statiskt analysverktyg från tredje part, till exempel Checker Framework, som samproducerades med specifikationen JSR 308 (specifikationen Annotations on Java Types). Denna ram innehåller sin egen uppsättning anteckningar som kan tillämpas på typer, plus ett antal "checkers" (annoteringsprocessorer) som hakar i kompileringsprocessen och utför specifika "kontroller" för varje typannonsering som ingår i Checker Framework.

Eftersom typannonser inte påverkar runtime-funktionen kan du använda Java 8-typannonser i dina projekt medan de återstår bakåtkompatibla med tidigare versioner av Java.

Stream API

Stream API erbjuder ett alternativt "rör-och-filter" -tillvägagångssätt för samlingsbehandling.

Före Java 8 manipulerade du samlingar manuellt, vanligtvis genom att iterera över samlingen och fungera på varje element i sin tur. Denna uttryckliga looping krävde mycket pannplatta, plus det är svårt att förstå for-loop-strukturen tills du når slingans kropp.

Stream API ger dig möjlighet att bearbeta data mer effektivt genom att utföra en enda körning över den data, oavsett hur mycket data du bearbetar, eller om du utför flera beräkningar.

I Java 8, varje klass som implementerar java.util.Collection har en strömma metod som kan omvandla sina instanser till Strömma objekt. Till exempel, om du har en Array:

String [] myArray = ny sträng [] "A", "B", "C", "D";

Då kan du konvertera det till en ström med följande:

Strömma myStream = Arrays.stream (myArray);

Stream-API behandlar data genom att överföra värden från en källa, genom en serie beräkningssteg, känd som a strömledningar. En strömrörledning består av följande:

  • En källa, som a Samling, array eller generatorfunktion.
  • Noll eller mer mellanliggande "lata" operationer. Mellanliggande operationer startar inte behandlingselementen förrän du anropar a terminal operation-Därför betraktas de som lata.Till exempel ringer Stream.filter () På en datakälla sätter man bara upp strömledningen. ingen filtrering sker faktiskt tills du ringer till terminaloperationen. Detta gör det möjligt att stränga flera operationer tillsammans, och sedan utföra alla dessa beräkningar i ett enda pass av data. Mellanliggande operationer ger en ny ström (till exempel, filtrera kommer att producera en ström som innehåller de filtrerade elementen) utan ändra datakällan så att du kan använda originaluppgifterna någon annanstans i ditt projekt eller skapa flera strömmar från samma källa.
  • En terminaloperation, t.ex. Stream.forEach. När du anropar terminaloperationen kör alla dina mellanliggande operationer och producera en ny ström. En ström kan inte lagra element, så snart som du anropar en terminaloperation anses den strömmen "förbrukad" och är inte längre användbar. Om du vill återkomma elementen i en ström måste du generera en ny ström från den ursprungliga datakällan.

Skapa en ström

Det finns olika sätt att få en ström från en datakälla, inklusive:

  • Stream.of ()Skapar en ström från enskilda värden:

Strömma stream = Stream.of ("A", "B", "C");
  • IntStream.range () Skapar en ström från ett antal nummer:

IntStream i = IntStream.range (0, 20);
  • Stream.iterate () Skapar en ström genom att flera gånger applicera en operatör på varje element. Till exempel skapar vi här en ström där varje element ökar i värde med en:

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

Transformera en ström med operationer

Det finns massor av operationer som du kan använda för att utföra funktionella beräkningar på dina strömmar. I det här avsnittet kommer jag att täcka bara några av de vanligaste strömoperationerna.

Karta

De Karta() operation tar ett lambda-uttryck som det enda argumentet och använder detta uttryck för att omvandla värdet eller typen av varje element i strömmen. Till exempel ger vi oss en ny ström, var var och en Sträng har konverterats till stor bokstav:

Strömma myNewStream = myStream.map (s -> s.toUpperCase ());

Begränsa

Denna operation ställer in en gräns för storleken på en ström. Om du till exempel vill skapa en ny ström med högst fem värden, kan du använda följande:

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

Filtrera

De filter (Predikat) funktionen kan du definiera filtreringskriterier med ett lambda-uttryck. Detta lambda uttryck måste returnera ett booleskt värde som bestämmer om varje element ska ingå i den resulterande strömmen. Om du till exempel hade en rad strängar och ville filtrera bort några strängar som innehöll mindre än tre tecken, skulle du använda följande:  

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

Sorterad

Denna operation sorterar elementen i en ström. Till exempel returnerar följande en ström av tal arrangerade i stigande ordning:

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

Parallell bearbetning

Alla flödesoperationer kan utföras i seriell eller parallell, även om flöden är sekventiella om du inte uttryckligen anger annat. Till exempel kommer följande att behandla varje element en efter en:

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

För att utföra en ström parallellt måste du uttryckligen markera den strömmen som parallell med hjälp av parallell() metod:

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

Under huven använder parallella strömmar Fork / Join Framework, så antalet tillgängliga trådar är alltid lika med antalet tillgängliga kärnor i processorn.

Nackdelen med parallella strömmar är att olika kärnor kan vara involverade varje gång koden exekveras, så får du vanligtvis en annan utmatning vid varje utförande. Därför bör du bara använda parallella strömmar när bearbetningsordern är obetydlig och undvika parallella strömmar vid utförande av orderbaserade operationer, t.ex. findfirst ().

Terminaloperationer

Du samlar resultaten från en ström med en terminaloperation, som är alltid det sista elementet i en kedja av strömmetoder, och returnerar alltid något annat än en ström.

Det finns några olika typer av terminaloperationer som returnerar olika typer av data, men i det här avsnittet kommer vi att titta på två av de vanligaste terminaloperationerna.

Samla

De Samla operationen samlar alla bearbetade element i en behållare, såsom a Lista eller Uppsättning. Java 8 ger en samlare nytta klass, så du behöver inte oroa dig för att genomföra samlare gränssnitt, plus fabriker för många vanliga samlare, inklusive att lista(), Att sätta(), och toCollection ().

Följande kod kommer att producera en Lista Endast röda former:

former.stream () .filter (s -> s.getColor (). motsvarar ("rött")) .samla (Samlare. tillList ());

Alternativt kan du samla in dessa filtrerade element i en Uppsättning:

 .samla (Collectors.toSet ());

för varje

De för varje() operation utför en viss åtgärd på varje element i strömmen, vilket gör det till ett Stream-APIs motsvarighet för ett för-varje uttalande.

Om du hade en objekt lista, då kan du använda för varje att skriva ut alla objekt som ingår i detta Lista:

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

I det ovanstående exemplet använder vi ett lambda-uttryck, så det är möjligt att utföra samma arbete i mindre kod, med hjälp av en metodreferens:

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

Funktionella gränssnitt

Ett funktionellt gränssnitt är ett gränssnitt som innehåller exakt en abstrakt metod, känd som funktionell metod.

Konceptet med enkätmetoder är inte nytt-Runnable, jämförelse, inlösbara, och OnClickListener är alla exempel på denna typ av gränssnitt, även om de i tidigare versioner av Java var kända som enkla abstrakta metodgränssnitt (SAM-gränssnitt).  

Det här är mer än en enkel namnändring, eftersom det finns några anmärkningsvärda skillnader i hur du arbetar med funktionella (eller SAM) -gränssnitt i Java 8, jämfört med tidigare versioner.

Före Java 8 skapade du vanligtvis ett funktionellt gränssnitt med en skrymmande anonym implementering av klasser. Till exempel skapar vi här en instans av Runnable använder en anonym klass:

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

När vi såg tillbaka i del ett, när du har ett enda metoden gränssnitt, kan du instantiate det gränssnittet med ett lambda uttryck snarare än en anonym klass. Nu kan vi uppdatera denna regel: du kan inställa funktionella gränssnitt, med användning av ett lambda-uttryck. Till exempel:

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

Java 8 introducerar också a @FunctionalInterface annotering som låter dig markera ett gränssnitt som ett funktionellt gränssnitt:

@FunctionalInterface public interface MyFuncInterface public void doSomething (); 

För att säkerställa bakåtkompatibilitet med tidigare versioner av Java, ska @FunctionalInterface anteckningen är valfri; Det rekommenderas dock att du säkerställer att du implementerar dina funktionsgränssnitt korrekt.

Om du försöker implementera två eller flera metoder i ett gränssnitt som är markerat som @FunctionalInterface, då kommer kompilatorn att klaga på att det har upptäckts flera oöverträffade abstrakta metoder. Till exempel kommer inte följande att kompilera:

@FunctionalInterface public interface MyFuncInterface void doSomething (); // Definiera en andra abstrakt metod // void doSomethingElse ();  

Och om du försöker kompilera en @FunctionInterface gränssnitt som innehåller nollmetoder, då kommer du att stöta på en Ingen målmetod hittades fel.

Funktionella gränssnitt måste innehålla exakt en abstrakt metod, men eftersom standard och statiska metoder inte har en kropp anses de vara icke abstrakta. Det betyder att du kan inkludera flera standard- och statiska metoder i ett gränssnitt, markera det som @FunctionalInterface, och det ska det fortfarande sammanställa.

Java 8 lade också till ett java.util.function-paket som innehåller många funktionella gränssnitt. Det är väl värt att ta sig tid att bekanta sig med alla dessa nya funktionella gränssnitt, bara så du vet exakt vad som finns tillgängligt ur lådan.

JSR-310: Java's nya datum och tid API

Att arbeta med datum och tid i Java har aldrig varit särskilt okomplicerat, med många API: er som saknar viktig funktionalitet, som tidszoninformation.

Java 8 introducerade ett nytt Date and Time API (JSR-310) som syftar till att lösa dessa problem, men tyvärr stöds inte detta API på Android-plattformen. Du kan dock använda några av de nya datum- och tidsfunktionerna i dina Android-projekt idag, med hjälp av ett bibliotek från tredje part.

I det här sista avsnittet ska jag visa dig hur du konfigurerar och använder två populära tredje partsbibliotek som gör det möjligt att använda Java 8: s Date and Time API på Android.

ThreeTen Android Backport

ThreeTen Android Backport (även känd som ThreeTenABP) är en anpassning av det populära ThreeTen backport-projektet, vilket ger en implementering av JSR-310 för Java 6.0 och Java 7.0. ThreeTenABP är utformad för att ge tillgång till alla klasserna Date and Time API (om än med ett annat paketnamn) utan att lägga till ett stort antal metoder för dina Android-projekt.

För att börja använda det här biblioteket, öppna din modulnivå build.gradle fil och lägg till ThreeTenABP som ett projektberoende:

beroenden // Lägg till följande rad // kompilera 'com.jakewharton.threetenabp: threetenabp: 1.0.5'

Därefter måste du lägga till treTenABP-importdeklarationen:

importera com.jakewharton.threetenabp.AndroidThreeTen;

Och initiera tidszoninformationen i din Application.onCreate () metod:

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

ThreeTenABP innehåller två klasser som visar två "typer" av tid och datum information:

  • LocalDateTime, som lagrar en tid och ett datum i formatet 2017-10-16T13: 17: 57,138
  • ZonedDateTime, vilken är tidszon medveten och lagrar datum och tid information i följande format: 2011-12-03T10: 15: 30 + 01: 00 [Europa / Paris]

För att ge dig en uppfattning om hur du använder detta bibliotek för att hämta datum och tid information, låt oss använda LocalDateTime klass för att visa aktuellt datum och tid:

importera android.support.v7.app.AppCompatActivity; importera android.os.Bundle; importera com.jakewharton.threetenabp.AndroidThreeTen; importera android.widget.TextView; importera org.threeten.bp.LocalDateTime; public class MainActivity utökar 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 ("Tid:" + LocalDateTime.now ()); setContentView (Textview); 

Det här är inte det mest användarvänliga sättet att visa datum och tid! För att analysera dessa rådata till något mer mänskligt läsbar kan du använda DateTimeFormatter klass och ange det till ett av följande värden:

  • BASIC_ISO_DATE. Formaterar datumet som 2017-1016 + 01.00
  • ISO_LOCAL_DATE. Formaterar datumet som 2017/10/16
  • ISO_LOCAL_TIME. Formaterar tiden som 14: 58: 43,242
  • ISO_LOCAL_DATE_TIME. Formaterar datum och tid som 2017-10-16T14: 58: 09,616
  • ISO_OFFSET_DATE. Formaterar datumet som 2017/10/16 + 01.00
  • ISO_OFFSET_TIME.  Formaterar tiden som 14: 58: 56,218 + 01: 00
  • ISO_OFFSET_DATE_TIME. Formaterar datum och tid som 2017-10-16T14: 5836,758 + 01: 00
  • ISO_ZONED_DATE_TIME. Formaterar datum och tid som 2017-10-16T14: 58: 51,324 + 01: 00 (Europa / London)
  • ISO_INSTANT. Formaterar datum och tid som 2017-10-16T13: 52: 45.246Z
  • ISO_DATE. Formaterar datumet som 2017/10/16 + 01: 00
  • ISO_TIME. Formaterar tiden som 14: 58: 40,945 + 01: 00
  • ISO_DATE_TIME. Formaterar datum och tid som 2017-10-16T14: 55: 32,263 + 01: 00 (Europa / London)
  • ISO_ORDINAL_DATE. Formaterar datumet som 2017-289 + 01: 00
  • ISO_WEEK_DATE. Formaterar datumet som 2017-W42-1 + 01: 00
  • RFC_1123_DATE_TIME. Formaterar datum och tid som Måndag, 16 oktober 2017 14: 58: 43 + 01: 00

Här uppdaterar vi vår app för att visa datum och tid med DateTimeFormatter.ISO_DATE formatering:

importera android.support.v7.app.AppCompatActivity; importera android.os.Bundle; importera com.jakewharton.threetenabp.AndroidThreeTen; importera android.widget.TextView; // Lägg till DateTimeFormatter importdeklaration // importera org.threeten.bp.format.DateTimeFormatter; importera org.threeten.bp.ZonedDateTime; public class MainActivity utökar 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; StringformattedZonedDate = formatter.format (ZonedDateTime.now ()); textView.setText ("Tid:" + formateradZonedDate); setContentView (Textview); 

För att visa denna information i ett annat format, ersätter du bara DateTimeFormatter.ISO_DATE för ett annat värde. Till exempel:

DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

Joda-Time

Före Java 8 betraktades Joda-Time-biblioteket som standardbiblioteket för hantering av datum och tid i Java, till den punkt där Java 8: s nya Date and Time API faktiskt drar "kraftigt på erfarenheten från Joda-Time-projektet".

Medan Joda-Time-webbplatsen rekommenderar att användare migrerar till Java 8 Datum och tid så snart som möjligt, eftersom Android för närvarande inte stöder detta API, är Joda-Time fortfarande ett lönsamt alternativ för Android-utveckling. Observera dock att Joda-Time har ett stort API och laddar tidszoninformation med hjälp av en JAR-resurs, som båda kan påverka din apps prestanda.

För att börja arbeta med Joda-Time-biblioteket, öppna din modulnivå build.gradle fil och lägg till följande:

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

Joda-Time-biblioteket har sex stora datum- och tidskurser:

  • Omedelbar: Representerar en punkt i tidslinjen; till exempel kan du få aktuellt datum och tid genom att ringa Instant.now ().
  • Datum Tid: En allmänt ändamålsenlig ersättning för JDK: s Kalender klass.
  • LocalDate: Ett datum utan tid eller någon hänvisning till en tidszon.
  • Lokal tid: En tid utan datum eller någon hänvisning till en tidszon, till exempel 14:00:00.
  • LocalDateTime: Lokalt datum och tid, fortfarande utan tidszoninformation.
  • ZonedDateTime: Ett datum och tid med en tidszon.

Låt oss ta en titt på hur du skriver ut datum och tid med Joda-Time. I det följande exemplet använder jag kod från vårt ThreeTenABP-exempel, för att göra sakerna mer intressanta använder jag också withZone att konvertera datum och tid till a ZonedDateTime värde.

importera android.support.v7.app.AppCompatActivity; importera android.os.Bundle; importera android.widget.TextView; importera org.joda.time.DateTime; importera org.joda.time.DateTimeZone; public class MainActivity utökar AppCompatActivity @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); DateTime today = new DateTime (); // Återgå en ny formaterare (använd medZone) och ange tidszonen med ZoneId // DateTime todayNy = today.withZone (DateTimeZone.forID ("America / New_York")); TextView textView = new TextView (this); textView.setText ("Tid:" + idagNy); setContentView (Textview); 

Du hittar en fullständig lista över tidszoner som stöds i de officiella Joda-Time-dokumenten.

Slutsats

I det här inlägget tittade vi på hur man skapar mer robust kod med hjälp av typannonser och utforskade "pipes and filters" -metoden för databehandling med Java 8: s nya Stream API.

Vi tittade också på hur gränssnitt har utvecklats i Java 8 och hur man använder dem i kombination med andra funktioner som vi har undersökt genom hela serien, inklusive lambda-uttryck och statiska gränssnittsmetoder.

För att paketera upp saker visade jag dig hur du får tillgång till några ytterligare Java 8-funktioner som Android för närvarande inte stöder som standard, med Joda-Time och ThreeTenABP-projekten.

Du kan lära dig mer om Java 8-utgåvan på Oracles webbplats.

Och medan du är här, kolla in några av våra andra inlägg om Java 8 och Android utveckling!