مقدمة إلى لامدا
تعتبر تعبيرات Lambda ميزة جديدة مهمة في Java SE 8. تعبيرات Lambda تتيح لك استبدال واجهات وظيفية بالتعبيرات. يشبه تعبير Lambda الطريقة التي توفر قائمة معلمات عادية وجسم (الجسم ، والتي يمكن أن تكون تعبيرًا أو كتلة رمز) تستخدم هذه المعلمات.
تعبيرات Lambda تعزز أيضًا مكتبة التجميع. يضيف Java SE 8 حزمتين يعملان على تشغيل عمليات على بيانات التجميع: java.util.function package و java.util.stream package. يشبه الدفق المتكرر ، ولكن مع وجود العديد من الميزات الإضافية المرفقة. بشكل عام ، تعتبر تعبيرات Lambda والتيارات أكبر التغييرات لأن لغة Java تضيف الأدوية والشروح.
تعبيرات Lambda هي أساليب مجهولة المصدر بشكل أساسي ، ويتم تنفيذ طبقتها الأساسية من خلال توجيهات invokedynamic لتوليد فصول مجهولة. يوفر بناء بناء جملة وطريقة الكتابة ، مما يتيح لك استبدال واجهات وظيفية بالتعبيرات. في عيون بعض الأشخاص ، يمكن أن تجعل Lambda رمزك أكثر إيجازًا ولا تستخدمه على الإطلاق - هذا الرأي أمر جيد بالتأكيد ، ولكن الشيء المهم هو أن Lambda يجلب عمليات الإغلاق إلى Java. بفضل دعم Lamdba للمجموعات ، قام Lambda بتحسين الأداء بشكل كبير عند اجتياز المجموعات في ظل ظروف المعالج متعددة النواة. بالإضافة إلى ذلك ، يمكننا معالجة المجموعات في شكل تدفقات البيانات - وهو أمر جذاب للغاية.
جملة لامدا
بناء جملة Lambda بسيط للغاية ، على غرار الهيكل التالي:
(المعلمات) -> التعبير
أو
(المعلمات) -> {عبارات ؛ }تعبيرات Lambda تتكون من ثلاثة أجزاء:
1. المعلمات: قائمة المعلمات الرسمية في طرق مماثلة ، المعلمات هنا هي معلمات في الواجهة الوظيفية. يمكن إعلان أنواع المعلمات هنا بشكل صريح أو لم يتم الإعلان عنها ولكنها تم استنتاجها ضمنيًا بواسطة JVM. بالإضافة إلى ذلك ، عندما يكون هناك نوع استنتاج واحد فقط ، يمكن حذف الأقواس.
2. ->: يمكن فهمه على أنه "يتم استخدامه"
3. طريقة الجسم: يمكن أن يكون تعبيرًا أو كتلة رمز ، وهو تنفيذ الطريقة في الواجهة الوظيفية. يمكن أن تُرجع كتلة الكود قيمة أو لا شيء. كتلة التعليمات البرمجية هنا تعادل هيئة الطريقة للطريقة. إذا كان تعبيرًا ، فيمكنك أيضًا إرجاع قيمة أو إرجاع أي شيء.
دعونا نستخدم الأمثلة التالية لتوضيح:
// مثال 1: لا حاجة لقبول المعلمات ، إرجاع مباشرة 10 ()-> 10 // مثال 2: قبول معلمتين من نوع int وإرجاع مجموع هاتين المعلمتين (int x ، int y)-> x+y ؛ // مثال 2: قبول معلمتين من x و y ، يتم استنتاج نوع هذه المعلمة بواسطة jvm استنادًا إلى الاتجاه ، وإرجاعها 3: قبول سلسلة وطباعة السلسلة للتحكم ، دون عكس النتيجة (اسم السلسلة)-> system.out.println (name) ؛ // مثال 4: قبول اسم المعلمة نوع مستنتج وطبع السلسلة إلى اسم وحدة التحكم- الجنس)-> {system.out.println (name) ؛ system.out.println (sex)} // مثال 6: قبول معلمة x وإرجاع المعلمة x-> 2*xأين تستخدم Lambda
في [الواجهة الوظيفية] [1] نعلم أن النوع الهدف من تعبير Lambda هو واجهة وظيفية - يمكن لكل Lambda مطابقة نوع معين من خلال واجهة وظيفية محددة. لذلك ، يمكن تطبيق تعبير Lambda في أي مكان يطابق نوعه المستهدف. يجب أن يحتوي تعبير Lambda على نفس نوع المعلمة مثل وصف الوظيفة التجريدية للواجهة الوظيفية ، ويجب أن يكون نوع الإرجاع متوافقًا أيضًا مع نوع الإرجاع في الوظيفة التجريدية ، والاستثناءات التي يمكن أن ترميها تقتصر على نطاق وصف الوظيفة.
بعد ذلك ، دعونا نلقي نظرة على مثال واجهة وظيفية مخصصة:
محول واجهة functionalInterface <f ، t> {t convert (f from) ؛}أولاً ، استخدم الواجهة بالطريقة التقليدية:
Converter <string ، integer> converter = new Converter <string ، integer> () {Override public integer convert (string from) {return integer.valueof (from) ؛ }} ؛ نتيجة عدد صحيح = converter.convert ("200") ؛ System.out.println (نتيجة) ؛من الواضح أنه لا توجد مشكلة في هذا ، لذا فإن الشيء التالي هو اللحظة التي يأتي فيها Lambda في الحقل ، باستخدام Lambda لتنفيذ واجهة المحول:
المحول <string ، integer> converter = (param) -> integer.valueof (param) ؛ نتيجة عدد صحيح = converter.convert ("101") ؛ System.out.println (نتيجة) ؛من خلال المثال أعلاه ، أعتقد أن لديك فهمًا بسيطًا لاستخدام Lambda. أدناه ، نحن نستخدم تشغيل شائع الاستخدام لإظهار:
في الماضي ، ربما كتبنا هذا الرمز:
New Thread (new RunNable () {Override public void run () {system.out.println ("Hello lambda") ؛}}). start () ؛في بعض الحالات ، يمكن لعدد كبير من الفئات المجهولة أن تجعل الرمز يظهر. يمكنك الآن استخدام Lambda لجعله بسيطًا:
Thread Thread (() -> system.out.println ("Hello lambda")). start () ؛طريقة المرجع
مرجع الطريقة هي طريقة مبسطة لكتابة تعبيرات Lambda. الطريقة المشار إليها هي في الواقع تنفيذ طريقة تعبير Lambda ، وهيكل بناء الجملة هو:
ObjectRef :: methodname
يمكن أن يكون الجانب الأيسر اسم الفئة أو اسم المثيل ، والوسط هو رمز مرجع الطريقة "::" ، والجانب الأيمن هو اسم الطريقة المقابلة.
تنقسم مراجع الطريقة إلى ثلاث فئات:
1. مرجع الطريقة الثابتة
في بعض الحالات ، قد نكتب رمز مثل هذا:
الفئة العامة المرجعية {public static void main (string [] args) {converter <string ، integer> converter = new Converter <string ، integer> () {Override public integer convert (string from) {return ReferenceTest.String2int (from) ؛ }} ؛ converter.convert ("120") ؛ } functionalInterface Interface Converter <f ، t> {t convert (f from) ؛ } static int string2int (سلسلة من) {return integer.valueof (من) ؛ }}في هذا الوقت ، إذا كنت تستخدم المراجع الثابتة ، فسيكون الرمز أكثر إيجازًا:
المحول <string ، integer> converter = ReferenCetest :: string2int ؛ converter.convert ("120") ؛2. مرجع طريقة المثيل
قد نكتب أيضًا رمزًا مثل هذا:
الفئة العامة المرجعية {public static void main (string [] args) {converter <string ، integer> converter = new Converter <string ، integer> () {Override public integer convert (string from) {return new helper (). string2int (from) ؛ }} ؛ converter.convert ("120") ؛ } functionalInterface Interface Converter <f ، t> {t convert (f from) ؛ } static class helper {public int string2int (string from) {return integer.valueof (from) ؛ }}}أيضًا ، سيظهر استخدام أساليب مثال للإشارة أكثر إيجازًا:
المساعد المساعد = المساعد الجديد () ؛ المحول <string ، integer> converter = helper :: string2int ؛ converter.convert ("120") ؛3. مرجع طريقة المنشئ
الآن دعونا نوضح الإشارات إلى المُنشئين. أولاً ، نحدد حيوان فئة الوالدين:
فئة Animal {اسم السلسلة الخاصة ؛ عصر INT الخاص ؛ الحيوانات العامة (اسم السلسلة ، int age) {this.name = name ؛ this.age = العمر ؛ } السلوك الفراغ العام () {}} بعد ذلك ، نحن نحدد فئتين فرعيتين للحيوان: Dog、Bird
يمتد الطيور العامة للحيوان {public bird (اسم السلسلة ، int age) {super (name ، age) ؛ } Override public void proacion () {system.out.println ("fly") ؛ }} class class يمتد {public dog (اسم السلسلة ، int age) {super (name ، age) ؛ } Override public void proacion () {system.out.println ("run") ؛ }}ثم نحدد واجهة المصنع:
Interface Factory <t يمتد Animal> {t Create (اسم السلسلة ، int age) ؛ }بعد ذلك ، سوف نستخدم الطريقة التقليدية لإنشاء كائنات من فصول الكلاب والطيور:
Factory Factory = New Factory () {Override Public Animal Create (اسم السلسلة ، int Age) {return New Dog (name ، Age) ؛ }} ؛ المصنع. إنشاء ("الاسم المستعار" ، 3) ؛ Factory = New Factory () {Override Public Animal Create (اسم السلسلة ، int age) {return new bird (name ، age) ؛ }} ؛ المصنع. إنشاء ("Smook" ، 2) ؛كتبت أكثر من عشرة رموز فقط لإنشاء كائنين. الآن دعنا نحاول استخدام مرجع المنشئ:
مصنع <imanation> dogfactory = dog :: new ؛ كلب حيوان = dogfactory.create ("الاسم المستعار" ، 4) ؛ المصنع <Bird> BirdFactory = Bird :: New ؛ Bird Bird = Birdfactory.create ("Smook" ، 3) ؛ بهذه الطريقة يبدو الرمز نظيفًا وأنيقًا. عند استخدام Dog::new لاختراق الكائنات ، حدد وظيفة الإنشاء المقابلة من خلال توقيع وظيفة Factory.create .
مجال Lambda وقيود الوصول
المجال هو النطاق ، والمعلمات في قائمة المعلمات في تعبير Lambda صالحة في نطاق تعبير Lambda (المجال). في تعبير Lambda ، يمكن الوصول إلى المتغيرات الخارجية: المتغيرات المحلية ، ومتغيرات الفئة والمتغيرات الثابتة ، ولكن درجة قيود التشغيل مختلفة.
الوصول إلى المتغيرات المحلية
سيتم تجميع المتغيرات المحلية خارج تعبير Lambda ضمنيًا بواسطة JVM إلى النوع النهائي ، بحيث لا يمكن الوصول إليها إلا ولكن لم يتم تعديلها.
الفئة العامة المرجعية {public static void main (string [] args) {int n = 3 ؛ حساب حساب = param -> {// n = 10 ؛ ترجمة خطأ إرجاع n + param ؛ } ؛ calculate.calculate (10) ؛ } @unctionalInterface واجهة حساب {int calculate (int value) ؛ }}الوصول إلى المتغيرات الثابتة والأعضاء
داخل تعبيرات Lambda ، يمكن قراءة المتغيرات الثابتة والأعضاء.
الفئة العامة المرجعية {العدد العام = 1 ؛ int static int num = 2 ؛ اختبار void العام () {حساب حساب = param -> {num = 10 ؛ // تعديل عدد المتغير الثابت = 3 ؛ // تعديل متغير العضو n + param ؛ } ؛ calculate.calculate (10) ؛ } public static void main (string [] args) {} functionalInterface واجهة حساب {int calculate (int value) ؛ }}لا يمكن Lambda الوصول إلى الطريقة الافتراضية لواجهة الوظيفة
يعزز Java8 واجهات ، بما في ذلك الطرق الافتراضية التي يمكن أن تضيف تعريفات الكلمات الرئيسية الافتراضية إلى الواجهات. نحتاج إلى ملاحظة هنا أن الوصول إلى الأساليب الافتراضية لا يدعم داخليًا.
ممارسة Lambda
في قسم [الواجهة الوظيفية] [2] ، ذكرنا أن العديد من الواجهات الوظيفية مدمجة في حزمة java.util.function ، والآن سنشرح الواجهات الوظيفية الشائعة الاستخدام.
واجهة مسند
أدخل معلمة وإرجاع قيمة Boolean ، والتي تحتوي على العديد من الأساليب الافتراضية للحكم المنطقي:
test public void predictTest () {estericate <string> predict = (s) -> s.length ()> 0 ؛ اختبار منطقي = predict.test ("اختبار") ؛ System.out.println ("طول السلسلة أكبر من 0:" + اختبار) ؛ test = predict.test ("") ؛ System.out.println ("طول السلسلة أكبر من 0:" + اختبار) ؛ المسند <boung> pre = objects :: nonnull ؛ كائن ob = null ؛ اختبار = pre.test (ob) ؛ System.out.println ("الكائن غير فارغ:" + اختبار) ؛ OB = كائن جديد () ؛ اختبار = pre.test (ob) ؛ System.out.println ("الكائن غير فارغ:" + اختبار) ؛ }واجهة وظيفة
تلقي معلمة وإرجاع نتيجة واحدة. يمكن للطريقة الافتراضية ( andThen ) ربط وظائف متعددة معًا لتشكيل Funtion مركبة (مع الإدخال ، والإخراج).
test public void functionTest () {function <string ، integer> toInteger = integer :: valueof ؛ // يتم استخدام نتيجة التنفيذ لـ toInteger كمدخل إلى وظيفة backtoString الثانية <string ، string> backtoString = tointeger.andthen (string :: valueof) ؛ النتيجة سلسلة = backtoString.apply ("1234") ؛ System.out.println (نتيجة) ؛ الدالة <integer ، integer> add = (i) -> {system.out.println ("FRIST INPUT:" + I) ؛ العودة أنا * 2 ؛ } ؛ الدالة <integer ، integer> Zero = add.andthen ((i) -> {system.out.println ("الإدخال الثاني:" + i) ؛ return i * 0 ؛}) ؛ integer res = zero.apply (8) ؛ System.out.println (res) ؛ }واجهة المورد
إرجاع نتيجة نوع معين. على عكس Function ، لا يحتاج Supplier إلى قبول المعلمات (المورد ، مع الإخراج ولكن لا يوجد مدخلات)
Test public void suppliertest () {المورد <string> المورد = () -> "قيمة النوع الخاص" ؛ سلسلة s = المورد. system.out.println (s) ؛ }واجهة المستهلك
يمثل العمليات التي يجب تنفيذها على معلمة إدخال واحدة. على عكس Function ، لا يقوم Consumer بإرجاع القيمة (المستهلك ، المدخلات ، بدون إخراج)
test public void estemperest () {consumer <integer> add5 = (p) -> {system.out.println ("القيمة القديمة:" + p) ؛ P = P + 5 ؛ System.out.println ("القيمة الجديدة:" + P) ؛ } ؛ add5.accept (10) ؛ } يمثل استخدام الواجهات الأربعة أعلاه الأنواع الأربعة في حزمة java.util.function . بعد فهم هذه الواجهات الوظيفية الأربع ، سيكون من السهل فهم واجهات أخرى. الآن دعونا نجعل ملخصًا بسيطًا:
يتم استخدام Predicate للحكم المنطقي ، وتستخدم Function في الأماكن التي توجد فيها مدخلات ومخرجات ، ويتم استخدام Supplier في الأماكن التي لا توجد فيها مدخلات ومخرجات ، ويتم استخدام Consumer في الأماكن التي توجد فيها مدخلات ولا مخرجات. يمكنك معرفة سيناريوهات الاستخدام بناءً على معنى اسمها.
تدفق
يجلب Lambda الإغلاق لـ Java 8 ، وهو أمر مهم بشكل خاص في عمليات التجميع: Java 8 يدعم العمليات الوظيفية على دفق كائنات التجميع. بالإضافة إلى ذلك ، تم دمج API Stream أيضًا في API Collection ، مما يسمح بعمليات الدُفعات على كائنات التجميع.
دعنا نتعرف على الدفق.
يمثل الدفق دفق بيانات. ليس له بنية بيانات ولا يخزن العناصر نفسها. لن تغير عملياتها دفق المصدر ، ولكنها تولد دفقًا جديدًا. كواجهة لبيانات التشغيل ، فإنه يوفر التصفية والفرز والتعيين والتنظيم. تنقسم هذه الطرق إلى فئتين وفقًا لنوع الإرجاع: أي طريقة تُرجع نوع الدفق تسمى طريقة وسيطة (العملية الوسيطة) ، والباقي هي أساليب الانتهاء (التشغيل الكامل). تقوم طريقة الإكمال بإرجاع قيمة من نوع ما ، بينما تقوم الطريقة الوسيطة بإرجاع دفق جديد. عادة ما يتم ربط نداء الأساليب المتوسطة ، وستشكل العملية خط أنابيب. عندما يتم استدعاء الطريقة النهائية ، فإن ذلك سيؤدي إلى استهلاك القيمة فورًا من خط الأنابيب. هنا يجب أن نتذكر: يتم تشغيل عمليات الدفق كـ "تأخر" قدر الإمكان ، وهو ما نسميه غالبًا "عمليات كسول" ، مما سيساعد على تقليل استخدام الموارد وتحسين الأداء. لجميع العمليات الوسيطة (باستثناء الفرز) يتم تشغيلها في وضع التأخير.
لا يوفر دفق إمكانيات تشغيل بيانات قوية فحسب ، ولكن الأهم من ذلك ، يدعم الدفق كل من التسلسل والتوازي. يسمح التوازي بالتيار أن يكون أداء أفضل على المعالجات متعددة النواة.
عملية استخدام الدفق لها نمط ثابت:
1. إنشاء دفق
2. من خلال العمليات المتوسطة ، "تغيير" الدفق الأصلي وإنشاء دفق جديد
3. استخدم عملية الإنجاز لإنشاء النتيجة النهائية
إنه
إنشاء -> تغيير -> أكمل
إنشاء تيار
بالنسبة لمجموعة ، يمكن إنشاءها عن طريق استدعاء stream() أو parallelStream() . بالإضافة إلى ذلك ، يتم تنفيذ هاتين الطريقتين أيضًا في واجهة التجميع. بالنسبة للمصفوفات ، يمكن إنشاءها بواسطة الطريقة الثابتة of(T … values) . بالإضافة إلى ذلك ، يوفر المصفوفات أيضًا الدعم للتيارات.
بالإضافة إلى إنشاء تدفقات بناءً على المجموعات أو المصفوفات أعلاه ، يمكنك أيضًا إنشاء دفق فارغ من خلال Steam.empty() ، أو استخدام generate() لإنشاء تدفقات لا حصر لها.
دعنا نأخذ التيار التسلسلي كمثال لتوضيح العديد من أساليب الدفق المتوسطة الشائعة الاستخدام. قم أولاً بإنشاء مجموعة قائمة:
قائمة <String> قوائم = جديد ArrayList <Tring> () ؛ lists.add ("A1") ؛ lists.add ("A2") ؛ lists.add ("B1") ؛ lists.add ("B2") ؛ lists.add ("B3") ؛ lists.add ("O1") ؛طريقة وسيطة
فلتر
إلى جانب الواجهة المسند ، يقوم تصفية بتصفية جميع العناصر في كائن التدفق. هذه العملية هي عملية وسيطة ، مما يعني أنه يمكنك إجراء عمليات أخرى بناءً على النتيجة التي تم إرجاعها بواسطة العملية.
public static void StreamFilterTest () {lists.stream (). filter ((s -> s.StartSwith ("a"))). foreach (system.out :: println) ؛ // أي ما يعادل العملية أعلاه <String> esture = (s) -> s.StartSwith ("a") ؛ lists.stream (). filter (predicate). for (system.out :: println) ؛ // ترشيح مستمر مسند <Tring> estercate1 = (s -> s.endswith ("1")) ؛ lists.stream (). filter (predicate) .filter (estercate1). for (system.out :: println) ؛ }فرز (مرتبة)
إلى جانب واجهة المقارنة ، تقوم هذه العملية بإرجاع طريقة عرض للتيار المصنف ، ولن يتغير ترتيب الدفق الأصلي. يتم تحديد قواعد الترتيب من خلال المقارنة ، والافتراضي هو فرزها بالترتيب الطبيعي.
Public Static Void StreamSortedTest () {system.out.println ("المقارنة الافتراضية") ؛ lists.stream (). sored (). filter ((s -> s.StartSwith ("a"))). foreach (system.out :: println) ؛ System.out.println ("Custom Comparator") ؛ lists.stream (). sorted ((p1 ، p2) -> p2.compareto (p1)). filter ((s -> s.startswith ("a")). foreach (system.out :: println) ؛ }الخريطة (الخريطة)
إلى جانب واجهة Function ، يمكن لهذه العملية أن تقوم بتخطيط كل عنصر في كائن الدفق في عنصر آخر ، مع تحقيق تحويل نوع العنصر.
public static void StreamMaptest () {lists.stream (). map (string :: touppercase) .sorted ((a ، b) -> b.compareto (a)). foreach (system.out :: println) ؛ System.out.println ("قواعد رسم الخرائط المخصصة") ؛ الدالة <string ، string> function = (p) -> {return p + ".txt" ؛ } ؛ lists.stream (). map (string :: touppercase) .map (function) .sorted ((a ، b) -> b.compareto (a)). foreach (system.out :: println) ؛ }يقدم ما سبق بإيجاز ثلاث عمليات شائعة الاستخدام ، والتي تبسط بشكل كبير معالجة المجموعة. بعد ذلك ، نقدم عدة طرق لإكمال:
طريقة الانتهاء
بعد عملية "التحويل" ، يجب الحصول على النتيجة ، أي أن العملية قد اكتملت. دعونا نلقي نظرة على العمليات ذات الصلة أدناه:
مباراة
يستخدم لتحديد ما إذا كان predicate يطابق كائن الدفق ، وأخيراً بإرجاع نتيجة نوع Boolean ، على سبيل المثال:
public static void StreamMatchTest () {// return true طالما أن عنصرًا واحدًا في كائن الدفق يطابق منطقية anystartwitha = lists.stream (). anymatch ((s -> s.startswith ("a"))) ؛ System.out.println (anystartwitha) ؛ // إرجاع صحيح عندما يتطابق كل عنصر في كائن الدفق إلى allstartwitha = lists.stream (). allmatch ((s -> s.Startswith ("a"))) ؛ System.out.println (allstartwitha) ؛ }يجمع
بعد التحول ، نجمع عناصر التيار المحول ، مثل حفظ هذه العناصر في مجموعة. في هذا الوقت ، يمكننا استخدام طريقة التجميع التي توفرها Stream ، على سبيل المثال:
public static void StreamCollectTest () {list <string> list = lists.stream (). filter ((p) -> p.startswith ("a")). sorted (). collect (collectors.toList ()) ؛ system.out.println (list) ؛ }عدد
يتم استخدام عدد شبيهة SQL لحساب إجمالي عدد العناصر في الدفق ، على سبيل المثال:
public static void StreamCountTest () {long count = lists.stream (). filter ((s -> s.Startswith ("a"))). count () ؛ system.out.println (count) ؛ }يقلل
تتيح لنا طريقة reduce حساب العناصر بطريقتنا الخاصة أو ربط العناصر في دفق مع بعض الأنماط ، على سبيل المثال:
public static static void StreamReducetest () {اختياري <string> اختياري = lists.stream (). sored (). اختزال ((s1 ، s2) -> {system.out.println (s1 + "|" + s2) ؛ return s1 + "|" + s2 ؛}) ؛ }نتائج التنفيذ هي كما يلي:
a1 | a2a1 | a2 | b1a1 | a2 | b1 | b2a1 | a2 | b1 | b2 | b3a1 | a2 | b1 | b2 | b3 | o1
دفق متوازي مقابل الدفق التسلسلي
حتى الآن ، قدمنا العمليات الوسيطة والمكتملة شائعة الاستخدام. بالطبع تستند جميع الأمثلة على التيار التسلسلي. بعد ذلك ، سنقدم الدراما الرئيسية - تيار متوازي (تيار متوازي). يتم تنفيذ الدفق المتوازي استنادًا إلى إطار التحلل الموازي الشوكي ، ويقسم مجموعة البيانات الكبيرة إلى بيانات صغيرة متعددة وتسليمها إلى مؤشرات ترابط مختلفة للمعالجة. وبهذه الطريقة ، سيتم تحسين الأداء بشكل كبير في ظل وضع المعالجة متعددة النواة. وهذا يتفق مع مفهوم التصميم لـ MapReduce: تصبح المهام الكبيرة أصغر ، ويتم إعادة تعيين المهام الصغيرة إلى آلات مختلفة للتنفيذ. لكن المهمة الصغيرة هنا يتم تسليمها إلى معالجات مختلفة.
قم بإنشاء دفق موازٍ عبر parallelStream() . للتحقق مما إذا كانت التدفقات المتوازية يمكن أن تحسن الأداء حقًا ، نقوم بتنفيذ رمز الاختبار التالي:
أولا قم بإنشاء مجموعة أكبر:
قائمة <Tring> biglists = new ArrayList <> () ؛ لـ (int i = 0 ؛ i <10000000 ؛ i ++) {uuid uuid = uuid.randomuuid () ؛ biglists.add (uuid.toString ()) ؛ }اختبار الوقت للفرز تحت التدفقات التسلسلية:
private static void notparallElsTreamSortedTest (قائمة <String> biglists) {long startTime = system.nanotime () ؛ العد الطويل = biglists.stream (). sorted (). count () ؛ endtime طويل = system.nanotime () ؛ long millis = timeUnit.nanoseconds.tomillis (endtime - startTime) ؛ System.out.println (System.out.printf ("Serial Sort: ٪ D MS" ، Millis)) ؛ }اختبر الوقت لفرز التدفقات المتوازية:
Private Static void ParallelsTreamSortedTest (قائمة <String> biglists) {long startTime = system.nanotime () ؛ العد الطويل = biglists.parallelstream (). sorted (). count () ؛ endtime طويل = system.nanotime () ؛ long millis = timeUnit.nanoseconds.tomillis (endtime - startTime) ؛ System.out.println (System.out.printf ("ParalLelsorting: ٪ d MS" ، millis)) ؛ }النتائج كما يلي:
النوع التسلسلي: 13336 مللي ثانية
النوع الموازي: 6755 مللي ثانية
بعد رؤية هذا ، وجدنا أن الأداء قد تحسن بنحو 50 ٪. هل تعتقد أيضًا أنك ستتمكن من استخدام parallel Stream في المستقبل؟ في الواقع ، هذا ليس هو الحال. إذا كنت لا تزال معالجًا أحادي النواة الآن ولم يكن حجم البيانات كبيرًا ، فلا يزال التدفق التسلسلي خيارًا جيدًا. ستجد أيضًا أنه في بعض الحالات ، يكون أداء التدفقات التسلسلية أفضل. أما بالنسبة للاستخدام المحدد ، فأنت بحاجة إلى اختباره أولاً ثم تحديده وفقًا للسيناريو الفعلي.
عملية كسول
لقد تحدثنا أعلاه عن تشغيل الدفق في وقت متأخر قدر الإمكان ، وهنا نوضح ذلك من خلال إنشاء دفق غير محدود:
أولاً ، استخدم طريقة generate الدفق لإنشاء تسلسل أرقام طبيعي ، ثم تحويل الدفق عبر map :
// فئة التسلسل الإضافي ، يقوم NateSeq بتنفيذ المورد <Nds> {value long = 0 ؛ Override Public Get () {value ++ ؛ قيمة الإرجاع }} public void dreamcreateTest () {Stream <mong> Dream = Stream.generate (new nateSeq ()) ؛ system.out.println ("عدد العناصر:"+dream.map ((param) -> {return param ؛}). limit (1000) .Count ()) ؛ }نتيجة التنفيذ هي:
عدد العناصر: 1000
لقد وجدنا أنه في البداية ، فإن أي عمليات وسيطة (مثل filter,map وما إلى ذلك ، ولكن لا يمكن القيام sorted ) على ما يرام. أي أن عملية إجراء العمليات الوسيطة على الدفق والبقاء على قيد الحياة لا تتعلق على الفور (أو سيتم تشغيل عملية map في هذا المثال إلى الأبد ويتم حظرها) ، ويبدأ الدفق في الحساب عند مواجهة طريقة الإكمال. من خلال طريقة limit() ، قم بتحويل هذا الدفق اللانهائي إلى دفق محدود.
لخص
ما سبق هو جميع محتويات المقدمة السريعة لجافا لامدا. بعد قراءة هذا المقال ، هل لديك فهم أعمق لجافا لامدا؟ آمل أن يكون هذا المقال مفيدًا للجميع لتعلم Java Lambda.