Java 8 Stream – Java Stream

Welkom bij de Java 8 Stream API handleiding. In de laatste paar Java 8 berichten hebben we gekeken naar Java 8 Interface-wijzigingen en Functionele interfaces en Lambda Expressies. Vandaag zullen we een van de belangrijke API’s die in Java 8 zijn geïntroduceerd bekijken – Java Stream.

Java 8 Stream

  1. Java 8 Stream
  2. Collecties en Java Stream
  3. Functionele interfaces in Java 8 Stream
    1. Function en BiFunction
    2. Predicate en BiPredicate
    3. Consumer en BiConsumer
    4. Supplier
  4. java.util.Optional
  5. java.util.Spliterator
  6. Java Stream Tussenliggende en Terminale Bewerkingen
  7. Java Stream Short-Circuiting Bewerkingen
  8. Java Stream Voorbeelden
    1. Java Streams Maken
    2. Java Stream Converteren naar Collectie of Array
    3. Java Stream Tussenliggende Bewerkingen
    4. Java Stream Terminale Bewerkingen
  9. Java 8 Stream API Beperkingen

Java Stream

Voor we Java Stream API-voorbeelden bekijken, laten we eens kijken waarom het nodig was. Stel dat we willen itereren over een lijst van integers en de som willen vinden van alle getallen groter dan 10. Vóór Java 8 zou de benadering als volgt zijn geweest:

private static int sumIterator(List<Integer> list) {
	Iterator<Integer> it = list.iterator();
	int sum = 0;
	while (it.hasNext()) {
		int num = it.next();
		if (num > 10) {
			sum += num;
		}
	}
	return sum;
}

Er zijn drie belangrijke problemen met de bovenstaande benadering:

  1. We willen gewoon de som van gehele getallen weten, maar we moeten ook aangeven hoe de iteratie zal plaatsvinden. Dit wordt ook wel externe iteratie genoemd omdat het clientprogramma de algoritmebehandeling voor de lijstiteratie verzorgt.
  2. Het programma is sequentieel van aard; er is geen gemakkelijke manier om dit parallel te doen.
  3. Er is veel code nodig, zelfs voor een eenvoudige taak.

Om al deze tekortkomingen te overwinnen, werd de Java 8 Stream API geïntroduceerd. We kunnen Java Stream API gebruiken om interne iteratie te implementeren, wat beter is omdat het Java-framework de iteratie controleert. Interne iteratie biedt verschillende functies zoals sequentiële en parallelle uitvoering, filteren op basis van de opgegeven criteria, mappen, enz. De meeste methodenargumenten van de Java 8 Stream API zijn functionele interfaces, dus lambda-uitdrukkingen werken er goed mee. Laten we eens kijken hoe we bovenstaande logica in een enkele regel kunnen schrijven met behulp van Java Streams.

private static int sumStream(List<Integer> list) {
	return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}

Merk op dat het bovenstaande programma de iteratiestrategie, filter- en mapmethoden van het Java-framework gebruikt en de efficiëntie zou verhogen. Allereerst zullen we de kernconcepten van de Java 8 Stream API bekijken en vervolgens zullen we enkele voorbeelden doornemen om de meest gebruikte methoden te begrijpen.

Collecties en Java Stream

A collection is an in-memory data structure to hold values and before we start using collection, all the values should have been populated. Whereas a java Stream is a data structure that is computed on-demand. Java Stream doesn’t store data, it operates on the source data structure (collection and array) and produce pipelined data that we can use and perform specific operations. Such as we can create a stream from the list and filter it based on a condition. Java Stream operations use functional interfaces, that makes it a very good fit for functional programming using lambda expression. As you can see in the above example that using lambda expressions make our code readable and short. Java 8 Stream internal iteration principle helps in achieving lazy-seeking in some of the stream operations. For example filtering, mapping, or duplicate removal can be implemented lazily, allowing higher performance and scope for optimization. Java Streams are consumable, so there is no way to create a reference to stream for future usage. Since the data is on-demand, it’s not possible to reuse the same stream multiple times. Java 8 Stream support sequential as well as parallel processing, parallel processing can be very helpful in achieving high performance for large collections. All the Java Stream API interfaces and classes are in the java.util.stream package. Since we can use primitive data types such as int, long in the collections using auto-boxing and these operations could take a lot of time, there are specific classes for primitive types – IntStream, LongStream and DoubleStream.

