Lazy Evaluation in Java Streams

العمليات اللي بتتم على العناصر في الـ Stream هتتم فقط في حالة الضرورة , وعادة ده بيحصل في العمليات النهائية اللي هي الـ Terminal Operations.
Lazy Evaluation in Java Streams
Lazy Evaluation in Java Streams

في هذه الصفحة

المقدمة

لو نفتكر مع بعض الـ Java Stream اصلًا ظهرت مع الـ Java 8 , ووقتها عملت طفرة في طريقة التعامل مع الـ Collections والبيانات في الـ Java. والـ Streams جت في الأساس كأداة قوية بتوفر منظور مختلف و High-Level شوية من خلال استعمال منهجية الـ Declarative في التعامل مع البيانات ومعالجتها.

وأحد أشهر وأقوى مميزات الـ Streams واللي كتير ما يعرفوش عنها هي الـ Lazy Evaluation. فخلونا قبل ما نتكلم عن الـ Lazy Evaluation نراجع مع بعض سريعا على الـ Java Streams.


Java Streams

الـ Streams في الـ Java بتوفر طريقة نقدر نعالج بيها مجموعة من البيانات سواء كان ده بشكل Sequential أو Parallel لانها بتقدملنا برضو مميزات اننا نعالج البيانات بشكل Parallel أي على التوازي ونستغل الـ CPU Cores في معالجة البيانات.

فالـ Stream Pipeline وخلونا نقول عليه هنا Pipeline لانه اشبه بـ Pipeline من العمليات أو الـ Operations اللي بتتم على البيانات واحدة تلو الأخرى فهو زي الـ Pipeline بالظبط , بيكون عندنا مصدر للبيانات دي وهو بالطبع الـ Collections أو مجموعة البيانات اللي محتاجين نعالجها.

بيتبع مصدر البيانات ده عمليات وسيطة بنقول عليها Intermediate Operations وعمليات نهائية بنقول عليها Terminal Operations.

وعشان منتوهش خلونا نوضح العمليات الوسيطة والنهائية دي مع بعض بشكل أوضح.

Intermediate Operations

العمليات دي بتكون زي الـ Filter والـ Map واللي من خلالها بنغير من العناصر اللي موجودة في الـ Stream لشكل تاني , والعمليات دي بنقول عليها Lazy Evaluated وهنعرف يعني ايه كمان شوية.

Terminal Operations

العمليات دي زي الـ forEach والـ collect ودي عمليات بتدي نتيجة نهائية , فبعد العملية النهائية دي الـ Stream خلاص مينفعش يتم استعماله لانه طلع نتيجة نهائية.


Lazy Evaluation

المصطلح ده من اسمه فهو شارح نفسه شوية , وده معناه ان العمليات اللي بتتم على العناصر في الـ Stream هتتم فقط في حالة الضرورة , وعادة ده بيحصل في العمليات النهائية اللي هي الـ Terminal Operations. والكلام ده طبعا عكس طبيعة الـ Eager Evaluation واللي فيها العمليات بتتم بشكل لحظي وفوري على البيانات.

💡
خلونا نبسط الموضوع أكتر , احنا دلوقتي لو عندنا Java Streams فالعمليات الوسيطة اللي هي الـ Intermediate Operations مش هتتم لغاية ما نوصل للـ Terminal Operations.

عاوزين نقرأ الجملة اللي فاتت دي تاني مع بعض ونركز فيها .. العمليات الوسيطة مش بتتم الا اما نوصل للعمليات النهائية , والكلام ده مهم في الواقع لانه بيحسن من الـ Performance خصوصا في التعامل مع الـ Datasets الكبيرة وده لانه بيقلل عدد الـ Iterations والحسابات اللي ممكن تتم على الـ Streams.

تعالوا نشوف مع بعض أمثلة عشان نفهم الموضوع أكتر ونشوف ازاي بيأثر في الـ Performance.


Example 1: Life Before Streams

في المثال ده احنا هنعمل List من الأعداد اللي هنعملها Generate , وبعدين هنطلع منهم الـ Even Numbers ونخزنهم عشان هنحتاج بعد كده نطلع منهم أول عدد Even , قد يبدوا المثال ساذج شوية , ولكن خلونا نكمل مع بعض , وهنفهم قوة الـ Streams هتفيدنا بايه

import java.util.ArrayList;
import java.util.List;

public class EagerEvaluation {
    public static void main(String[] args) {
        // Create a large list of numbers
        List<Integer> numbers = new ArrayList<>();
        for (int i = 1; i <= 1000000; i++) {
            numbers.add(i);
        }

        // Find all even numbers and store them in a new list
        List<Integer> evenNumbers = new ArrayList<>();
        for (Integer number : numbers) {
            if (number % 2 == 0) {
                evenNumbers.add(number);
            }
        }
        
        // Get the first even number from the list
        Integer firstEven = evenNumbers.get(0);
        System.out.println(firstEven);
    }
}

