Mester Java Streams: En komplett guide for nybegynnere og eksperter

I Java representerer en strøm en sekvens av elementer der operasjoner kan utføres, enten sekvensielt eller parallelt.

En strøm kan inneholde et vilkårlig antall mellomliggende operasjoner, etterfulgt av en enkelt terminaloperasjon som genererer det endelige resultatet.

Hva er egentlig en strøm i Java?

Strømmer blir håndtert av Stream API, introdusert i Java 8.

Tenk på en strøm som en produksjonslinje: råvarer ankommer, gjennomgår prosesser som sortering og pakking, og til slutt sendes ut som ferdige produkter. I Java-sammenheng representerer råvarene objekter eller samlinger av objekter, operasjonene er transformasjoner og bearbeiding, og selve produksjonslinjen er strømmen.

En strøm består typisk av:

  • En startkilde for innmating.
  • En rekke mellomliggende operasjoner.
  • En avsluttende terminaloperasjon.
  • Det endelige resultatet av prosessen.

La oss se nærmere på noen sentrale egenskaper ved strømmer i Java:

  • En strøm er ikke en datastruktur i seg selv, men heller en sekvens av elementer (fra matriser, objekter eller samlinger) som bearbeides med bestemte metoder.
  • Strømmer er deklarative; man spesifiserer *hva* man ønsker å gjøre, ikke *hvordan* det skal utføres.
  • En strøm kan kun konsumeres én gang, da den ikke lagrer de bearbeidede dataene.
  • Strømmer endrer ikke den opprinnelige datastrukturen, men genererer en ny struktur basert på den opprinnelige.
  • Den returnerer det ferdige resultatet, frembrakt av den avsluttende operasjonen i strømmen.

Stream API kontra samlingsbehandling

En samling er en datastruktur som holder og administrerer data i minnet. Samlinger, som lister, sett og kart, tilbyr måter å organisere data. En strøm er derimot et effektivt system for å overføre og transformere data gjennom en serie operasjoner.

Et enkelt eksempel på en ArrayList-samling:

import java.util.ArrayList;

public class Main {
    public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(0, 3);
        System.out.println(list);
    }
}

Output: 
[3]

Som eksemplet viser, kan man opprette en ArrayList, legge til data og deretter manipulere disse dataene med metoder.

Med en strøm kan man behandle eksisterende datastrukturer og generere nye, modifiserte verdier. Her er et eksempel på hvordan man oppretter en ArrayList og filtrerer den ved hjelp av en strøm.

import java.util.ArrayList;
import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList();

        for (int i = 0; i < 20; i++) {
            list.add(i+1);
        }

        System.out.println(list);

        Stream<Integer> filtered = list.stream().filter(num -> num > 10);
        filtered.forEach(num -> System.out.println(num + " "));
    }
}

#Output

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 

I dette eksemplet konverteres den eksisterende listen til en strøm, og deretter filtreres tallene større enn 10. Merk at strømmen ikke lagrer resultatet; den bare itererer over listen, og skriver ut resultatet. Hvis man forsøker å skrive ut strømmen i seg selv, vil man få en referanse til strømmen, ikke de faktiske verdiene.

Arbeid med Java Stream API

Java Stream API tar inn en kilde av elementer (enten en samling eller en sekvens) og utfører operasjoner for å produsere et endelig resultat. En strøm er en «rørledning» der elementer passerer og transformeres.

En strøm kan opprettes fra forskjellige kilder, inkludert:

  • Samlinger som lister og sett.
  • Matriser.
  • Filer og deres stier, ved bruk av en buffer.

Det finnes to hovedtyper operasjoner i en strøm:

  • Mellomliggende operasjoner.
  • Terminaloperasjoner.

Mellomliggende kontra terminaloperasjoner

En mellomliggende operasjon genererer en ny strøm basert på transformasjonen av input. Ingen bearbeiding av elementer skjer umiddelbart. I stedet sendes strømmen videre til neste operasjon. Det er først ved terminaloperasjonen at hele strømmen bearbeides for å produsere det endelige resultatet.

Tenk deg at du har en liste med 10 tall, og at du ønsker å filtrere dem og deretter transformere dem. Det er ikke slik at alle elementer i listen blir umiddelbart filtrert og transformert. Isteden sjekkes hvert element individuelt, og hvis det tilfredsstiller betingelsen blir det transformert. Hvert element gjennomgår sine egne strømmer.

Transformasjonen utføres kun på de elementene som har gått gjennom filteret. På tidspunktet for terminaloperasjonen kombineres alle resultater til ett enkelt resultat.

Etter at en terminaloperasjon er fullført, kan ikke den samme strømmen brukes på nytt. Man må opprette en ny strøm for å gjenta de samme operasjonene.

Kilde: The Bored Dev

Nå som vi har et grunnleggende overblikk over hvordan strømmer fungerer, skal vi se nærmere på detaljene rundt implementering av strømmer i Java.

#1. En tom strøm

En tom strøm opprettes ved hjelp av den tomme metoden i Stream API.

import java.util.stream.Stream;

public class Main {
    public static void main(String[] args) {
        Stream emptyStream = Stream.empty();
        System.out.println(emptyStream.count());
    }
}