Functionele Interfaces in Java 8 Stream

Enkele veelgebruikte functionele interfaces in de Java 8 Stream API-methoden zijn:

  1. Function en BiFunction: Function vertegenwoordigt een functie die één type argument accepteert en een ander type argument retourneert. Function<T, R> is de generieke vorm waarbij T het type is van de invoer voor de functie en R het type is van het resultaat van de functie. Voor het verwerken van primitieve typen zijn er specifieke Function-interfaces – ToIntFunction, ToLongFunction, ToDoubleFunction, ToIntBiFunction, ToLongBiFunction, ToDoubleBiFunction, LongToIntFunction, LongToDoubleFunction, IntToLongFunction, IntToDoubleFunction, enzovoort. Enkele van de Stream-methoden waar Function of zijn primitieve specialisatie wordt gebruikt, zijn:
    • <R> Stream<R> map(Function<? super T, ? extends R> mapper)
    • IntStream mapToInt(ToIntFunction<? super T> mapper) – evenzo voor long- en double-returning primitieve specifieke stream.
    • IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) – evenzo voor long- en double
    • <A> A[] toArray(IntFunction<A[]> generator)
    • <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
  2. Predicate en BiPredicate: Het vertegenwoordigt een predicaat waartegen elementen van de stream worden getest. Dit wordt gebruikt om elementen uit de Java-stream te filteren. Net als Function, zijn er primitief specifieke interfaces voor int, long en double. Enkele van de Stream-methoden waar Predicate of BiPredicate specialisaties worden gebruikt, zijn:
    • Stream<T> filter(Predicate<? super T> predicate)
    • boolean anyMatch(Predicate<? super T> predicate)
    • boolean allMatch(Predicate<? super T> predicate)
    • boolean noneMatch(Predicate<? super T> predicate)
  3. Consument en BiConsument: Het vertegenwoordigt een bewerking die een enkel invoerargument accepteert en geen resultaat retourneert. Het kan worden gebruikt om een bepaalde actie uit te voeren op alle elementen van de Java-stream. Enkele van de Java 8 Stream-methoden waar Consument, BiConsument of de primitieve specialisatie-interfaces ervan worden gebruikt, zijn:
    • Stream<T> peek(Consument<? super T> actie)
    • leegte forEach(Consument<? super T> actie)
    • leegte forEachOrdered(Consument<? super T> actie)
  4. Leverancier: Leverancier vertegenwoordigt een bewerking waarmee we nieuwe waarden in de stream kunnen genereren. Enkele van de methoden in Stream die een Leverancier argument gebruiken, zijn:
    • openbare statische<T> Stream<T> genereren(Leverancier<T> s)
    • <R> R verzamelen(Leverancier<R> leverancier, BiConsument<R, ? super T> accumulator, BiConsument<R, R> combiner)

java.util.Optioneel

Java Optional is een containerobject dat al dan niet een niet-null waarde kan bevatten. Als er een waarde aanwezig is, retourneert isPresent() true en retourneert get() de waarde. Stroomterminale bewerkingen retourneren een Optioneel object. Enkele van deze methoden zijn:

  • Optioneel<T> reduce(BinaryOperator<T> accumulator)
  • Optioneel<T> min(Comparator<? super T> comparator)
  • Optioneel<T> max(Comparator<? super T> comparator)
  • Optioneel<T> findFirst()
  • Optioneel<T> findAny()

java.util.Spliterator

Voor ondersteuning van parallelle uitvoering in Java 8 Stream API wordt de Spliterator-interface gebruikt. De methode trySplit van Spliterator retourneert een nieuwe Spliterator die een subset van de elementen van de originele Spliterator beheert.

