Java 8 voor Android-ontwikkeling Stream API en datum- en tijdbibliotheken

In deze driedelige serie hebben we alle belangrijke Java 8-functies onderzocht die u vandaag in uw Android-projecten kunt gebruiken.

In Cleaner Code met Lambda Expressions hebben we ons gericht op het uitsnijden van boilerplate van uw projecten met lambda-expressies, en vervolgens in Default en Static Methods, zagen we hoe we deze lambda-expressies beknopter kunnen maken door ze te combineren met methodeverwijzingen. We hebben ook aandacht besteed aan het herhalen van annotaties en het aangeven van niet-abstracte methoden in uw interfaces met behulp van standaard en statische interfacemethoden.

In dit laatste bericht gaan we kijken naar typeannotaties, functionele interfaces en hoe u een meer functionele benadering van gegevensverwerking kunt aannemen met de nieuwe Stream API van Java 8.

Ik zal je ook laten zien hoe je toegang kunt krijgen tot een aantal extra Java 8-functies die momenteel niet worden ondersteund door het Android-platform, met behulp van de Joda-Time en ThreeTenABP-bibliotheken.

Typ Annotaties

Annotaties helpen u om code te schrijven die robuuster en minder foutgevoelig is, door code-inspectietools zoals Lint te informeren over de fouten waar ze op moeten letten. Deze inspectietools zullen u dan waarschuwen als een stuk code niet voldoet aan de regels die zijn vastgelegd in deze annotaties.

Annotaties zijn geen nieuwe functie (ze dateren al van Java 5.0), maar in eerdere versies van Java was het alleen mogelijk om annotaties toe te passen op declaraties.

Met de release van Java 8 kunt u nu overal waar u een type gebruikt annotaties gebruiken, inclusief methode-ontvangers; klasse instantie creatie uitdrukkingen; de implementatie van interfaces; generieken en matrices; de specificatie van worpen en gereedschap clausules; en typ casting.

Frustrerend genoeg biedt Java 8 weliswaar annotaties op meer locaties dan ooit tevoren, maar het biedt geen annotaties die specifiek zijn voor typen.

Android Annotations Support Library biedt toegang tot enkele extra annotaties, zoals @Nullable, @NonNull, en annotaties voor het valideren van resourcetypen zoals  @DrawableRes, @DimenRes, @ColorRes, en @StringRes. U kunt echter ook een statische analysetool van derden gebruiken, zoals het Checker Framework, dat mede is ontwikkeld met de JSR 308-specificatie (de specificatie Annotaties op Java-typen). Dit framework biedt zijn eigen set annotaties die op typen kunnen worden toegepast, plus een aantal "checkers" (annotatieprocessors) die aansluiten op het compilatieproces en specifieke "controles" uitvoeren voor elke typeannotatie die is opgenomen in het Checker Framework.

Omdat Type Annotaties de werking van de runtime niet beïnvloeden, kunt u Type Annotaties van Java 8 in uw projecten gebruiken terwijl u achterwaarts compatibel blijft met eerdere versies van Java.

Stream API

De Stream API biedt een alternatief, "pipes-and-filters" -benadering voor het verwerken van collecties.

Vóór Java 8 manipuleerde u verzamelingen handmatig, meestal door telkens opnieuw te bladeren over de verzameling en op elk element te werken. Deze expliciete lussen vereisten veel boilerplate, plus het is moeilijk om de for-loop-structuur te begrijpen totdat je de body van de lus bereikt.

De Stream API biedt u een manier om gegevens efficiënter te verwerken door een enkele run over die gegevens uit te voeren, ongeacht de hoeveelheid gegevens die u verwerkt, of dat u meerdere berekeningen uitvoert.

In Java 8, elke klasse die implementeert java.util.Collection heeft een stroom methode die zijn instanties kan omzetten in Stroom voorwerpen. Als u bijvoorbeeld een reeks:

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

Dan kun je het omzetten in een Stream met het volgende:

Stroom myStream = Arrays.stream (myArray);

