Java 8 Stream – Java Stream

مرحبًا بك في دليل تعليمي حول Java 8 Stream API. في النصوص الأخيرة لجافا 8، قمنا بالنظر إلى تغييرات واجهة Java 8 و الواجهات الوظيفية وتعبيرات Lambda. اليوم سنتعرض إلى واحدة من أهم واجهات البرمجة التطبيقية التي تم إدخالها في جافا 8 – Java Stream.

Java 8 Stream

  1. Java 8 Stream
  2. المجموعات و Java Stream
  3. الواجهات الوظيفية في Java 8 Stream
    1. الدالة والدالة المزدوجة
    2. الشرط والشرط المزدوج
    3. المستهلك والمستهلك المزدوج
    4. المورد
  4. java.util.Optional
  5. java.util.Spliterator
  6. عمليات وسيطة ونهائية في تدفق جافا
  7. عمليات تدفق جافا للتوصيل القصير
  8. أمثلة على تدفق جافا
    1. إنشاء تدفق جافا
    2. تحويل تدفق جافا إلى مجموعة أو مصفوفة
    3. عمليات وسيطة في تدفق جافا
    4. عمليات نهائية في تدفق جافا
  9. قيود واجهة برمجة التطبيقات لتدفق جافا 8

تدفق جافا

قبل أن نلقي نظرة على أمثلة واجهة برمجة التطبيقات لتدفق جافا ، دعنا نرى لماذا كان مطلوبًا. لنفترض أننا نريد تكرار قائمة من الأعداد الصحيحة وإيجاد مجموع جميع الأعداد الأكبر من 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;
}

هناك ثلاثة مشاكل رئيسية مع النهج المذكور أعلاه:

  1. نريد فقط معرفة مجموع الأعداد الصحيحة لكن علينا أيضًا تحديد كيف ستتم العملية، وهذا ما يُعرف بـ التكرار الخارجي لأن برنامج العميل يتولى خوارزمية التكرار عبر القائمة.
  2. البرنامج ذو طبيعة تتابعية، لا يمكننا فعل ذلك بسهولة بطريقة متوازية.
  3. هناك الكثير من الشفرة حتى لمهمة بسيطة.

للتغلب على جميع العيوب المذكورة أعلاه، تم تقديم واجهة برمجة تطبيقات Java 8 Stream API. يمكننا استخدام واجهة برمجة تطبيقات Java Stream لتنفيذ التكرار الداخلي، وهذا أفضل لأن إطار العمل Java يتحكم في عملية التكرار. التكرار الداخلي يوفر العديد من الميزات مثل التنفيذ التتابعي والمتوازي، والتصفية بناءً على المعايير المعطاة، والتعيين وما إلى ذلك. معظم وسائل واجهة برمجة تطبيقات 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 وطرق التصفية والتعيين وسيزيد من الكفاءة. أولاً وقبل كل شيء سنلقي نظرة على المفاهيم الأساسية لواجهة برمجة تطبيقات 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.

الواجهات الوظيفية في تيار جافا 8