Tussenliggende en terminale bewerkingen van Java Stream

Java Stream API-operaties die een nieuwe Stream retourneren, worden tussenliggende operaties genoemd. Meestal zijn deze operaties lui van aard, dus ze beginnen nieuwe stroomelementen te produceren en sturen deze naar de volgende bewerking. Tussenliggende operaties zijn nooit de uiteindelijke resultaatproducerende operaties. Veelgebruikte tussenliggende operaties zijn filter en map. Java 8 Stream API-operaties die een resultaat retourneren of een neveneffect produceren. Zodra de terminale methode op een stroom wordt aangeroepen, verbruikt deze de stroom en daarna kunnen we de stroom niet meer gebruiken. Terminale operaties zijn ijverig van aard, d.w.z. ze verwerken alle elementen in de stroom voordat ze het resultaat retourneren. Veelgebruikte terminale methoden zijn forEach, toArray, min, max, findFirst, anyMatch, allMatch, enz. U kunt terminale methoden identificeren aan de hand van het retourtype; ze zullen nooit een Stream retourneren.

Java Stream Short Circuiting Operations

Een tussenliggende bewerking wordt short-circuiting genoemd als deze een eindige stroom kan produceren voor een oneindige stroom. Bijvoorbeeld limit() en skip() zijn twee short-circuiting tussenliggende bewerkingen. Een terminale bewerking wordt short-circuiting genoemd als deze in eindige tijd kan eindigen voor een oneindige stroom. Bijvoorbeeld anyMatch, allMatch, noneMatch, findFirst en findAny zijn short-circuiting terminale bewerkingen.

Voorbeelden van Java Stream

I have covered almost all the important parts of the Java 8 Stream API. It’s exciting to use this new API features and let’s see it in action with some java stream examples.

Het creëren van Java Streams

Er zijn verschillende manieren waarop we een Java-stream kunnen maken van arrays en collecties. Laten we deze bekijken aan de hand van eenvoudige voorbeelden.

  1. We kunnen Stream.of() gebruiken om een stream te maken van vergelijkbaar type gegevens. Bijvoorbeeld, we kunnen een Java Stream van integers maken van een groep int of Integer objecten.

    Stream<Integer> stream = Stream.of(1,2,3,4);
    
  2. We kunnen Stream.of() gebruiken met een array van Objecten om de stream terug te geven. Let op dat het geen autoboxing ondersteunt, dus we kunnen geen array van primitieve types doorgeven.

    Stream<Integer> stream = Stream.of(new Integer[]{1,2,3,4}); 
    // werkt prima
    
    Stream<Integer> stream1 = Stream.of(new int[]{1,2,3,4}); 
    // Compileerfout, Type mismatch: kan niet converteren van Stream<int[]> naar Stream<Integer>
    
  3. We kunnen de methode stream() van Collection gebruiken om een sequentiële stream te maken en parallelStream() om een parallelle stream te maken.

    List<Integer> myList = new ArrayList<>();
    for(int i=0; i<100; i++) myList.add(i);
    		
    //sequentieel stream
    Stream<Integer> sequentialStream = myList.stream();
    		
    //parallelle stream
    Stream<Integer> parallelStream = myList.parallelStream();
    
  4. We kunnen de methoden Stream.generate() en Stream.iterate() gebruiken om Stream te maken.

    Stream<String> stream1 = Stream.generate(() -> {return "abc";});
    Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
    
  5. Het gebruik van de methoden Arrays.stream() en String.chars().

    LongStream is = Arrays.stream(new long[]{1,2,3,4});
    IntStream is2 = "abc".chars();
    

Het omzetten van Java Stream naar Collection of Array