ده كده شكل الـ Code من غير استعمال الـ Streams طبعا مع الـ Large Datasets احنا متخيلين ومع وجود Logic Complex احنا بنحتاج عمليات Intermediary تتم وهنحتاج عشان نخزنهم Intermediate Data Structures واللي بالتالي كل ده هيتخزن في الـ Memory.

طب وجود الـ Streams هتفيد بايه ؟ خلونا نشوف.


Example 2: Life After Streams

import java.util.stream.IntStream;

public class LazyEvaluation {
    public static void main(String[] args) {
        // Create a large range of numbers
        IntStream numbers = IntStream.rangeClosed(1, 1000000);

        // Find the first even number using lazy evaluation
        int firstEven = numbers.filter(n -> n % 2 == 0)
                               .findFirst()
                               .orElseThrow(() -> new RuntimeException("No even number found"));

        System.out.println(firstEven);
    }
}

في المثال ده احنا استعملنا الـ Stream وخصوصا الـ IntStream عشان نعمل Generate للاعداد , ولكن تعالوا باه سوا نعيد مع بعض الكلام اللي ذكرناه فوق مع بعض.

الـ Stream هي Lazy Evaluated ومفيش أي عملية Intermediary بتتم الا اما بنعمل Terminal Operation فده معناه ايه باه في الكود ده ؟

ده معناه ان الـ Generating بتاع الأعداد مش هيتم يعني السطر ده مش هيروح يخزن في الـ Memory 1,000,000 ولكن ولكن لما يحتاج ليهم هيعملهم Generate on the Fly.

تاني حاجة الـ Filter Operation اللي بتشوف العدد زوجي ولا لا برضو مش هتتنفذ.

امال الكلام ده هيتنفذ امتة ؟ هيتنفذ مع أول الـ Terminal Operations تقابلني الا وهي الـ Find First وهنيجي ليها في آخر المقال لان دي بنقول عليها زي Short Circuit كده وليها وضح خاص شوية هنفهمه بعدين.

والعمليات النهائية اللي هي الـ Terminal زي ما قولنا شبه collect / forEach وما الى ذلك.

فكده أول ما يجي الجزء بتاع الـ findFirst هنروح نبدأ نـ Generate الاعداد لو تفتكروا اللي كانت في اول سطر! وهنروح نعمل ده On the fly يعني ايه ؟ يعني انا هبدأ بالـ 1 وهشوف هل هو Even ولا لا , وبعدين هروح للـ 2 وهكذا ومع أول شرط بيحقق الـ Filteration والـ FindFirst انا اصلا كده خلاص هكون خلصت!

لا احتاجت اخزن الـ 1,000,000 Record كلهم في الـ Memory , ولا احتجت اني اخزن في Data Structures وسيطة عشان اعمل عليها عمليات تانية في الـ Memory ولا اي حاجة , اكني بالظبط كنت عامل List من رقمين ورجعت اول رقم زوجي الي هو الـ 2.

فالـ Code Efficient جدًا مقارنة بالمثال اللي قبليه.


تقدروا دلوقتي تشتركوا في النشرة الأسبوعية لاقرأ-تِك بشكل مجاني تمامًا عشان يجيلكوا كل جديد بشكل أسبوعي فيما يخص مواضيع متنوعة وبشروحات بسيطة وسهلة وبجودة عالية 🚀

النشرة هيكون ليها شكل جديد ومختلف عن شكلها القديم وهنحاول انها تكون مميزة ومختلفة وخليط بين المحتوى الأساسي اللي بينزل ومفاجآت تانية كتير 🎉

Eqraatech Newsletter | Eqraatech - اقرأ-تِك | Substack
محتوى تقني متميز في مختلف مجالات هندسة البرمجيات باللغة العربية عن طريق تبسيط المفاهيم البرمجية المعقدة بشكل سلس وباستخدام صور توضيحية مذهلة. Click to read Eqraatech Newsletter, a Substack publication with hundreds of subscribers.

Example 3: Infinite Streams

تخيلوا معايا انك ممكن دلوقتي تعمل الـ Code الآتي , والـبرنامج ميضربش منك!

Stream.iterate(0, n -> n + 1)
      .filter(n -> n % 2 == 0)
      .limit(10)
      .forEach(System.out::println);

ده معناه ايه ؟ انك مش هتخزن عدد لا نهائي من الأرقام في الـ Memory ولكن , انت هتفضل تـ Generate لحد ما توصل لان معاك 10 أعداد زوجية بداية طبعا من الصفر لحد ما توصل لـ 10 أعداد!