Output:
0

Her vil utskriften av antall elementer i strømmen være 0, da det er en tom strøm uten innhold. Tomme strømmer er nyttige for å unngå nullpekerunntak.

#2. Strøm fra samlinger

Samlinger som Lists og Set tilbyr en stream()-metode som lar deg opprette en strøm fra samlingen. Den genererte strømmen kan deretter bearbeides for å produsere resultatet.

ArrayList<Integer> list = new ArrayList();

for (int i = 0; i < 20; i++) {
    list.add(i+1);
}

System.out.println(list);

Stream<Integer> filtered = list.stream().filter(num -> num > 10);
filtered.forEach(num -> System.out.println(num + " "));

#Output

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 

#3. Strøm fra matriser

Metoden Arrays.stream() benyttes for å opprette en strøm fra en matrise.

import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        String[] stringArray = new String[]{"this", "is", "tipsbilk.net"};
        Arrays.stream(stringArray).forEach(item -> System.out.print(item + " "));
    }
}

#Output

this is tipsbilk.net 

Det er også mulig å spesifisere start- og sluttindeksen for elementene man ønsker å benytte i strømmen. Startindeksen er inkludert, mens sluttindeksen er ekskludert.

String[] stringArray = new String[]{"this", "is", "tipsbilk.net"};
Arrays.stream(stringArray, 1, 3).forEach(item -> System.out.print(item + " "));

Output:
is tipsbilk.net

#4. Finne minste og største tall med strømmer

For å finne minimums- og maksimumsverdier i en samling eller en matrise kan man benytte komparatorer i Java. Metodene min() og max() tar en komparator som argument, og returnerer et Optional-objekt.

Et Optional-objekt er en beholder som kan inneholde en ikke-nullverdi. Hvis den inneholder en verdi, returneres den ved å kalle get()-metoden.

import java.util.Arrays;
import java.util.Optional;

public class MinMax {
    public static void main(String[] args) {
        Integer[] numbers = new Integer[]{21, 82, 41, 9, 62, 3, 11};

        Optional<Integer> maxValue = Arrays.stream(numbers).max(Integer::compare);
        System.out.println(maxValue.get());

        Optional<Integer> minValue = Arrays.stream(numbers).min(Integer::compare);
        System.out.println(minValue.get());
    }
}

#Output
82
3

Læringsressurser

Nå som du har en grunnleggende forståelse av strømmer i Java, presenterer vi 5 ressurser som kan hjelpe deg å mestre Java 8:

#1. Java 8 i praksis

Denne boken er en guide som gir en god gjennomgang av de nye funksjonene i Java 8, inkludert strømmer, lambda-uttrykk og funksjonell programmering. Boken inneholder også oppgaver og kontrollspørsmål som hjelper deg å teste kunnskapen du har tilegnet deg.

Boken er tilgjengelig i pocketformat og som lydbok på Amazon.

#2. Java 8 Lambdas: Funksjonell programmering for massene

Denne boken er spesielt rettet mot Java SE-utviklere, og forklarer hvordan innføringen av lambda-uttrykk har endret Java-språket. Boken inneholder gode forklaringer, kodeeksempler og øvelser som hjelper deg å mestre lambda-uttrykk.

Den er tilgjengelig i pocketformat og som Kindle-utgave på Amazon.

#3. Java SE 8 for de virkelig utålmodige

Denne boken er for erfarne Java SE-utviklere, og guider deg gjennom forbedringene som er gjort i Java SE 8, inkludert Stream API, lambda-uttrykk, forbedringer i parallell programmering, samt noen Java 7-funksjoner som mange ikke er klar over.

Boken er kun tilgjengelig i pocketformat på Amazon.

#4. Lær Java funksjonell programmering med lambdaer og strømmer

Dette Udemy-kurset går gjennom grunnprinsippene i funksjonell programmering i Java 8 og 9. Lambda-uttrykk, metodesignaturer, strømmer og funksjonelle grensesnitt er de viktigste temaene.

Kurset inneholder også mange Java-relaterte oppgaver og øvelser med fokus på funksjonell programmering.

#5. Java klassebibliotek

Java Class Library er en del av Core Java Specialization som tilbys av Coursera. Her lærer du å skrive type-sikker kode med Java Generics, forstå klassebiblioteket som omfatter over 4000 klasser, hvordan man jobber med filer og håndterer feil under kjøring. Det er imidlertid noen forutsetninger for å ta dette kurset:

  • Introduksjon til Java.
  • Introduksjon til objektorientert programmering med Java.
  • Objektorienterte hierarkier i Java.

Avsluttende tanker

Java Stream API og introduksjonen av lambda-uttrykk i Java 8 har forenklet og forbedret mange aspekter ved Java, som parallell iterasjon, funksjonelle grensesnitt, mindre kode, osv.

Strømmer har likevel sine begrensninger. Den viktigste er at de kun kan konsumeres en gang. Hvis du er Java-utvikler vil ressursene over hjelpe deg med å forstå disse emnene i detalj. Ta en titt på disse ressursene.

Du kan også være interessert i å lære mer om unntakshåndtering i Java.