بعض الواجهات الوظيفية الشائعة المستخدمة في أساليب تيار جافا 8 API هي:

  1. وظيفة و BiFunction: تمثل الوظيفة وظيفة تأخذ نوعًا واحدًا من الوسيطة وتُرجع نوعًا آخر من الوسيطة. Function<T، R> هي النموذج العام حيث يكون T هو نوع الإدخال إلى الوظيفة و R هو نوع نتيجة الوظيفة. للتعامل مع الأنواع الأساسية، هناك واجهات وظيفة محددة – ToIntFunction، ToLongFunction، ToDoubleFunction، ToIntBiFunction، ToLongBiFunction، ToDoubleBiFunction، LongToIntFunction، LongToDoubleFunction، IntToLongFunction، IntToDoubleFunction إلخ. بعض أساليب تيار حيث يتم استخدام Function أو تخصصه الأساسي هي:
    • <R> Stream<R> map(Function<? super T، ? extends R> mapper)
    • IntStream mapToInt(ToIntFunction<? super T> mapper) – بالمثل للتيار المحدد بالأساسيات للعائدات طويلة ومزدوجة.
    • IntStream flatMapToInt(Function<? super T، ? extends IntStream> mapper) – بالمثل للطويلة والمزدوجة
    • <A> A[] toArray(IntFunction<A[]> generator)
    • <U> U reduce(U identity، BiFunction<U، ? super T، U> accumulator، BinaryOperator<U> combiner)
  2. Predicate and BiPredicate: تُمثل شرطًا يتم بموجبه اختبار عناصر التدفق. يُستخدم هذا لتصفية العناصر من تدفق جافا. تمامًا مثل Function، هناك واجهات متخصصة للأرقام الصحيحة، الطويلة والمزدوجة. بعض أساليب Stream حيث تم استخدام التخصيصات الخاصة بـ 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)
  3. المستهلك والمستهلك الثنائي: يمثل عملية تقبل واحدة لمدخل ولا تُرجع أي نتيجة. يمكن استخدامها لأداء بعض الإجراءات على جميع عناصر Java Stream. بعض طرق Java 8 Stream حيث يتم استخدام المستهلك، المستهلك الثنائي أو واجهات التخصيص الأساسية الخاصة بها هي:
    • Stream<T> peek(Consumer<? super T> action)
    • void forEach(Consumer<? super T> action)
    • void forEachOrdered(Consumer<? super T> action)
  4. المورد: يمثل المورد عملية يمكننا من خلالها توليد قيم جديدة في التدفق. بعض الطرق في Stream التي تأخذ المورد كوسيطة هي:
    • 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

جافا اختياري هو كائن حاوية يمكن أن يحتوي أو لا يحتوي على قيمة غير قابلة للإلغاء. إذا كانت القيمة موجودة، ستعيد isPresent() القيمة صحيحة وستعيد get() القيمة. تُعيد عمليات نهائية للتيار كائن اختياريًا. بعض هذه الطرق هي:

  • 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

لدعم التنفيذ الموازي في واجهة برمجة تطبيقات جافا 8 للتيار، يتم استخدام واجهة Spliterator. تُعيد طريقة Spliterator trySplit Spliterator جديدًا يدير مجموعة فرعية من عناصر Spliterator الأصلي.

عمليات التيار الوسيطة والنهائية في جافا

Java Stream API العمليات التي تعيد تيارًا جديدًا تسمى عمليات وسيطة. في معظم الأحيان، هذه العمليات تكون كسولة الطبيعة، لذا يتم بدء إنتاج عناصر التيار الجديدة وإرسالها إلى العملية التالية. العمليات الوسيطة ليست أبدًا عمليات إنتاج النتيجة النهائية. العمليات الوسيطة الشائع استخدامها هي filter و map. في Java 8 Stream API، تُعد العمليات التي تعيد نتيجة أو تنتج تأثير جانبي عملياتاً نهائية. بمجرد استدعاء الطريقة النهائية على التيار، يستهلك التيار وبعد ذلك لا يمكننا استخدام التيار. العمليات النهائية هي غير كسولة، أي أنها تعالج جميع العناصر في التيار قبل إرجاع النتيجة. الطرق النهائية المستخدمة شائعًا هي forEach، toArray، min، max، findFirst، anyMatch، allMatch وما إلى ذلك. يمكنك التعرف على الطرق النهائية من خلال نوع الإرجاع، حيث لن تعيد Stream أبدًا.

عمليات اختصار التيار في Java

تُطلق عملية وسيطة اختصارية إذا كانت قد تنتج تيارًا محدد المدى لتيار لانهائي. على سبيل المثال، limit() و skip() هما عمليتين وسيطتين للاختصار. تسمى العملية النهائية اختصارية إذا كان بإمكانها الانتهاء في وقت محدد لتيار لانهائي. على سبيل المثال، anyMatch، allMatch، noneMatch، findFirst و findAny هي عمليات نهائية اختصارية.