De Stream API verwerkt gegevens door waarden van een bron te dragen, via een reeks computationele stappen, bekend als a stroom pijplijn. Een stroompijplijn bestaat uit het volgende:

  • Een bron, zoals een Verzameling, array- of generatorfunctie.
  • Nul of meer tussentijdse luie operaties. Tussenliggende bewerkingen beginnen niet met het verwerken van elementen totdat u a terminal bediening-dat is waarom ze als lui worden beschouwd.Bijvoorbeeld bellen Stream.filter () op een gegevensbron stelt alleen de stroompijplijn in; er vindt geen filtering plaats totdat u de terminalbewerking belt. Dit maakt het mogelijk om meerdere bewerkingen tegelijk uit te voeren en vervolgens al deze berekeningen in één keer door de gegevens uit te voeren. Tussenliggende bewerkingen produceren een nieuwe stream (bijvoorbeeld, filter zal een stroom produceren die de gefilterde elementen bevat) zonder het wijzigen van de gegevensbron, zodat u de oorspronkelijke gegevens elders in uw project kunt gebruiken of meerdere streams van dezelfde bron kunt maken.
  • Een terminalbewerking, zoals Stream.forEach. Wanneer u de terminalbewerking aanroept, zullen al uw tussenliggende bewerkingen worden uitgevoerd en een nieuwe stream produceren. Een stream kan geen elementen opslaan, dus zodra u een terminalbewerking aanroept, wordt die stream als "verbruikt" beschouwd en is deze niet langer bruikbaar. Als u de elementen van een stream opnieuw wilt bezoeken, moet u een nieuwe stream genereren van de oorspronkelijke gegevensbron.

Een stream maken

Er zijn verschillende manieren om een ​​stream uit een gegevensbron te verkrijgen, waaronder:

  • Stroom van()Creëert een stream van individuele waarden:

Stroom stream = Stream.of ("A", "B", "C");
  • IntStream.range () Creëert een stream uit een reeks nummers:

IntStream i = IntStream.range (0, 20);
  • Stream.iterate () Creëert een stream door herhaaldelijk een operator op elk element toe te passen. Hier maken we bijvoorbeeld een stream waarin elk element met één wordt verhoogd:

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

Een stroom transformeren met bewerkingen

Er zijn een hoop bewerkingen die u kunt gebruiken om functionele stijlberekeningen uit te voeren op uw streams. In dit gedeelte ga ik slechts enkele van de meest gebruikte streambewerkingen behandelen.

Kaart

De kaart() bewerking neemt een lambda-expressie als zijn enige argument, en gebruikt deze expressie om de waarde of het type van elk element in de stream te transformeren. Het volgende geeft ons bijvoorbeeld een nieuwe stream, waarbij elke Draad is omgezet naar hoofdletters:

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

Begrenzing

Deze bewerking stelt een limiet in voor de grootte van een stream. Als u bijvoorbeeld een nieuwe stream met een maximum van vijf waarden wilt maken, gebruikt u het volgende:

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

Filter

De Filter (Predicate) Met de bewerking kunt u filtercriteria definiëren met behulp van een lambda-uitdrukking. Deze lambda-expressie moet retourneer een Booleaanse waarde die bepaalt of elk element moet worden opgenomen in de resulterende stream. Als u bijvoorbeeld een reeks tekenreeksen had en alle reeksen met minder dan drie tekens wilde filteren, zou u het volgende gebruiken:  

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

gesorteerd

Deze bewerking sorteert de elementen van een stream. Het volgende geeft bijvoorbeeld een stroom nummers terug die in oplopende volgorde zijn gerangschikt:

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

Parallelle verwerking

Alle stream-bewerkingen kunnen serieel of parallel worden uitgevoerd, hoewel streams sequentieel zijn tenzij u expliciet anders opgeeft. Het volgende zal elk element bijvoorbeeld één voor één verwerken:

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

Als u een stream parallel wilt uitvoeren, moet u die stream expliciet markeren als parallel, met behulp van de parallel() methode:

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

Onder de motorkap gebruiken parallelle streams het Fork / Join Framework, dus het aantal beschikbare threads is altijd gelijk aan het aantal beschikbare kernen in de CPU.

Het nadeel van parallelle streams is dat telkens wanneer de code wordt uitgevoerd, er verschillende cores bij betrokken kunnen zijn, dus je krijgt meestal een andere output bij elke uitvoering. Daarom moet u alleen parallelle streams gebruiken wanneer de verwerkingsorder niet belangrijk is en parallelle streams vermijden bij het uitvoeren van op orders gebaseerde bewerkingen, zoals FindFirst ().