Er zijn verschillende manieren waarop we een Collection of Array kunnen krijgen van een Java Stream.

  1. We kunnen de Java Stream collect()-methode gebruiken om een lijst, map of set te verkrijgen vanuit een stream.

    Stream<Integer> intStream = Stream.of(1,2,3,4);
    List<Integer> intList = intStream.collect(Collectors.toList());
    System.out.println(intList); //prints [1, 2, 3, 4]
    
    intStream = Stream.of(1,2,3,4); //de stream is gesloten, dus we moeten deze opnieuw maken
    Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10));
    System.out.println(intMap); //prints {1=11, 2=12, 3=13, 4=14}
    
  2. We kunnen de stream toArray()-methode gebruiken om een array te maken vanuit de stream.

    Stream<Integer> intStream = Stream.of(1,2,3,4);
    Integer[] intArray = intStream.toArray(Integer[]::new);
    System.out.println(Arrays.toString(intArray)); //prints [1, 2, 3, 4]
    

Java Stream Intermediate Operations

  1. Stream filter() voorbeeld: We kunnen de filter() methode gebruiken om stroomelementen te testen op een voorwaarde en een gefilterde lijst te genereren.

    List<Integer> myList = new ArrayList<>();
    for(int i=0; i<100; i++) myList.add(i);
    Stream<Integer> sequentialStream = myList.stream();
    
    Stream<Integer> highNums = sequentialStream.filter(p -> p > 90); //filter getallen groter dan 90
    System.out.print("Hoge getallen groter dan 90=");
    highNums.forEach(p -> System.out.print(p+" "));
    //print "Hoge getallen groter dan 90=91 92 93 94 95 96 97 98 99 "
    
  2. Stream map() voorbeeld: We kunnen map() gebruiken om functies toe te passen op een stroom. Laten we eens kijken hoe we het kunnen gebruiken om de uppercase functie toe te passen op een lijst van strings.

    Stream<String> namen = Stream.of("aBc", "d", "ef");
    System.out.println(namen.map(s -> {
    		return s.toUpperCase();
    	}).collect(Collectors.toList()));
    //print [ABC, D, EF]
    
  3. Voorbeeld van gesorteerde stroom: We kunnen sorted() gebruiken om de stroomelementen te sorteren door een Comparator-argument door te geven.

    Stream<String> namen2 = Stream.of("aBc", "d", "ef", "123456");
    List<String> omgekeerdGesorteerd = namen2.sorted(Comparator.reverseOrder()).collect(Collectors.toList());
    System.out.println(omgekeerdGesorteerd); // [ef, d, aBc, 123456]
    
    Stream<String> namen3 = Stream.of("aBc", "d", "ef", "123456");
    List<String> natuurlijkGesorteerd = namen3.sorted().collect(Collectors.toList());
    System.out.println(natuurlijkGesorteerd); //[123456, aBc, d, ef]
    
  4. Voorbeeld van flatMap() in stroom: We kunnen flatMap() gebruiken om een stroom te maken van de stroom van lijsten. Laten we een eenvoudig voorbeeld bekijken om deze twijfel weg te nemen.

    Stream<List<String>> origineleNamenLijst = Stream.of(
    	Arrays.asList("Pankaj"), 
    	Arrays.asList("David", "Lisa"),
    	Arrays.asList("Amit"));
    //flat de stroom van List<String> naar String stroom
    Stream<String> vlakkeStroom = origineleNamenLijst
    	.flatMap(strLijst -> strLijst.stream());
    
    vlakkeStroom.forEach(System.out::println);
    

Java Stream Terminal Operations

