لقد كنا ننتظر لفترة طويلة أن تقوم لامدا بإحضار مفهوم الإغلاق إلى Java، ولكن إذا لم نستخدمه في المجموعات، فإننا نفقد الكثير من القيمة. تم حل مشكلة ترحيل الواجهات الحالية إلى نمط لامدا من خلال الطرق الافتراضية. في هذه المقالة، سنحلل بعمق عملية البيانات المجمعة (العملية المجمعة) في مجموعات Java ونكشف سر أقوى دور لامدا.
1. حول JSR335
JSR هو اختصار لطلبات مواصفات Java، وهو ما يعني طلب مواصفات Java. التحسين الرئيسي لإصدار Java 8 هو مشروع Lambda (JSR 335)، الذي يهدف إلى تسهيل كتابة التعليمات البرمجية لـ Java للمعالجات متعددة النواة. JSR 335=تعبير لامدا + تحسين الواجهة (الطريقة الافتراضية) + تشغيل البيانات المجمعة. جنبًا إلى جنب مع المقالتين السابقتين، تعلمنا تمامًا المحتوى ذي الصلة بـ JSR335.
2. التكرار الخارجي مقابل التكرار الداخلي
في الماضي، لم تكن مجموعات Java قادرة على التعبير عن التكرار الداخلي، ولكنها قدمت فقط طريقة واحدة للتكرار الخارجي، وهي حلقة for أو while.
انسخ رمز الكود كما يلي:
قائمة الأشخاص = asList(new Person("Joe"), new Person("Jim"), new Person("John"));
لـ (الشخص ع : الأشخاص) {
p.setLastName("الظبية");
}
المثال أعلاه هو نهجنا السابق، وهو ما يسمى بالتكرار الخارجي، الحلقة عبارة عن حلقة تسلسل ثابتة. في عصر تعدد النواة اليوم، إذا أردنا إجراء حلقات بشكل متوازٍ، فيجب علينا تعديل الكود أعلاه. لا يزال من غير المؤكد مدى إمكانية تحسين الكفاءة، وسيجلب بعض المخاطر (مشكلات سلامة الخيط، وما إلى ذلك).
لوصف التكرار الداخلي، نحتاج إلى استخدام مكتبة فئة مثل Lambda، فلنعد كتابة الحلقة أعلاه باستخدام lambda وCollection.forEach.
انسخ الرمز كما يلي: الأشخاص.forEach(p->p.setLastName("Doe"));
الآن تتحكم مكتبة jdk في الحلقة، ولا نحتاج إلى الاهتمام بكيفية تعيين الاسم الأخير لكل كائن، يمكن للمكتبة أن تقرر كيفية القيام بذلك وفقًا لبيئة التشغيل، سواء كانت متوازية أو غير مرتبة أو كسولة تحميل. هذا هو التكرار الداخلي، ويقوم العميل بتمرير السلوك p.setLastName كبيانات إلى واجهة برمجة التطبيقات.
في الواقع، لا يرتبط التكرار الداخلي ارتباطًا وثيقًا بالعمليات المجمعة للمجموعات، فبمساعدته يمكننا أن نشعر بالتغييرات في التعبير النحوي. الشيء المثير للاهتمام حقًا فيما يتعلق بالعمليات المجمعة هو واجهة برمجة تطبيقات الدفق الجديدة. تمت إضافة حزمة java.util.stream الجديدة إلى JDK 8.
3.ستريم API
يمثل الدفق تدفق بيانات فقط ولا يحتوي على بنية بيانات، لذلك لم يعد من الممكن اجتيازه بعد اجتيازه مرة واحدة (يجب الانتباه إلى هذا عند البرمجة، على عكس التجميع، لا يزال هناك بيانات فيه بغض النظر عن عدد المرات يتم اجتيازه). يمكن أن يكون المصدر Collection أو array أو io وما إلى ذلك.
3.1 طرق الوسيطة ونقطة النهاية
يوفر الدفق واجهة لتشغيل البيانات الضخمة، مما يجعل عمليات البيانات أسهل وأسرع. لديها طرق مثل التصفية ورسم الخرائط وتقليل عدد عمليات الاجتياز. تنقسم هذه الطرق إلى نوعين: الطرق الوسيطة والأساليب الطرفية. يجب أن يكون تجريد "الدفق" مستمرًا بطبيعته نريد الحصول على النتيجة النهائية. إذا كان الأمر كذلك، فيجب استخدام عمليات نقطة النهاية لجمع النتائج النهائية التي ينتجها الدفق. الفرق بين هاتين الطريقتين هو النظر إلى قيمة الإرجاع الخاصة بها. إذا كانت دفقًا، فهي طريقة متوسطة، وإلا فهي طريقة نهائية. يرجى الرجوع إلى واجهة برمجة التطبيقات الخاصة بـ Stream للحصول على التفاصيل.
قدم بإيجاز عدة طرق وسيطة (مرشح، خريطة) وطرق نقطة النهاية (جمع، مجموع)
3.1.1 التصفية
يعد تنفيذ وظائف التصفية في تدفقات البيانات أكثر العمليات الطبيعية التي يمكن أن نفكر فيها. تعرض واجهة الدفق طريقة تصفية، والتي تقبل تنفيذ المسند الذي يمثل عملية لاستخدام تعبير لامدا الذي يحدد شروط التصفية.
انسخ رمز الكود كما يلي:
قائمة الأشخاص = ...
تيار الأشخاصOver18 = الأشخاص.stream().filter(p -> p.getAge() > 18);// تصفية الأشخاص الذين تزيد أعمارهم عن 18 عامًا
3.1.2 الخريطة
لنفترض أننا نقوم بتصفية بعض البيانات الآن، كما هو الحال عند تحويل الكائنات. تسمح لنا عملية الخريطة بتنفيذ تنفيذ دالة (تمثل T وR العامة للوظيفة<T, R> مدخلات التنفيذ ونتائج التنفيذ على التوالي)، والتي تقبل معلمات الإدخال وتعيدها. أولاً، دعونا نرى كيفية وصفها بأنها فئة داخلية مجهولة:
انسخ رمز الكود كما يلي:
تيار الكبار = الأشخاص
.تدفق()
.filter(p -> p.getAge() > 18)
خريطة (وظيفة جديدة () {
@تجاوز
ينطبق الكبار العام (شخص شخص) {
return new Adult(person);//تحويل شخص يزيد عمره عن 18 عامًا إلى شخص بالغ
}
});
الآن، قم بتحويل المثال أعلاه إلى تعبير لامدا:
انسخ رمز الكود كما يلي:
خريطة الدفق = الأشخاص. تيار ()
.filter(p -> p.getAge() > 18)
.map(person -> new Adult(person));
3.1.3العدد
طريقة العد هي طريقة نقطة النهاية للتدفق، والتي يمكنها إجراء الإحصائيات النهائية لنتائج الدفق وإرجاع عدد صحيح، على سبيل المثال، لنحسب العدد الإجمالي للأشخاص الذين تبلغ أعمارهم 18 عامًا أو أكثر:
انسخ رمز الكود كما يلي:
int countOfAdult=persons.stream()
.filter(p -> p.getAge() > 18)
.map(شخص -> شخص بالغ جديد)
.عدد()؛
3.1.4 جمع
تعد طريقة التجميع أيضًا طريقة نقطة النهاية للتدفق، والتي يمكنها جمع النتائج النهائية.
انسخ رمز الكود كما يلي:
قائمة الكبارList=people.stream()
.filter(p -> p.getAge() > 18)
.map(شخص -> شخص بالغ جديد)
.collect(Collectors.toList());
أو إذا أردنا استخدام فئة تنفيذ محددة لجمع النتائج:
انسخ رمز الكود كما يلي:
قائمة الكبارList = الأشخاص
.تدفق()
.filter(p -> p.getAge() > 18)
.map(شخص -> شخص بالغ جديد)
.collect(Collectors.toCollection(ArrayList::new));
نظرًا للمساحة المحدودة، لن يتم تقديم الطرق الوسيطة وطرق النهاية الأخرى واحدة تلو الأخرى. بعد قراءة الأمثلة المذكورة أعلاه، ما عليك سوى فهم الفرق بين هاتين الطريقتين، ويمكنك أن تقرر استخدامها وفقًا لاحتياجاتك. لاحقاً.
3.2 التدفق المتسلسل والتدفق الموازي
يحتوي كل دفق على وضعين: التنفيذ المتسلسل والتنفيذ المتوازي.
تدفق التسلسل:
انسخ رمز الكود كما يلي:
قائمة <Person> الأشخاص = list.getStream.collect(Collectors.toList());
تيارات موازية:
انسخ رمز الكود كما يلي:
قائمة <Person> الأشخاص = list.getStream.parallel().collect(Collectors.toList());
كما يوحي الاسم، عند استخدام الطريقة التسلسلية للاجتياز، تتم قراءة كل عنصر قبل قراءة العنصر التالي. عند استخدام الاجتياز المتوازي، سيتم تقسيم المصفوفة إلى أجزاء متعددة، تتم معالجة كل منها في مؤشر ترابط مختلف، ثم يتم إخراج النتائج معًا.
3.2.1 مبدأ التيار الموازي:
انسخ رمز الكود كما يلي:
List originalList = someData;
Split1 = originalList(0, mid);// قسّم البيانات إلى أجزاء صغيرة
Split2 = originalList(mid,end);
new Runnable(split1.process());// تنفيذ العمليات في أجزاء صغيرة
جديد Runnable(split2.process());
القائمة المنقحةList = Split1 + Split2;// دمج النتائج
3.2.2 مقارنة اختبارات الأداء المتتابعة والمتوازية
إذا كان الجهاز متعدد النواة، فمن الناحية النظرية، سيكون الدفق المتوازي أسرع بمرتين من الدفق المتسلسل. ما يلي هو رمز الاختبار
انسخ رمز الكود كما يلي:
long t0 = System.nanoTime();
// تهيئة دفق عدد صحيح بمدى 1 مليون وابحث عن رقم قابل للقسمة على 2. toArray() هي طريقة نقطة النهاية
int a[]=IntStream.range(0, 1_000_000).filter(p -> p % 2==0).toArray();
long t1 = System.nanoTime();
// نفس الوظيفة المذكورة أعلاه، هنا نستخدم الدفق الموازي للحساب
int b[]=IntStream.range(0, 1_000_000).parallel().filter(p -> p % 2==0).toArray();
long t2 = System.nanoTime();
// نتائج جهازي المحلي متسلسلة: 0.06 ثانية، متوازية 0.02 ثانية، مما يثبت أن التدفق الموازي أسرع بالفعل من التدفق المتسلسل.
System.out.printf ("التسلسل: %.2fs، بالتوازي %.2fs%n"، (t1 - t0) * 1e-9، (t2 - t1) * 1e-9)؛
3.3 حول إطار العمل/الانضمام
يتوفر توازي أجهزة التطبيق في Java 7. إحدى الميزات الجديدة لحزمة java.util.concurrent هي إطار التحلل المتوازي بأسلوب الشوكة، كما أنه قوي جدًا وفعال ويمكن للطلاب المهتمين دراسته انتقل إلى التفاصيل هنا. بالمقارنة مع Stream.parallel()، أفضل الخيار الأخير.
4. ملخص
بدون لامدا، سيكون استخدام Stream أمرًا صعبًا للغاية، وسيؤدي إلى إنشاء عدد كبير من الفئات الداخلية المجهولة، مثل مثال 3.1.2map أعلاه. إذا لم تكن هناك طريقة افتراضية، فستتسبب التغييرات في إطار عمل المجموعة حتمًا في الكثير من التغييرات. لذا فإن طريقة lambda + الافتراضية تجعل مكتبة jdk أكثر قوة ومرونة، والتحسينات في إطار البث والتجميع هي أفضل دليل.