Java >> Java Program >  >> Java

Introduktion till Java 8 Streams

Introduktion

Huvudämnet för den här artikeln är avancerade databehandlingsämnen med en ny funktion som lagts till i Java 8 – Stream API och Collector API.

För att få ut det mesta av den här artikeln bör du redan vara bekant med de viktigaste Java API:erna, Object och String klasser och Collection API.

Stream API

java.util.stream Paketet består av klasser, gränssnitt och många typer för att möjliggöra funktionella operationer över element. Java 8 introducerar ett koncept för en ström som gör att programmeraren kan bearbeta data beskrivande och förlita sig på en flerkärnig arkitektur utan att behöva skriva någon speciell kod.

Vad är en Stream?

En Stream representerar en sekvens av objekt härledda från en källa, över vilka aggregerade operationer kan utföras.

Ur en rent teknisk synvinkel är en Stream ett maskinskrivet gränssnitt - en ström av T . Det betyder att en ström kan definieras för alla slags objekt , en ström av siffror, en ström av tecken, en ström av människor eller till och med en ström av en stad.

Ur en utvecklarsynpunkt är det ett nytt koncept som kanske bara ser ut som en samling, men det skiljer sig faktiskt mycket från en samling.

Det finns några nyckeldefinitioner som vi måste gå igenom för att förstå denna uppfattning om en ström och varför den skiljer sig från en samling:

En ström innehåller inga data

Den vanligaste missuppfattningen som jag skulle vilja ta upp först - en ström inte hålla någon data. Detta är mycket viktigt att ha det i åtanke och förstå.

Det finns ingen data i en ström , men det finns data i en samling .

En Collection är en struktur som innehåller dess data. En Stream är bara där för att bearbeta data och dra ut den från den givna källan, eller flytta den till en destination. Källan kan vara en samling, men den kan också vara en array eller I/O-resurs. Strömmen kommer att ansluta till källan, konsumera data och bearbeta elementen i den på något sätt.

En ström bör inte ändra källan

En ström bör inte ändra källan till den data som den bearbetar. Detta upprätthålls inte riktigt av kompilatorn av JVM själv, så det är bara ett kontrakt. Om jag ska bygga min egen implementering av en ström bör jag inte ändra källan till den data jag bearbetar. Även om det går alldeles utmärkt att ändra data i strömmen.

Varför är det så? För om vi vill behandla denna data parallellt, kommer vi att distribuera den bland alla kärnor i våra processorer och vi vill inte ha någon form av synlighet eller synkroniseringsproblem som kan leda till dåliga prestanda eller fel. Att undvika den här typen av störningar innebär att vi inte bör ändra källan till data medan vi bearbetar den.

En källa kan vara obegränsad

Förmodligen den mest kraftfulla punkten av dessa tre. Det innebär att strömmen i sig kan bearbeta hur mycket data vi vill. Obegränsad betyder inte att en källa måste vara oändlig. Faktum är att en källa kan vara ändlig, men vi kanske inte har tillgång till elementen i den källan.

Anta att källan är en enkel textfil. En textfil har en känd storlek även om den är väldigt stor. Anta också att elementen i den källan i själva verket är raderna i denna textfil.

Nu kanske vi vet den exakta storleken på denna textfil, men om vi inte öppnar den och manuellt går igenom innehållet kommer vi aldrig att veta hur många rader den har. Detta är vad obegränsat betyder - vi kanske inte alltid i förväg vet hur många element en ström kommer att bearbeta från källan.

Det är de tre definitionerna av en ström. Så vi kan se från de tre definitionerna att en ström verkligen inte har något med en samling att göra. En samling innehåller sina data. En samling kan modifiera den data den innehåller. Och naturligtvis innehåller en samling en känd och begränsad mängd data.

Strömegenskaper

  • Elementsekvens - Strömmar tillhandahåller en uppsättning element av en viss typ på ett sekventiellt sätt. Strömmen får ett element på begäran och lagrar aldrig en vara.
  • Källa - Strömmar tar en samling, array eller I/O-resurser som källa för sina data.
  • Aggregerade operationer - Strömmar stöder aggregerade operationer som forEach , filter , karta , sorterade , match , och andra.
  • Åsidosätter - De flesta operationer över en Stream returnerar en Stream, vilket innebär att deras resultat kan kedjas. Funktionen för dessa operationer är att ta indata, bearbeta den och returnera målutgången. collect() metod är en terminaloperation som vanligtvis finns i slutet av operationer för att indikera slutet av Stream-bearbetningen.
  • Automatiska iterationer - Strömoperationer utför iterationer internt över elementens källa, i motsats till samlingar där explicit iteration krävs.

Skapa en ström

Vi kan generera en ström med hjälp av några metoder:

ström()

stream() metod returnerar den sekventiella strömmen med en samling som källa. Du kan använda vilken samling objekt som helst som källa:

private List<String> list = new Arrays.asList("Scott", "David", "Josh");
list.stream();
parallelStream()

parallelStream() metod returnerar en parallell ström med en samling som källa:

private List<String> list = new Arrays.asList("Scott", "David", "Josh");
list.parallelStream().forEach(element -> method(element));

Grejen med parallella strömmar är att när man kör en sådan operation, segregerar Java runtime strömmen i flera underströmmar. Den utför de aggregerade operationerna och kombinerar resultatet. I vårt fall anropar den method med varje element i strömmen parallellt.

Även om detta kan vara ett tveeggat svärd, eftersom att utföra tunga operationer på detta sätt kan blockera andra parallella strömmar eftersom det blockerar trådarna i poolen.

Stream.of()

Den statiska of() metod kan användas för att skapa en ström från en array av objekt eller enskilda objekt:

