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

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

واجهات الوظائف في جافا 8

إذا نظرنا إلى بعض لغات البرمجة الأخرى مثل C++، JavaScript؛ يُطلق عليها لغات برمجة وظيفية لأننا يمكننا كتابة الوظائف واستخدامها عند الحاجة. تدعم بعض هذه اللغات البرمجة الكائنية بالإضافة إلى البرمجة الوظيفية. أن تكون موجهًا نحو الكائنات ليس سيئًا، ولكنه يجلب الكثير من الكلام الزائد إلى البرنامج. على سبيل المثال، دعونا نقول إنه يتعين علينا إنشاء مثيل من Runnable. عادةً ما نفعل ذلك باستخدام فئات مجهولة الهوية مثلما هو موضح أدناه.

Runnable r = new Runnable(){
			@Override
			public void run() {
				System.out.println("My Runnable");
			}};

إذا نظرنا إلى الكود أعلاه، فإن الجزء الفعلي الذي يتم استخدامه هو الكود داخل طريقة run(). بينما الجزء الآخر من الكود هو بسبب كيفية هيكلة برامج جافا. واجهات الوظائف في جافا 8 وتعبيرات لامبدا يساعدنا في كتابة كود أصغر وأنظف عن طريق إزالة الكثير من الكود القياسي.

واجهة Java 8 الوظيفية

الواجهة التي تحتوي على دالة واحدة تسمى واجهة وظيفية. تمت إضافة التعليق @FunctionalInterface بحيث يمكننا تمييز الواجهة كواجهة وظيفية. ليس من الضروري استخدامها ، لكن من الممارسات المثلى استخدامها مع الواجهات الوظيفية لتجنب إضافة طرق إضافية عن طريق الخطأ. إذا تم تعليم الواجهة بالتعليق @FunctionalInterface وحاولنا إضافة أكثر من طريقة واحدة ، فإنه يُطلق خطأ المترجم. الفائدة الرئيسية لواجهات جافا 8 الوظيفية هي أنه يمكننا استخدام تعبيرات لامبدا لتعيينها وتجنب استخدام التنفيذ الضخم لفئة مجهولة الاسم. تمت إعادة كتابة واجهة مجموعات جافا 8 API وتم إدخال واجهة Stream API الجديدة التي تستخدم العديد من الواجهات الوظيفية. قامت جافا 8 بتعريف العديد من الواجهات الوظيفية في الحزمة java.util.function. بعض الواجهات الوظيفية الجافا 8 المفيدة هي Consumer, Supplier, Function و Predicate. يمكنك العثور على مزيد من التفاصيل حولها في مثال جافا 8 Stream. java.lang.Runnable هو مثال رائع على واجهة وظيفية تحتوي على دالة واحدة تسمى run(). الكود أدناه يقدم بعض التوجيهات للواجهات الوظيفية:

interface Foo { boolean equals(Object obj); }
// غير صالح بسبب أن equals بالفعل عضو ضمني (فئة Object)


interface Comparator {
 boolean equals(Object obj);
 int compare(T o1, T o2);
}
// قابل للاستخدام لأن Comparator لديها فقط طريقة واحدة غير مجردة غير متعلقة بالكائنات


interface Foo {
  int m();
  Object clone();
}
// غير صالح بسبب أن الطريقة Object.clone غير عامة


interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// قابل للاستخدام: طريقتان، ولكن لديهما نفس التوقيع


interface X { Iterable m(Iterable arg); }
interface Y { Iterable m(Iterable arg); }
interface Z extends X, Y {}
// قابل للاستخدام: Y.m هو توقيع فرعي وقابل للاستبدال في النوع العائد


interface X { int m(Iterable arg); }
interface Y { int m(Iterable arg); }
interface Z extends X, Y {}
// غير قابل للاستخدام: لا توجد طريقة لها توقيع فرعي لجميع الطرق الغير مجردة


interface X { int m(Iterable arg, Class c); }
interface Y { int m(Iterable arg, Class c); }
interface Z extends X, Y {}
// غير قابل للاستخدام: لا توجد طريقة لها توقيع فرعي لجميع الطرق الغير مجردة


interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// خطأ في المترجم: لا توجد طريقة قابلة للاستبدال في النوع العائد


interface Foo { void m(T arg); }
interface Bar { void m(T arg); }
interface FooBar extends Foo, Bar {}
// خطأ في المترجم: تواقيع مختلفة، ولكن نفس المسح

تعبير لامبدا

تعبيرات لامبدا هي الطريقة التي من خلالها يمكننا تصور البرمجة الوظيفية في عالم البرمجة الموجهة للكائنات في جافا. الكائنات هي أساس لغة برمجة جافا ولا يمكننا أبدًا الحصول على وظيفة بدون كائن، ولهذا السبب توفر لغة جافا دعمًا لاستخدام تعبيرات لامبدا فقط مع الواجهات الوظيفية. نظرًا لوجود وظيفة مجردة واحدة فقط في الواجهات الوظيفية، لا يوجد ارتباك في تطبيق التعبير لامبدا على الطريقة. تتبع تركيبة تعبيرات لامبدا هذه: (المعامل) -> (الجسم). الآن دعونا نرى كيف يمكننا كتابة Runnable المجهول أعلاه باستخدام تعبير لامبدا.

Runnable r1 = () -> System.out.println("My Runnable");

لنحاول فهم ما يحدث في تعبير لامبدا أعلاه.

  • Runnable هو واجهة وظيفية، ولهذا السبب يمكننا استخدام تعبير lambda لإنشاء مثيل لها.
  • نظرًا لأن الطريقة run() لا تأخذ أي وسيطات، فإن تعبيرنا lambda ليس لديه وسيطات أيضًا.
  • تمامًا مثل كتل if-else، يمكننا تجنب الأقواس المنحنية ({}) لأن لدينا عبارة واحدة في جسم الطريقة. بالنسبة للعبارات المتعددة، سيتعين علينا استخدام الأقواس المنحنية مثل أي طرق أخرى.

