ברוכים הבאים למדריך של Java 8 Stream API. בפוסטים האחרונים של Java 8, חקרנו את שינויים בממשקי Java 8 ואת ממשקים פונקציונליים וביטויי Lambda. היום נתעקב אחרי אחד מ-API המרכזיים שהוצגו ב-Java 8 – Java Stream.
Java 8 Stream
- Java 8 Stream
- אוסף ו-Java Stream
- ממשקים פונקציונליים ב-Java 8 Stream
- java.util.Optional
- java.util.Spliterator
- ביצועים ופעולות סופיות בזרם ג'אווה
- פעולות הפסק בזרם ג'אווה
- דוגמאות לזרם ג'אווה
- מגבלות גרסה 8 של ממשק הזרם של ג'אווה
זרם ג'אווה
לפני שנכנס לדוגמאות API של זרם ג'אווה, בואו נראה למה זה נדרש. נניח שאנחנו רוצים לעבור על רשימת מספרים שלמים ולמצוא את סכום כל המספרים שגדולים מ-10. לפני גרסת ג'אווה 8, הדרך הנפוצה לעשות זאת הייתה:
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;
}
יש שלוש בעיות עיקריות עם השיטה שנתונה לעיל:
- אנו רק רוצים לדעת את סכום המספרים שלמים אך נצטרך גם לספק איך האיטרציה תתבצע, זה נקרא גם איטרציה חיצונית מכיוון שהתוכנית הלקוח טופלת את האלגוריתם לעבור על הרשימה.
- התוכנית היא סדרתית לטבעה, אין לנו דרך לעשות זאת בצורה מקבילית בקלות.
- יש הרבה קוד כדי לעשות אפילו משימה פשוטה.
כדי להתגבר על כל החסרונות האלה, נוסף Java 8 Stream API. אנו יכולים להשתמש ב-Java Stream API כדי ליישם איטרציה פנימית, שזה טוב יותר מכיוון שהפריימוורק של ג'אווה שולט באיטרציה. איטרציה פנימית מספקת מספר תכונות כגון ביצוע רצופי ומקבילי, סינון בהתבסס על הקריטריונים הנתונים, מיפוי וכו '. רוב הארגומנטים של שיטות Java 8 Stream API הם ממשקי פונקציונליים, לכן ביטויי למדה עובדים מאוד טוב עמם. בואו נראה כיצד נוכל לכתוב את הלוגיקה לעיל בהצהרת שורה אחת באמצעות Java Streams.
private static int sumStream(List<Integer> list) {
return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}
שים לב שהתוכנית למעלה משתמשת באסטרטגיית איטרציה של הפריימוורק של ג'אווה, בסינון ובמיפוי של שיטות ויכולה להגביר את היעילות. תחילה נבחן את המושגים העיקריים של Java 8 Stream API ואז נעבור על כמה דוגמאות להבנת השימוש הנפוץ ביותר בשיטות.
אוספים ו-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
.
ממשקי פונקציונליות ב-Java 8 Stream
חלק מממשקי הפונקציונליות הנפוצים בשיטות API של Java 8 Stream הם:
- Function ו-BiFunction: פונקציה מייצגת פונקציה שמקבלת סוג אחד של ארגומנט ומחזירה סוג אחר של ארגומנט.
Function<T, R>
הוא הצורה הגנרית בה T הוא סוג הקלט לפונקציה ו-R הוא סוג התוצאה של הפונקציה. לטיפול בסוגי פרימיטיביים, ישנן ממשקי Function ספציפיים –ToIntFunction
,ToLongFunction
,ToDoubleFunction
,ToIntBiFunction
,ToLongBiFunction
,ToDoubleBiFunction
,LongToIntFunction
,LongToDoubleFunction
,IntToLongFunction
,IntToDoubleFunction
וכו '. חלק משיטות ה-Stream שבהן משתמשFunction
או התמחותו הפרימיטיבית הם:- <R> Stream<R> map(Function<? super T, ? extends R> mapper)
- IntStream mapToInt(ToIntFunction<? super T> mapper) – באופן דומה לזה עבור long ו-double שמחזירים סטרים ספציפיים לפרימיטיבי.
- IntStream flatMapToInt(Function<? super T, ? extends IntStream> mapper) – באופן דומה לזה עבור long ו-double
- <A> A[] toArray(IntFunction<A[]> generator)
- <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)
- Predicate ו־BiPredicate: מייצג Predicate שנבדקים נגדם אלמנטים של הזרם. משמש לסינון של אלמנטים מתוך הזרם ב-Java. כמו
Function
, ישנן ממשקים ספציפיים לעצמים int, long ו־double. כמה ממתודות הזרם בהן משתמשיםPredicate
או התמחות ב־BiPredicate
הן:- 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)
- צרכן ובי-צרכן: הוא מייצג פעולה שמקבלת ארגומנט כניסה אחד ואינה מחזירה תוצאה. ניתן להשתמש בו כדי לבצע פעולה מסוימת על כל איברי זרם ה-Java. חלק משיטות זרם ה-Java 8 שבהן משמשים
Consumer
,BiConsumer
או ממשקי התמחות פרימיטיביים שלו הם:- Stream<T> peek(Consumer<? super T> action)
- void forEach(Consumer<? super T> action)
- void forEachOrdered(Consumer<? super T> action)
- ספק: ספק מייצג פעולה שבאמצעותה ניתן ליצור ערכים חדשים בזרם. חלק מהשיטות בזרם שמקבלות ארגומנט של
Supplier
הם:- public static<T> Stream<T> generate(Supplier<T> s)
- <R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner)
java.util.Optional
Optional ב-Java הוא אובייקט תוכן שעשוי או לא להכיל ערך שאינו null. אם ערך קיים, `isPresent()` תחזיר אמת ו-`get()` תחזיר את הערך. פעולות סיום של זרימה מחזירות אובייקט Optional. חלק מהשיטות האלה הם:
- Optional<T> reduce(BinaryOperator<T> accumulator)
- Optional<T> min(Comparator<? super T> comparator)
- Optional<T> max(Comparator<? super T> comparator)
- Optional<T> findFirst()
- Optional<T> findAny()
java.util.Spliterator
כדי לתמוך בביצוע מקבילי ב-API של זרימה ב-Java 8, ממשק Spliterator משמש. שיטת `trySplit` של Spliterator מחזירה Spliterator חדש שמנהל תת-קבוצה של האלמנטים של Spliterator המקורי.
פעולות ביניים וסופיות של Java Stream
פעולות Java Stream API שמחזירות זרם חדש נקראות פעולות אמצעיות. ברוב המקרים, פעולות אלו הן עצלות בטבען, כך שהן מתחילות ליצור איברי זרם חדשים ושולחות אותם לפעולה הבאה. פעולות אמצעיות אינן פעולות היוצרות תוצאה סופית. פעולות אמצעיות נפוצות הן filter
ו־map
. פעולות Java 8 Stream API שמחזירות תוצאה או יוצרות אפקט צד. פעם אחת שמועברת שיטת סוף על זרם, היא צורפת את הזרם ולאחר מכן אין לנו אפשרות להשתמש בזרם. פעולות סופיות הן עצלות בטבען, כלומר הן מעבדות את כל האיברים בזרם לפני החזרת התוצאה. שיטות סופיות נפוצות הן forEach
, toArray
, min
, max
, findFirst
, anyMatch
, allMatch
וכו'. ניתן לזהות את שיטות הסיום מסוג ההחזרה, הן לעולם לא תחזירו זרם.
פעולות הקצרה ביותר של Java Stream
פעולת אמצעי נקראת "קיצור", אם היא עשויה לייצר זרם סופי עבור זרם בלתי סופי. לדוגמה, limit()
ו־skip()
הן שתי פעולות אמצעיות של קיצור. פעולה סופית נקראת "קיצור", אם היא עשויה להסתיים בזמן סופי עבור זרם בלתי סופי. לדוגמה, anyMatch
, allMatch
, noneMatch
, findFirst
ו־findAny
הן פעולות סופיות של קיצור.
דוגמאות ל- 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.
יצירת נתוני Java Streams
קיימות מספר דרכים באמצעותן ניתן ליצור נתוני Java Stream ממערכים ואוספים. נסתכל על כמה מהן עם דוגמאות פשוטות.
-
ניתן להשתמש ב-
Stream.of()
כדי ליצור נתון מסוג דומה של נתונים. לדוגמה, ניתן ליצור Java Stream של מספרים שלמים מקבוצה של int או אובייקטים Integer.Stream<Integer> stream = Stream.of(1,2,3,4);
-
ניתן להשתמש ב-
Stream.of()
עם מערך של אובייקטים כדי להחזיר את הנתון. שימו לב שזה לא תומך ב- autoboxing, ולכן אין אפשרות להעביר מערך של טיפוס פרימיטיבי.Stream<Integer> stream = Stream.of(new Integer[]{1,2,3,4}); //עובד בסדר Stream<Integer> stream1 = Stream.of(new int[]{1,2,3,4}); //שגיאה בזמן קומפילציה, התאמת טיפוסים לא מתאפשרת: לא ניתן להמיר מ- Stream<int[]> ל- Stream<Integer>
-
אנו יכולים להשתמש בפעולת
stream()
של Collection כדי ליצור זרם רצוף וב-parallelStream()
כדי ליצור זרם מקביל.List<Integer> myList = new ArrayList<>(); for(int i=0; i<100; i++) myList.add(i); // זרם רצוף Stream<Integer> sequentialStream = myList.stream(); // זרם מקביל Stream<Integer> parallelStream = myList.parallelStream();
-
אנו יכולים להשתמש בפעולות
Stream.generate()
ו-Stream.iterate()
כדי ליצור זרם.Stream<String> stream1 = Stream.generate(() -> {return "abc";}); Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
-
באמצעות
Arrays.stream()
ובאמצעות פעולתString.chars()
.LongStream is = Arrays.stream(new long[]{1,2,3,4}); IntStream is2 = "abc".chars();
המרת Java Stream לאוסף או מערך
ישנן מספר דרכים שבאמצעותן ניתן לקבל אוסף או מערך מ- Java Stream.
-
ניתן להשתמש במתודת
collect()
של java Stream כדי לקבל List, Map או Set מתוך זרם.Stream<Integer> intStream = Stream.of(1,2,3,4); List<Integer> intList = intStream.collect(Collectors.toList()); System.out.println(intList); //מדפיס [1, 2, 3, 4] intStream = Stream.of(1,2,3,4); //הזרם נסגר, ולכן יש ליצור אותו מחדש Map<Integer,Integer> intMap = intStream.collect(Collectors.toMap(i -> i, i -> i+10)); System.out.println(intMap); //מדפיס {1=11, 2=12, 3=13, 4=14}
-
ניתן להשתמש במתודת
toArray()
של זרם כדי ליצור מערך מהזרם.Stream<Integer> intStream = Stream.of(1,2,3,4); Integer[] intArray = intStream.toArray(Integer[]::new); System.out.println(Arrays.toString(intArray)); //מדפיס [1, 2, 3, 4]
פעולות ביניים של Java Stream
בואו נבחן דוגמאות לפעולות ביניים של java Stream שנעשה בהן שימוש נפוץ.
-
דוגמת פילטור של נתוני זרם: ניתן להשתמש בשיטת filter() כדי לבדוק איברי נתונים בזרם עבור תנאי וליצור רשימה מסוננת.
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 numbers greater than 90 System.out.print("High Nums greater than 90="); highNums.forEach(p -> System.out.print(p+" ")); //prints "High Nums greater than 90=91 92 93 94 95 96 97 98 99 "
-
דוגמת מיפוי של נתוני זרם: ניתן להשתמש בשיטת map() כדי ליישם פונקציות על נתוני זרם. בואו נראה כיצד ניתן להשתמש בה כדי ליישם פונקציה שממירה אותיות לאותיות רישיות ברשימת מחרוזות.
Stream<String> names = Stream.of("aBc", "d", "ef"); System.out.println(names.map(s -> { return s.toUpperCase(); }).collect(Collectors.toList())); //prints [ABC, D, EF]
-
דוגמה לשימוש ב- sorted() בזרם: אנו יכולים להשתמש ב- sorted() כדי למיין את אלמנטי הזרם על ידי מעבירת הביטוי Comparator.
Stream<String> names2 = Stream.of("aBc", "d", "ef", "123456"); List<String> reverseSorted = names2.sorted(Comparator.reverseOrder()).collect(Collectors.toList()); System.out.println(reverseSorted); // [ef, d, aBc, 123456] Stream<String> names3 = Stream.of("aBc", "d", "ef", "123456"); List<String> naturalSorted = names3.sorted().collect(Collectors.toList()); System.out.println(naturalSorted); //[123456, aBc, d, ef]
-
דוגמה לשימוש ב- flatMap() בזרם: אנו יכולים להשתמש ב- flatMap() כדי ליצור זרם מזרם של רשימות. בואו נראה דוגמה פשוטה לפירוט זה.
Stream<List<String>> namesOriginalList = Stream.of( Arrays.asList("Pankaj"), Arrays.asList("David", "Lisa"), Arrays.asList("Amit")); //flat the stream from List<String> to String stream Stream<String> flatStream = namesOriginalList .flatMap(strList -> strList.stream()); flatStream.forEach(System.out::println);
פעולות סופיות של זרם ג'אווה
בואו נסתכל על כמה מדוגמאות לפעולות סופיות של זרם ג'אווה.
-
דוגמה ל־reduce() של זרם: ניתן להשתמש ב־reduce() כדי לבצע הפחתה על איברי הזרם, באמצעות פונקציית צבירה אסוציאטיבית, ולהחזיר אופציונל. בואו נראה איך ניתן להשתמש בו כדי לכפול את המספרים בזרם.
Stream<Integer> numbers = Stream.of(1,2,3,4,5); Optional<Integer> intOptional = numbers.reduce((i,j) -> {return i*j;}); if(intOptional.isPresent()) System.out.println("Multiplication = "+intOptional.get()); //120
-
דוגמה ל־count() של זרם: ניתן להשתמש בפעולה סופית זו כדי לספור את מספר הפריטים בזרם.
Stream<Integer> numbers1 = Stream.of(1,2,3,4,5); System.out.println("Number of elements in stream="+numbers1.count()); //5
-
דוגמה לשימוש ב־forEach() בזרימת מידע: ניתן להשתמש בזה עבור סריקה על כל הרצפים. אנו יכולים להשתמש בזה במקום iterator. בואו נראה כיצד להשתמש בו כדי להדפיס את כל האיברים של הזרימה.
Stream<Integer> numbers2 = Stream.of(1,2,3,4,5); numbers2.forEach(i -> System.out.print(i+",")); //1,2,3,4,5,
-
דוגמאות ל־match() בזרימת מידע: בואו נראה כמה דוגמאות לשימוש בשיטות התאמה ב־Stream API.
Stream<Integer> numbers3 = Stream.of(1,2,3,4,5); System.out.println("האם הזרימה מכילה 4? "+numbers3.anyMatch(i -> i==4)); //האם הזרימה מכילה 4? true Stream<Integer> numbers4 = Stream.of(1,2,3,4,5); System.out.println("האם הזרימה מכילה את כל האיברים הקטנים מ־10? "+numbers4.allMatch(i -> i<10)); //האם הזרימה מכילה את כל האיברים הקטנים מ־10? true Stream<Integer> numbers5 = Stream.of(1,2,3,4,5); System.out.println("האם הזרימה לא מכילה 10? "+numbers5.noneMatch(i -> i==10)); //האם הזרימה לא מכילה 10? true
-
דוגמה לשימוש ב-findFirst() של Stream: זהו פעולה סופית של הקצה עם התנגדות קצרת, בואו נראה איך אפשר להשתמש בה כדי למצוא את המחרוזת הראשונה מתוך זרם שמתחילה ב-D.
Stream<String> names4 = Stream.of("Pankaj","Amit","David", "Lisa"); Optional<String> firstNameWithD = names4.filter(i -> i.startsWith("D")).findFirst(); if(firstNameWithD.isPresent()){ System.out.println("First Name starting with D="+firstNameWithD.get()); //David }
מגבלות ב- Java 8 Stream API
API של Java 8 Stream מביא הרבה דברים חדשים לעבודה עם רשימות ומערכים, אך יש לו גם מספר מגבלות.
-
ביטויי למבדל ללא מצב: אם אתה משתמש בזרם מקביל וביטויי למבדל הם במצב מצבי, זה עשוי להביא לתגובות אקראיות. בואו נראה זאת עם תוכנית פשוטה.
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); } }
כאשר אנו מריצים את התוכנית למעלה, תקבל תוצאות שונות מכיוון שהן תלויות בדרך בה הזרם נע בו ואין לנו שום סדר שהוגדר עבור עיבוד מקבילי. אם נשתמש בזרם רצפי, אז הבעיה הזו לא תעלה.
-
פעם שהזרם נצרף, אי אפשר להשתמש בו מאוחר יותר. כפי שאתה יכול לראות בדוגמאות שלמעלה, כל פעם אני יוצר זרם.
-
יש הרבה שיטות ב-Stream API והחלק המבלבל ביותר הוא השיטות המוטעות. זה עושה את הסיפור של למידה ארוך יותר.
זהו הכל לדוגמה על זרם Java 8. אני מצפה להשתמש בתכונה הזו ולהפוך את הקוד לנקרא עם ביצועים טובים יותר דרך עיבוד מקבילי. מקור: מסמך API של Java Stream
Source:
https://www.digitalocean.com/community/tutorials/java-8-stream