أمثلة على تيار جافا

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.

إنشاء تيارات جافا

هناك عدة طرق يمكننا من خلالها إنشاء تيار جافا من مصفوفة ومجموعات. دعونا نلقي نظرة على هذه بأمثلة بسيطة.

  1. يمكننا استخدام Stream.of() لإنشاء تيار من نوع مماثل من البيانات. على سبيل المثال، يمكننا إنشاء تيار جافا من الأعداد الصحيحة من مجموعة من الأعداد الصحيحة أو الكائنات Integer.

    Stream stream = Stream.of(1,2,3,4);
    
  2. يمكننا استخدام Stream.of() مع مصفوفة من الكائنات لإرجاع التيار. لاحظ أنه لا يدعم التعبئة التلقائية، لذا لا يمكننا تمرير مصفوفة من الأنواع الأساسية.

    Stream stream = Stream.of(new Integer[]{1,2,3,4}); 
    // يعمل بشكل جيد
    
    Stream stream1 = Stream.of(new int[]{1,2,3,4}); 
    // خطأ في وقت الترجمة، عدم تطابق النوع: لا يمكن تحويله من Stream إلى Stream
    
  3. يمكننا استخدام 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();
    
  4. يمكننا استخدام Stream.generate() و Stream.iterate() الأساليب لإنشاء Stream.

    Stream<String> stream1 = Stream.generate(() -> {return "abc";});
    Stream<String> stream2 = Stream.iterate("abc", (i) -> i);
    
  5. استخدام Arrays.stream() و String.chars() الأساليب.

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

تحويل Java Stream إلى مجموعة أو مصفوفة

هناك عدة طرق يمكننا من خلالها الحصول على مجموعة أو مصفوفة من Java Stream.

  1. يمكننا استخدام طريقة collect() في جافا للحصول على قائمة، خريطة أو مجموعة من التدفق.

    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}
    
  2. يمكننا استخدام طريقة 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]
    

عمليات جافا الوسيطة للتدفق

لنلقي نظرة على مثال لعمليات التدفق الوسيطة في جافا المستخدمة بشكل شائع.

  1. مثال على تصفية التيار filter(): يمكننا استخدام طريقة 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); // يصف الأرقام الأكبر من 90
    System.out.print("High Nums greater than 90=");
    highNums.forEach(p -> System.out.print(p+" "));
    // يطبع "High Nums greater than 90=91 92 93 94 95 96 97 98 99 "
    
  2. مثال على تطبيق تابع التيار map(): يمكننا استخدام map() لتطبيق وظائف على التيار. دعونا نرى كيف يمكننا استخدامه لتطبيق وظيفة الحروف الكبيرة على قائمة من السلاسل.

    Stream<String> names = Stream.of("aBc", "d", "ef");
    System.out.println(names.map(s -> {
    		return s.toUpperCase();
    	}).collect(Collectors.toList()));
    // يطبع [ABC, D, EF]
    
  3. مثال على sorted() للتيار: يمكننا استخدام sorted() لفرز عناصر التيار عن طريق تمرير مُعامِل المُقارَنة.

    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]
    
  4. مثال على flatMap() للتيار: يمكننا استخدام flatMap() لإنشاء تيار من تيار القوائم. لنرى مثالًا بسيطًا لتوضيح هذا الشك.

    Stream<List<String>> namesOriginalList = Stream.of(
    	Arrays.asList("Pankaj"), 
    	Arrays.asList("David", "Lisa"),
    	Arrays.asList("Amit"));
    // تفريغ التيار من List<String> إلى تيار String
    Stream<String> flatStream = namesOriginalList
    	.flatMap(strList -> strList.stream());
    
    flatStream.forEach(System.out::println);
    