لكم ان تتخيلوا الكلام ده لو هيتم من غير Streams كنا هنضطر نعمل ايه عشان نـ Optimize حاجة بالشكل ده , والـ Data Structures الوسيطة اللي هنحتاجها عشان نخزن فيها نتاج العمليات اللي بنعملها.

وطبعا الكلام ده Data Sets كبيرة بيكون حيوي جدًا ومهم!


Terminal Operations and Short-Circuiting

اتفقنا مع بعض وخلونا نكررها تاني أن الـ Lazy Evaluation بيفضل مكمل معانا , لحدا ما نوصل للـ Terminal Operations , وده اللي بيكون المنبه اللي بيعمل Trigger للـ Pipeline انه يشتغل.

ووقتها الـ JVM بيبدأ يعالج البيانات اللي موجودة في الـ Pipeline كالآتي:

  • الـ JVM هيبدأ يـ Iterate على مصدر البيانات
  • الـ JVM هينفذ العمليات الوسيطة اللي هي الـ Intermediate بالـ Order اللي انت محدده في الـ Pipeline واحدة ثم التانية
  • الـ JVM هينفذ العملية النهائية اللي انت محددها ويحسب النتيجة النهائية

فأنت ممكن تبني الـ Pipeline بتاعك , وفي النص تعمل كل اللي نفسك فيه , وكل ده لسه مش هيتنفذ دلوقتي وتيجي مثلا في آخر الـ Function تقول:

List<Integer> result = filteredStream.collect(Collectors.toList());

واللي بناء عليه هيبدأ باه هنا يـ Trigger الـ Pipeline اللي انت كنت انشأته من الأول.


كنا اتكلمنا عن الـ findFirst وعرفنا اننا بنسميها Short Circuiting ومن الاسم او المصطلح ده واضح جدًا ان العمليات دي بتوقف المعالجة اللي بتتم على البيانات علطول مجرد مالشرط يتحقق!

والعمليات دي زي findFirst و findAny و limit


مميزات الـ Lazy Evaluation

  • تحسين الـ Performance: وده لان واضح جدًا من خلال استعمالنا لـ Data Sets ضخمة , المعالجة على البيانات دي من خلال الـ Streams هيكون كفء جدًا وهيحسن من الطريقة التقليدية والقديمة واللي هتطلب Complex Logic عشان نوصل لكفاءة الـ Streams واللي بتعمله! وشوفنا مع بعض مثال على ده من خلال الـ Short Circuiting Operations واللي بتوقف شغل على باقي الـ Data Set طالما مش مهمة وبترجع النتيجة علطول!
  • تحسين الـ Memory: شوفنا مع بعض برضو ازاي الـ Lazy Evaluation بيحسن من الـ Memory من خلال الـ Infinite Stream واننا مبقناش محتاجين نخزن الـ Intermediary Results في الـ Memory
  • المرونة: وده من خلال انك سهل تبني Stream Pipeline ويكون Complex وبيعمل عمليات معقدة عن الأمثلة اللي ذكرناها في المقال

Real World Applications and Use Cases

بناء على تجربتي الشخصية واللي شوفت فيها قوة الـ Java Streams ظهر ده من خلال شغلي في الـ Data Processing والـ Web Services ازاي ؟

  • الـ Data Processing : الـ Lazy Evaluation مناسب جدًا جدًا في التعامل مع الـ Big Data , واللي بالطبع بيكون فيه Datasets ضخمة انها يحصلها Processing في الـ Memory , وهنلاقي حاجة زي Flink وهو Distributed Data Stream Processing في تعامله مع الـ Java Stream مناسب جدًا لانك ممكن تبني Pipelines قوية وEfficient بالشكل ده.
  • الـ Web Services: لو انت شغال مع Paginated Results من Web Service بتكلمها , فانت ممكن بقوة الـ Streams تطلب الـ Pages اللي انت عاوزها وكمان تستغل قوة الـ Parallel Stream في تحقيق ده وانك تبعت أكتر من Request لأكتر من Page على التوازي!

في الختام

معرفتنا بالـ Lazy Evaluation مهم واحنا بنبني Java Applications , وهيشجعنا اننا نتجه أكتر ناحية استعمال الـ Streams بعد كده , وهيعودنا اننا نكتب Efficient و Readable Code بدلا من تعقيدات كانت هتتم بدون استعماله.

اشترك الآن بنشرة اقرأ-تِك الأسبوعية

لا تدع أي شيء يفوتك. واحصل على أحدث المقالات المميزة مباشرة إلى بريدك الإلكتروني وبشكل مجاني!