الفصل الثاني: استخدام المجموعات
غالبًا ما نستخدم مجموعات وأرقام وسلاسل وكائنات مختلفة. إنها موجودة في كل مكان، وحتى إذا كان من الممكن تحسين الكود الذي يشغل المجموعة قليلاً، فإن ذلك سيجعل الكود أكثر وضوحًا. في هذا الفصل، نستكشف كيفية استخدام تعبيرات لامدا لمعالجة المجموعات. نستخدمها لاجتياز المجموعات، وتحويل المجموعات إلى مجموعات جديدة، وإزالة العناصر من المجموعات، ودمج المجموعات.
اجتياز القائمة
إن اجتياز القائمة هو عملية المجموعة الأساسية، وقد خضعت عملياتها لبعض التغييرات على مر السنين. ونستخدم مثالاً صغيراً على اجتياز الأسماء، لنقدمها من الإصدار الأقدم إلى الإصدار الأكثر أناقة اليوم.
يمكننا بسهولة إنشاء قائمة أسماء غير قابلة للتغيير باستخدام الكود التالي:
انسخ رمز الكود كما يلي:
القائمة النهائية <String> friends =
Arrays.asList("Brian"، "Nate"، "Neal"، "Raju"، "Sara"، "Scott");
System.out.println(friends.get(i));
}
فيما يلي الطريقة الأكثر شيوعًا لاجتياز القائمة وطباعتها، على الرغم من أنها أيضًا الأكثر عمومية:
انسخ رمز الكود كما يلي:
for(int i = 0; i < friends.size(); i++) {
System.out.println(friends.get(i));
}
أنا أسمي هذه الطريقة في الكتابة ماسوشية، فهي مطولة وعرضة للخطأ. علينا أن نتوقف ونفكر في الأمر، "هل هو i< أم i<=؟" هذا منطقي فقط عندما نحتاج إلى العمل على عنصر معين، ولكن حتى ذلك الحين، لا يزال بإمكاننا استخدام التعبيرات الوظيفية التي تلتزم بمبدأ عدم قابلية التغيير، وهو ما سنناقشه بعد قليل.
توفر Java أيضًا بنية متقدمة نسبيًا.
انسخ رمز الكود كما يلي:
Collections/fpij/Iteration.java
لـ (اسم السلسلة: الأصدقاء) {
System.out.println(name);
}
ضمن الغطاء، يتم تنفيذ التكرار بهذه الطريقة باستخدام واجهة Iterator، واستدعاء الطرق hasNext والتالي الخاصة به. كلتا الطريقتين عبارة عن تكرارات خارجية، وتجمع بين كيفية القيام بذلك وما تريد القيام به. نحن نتحكم بشكل واضح في التكرار، ونخبره أين يبدأ وأين ينتهي؛ الإصدار الثاني يفعل ذلك تحت الغطاء من خلال طريقة Iterator. ضمن العملية الصريحة، يمكنك أيضًا استخدام عبارات الاستراحة والمتابعة للتحكم في التكرار. الإصدار الثاني لديه بعض الأشياء المفقودة من الأول. هذا الأسلوب أفضل من الأول إذا لم نكن ننوي تعديل أحد عناصر المجموعة. ومع ذلك، كلتا الطريقتين ضروريتان ويجب التخلي عنهما في Java الحالية. هناك عدة أسباب للتغيير إلى النمط الوظيفي:
1. حلقة for نفسها تسلسلية ويصعب موازنتها.
2. هذه الحلقة غير متعددة الأشكال؛ ما تحصل عليه هو ما تطلبه. نقوم بتمرير المجموعة مباشرة إلى حلقة for، بدلاً من استدعاء طريقة (تدعم تعدد الأشكال) على المجموعة لإجراء عملية محددة.
3. من منظور التصميم، الكود المكتوب بهذه الطريقة ينتهك مبدأ "أخبر، لا تسأل". نطلب إجراء التكرار بدلاً من ترك التكرار للمكتبة الأساسية.
لقد حان الوقت للتبديل من البرمجة الحتمية القديمة إلى البرمجة الوظيفية الأكثر أناقة للمكررات الداخلية. بعد استخدام التكرارات الداخلية، نترك العديد من العمليات المحددة لمكتبة الطرق الأساسية للتنفيذ، حتى تتمكن من التركيز بشكل أكبر على متطلبات عمل محددة. ستكون الوظيفة الأساسية مسؤولة عن التكرار. نستخدم أولاً مكررًا داخليًا لتعداد قائمة الأسماء.
تم تحسين الواجهة القابلة للتكرار في JDK8 ولها اسم خاص يسمى forEach، والذي يتلقى معلمة من النوع Comsumer. كما يوحي الاسم، يستهلك مثيل المستهلك الكائن الذي تم تمريره إليه من خلال طريقة القبول الخاصة به. نستخدم صيغة الطبقة الداخلية المجهولة المألوفة لاستخدام التابع forEach :
انسخ رمز الكود كما يلي:
friends.forEach(new Consumer<String>() { public void Accept(final String name) {
System.out.println(name });
});
قمنا بتسمية التابع forEach في مجموعة الأصدقاء، وتمريره إلى تطبيق مجهول للمستهلك. تستدعي طريقة forEach هذه طريقة القبول التي تم تمريرها في المستهلك لكل عنصر في المجموعة، مما يسمح لها بمعالجة هذا العنصر. في هذا المثال نقوم فقط بطباعة قيمته، وهو الاسم. دعونا نلقي نظرة على مخرجات هذا الإصدار، وهو نفس الإصدارين السابقين:
انسخ رمز الكود كما يلي:
بريان
نيت
نيل
راجو
سارة
سكوت
لقد قمنا بتغيير شيء واحد فقط: لقد تخلصنا من حلقة for القديمة واستخدمنا مكررًا داخليًا جديدًا. الميزة هي أننا لا نحتاج إلى تحديد كيفية تكرار المجموعة ويمكننا التركيز بشكل أكبر على كيفية معالجة كل عنصر. العيب هو أن الكود يبدو أكثر تفصيلاً - وهو ما يقتل تقريبًا متعة أسلوب الترميز الجديد. لحسن الحظ، من السهل تغيير هذا، وهنا يأتي دور قوة تعبيرات لامدا والمترجمين الجدد. لنقم بإجراء تعديل آخر واستبدال الفئة الداخلية المجهولة بتعبير لامدا.
انسخ رمز الكود كما يلي:
friends.forEach((اسم السلسلة النهائي) -> System.out.println(name));
يبدو أفضل بكثير بهذه الطريقة. هناك تعليمات برمجية أقل، ولكن دعونا نلقي نظرة أولاً على ما يعنيه ذلك. طريقة forEach هي وظيفة ذات ترتيب أعلى تتلقى تعبير لامدا أو كتلة تعليمات برمجية للعمل على العناصر الموجودة في القائمة. في كل استدعاء، سيتم ربط العناصر الموجودة في المجموعة بمتغير الاسم. تستضيف المكتبة الأساسية نشاط استدعاء تعبير لامدا. ويمكنه أن يقرر تأخير تنفيذ التعبيرات، وإجراء حسابات متوازية إذا كان ذلك مناسبًا. إخراج هذا الإصدار هو أيضًا نفس الإصدار السابق.
انسخ رمز الكود كما يلي:
بريان
نيت
نيل
راجو
سارة
سكوت
إصدار المكرر الداخلي أكثر إيجازًا. علاوة على ذلك، باستخدامه، يمكننا التركيز أكثر على معالجة كل عنصر بدلاً من اجتيازه - وهذا تصريحي.
ومع ذلك، هذا الإصدار لديه عيوب. بمجرد بدء تنفيذ طريقة forEach، على عكس الإصدارين الآخرين، لا يمكننا الخروج من هذا التكرار. (بالطبع هناك طرق أخرى للقيام بذلك). ولذلك، يتم استخدام طريقة الكتابة هذه بشكل أكثر شيوعًا عندما يحتاج كل عنصر في المجموعة إلى المعالجة. سنقدم لاحقًا بعض الوظائف الأخرى التي تسمح لنا بالتحكم في عملية الحلقة.
بناء الجملة القياسي لتعبيرات لامدا هو وضع المعلمات داخل ()، وتوفير معلومات النوع واستخدام الفواصل لفصل المعلمات. من أجل تحريرنا، يمكن لمترجم Java أيضًا إجراء خصم النوع تلقائيًا. بالطبع، من الملائم عدم كتابة الأنواع، فالعمل أقل والعالم أكثر هدوءًا. وفيما يلي الإصدار السابق بعد إزالة نوع المعلمة:
انسخ رمز الكود كما يلي:
friends.forEach((name) -> System.out.println(name));
في هذا المثال، يعرف مترجم Java أن نوع الاسم هو سلسلة من خلال تحليل السياق. فهو ينظر إلى توقيع الطريقة المسماة forEach ثم يقوم بتحليل الواجهة الوظيفية في المعلمات. ثم سيقوم بتحليل الطريقة المجردة في هذه الواجهة والتحقق من عدد ونوع المعلمات. حتى إذا تلقى تعبير لامدا معلمات متعددة، فلا يزال بإمكاننا إجراء خصم النوع، ولكن في هذه الحالة لا يمكن أن تحتوي جميع المعلمات على أنواع معلمات؛ في تعبيرات لامدا، يجب كتابة أنواع المعلمات على الإطلاق، أو إذا كانت مكتوبة، فيجب كتابتها بالكامل.
يتعامل مترجم Java مع تعبيرات lambda بمعلمة واحدة بشكل خاص: إذا كنت تريد إجراء استنتاج النوع، فيمكن حذف الأقواس حول المعلمة.
انسخ رمز الكود كما يلي:
friends.forEach(name -> System.out.println(name));
هناك تحذير صغير هنا: المعلمات المستخدمة لاستدلال النوع ليست من النوع النهائي. في المثال السابق للإعلان بشكل صريح عن نوع ما، قمنا أيضًا بوضع علامة على المعلمة على أنها نهائية. يمنعك هذا من تغيير قيمة المعلمة في تعبير لامدا. بشكل عام، يعد تعديل قيمة المعلمة عادة سيئة، مما قد يتسبب بسهولة في حدوث خطأ، لذلك من الجيد وضع علامة عليها على أنها نهائية. لسوء الحظ، إذا أردنا استخدام استنتاج النوع، فيجب علينا اتباع القواعد بأنفسنا وعدم تعديل المعلمات، لأن المترجم لم يعد يحمينا.
لقد استغرق الأمر الكثير من الجهد للوصول إلى هذه النقطة، ولكن الآن أصبح حجم التعليمات البرمجية أصغر قليلاً بالفعل. ولكن هذا ليس هو الأبسط حتى الآن. دعونا نجرب هذا الإصدار البسيط الأخير.
انسخ رمز الكود كما يلي:
friends.forEach(System.out::println);
في الكود أعلاه نستخدم مرجع الطريقة. يمكننا استبدال الكود بالكامل مباشرة باسم الطريقة. سنستكشف هذا بعمق في القسم التالي، ولكن الآن دعونا نتذكر مقولة شهيرة لأنطوان دو سانت إكزوبيري: الكمال ليس ما يمكن إضافته، ولكن ما لم يعد من الممكن إزالته.
تسمح لنا تعبيرات Lambda باجتياز المجموعات بإيجاز ووضوح. في القسم التالي، سنتحدث عن كيف يمكننا من كتابة مثل هذه التعليمات البرمجية الموجزة عند إجراء عمليات الحذف وتحويلات المجموعة.