Terminalbewerkingen

Je verzamelt de resultaten van een stream met behulp van een terminalbewerking, dat is altijd het laatste element in een keten van stream-methoden en retourneert altijd iets anders dan een stream.

Er zijn een paar verschillende typen terminalbewerkingen die verschillende soorten gegevens retourneren, maar in deze sectie gaan we kijken naar twee van de meest gebruikte terminalbewerkingen.

Verzamelen

De Verzamelen bewerking verzamelt alle verwerkte elementen in een container, zoals een Lijst of set. Java 8 biedt a Verzamelaars hulpprogramma klasse, zodat u zich geen zorgen hoeft te maken over de implementatie van de Verzamelaars interface, plus fabrieken voor veel gemeenschappelijke verzamelaars, waaronder ToList (), toSet (), en toCollection ().

De volgende code levert een Lijst alleen rode vormen bevatten:

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

Als alternatief kunt u deze gefilterde elementen verzamelen in een set:

 .verzamelen (Collectors.toSet ());

forEach

De forEach () bewerking voert enige actie uit op elk element van de stream, waardoor het de Stream API-equivalent is van een for-elke-instructie.

Als je een had items lijst, dan zou je kunnen gebruiken forEach om alle items af te drukken die hierin zijn opgenomen Lijst:

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

In het bovenstaande voorbeeld gebruiken we een lambda-expressie, dus het is mogelijk om hetzelfde werk in minder code uit te voeren, met behulp van een methodeverwijzing:

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

Functionele interfaces

Een functionele interface is een interface die exact één abstracte methode bevat, de zogenaamde functionele methode.

Het concept van interfaces met één methode is niet nieuw-uitvoerbare, Comparator, Callable, en OnClickListener zijn allemaal voorbeelden van dit soort interfaces, hoewel ze in eerdere versies van Java bekend stonden als Single Abstract Method Interfaces (SAM-interfaces).  

Dit is meer dan een simpele naamswijziging, omdat er enkele opmerkelijke verschillen zijn in de manier waarop u werkt met functionele (of SAM) interfaces in Java 8, vergeleken met eerdere versies.

Voorafgaand aan Java 8 heeft u meestal een functionele interface gemaakt met behulp van een omvangrijke anonieme klasse-implementatie. Hier maken we bijvoorbeeld een instantie van uitvoerbare een anonieme klasse gebruiken:

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

Zoals we in deel een hebben gezien, kun je, wanneer je een interface met één methode hebt, die interface instantiëren met behulp van een lambda-expressie in plaats van een anonieme klasse. Nu kunnen we deze regel bijwerken: u kunt een instantie maken functionele interfaces, met behulp van een lambda-expressie. Bijvoorbeeld:

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

Java 8 introduceert ook een @FunctionalInterface annotatie waarmee u een interface kunt markeren als een functionele interface:

@FunctionalInterface public interface MyFuncInterface public void doSomething (); 

Om achterwaartse compatibiliteit met eerdere versies van Java te garanderen, is de @FunctionalInterface annotatie is optioneel; Het is echter aan te bevelen om ervoor te zorgen dat u uw functionele interfaces correct implementeert.

Als u twee of meer methoden probeert te implementeren in een interface die is gemarkeerd als @FunctionalInterface, dan klaagt de compiler dat hij meerdere niet-overheersende abstracte methoden heeft ontdekt. Het volgende compileert bijvoorbeeld niet:

@FunctionalInterface public interface MyFuncInterface void doSomething (); // Definieer een tweede abstracte methode // void doSomethingElse ();  

En, als je een compilatie probeert te compileren @FunctionInterface interface die nul methoden bevat, dan zul je een tegenkomen Geen doelmethode gevonden fout.

Functionele interfaces moeten exact één abstracte methode bevatten, maar omdat standaard- en statische methoden geen hoofdtekst hebben, worden ze als niet-abstract beschouwd. Dit betekent dat u meerdere standaard- en statische methoden in een interface kunt opnemen, markeer het als @FunctionalInterface, en het zal nog steeds compileren.

Java 8 heeft ook een pakket java.util.function toegevoegd met veel functionele interfaces. Het is de moeite waard om de tijd te nemen om vertrouwd te raken met al deze nieuwe functionele interfaces, zodat u precies weet wat er uit de doos beschikbaar is..