Laten we eens kijken naar enkele voorbeelden van eindbewerkingen van Java-streams.

  1. Stream reduce() voorbeeld: We kunnen reduce() gebruiken om een reductie uit te voeren op de elementen van de stream, met behulp van een associatieve accumulatiefunctie, en een Optioneel terug te geven. Laten we eens kijken hoe we het kunnen gebruiken om de gehele getallen in een stream te vermenigvuldigen.

    Stream<Integer> getallen = Stream.of(1,2,3,4,5);
    		
    Optioneel<Integer> intOptioneel = getallen.reduce((i,j) -> {return i*j;});
    if(intOptioneel.isPresent()) System.out.println("Vermenigvuldiging = "+intOptioneel.get()); //120
    
  2. Stream count() voorbeeld: We kunnen deze eindbewerking gebruiken om het aantal items in de stream te tellen.

    Stream<Integer> getallen1 = Stream.of(1,2,3,4,5);
    		
    System.out.println("Aantal elementen in de stream="+getallen1.count()); //5
    
  3. Voorbeeld van Stream forEach(): Dit kan worden gebruikt voor het itereren over de stream. We kunnen dit gebruiken ter vervanging van de iterator. Laten we eens kijken hoe we het kunnen gebruiken om alle elementen van de stream af te drukken.

    Stream<Integer> getallen2 = Stream.of(1,2,3,4,5);
    getallen2.forEach(i -> System.out.print(i+",")); //1,2,3,4,5,
    
  4. Voorbeelden van Stream match(): Laten we eens kijken naar enkele voorbeelden van matchende methoden in de Stream API.

    Stream<Integer> getallen3 = Stream.of(1,2,3,4,5);
    System.out.println("Bevat de stream 4? "+getallen3.anyMatch(i -> i==4));
    //Bevat de stream 4? true
    
    Stream<Integer> getallen4 = Stream.of(1,2,3,4,5);
    System.out.println("Bevat de stream alle elementen kleiner dan 10? "+getallen4.allMatch(i -> i<10));
    //Bevat de stream alle elementen kleiner dan 10? true
    
    Stream<Integer> getallen5 = Stream.of(1,2,3,4,5);
    System.out.println("Bevat de stream niet het getal 10? "+getallen5.noneMatch(i -> i==10));
    //Bevat de stream niet het getal 10? true
    
  5. Voorbeeld van findFirst() op Stream: Dit is een kortsluitende terminale bewerking, laten we eens kijken hoe we het kunnen gebruiken om de eerste string te vinden uit een stream die begint met D.

    Stream<String> namen4 = Stream.of("Pankaj","Amit","David", "Lisa");
    Optioneel<String> voornaamMetD = namen4.filter(i -> i.startsWith("D")).findFirst();
    if(voornaamMetD.isPresent()){
    	System.out.println("Eerste naam beginnend met D="+voornaamMetD.get()); //David
    }
    

Beperkingen van Java 8 Stream API

De Java 8 Stream API brengt veel nieuwe dingen om mee te werken met lijsten en arrays, maar het heeft ook enkele beperkingen.

  1. Stateless lambda expressions: Als u parallelle stream gebruikt en lambda-expressies zijn stateful, kan dit resulteren in willekeurige reacties. Laten we dit zien aan de hand van een eenvoudig programma. StatefulParallelStream.java

    package com.journaldev.java8.stream;
    
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Stream;
    
    public class StatefulParallelStream {
    
    	public static void main(String[] args) {
    
    		List<Integer> ss = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
    		List<Integer> result = new ArrayList<Integer>();
    		 
    		Stream<Integer> stream = ss.parallelStream();
    		 
    		stream.map(s -> {
    		        synchronized (result) {
    		          if (result.size() < 10) {
    		            result.add(s);
    		          }
    		        }
    				return s;
    		    }).forEach( e -> {});
    		 System.out.println(result);   
    	}
    }
    

    If we run above program, you will get different results because it depends on the way stream is getting iterated and we don’t have any order defined for parallel processing. If we use sequential stream, then this problem will not arise.

  2. Eenmaal een Stream is geconsumeerd, kan deze niet later worden gebruikt. Zoals je kunt zien in de bovenstaande voorbeelden, maak ik telkens een nieuwe stream aan.

  3. Er zijn veel methoden in de Stream API en het meest verwarrende deel zijn de overloaded methoden. Het maakt de leercurve tijdrovend.

Dat is alles voor het voorbeeld van de Java 8 Stream tutorial. Ik kijk ernaar uit om deze functie te gebruiken en de code leesbaarder te maken met betere prestaties door parallelle verwerking. Referentie: Java Stream API Doc

Source:
https://www.digitalocean.com/community/tutorials/java-8-stream