Stream.of(new Employee("David"), new Employee("Scott"), new Employee("Josh"));
Stream.builder()

Och slutligen kan du använda den statiska .builder() metod för att skapa en ström av objekt:

Stream.builder<String> streamBuilder = Stream.builder();

streamBuilder.accept("David");
streamBuilder.accept("Scott");
streamBuilder.accept("Josh");

Stream<String> stream = streamBuilder.build();

Genom att ringa .build() metod packar vi de accepterade objekten i en vanlig Stream.

Filtrering med en ström

public class FilterExample {
    public static void main(String[] args) {
    List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Orange");

    // Traditional approach
    for (String fruit : fruits) {
        if (!fruit.equals("Orange")) {
            System.out.println(fruit + " ");
        }
    }

    // Stream approach
    fruits.stream() 
            .filter(fruit -> !fruit.equals("Orange"))
            .forEach(fruit -> System.out.println(fruit));
    }
}

En traditionell metod för att filtrera bort en enskild frukt skulle vara med en klassisk för-varje loop.

Den andra metoden använder en ström för att filtrera ut elementen i Streamen som matchar det givna predikatet, till en ny Stream som returneras av metoden.

Dessutom använder denna metod en forEach() metod, som utför en åtgärd för varje element i den returnerade strömmen. Du kan ersätta detta med något som kallas metodreferens . I Java 8 är en metodreferens förkortningssyntaxen för ett lambdauttryck som kör bara en metod.

Metodreferenssyntaxen är enkel, och du kan till och med ersätta det tidigare lambdauttrycket .filter(fruit -> !fruit.equals("Orange")) med det:

Object::method;

Låt oss uppdatera exemplet och använda metodreferenser och se hur det ser ut:

public class FilterExample {
    public static void main(String[] args) {
    List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Orange");

    fruits.stream()
            .filter(FilterExample::isNotOrange)
            .forEach(System.out::println);
    }
    
    private static boolean isNotOrange(String fruit) {
        return !fruit.equals("Orange");
    }
}

Strömmar är enklare och bättre att använda med Lambda-uttryck och det här exemplet visar hur enkel och ren syntaxen ser ut jämfört med den traditionella metoden.

Mappning med en ström

Ett traditionellt tillvägagångssätt skulle vara att iterera genom en lista med en förbättrad för loop:

List<String> models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");

System.out.print("Imperative style: " + "\n");

for (String car : models) {
    if (!car.equals("Fiat")) {
        Car model = new Car(car);
        System.out.println(model);
    }
}

Å andra sidan är ett modernare tillvägagångssätt att använda en ström för att kartlägga:

List<String> models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");
        
System.out.print("Functional style: " + "\n");

models.stream()
        .filter(model -> !model.equals("Fiat"))
//      .map(Car::new)                 // Method reference approach
//      .map(model -> new Car(model))  // Lambda approach
        .forEach(System.out::println);

För att illustrera mappning, överväg denna klass:

private String name;
    
public Car(String model) {
    this.name = model;
}

// getters and setters

@Override
public String toString() {
    return "name='" + name + "'";
}

Det är viktigt att notera att models lista är en lista med strängar – inte en lista med Car . .map() metod förväntar sig ett objekt av typen T och returnerar ett objekt av typen R .

Vi konverterar String till en typ av bil, i huvudsak.

Om du kör den här koden bör imperativstilen och funktionsstilen returnera samma sak.

Samla med en ström

Ibland skulle du vilja konvertera en ström till en samling eller Karta . Använda verktygsklassen Collectors och de funktioner som den erbjuder:

List<String> models = Arrays.asList("BMW", "Audi", "Peugeot", "Fiat");

List<Car> carList = models.stream()
        .filter(model -> !model.equals("Fiat"))
        .map(Car::new)
        .collect(Collectors.toList());

Matcha med en ström

En klassisk uppgift är att kategorisera objekt enligt vissa kriterier. Vi kan göra detta genom att matcha den nödvändiga informationen med objektinformationen och kontrollera om det är vad vi behöver:

List<Car> models = Arrays.asList(new Car("BMW", 2011), new Car("Audi", 2018), new Car("Peugeot", 2015));

boolean all = models.stream().allMatch(model -> model.getYear() > 2010);
System.out.println("Are all of the models newer than 2010: " + all);

boolean any = models.stream().anyMatch(model -> model.getYear() > 2016);
System.out.println("Are there any models newer than 2016: " + any);

boolean none = models.stream().noneMatch(model -> model.getYear() < 2010);
System.out.println("Is there a car older than 2010: " + none);
  • allMatch() - Returnerar true om alla element i denna ström matchar det angivna predikatet.
  • anyMatch() - Returnerar true om något element i denna ström matchar det angivna predikatet.
  • noneMatch() - Returnerar true om inget element i denna ström matchar det angivna predikatet.

I det föregående kodexemplet är alla de givna predikaten uppfyllda och alla kommer att returnera true .

Slutsats

De flesta människor idag använder Java 8. Även om inte alla använder Streams. Bara för att de representerar ett nyare tillvägagångssätt för programmering och representerar en touch med funktionell stilprogrammering tillsammans med lambda-uttryck för Java, betyder det inte nödvändigtvis att det är ett bättre tillvägagångssätt. De erbjuder helt enkelt ett nytt sätt att göra saker på. Det är upp till utvecklarna själva att bestämma om de ska förlita sig på funktionell eller imperativ stilprogrammering. Med en tillräcklig träningsnivå kan en kombination av båda principerna hjälpa dig att förbättra din programvara.

Som alltid uppmuntrar vi dig att kolla in den officiella dokumentationen för ytterligare information.


Java-tagg