لماذا نحتاج إلى تعبير Lambda

  1. تقليل عدد الأسطر واحدة من الفوائد الواضحة لاستخدام تعبير lambda هي أن كمية الشفرة تقل، لقد رأينا بالفعل كيف يمكننا بسهولة إنشاء مثيل لواجهة وظيفية باستخدام تعبير lambda بدلاً من استخدام فئة مجهولة.

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

    //النهج التقليدي
    private static boolean isPrime(int number) {		
    	if(number < 2) return false;
    	for(int i=2; i<number; i++){
    		if(number % i == 0) return false;
    	}
    	return true;
    }
    

    المشكلة في الكود أعلاه هي أنه تسلسلي الطابع، إذا كان الرقم ضخمًا جدًا فسيستغرق وقتًا كبيرًا. مشكلة أخرى في الكود هي وجود العديد من نقاط الخروج وأنه غير قابل للقراءة. دعونا نرى كيف يمكننا كتابة نفس الطريقة باستخدام تعابير lambda وواجهة برمجة التطبيقات Stream.

    //النهج الاستعلامي
    private static boolean isPrime(int number) {		
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					index -> number % index == 0);
    }
    

    IntStream هو سلسلة من العناصر الأساسية int تدعم العمليات الكلية التسلسلية والموازية. هذا هو التخصيص الأساسي للنوع int لـ Stream. لمزيد من القراءة، يمكننا أيضًا كتابة الطريقة كما هو موضح أدناه بشكل أكثر وضوحًا.

    private static boolean isPrime(int number) {
    	IntPredicate isDivisible = index -> number % index == 0;
    	
    	return number > 1
    			&& IntStream.range(2, number).noneMatch(
    					isDivisible);
    }
    

    إذا لم تكن على دراية بـ IntStream، فإن طريقته range() تُرجع IntStream مرتبًا تسلسليًا من startInclusive (شاملاً) إلى endExclusive (غير شامل) بخطوة تزايدية تبلغ 1. طريقة noneMatch() تُرجع ما إذا كانت لا توجد عناصر في هذا التيار تطابق الشرط المُقدم. قد لا تقوم بتقييم الشرط على جميع العناصر إذا لم يكن ذلك ضروريًا لتحديد النتيجة.

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

    public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) {
    	    return numbers.parallelStream()
    	    		.filter(predicate)
    	    		.mapToInt(i -> i)
    	    		.sum();
    	}
    

    استخدام عينة:

    //مجموع جميع الأرقام
    sumWithCondition(numbers, n -> true)
    //مجموع جميع الأرقام الزوجية
    sumWithCondition(numbers, i -> i%2==0)
    //مجموع جميع الأرقام أكبر من 5
    sumWithCondition(numbers, i -> i>5)
    
  4. كفاءة أعلى مع الكسل ميزة أخرى لاستخدام تعبيرات اللامبدا هي التقييم الكسلي. على سبيل المثال، دعونا نفترض أننا بحاجة إلى كتابة طريقة للعثور على أكبر عدد فردي في النطاق من 3 إلى 11 وإرجاع مربعه. عادةً ما نكتب الكود لهذه الطريقة مثل هذا:

    private static int findSquareOfMaxOdd(List<Integer> numbers) {
    		int max = 0;
    		for (int i : numbers) {
    			if (i % 2 != 0 && i > 3 && i < 11 && i > max) {
    				max = i;
    			}
    		}
    		return max * max;
    	}
    

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

    public static int findSquareOfMaxOdd(List<Integer> numbers) {
    		return numbers.stream()
    				.filter(NumberTest::isOdd) 		//Predicate is functional interface and
    				.filter(NumberTest::isGreaterThan3)	// we are using lambdas to initialize it
    				.filter(NumberTest::isLessThan11)	// rather than anonymous inner classes
    				.max(Comparator.naturalOrder())
    				.map(i -> i * i)
    				.get();
    	}
    
    	public static boolean isOdd(int i) {
    		return i % 2 != 0;
    	}
    	
    	public static boolean isGreaterThan3(int i){
    		return i > 3;
    	}
    	
    	public static boolean isLessThan11(int i){
    		return i < 11;
    	}
    

    إذا كنت متفاجئًا من العامل (::)، فقد تم إدخاله في جافا 8 ويستخدم لمراجعة الطرق. يهتم مترجم جافا برسم خريطة الوسيطات إلى الطريقة المستدعاة. إنها النموذج المختصر لتعبيرات اللامبدا i -> isGreaterThan3(i) أو i -> NumberTest.isGreaterThan3(i).

أمثلة على التعبيرات اللامبدأة

أدناه، أقدم بعض مقتطفات الشفرة للتعبيرات اللامبدأة مع تعليقات صغيرة توضحها.

() -> {}                     // No parameters; void result

() -> 42                     // No parameters, expression body
() -> null                   // No parameters, expression body
() -> { return 42; }         // No parameters, block body with return
() -> { System.gc(); }       // No parameters, void block body


// جسم معقد مع عودات متعددة

() -> {
  if (true) return 10;
  else {
    int result = 15;
    for (int i = 1; i < 10; i++)
      result *= i;
    return result;
  }
}                          

(int x) -> x+1             // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1                 // Single inferred-type argument, same as below
x -> x+1                   // Parenthesis optional for single inferred-type case

(String s) -> s.length()   // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length()              // Single inferred-type argument
t -> { t.start(); }          // Single inferred-type argument

(int x, int y) -> x+y      // Multiple declared-type parameters
(x,y) -> x+y               // Multiple inferred-type parameters
(x, final y) -> x+y        // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y          // Illegal: can't mix inferred and declared types

إشارات الطرق والبناة

A method reference is used to refer to a method without invoking it; a constructor reference is similarly used to refer to a constructor without creating a new instance of the named class or array type. Examples of method and constructor references:

System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new

هذا كل شيء بالنسبة لتعلم واجهات الوظائف الوظيفية وتعبيرات لامبدا في جافا 8. أوصي بشدة بالبدء في استخدامها لأن هذه الصيغة جديدة في جافا وستحتاج بعض الوقت لفهمها. يجب أيضًا عليك التحقق من ميزات جافا 8 لتعلم جميع التحسينات والتغييرات في إصدار جافا 8.

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