JSR-310: Java's nieuwe datum en tijd API

Het werken met datum en tijd in Java was nog nooit zo eenvoudig, met veel API's die belangrijke functionaliteit weglaten, zoals tijdzonegegevens.

Java 8 heeft een nieuwe Date and Time API (JSR-310) geïntroduceerd die tot doel heeft deze problemen op te lossen, maar helaas op het moment van schrijven wordt deze API niet ondersteund op het Android-platform. U kunt echter vandaag een deel van de nieuwe datum- en tijdfuncties in uw Android-projecten gebruiken met behulp van een externe bibliotheek.

In deze laatste sectie laat ik je zien hoe je twee populaire externe bibliotheken kunt instellen en gebruiken die het mogelijk maken om de Date & Time API van Java 8 op Android te gebruiken.

ThreeTen Android Backport

ThreeTen Android Backport (ook bekend als ThreeTenABP) is een aanpassing van het populaire ThreeTen-backportproject, dat een implementatie van JSR-310 voor Java 6.0 en Java 7.0 biedt. ThreeTenABP is ontworpen om toegang te bieden tot alle API's van datum en tijd (hoewel met een andere pakketnaam) zonder een groot aantal methoden toe te voegen aan uw Android-projecten.

Om je bibliotheek te gebruiken, open je je moduleniveau build.gradle bestand en voeg ThreeTenABP toe als projectafhankelijkheid:

afhankelijkheden // Voeg de volgende regel toe // compile 'com.jakewharton.threetenabp: threetenabp: 1.0.5'

U moet dan de import-instructie ThreeTenABP toevoegen:

import com.jakewharton.threetenabp.AndroidThreeTen;

En initialiseer de tijdzone-informatie in uw Application.onCreate () methode:

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

ThreeTenABP bevat twee klassen die twee soorten informatie over tijd en datum weergeven:

  • LocalDateTime, waarin een tijd en een datum in het formaat worden opgeslagen 2017-10-16T13: 17: 57,138
  • ZonedDateTime, die tijdzone-bewust is en datum- en tijdinformatie opslaat in het volgende formaat: 2011-12-03T10: 15: 30 + 01: 00 [Europe / Paris]

Om u een idee te geven van hoe u deze bibliotheek zou gebruiken om datum- en tijdinformatie op te halen, gebruiken we de LocalDateTime klasse om de huidige datum en tijd weer te geven:

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

Dit is niet de meest gebruiksvriendelijke manier om de datum en tijd weer te geven! Om deze onbewerkte gegevens te ontleden in iets menselijks leesbaars, kunt u de DateTimeFormatter class en stel deze in op een van de volgende waarden:

  • BASIC_ISO_DATE. Formatteert de datum als 2017-1016 + 01.00
  • ISO_LOCAL_DATE. Formatteert de datum als 2017/10/16
  • ISO_LOCAL_TIME. Formatteert de tijd als 14: 58: 43,242
  • ISO_LOCAL_DATE_TIME. Formatteert de datum en de tijd als 2017-10-16T14: 58: 09,616
  • ISO_OFFSET_DATE. Formatteert de datum als 2017/10/16 + 01.00
  • ISO_OFFSET_TIME.  Formatteert de tijd als 14: 58: 56,218 + 01: 00
  • ISO_OFFSET_DATE_TIME. Formatteert de datum en tijd als 2017-10-16T14: 5836,758 + 01: 00
  • ISO_ZONED_DATE_TIME. Formatteert de datum en tijd als 2017-10-16T14: 58: 51,324 + 01: 00 (Europe / London)
  • ISO_INSTANT. Formatteert de datum en tijd als 2017-10-16T13: 52: 45.246Z
  • ISO_DATE. Formatteert de datum als 2017/10/16 + 01: 00
  • ISO_TIME. Formatteert de tijd als 14: 58: 40,945 + 01: 00
  • ISO_DATE_TIME. Formatteert de datum en tijd als 2017-10-16T14: 55: 32,263 + 01: 00 (Europe / London)
  • ISO_ORDINAL_DATE. Formatteert de datum als 2017-289 + 01: 00
  • ISO_WEEK_DATE. Formatteert de datum als 2017-W42-1 + 01: 00
  • RFC_1123_DATE_TIME. Formatteert de datum en tijd als Ma, 16 oktober 2017 14: 58: 43 + 01: 00