عمليات مصطلح جافا النهائية

لنلقي نظرة على بعض أمثلة على عمليات مصطلح جافا النهائية في التيار.

  1. مثال على تيار 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("الضرب = "+intOptional.get()); //120
    
  2. مثال على تيار count(): يمكننا استخدام هذا العملية النهائية لحساب عدد العناصر في التيار.

    Stream<Integer> numbers1 = Stream.of(1,2,3,4,5);
    		
    System.out.println("عدد العناصر في التيار="+numbers1.count()); //5
    
  3. مثال forEach() للتيار: يمكن استخدام هذا للتكرار عبر التيار. يمكننا استخدام هذا بدلاً من المُدرج. دعنا نرى كيفية استخدامه لطباعة جميع عناصر التيار.

    Stream<Integer> numbers2 = Stream.of(1,2,3,4,5);
    numbers2.forEach(i -> System.out.print(i+",")); //1,2,3,4,5,
    
  4. أمثلة على match() للتيار: دعنا نرى بعض الأمثلة على طرق المطابقة في واجهة برمجة التيار.

    Stream<Integer> numbers3 = Stream.of(1,2,3,4,5);
    System.out.println("هل يحتوي التيار على 4؟ "+numbers3.anyMatch(i -> i==4));
    //هل يحتوي التيار على 4؟ صحيح
    
    Stream<Integer> numbers4 = Stream.of(1,2,3,4,5);
    System.out.println("هل يحتوي التيار على جميع العناصر أقل من 10؟ "+numbers4.allMatch(i -> i<10));
    //هل يحتوي التيار على جميع العناصر أقل من 10؟ صحيح
    
    Stream<Integer> numbers5 = Stream.of(1,2,3,4,5);
    System.out.println("هل لا يحتوي التيار على 10؟ "+numbers5.noneMatch(i -> i==10));
    //هل لا يحتوي التيار على 10؟ صحيح
    
  5. مثال على findFirst() في الجريان: هذه عملية نهائية للتشعب القصيرة، دعنا نرى كيف يمكننا استخدامها للعثور على أول سلسلة من الجريان تبدأ بـ 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
    }
    

قيود واجهة برمجة تطبيقات جافا 8 للجريان

تقدم واجهة برمجة تطبيقات جافا 8 للجريان الكثير من الأشياء الجديدة للعمل مع القوائم والمصفوفات، ولكن لديها بعض القيود أيضًا.

  1. تعبيرات لامبدا بدون حالة: إذا كنت تستخدم تيارًا متوازيًا وتعبيرات لامبدا تحتوي على حالة، فقد تؤدي إلى استجابات عشوائية. دعنا نرى ذلك ببرنامج بسيط. 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);   
    	}
    }
    

    إذا قمنا بتشغيل البرنامج أعلاه، ستحصل على نتائج مختلفة لأنه يعتمد على الطريقة التي يتم بها تكرار التيار وليس لدينا أي ترتيب محدد للمعالجة المتوازية. إذا استخدمنا تيارًا تسلسليًا، فلن يحدث هذا المشكل.

  2. بمجرد استهلاك التيار، لا يمكن استخدامه لاحقًا. كما يمكنك رؤية في الأمثلة أعلاه أنني في كل مرة أقوم بإنشاء تيار.

  3. هناك العديد من الأساليب في واجهة برمجة التطبيقات للتيار وأكثر الأجزاء الخلفية إثارة للالتباس هي الأساليب المتعددة الزائدة. يجعل ذلك منحنى التعلم يستغرق وقتًا طويلاً.

هذا كل شيء بالنسبة لبرنامج تعليمي مثالي لتيار Java 8. أتطلع إلى استخدام هذه الميزة وجعل الشفرة قابلة للقراءة بأداء أفضل من خلال المعالجة المتوازية. المرجع: Java Stream API Doc

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