Hier updaten we onze app om de datum en tijd weer te geven DateTimeFormatter.ISO_DATE opmaak:

import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import com.jakewharton.threetenabp.AndroidThreeTen; import android.widget.TextView; // Voeg de DateTimeFormatter-importinstructie toe // import org.threeten.bp.format.DateTimeFormatter; import org.threeten.bp.ZonedDateTime; public class MainActivity breidt AppCompatActivity uit @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 ("Tijd:" + geformatteerdZonedDate); setContentView (TextView); 

Om deze informatie in een ander formaat weer te geven, hoeft u alleen maar te vervangen DateTimeFormatter.ISO_DATE voor een andere waarde. Bijvoorbeeld:

DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

Joda-Time

Vóór Java 8 werd de Joda-Time-bibliotheek beschouwd als de standaardbibliotheek voor het verwerken van datum en tijd in Java, tot het punt waarop de nieuwe Date and Time API van Java 8 feitelijk "zwaar put uit de ervaring die is opgedaan met het Joda-Time-project."

Hoewel de Joda-Time-website aanbeveelt dat gebruikers zo snel mogelijk naar Java 8 Date and Time migreren, aangezien Android deze API momenteel niet ondersteunt, is Joda-Time nog steeds een haalbare optie voor Android-ontwikkeling. Houd er echter rekening mee dat Joda-Time een grote API heeft en informatie over tijdzones laadt met behulp van een JAR-resource, die beide de prestaties van uw app kunnen beïnvloeden.

Begin met het werken met de Joda-Time-bibliotheek op moduleniveau build.gradle bestand en voeg het volgende toe:

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

De Joda-Time-bibliotheek heeft zes belangrijke datum- en tijdklassen:

  • ogenblik: Vertegenwoordigt een punt in de tijdlijn; U kunt bijvoorbeeld de huidige datum en tijd opvragen door te bellen Instant.now ().
  • Datum Tijd: Een algemene vervanging voor JDK's Kalender klasse.
  • LOCALDATE: Een datum zonder tijd of een verwijzing naar een tijdzone.
  • Lokale tijd: Een tijd zonder datum of een verwijzing naar een tijdzone, bijvoorbeeld 14:00:00.
  • LocalDateTime: Een lokale datum en tijd, nog steeds zonder tijdzone-informatie.
  • ZonedDateTime: Een datum en tijd met een tijdzone.

Laten we eens kijken hoe u de datum en tijd afdrukt met Joda-Time. In het volgende voorbeeld gebruik ik de code uit ons voorbeeld van ThreeTenABP, dus om dingen interessanter te maken, gebruik ik ook withZone om de datum en tijd om te zetten in een ZonedDateTime waarde.

import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.TextView; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; public class MainActivity breidt AppCompatActivity uit @Override protected void onCreate (Bundle savedInstanceState) super.onCreate (savedInstanceState); setContentView (R.layout.activity_main); DateTime today = new DateTime (); // Retourneer een nieuwe formatter (met withZone) en geef de tijdzone op met behulp van ZoneId // DateTime todayNy = today.withZone (DateTimeZone.forID ("America / New_York")); TextView textView = new TextView (this); textView.setText ("Time:" + todayNy); setContentView (TextView); 

Je vindt een volledige lijst met ondersteunde tijdzones in de officiële Joda-Time-documenten.

Conclusie

In dit bericht hebben we gekeken naar hoe u meer robuuste code kunt maken met behulp van typeannotaties en de "pipes-and-filters" -benadering van gegevensverwerking met de nieuwe Stream API van Java 8 onderzocht.

We hebben ook gekeken naar hoe interfaces zijn geëvolueerd in Java 8 en hoe deze te gebruiken in combinatie met andere functies die we in deze serie hebben onderzocht, inclusief lambda-expressies en statische interfacemethoden.

Ik heb je laten zien hoe je toegang krijgt tot een aantal extra Java 8-functies die Android momenteel niet ondersteunt, met behulp van de Joda-Time- en ThreeTenABP-projecten.

Je kunt meer lezen over de Java 8-release op de website van Oracle.

En terwijl je hier bent, bekijk enkele van onze andere berichten over Java 8 en Android-ontwikkeling!