JavaRush /مدونة جافا /Random-AR /واجهات وظيفية في جافا

واجهات وظيفية في جافا

نشرت في المجموعة
مرحبًا! في مهمة Java Syntax Pro، درسنا تعبيرات lambda وقلنا إنها ليست أكثر من مجرد تطبيق لطريقة وظيفية من واجهة وظيفية. بمعنى آخر، هذا هو تنفيذ بعض الفئات المجهولة (غير المعروفة)، وطريقتها غير المحققة. وإذا بحثنا في محاضرات الدورة في التلاعب بتعبيرات لامدا، فسننظر الآن، إذا جاز التعبير، في الجانب الآخر: أي هذه الواجهات ذاتها. واجهات وظيفية في جافا - 1قدم الإصدار الثامن من Java مفهوم الواجهات الوظيفية . ما هذا؟ تعتبر الواجهة التي تحتوي على طريقة واحدة (مجردة) غير منفذة وظيفية. وتندرج العديد من الواجهات المبتكرة تحت هذا التعريف، مثل الواجهة التي تمت مناقشتها مسبقًا Comparator. وأيضا الواجهات التي نقوم بإنشائها بأنفسنا مثل:
@FunctionalInterface
public interface Converter<T, N> {
   N convert(T t);
}
لدينا واجهة تتمثل مهمتها في تحويل كائنات من نوع ما إلى كائنات من نوع آخر (نوع من المحول). التعليق التوضيحي @FunctionalInterfaceليس شيئًا معقدًا أو مهمًا جدًا، حيث أن الغرض منه هو إخبار المترجم أن هذه الواجهة وظيفية ويجب ألا تحتوي على أكثر من طريقة واحدة. إذا كانت الواجهة التي تحتوي على هذا التعليق التوضيحي تحتوي على أكثر من طريقة (مجردة) غير منفذة، فلن يتخطى المترجم هذه الواجهة، لأنه سيعتبرها رمزًا خاطئًا. يمكن اعتبار الواجهات التي لا تحتوي على هذا التعليق التوضيحي وظيفية وستعمل، ولكن @FunctionalInterfaceهذا ليس أكثر من تأمين إضافي. دعونا نعود إلى الصف Comparator. إذا نظرت إلى الكود الخاص به (أو الوثائق )، يمكنك أن ترى أن لديه أكثر من طريقة واحدة. ثم تسأل: كيف إذن يمكن اعتبارها واجهة وظيفية؟ يمكن أن تحتوي الواجهات المجردة على طرق لا تقع ضمن نطاق طريقة واحدة:
  • ثابتة
يشير مفهوم الواجهات إلى أن وحدة معينة من التعليمات البرمجية لا يمكن أن تحتوي على أي طرق يتم تنفيذها. ولكن بدءًا من Java 8، أصبح من الممكن استخدام الأساليب الثابتة والافتراضية في الواجهات. ترتبط الأساليب الثابتة مباشرة بفئة معينة ولا تتطلب كائنًا محددًا من تلك الفئة لاستدعاء مثل هذه الطريقة. أي أن هذه الأساليب تتناسب بشكل متناغم مع مفهوم الواجهات. على سبيل المثال، دعونا نضيف طريقة ثابتة للتحقق من وجود كائن خاليًا في الفئة السابقة:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }
}
بعد تلقي هذه الطريقة، لم يشتكي المترجم، مما يعني أن واجهتنا لا تزال تعمل.
  • الأساليب الافتراضية
قبل Java 8، إذا كنا بحاجة إلى إنشاء طريقة في واجهة ورثتها فئات أخرى، فيمكننا فقط إنشاء طريقة مجردة تم تنفيذها في كل فئة محددة. ولكن ماذا لو كانت هذه الطريقة هي نفسها لجميع الفئات؟ في هذه الحالة، تم استخدام الفئات المجردة في أغلب الأحيان . ولكن بدءًا من Java 8، هناك خيار لاستخدام الواجهات ذات الأساليب المنفذة - الطرق الافتراضية. عند وراثة واجهة ما، يمكنك تجاوز هذه الأساليب أو ترك كل شيء كما هو (اترك المنطق الافتراضي). عند إنشاء طريقة افتراضية، يجب علينا إضافة الكلمة الأساسية - default:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }

   default void writeToConsole(T t) {
       System.out.println("Текущий an object - " + t.toString());
   }
}
مرة أخرى، نرى أن المترجم لم يبدأ في الشكوى، ولم نتجاوز حدود الواجهة الوظيفية.
  • أساليب فئة الكائن
تحدثنا في محاضرة مقارنة الكائناتObject عن حقيقة أن جميع الفئات ترث من الفصل . هذا لا ينطبق على الواجهات. ولكن إذا كان لدينا طريقة مجردة في الواجهة تطابق التوقيع مع بعض طرق الفئة Object، فإن مثل هذه الطريقة (أو الطرق) لن تكسر قيود الواجهة الوظيفية لدينا:
@FunctionalInterface
public interface Converter<T, N> {

   N convert(T t);

   static <T> boolean isNotNull(T t){
       return t != null;
   }

   default void writeToConsole(T t) {
       System.out.println("Текущий an object - " + t.toString());
   }

   boolean equals(Object obj);
}
ومرة أخرى، لا يشكو مترجمنا، لذلك Converterلا تزال الواجهة تعتبر وظيفية. والسؤال المطروح الآن هو: لماذا يتعين علينا أن نقتصر على طريقة واحدة غير منفذة في الواجهة الوظيفية؟ وبعد ذلك حتى نتمكن من تنفيذه باستخدام لامدا. دعونا ننظر إلى هذا مع مثال Converter. للقيام بذلك، دعونا إنشاء فئة Dog:
public class Dog {
  String name;
  int age;
  int weight;

  public Dog(final String name, final int age, final int weight) {
     this.name = name;
     this.age = age;
     this.weight = weight;
  }
}
ومثله Raccoon(الراكون):
public class Raccoon {
  String name;
  int age;
  int weight;

  public Raccoon(final String name, final int age, final int weight) {
     this.name = name;
     this.age = age;
     this.weight = weight;
  }
}
لنفترض أن لدينا كائنًا Dog، ونحتاج إلى إنشاء كائن بناءً على حقوله Raccoon. أي أنه Converterيحول كائنًا من نوع إلى آخر. كيف سيبدو:
public static void main(String[] args) {
  Dog dog = new Dog("Bobbie", 5, 3);

  Converter<Dog, Raccoon> converter = x -> new Raccoon(x.name, x.age, x.weight);

  Raccoon raccoon = converter.convert(dog);

  System.out.println("Raccoon has parameters: name - " + raccoon.name + ", age - " + raccoon.age + ", weight - " + raccoon.weight);
}
عندما نقوم بتشغيله، نحصل على الإخراج التالي إلى وحدة التحكم:

Raccoon has parameters: name - Bobbbie, age - 5, weight - 3
وهذا يعني أن طريقتنا تعمل بشكل صحيح.واجهات وظيفية في جافا - 2

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

حسنًا، دعونا الآن نلقي نظرة على العديد من الواجهات الوظيفية التي قدمها لنا Java 8 والتي يتم استخدامها بشكل نشط مع Stream API.

فاعل

Predicate- واجهة وظيفية للتحقق من استيفاء شرط معين. إذا تم استيفاء الشرط، فسيتم إرجاعه true، وإلا - false:
@FunctionalInterface
public interface Predicate<T> {
   boolean test(T t);
}
على سبيل المثال، فكر في إنشاء ملف Predicateللتحقق من تكافؤ عدد من الأنواع Integer:
public static void main(String[] args) {
   Predicate<Integer> isEvenNumber = x -> x % 2==0;

   System.out.println(isEvenNumber.test(4));
   System.out.println(isEvenNumber.test(3));
}
إخراج وحدة التحكم:

true
false

مستهلك

Consumer(من الإنجليزية - "المستهلك") - واجهة وظيفية تأخذ كائنًا من النوع T كوسيطة إدخال، وتنفذ بعض الإجراءات، ولكنها لا تُرجع شيئًا:
@FunctionalInterface
public interface Consumer<T> {
   void accept(T t);
}
على سبيل المثال، خذ بعين الاعتبار ، الذي تتمثل مهمته في إخراج تحية إلى وحدة التحكم باستخدام وسيطة السلسلة التي تم تمريرها: Consumer
public static void main(String[] args) {
   Consumer<String> greetings = x -> System.out.println("Hello " + x + " !!!");
   greetings.accept("Elena");
}
إخراج وحدة التحكم:

Hello Elena !!!

المورد

Supplier(من الإنجليزية - الموفر) - واجهة وظيفية لا تأخذ أي وسيطات، ولكنها تُرجع كائنًا من النوع T:
@FunctionalInterface
public interface Supplier<T> {
   T get();
}
على سبيل المثال، خذ بعين الاعتبار Supplier، الذي سينتج أسماء عشوائية من القائمة:
public static void main(String[] args) {
   ArrayList<String> nameList = new ArrayList<>();
   nameList .add("Elena");
   nameList .add("John");
   nameList .add("Alex");
   nameList .add("Jim");
   nameList .add("Sara");

   Supplier<String> randomName = () -> {
       int value = (int)(Math.random() * nameList.size());
       return nameList.get(value);
   };

   System.out.println(randomName.get());
}
وإذا قمنا بتشغيل هذا، فسنرى نتائج عشوائية من قائمة الأسماء في وحدة التحكم.

وظيفة

Function- تأخذ هذه الواجهة الوظيفية وسيطة T وتحولها إلى كائن من النوع R، والذي يتم إرجاعه كنتيجة:
@FunctionalInterface
public interface Function<T, R> {
   R apply(T t);
}
كمثال، لنأخذ ، الذي يحول الأرقام من تنسيق السلسلة ( ) إلى تنسيق الأرقام ( ): FunctionStringInteger
public static void main(String[] args) {
   Function<String, Integer> valueConverter = x -> Integer.valueOf(x);
   System.out.println(valueConverter.apply("678"));
}
عندما نقوم بتشغيله، نحصل على الإخراج التالي إلى وحدة التحكم:

678
ملاحظة: إذا قمنا بتمرير ليس فقط أرقامًا، بل أيضًا أحرفًا أخرى في السلسلة، فسيتم طرح استثناء - NumberFormatException.

UnaryOperator

UnaryOperator- واجهة وظيفية تأخذ كائنًا من النوع T كمعلمة، وتنفذ بعض العمليات عليه وترجع نتيجة العمليات في شكل كائن من نفس النوع T:
@FunctionalInterface
public interface UnaryOperator<T> {
   T apply(T t);
}
UnaryOperator، والذي يستخدم طريقته applyلتربيع الرقم:
public static void main(String[] args) {
   UnaryOperator<Integer> squareValue = x -> x * x;
   System.out.println(squareValue.apply(9));
}
إخراج وحدة التحكم:

81
نظرنا إلى خمس واجهات وظيفية. هذا ليس كل ما هو متاح لنا بدءًا من Java 8 - فهذه هي الواجهات الرئيسية. أما باقي المنتجات المتاحة فهي نظائرها المعقدة. يمكن العثور على القائمة الكاملة في وثائق أوراكل الرسمية .

الواجهات الوظيفية في Stream

كما تمت مناقشته أعلاه، فإن هذه الواجهات الوظيفية مقترنة بإحكام بواجهة برمجة تطبيقات Stream. كيف تسأل؟ واجهات وظيفية في جافا - 3بحيث Streamتعمل العديد من الأساليب خصيصًا مع هذه الواجهات الوظيفية. دعونا نلقي نظرة على كيفية استخدام الواجهات الوظيفية في Stream.

الطريقة مع المسند

على سبيل المثال، لنأخذ طريقة الفصل Stream- filterالتي تأخذ وسيطة Predicateوترجع Streamفقط تلك العناصر التي تستوفي الشرط Predicate. في سياق Stream-a، هذا يعني أنه يمر فقط عبر تلك العناصر التي يتم إرجاعها trueعند استخدامها في طريقة testالواجهة Predicate. هذا هو ما سيبدو عليه مثالنا Predicate، ولكن بالنسبة لمرشح العناصر في Stream:
public static void main(String[] args) {
   List<Integer> evenNumbers = Stream.of(1, 2, 3, 4, 5, 6, 7, 8)
           .filter(x -> x % 2==0)
           .collect(Collectors.toList());
}
ونتيجة لذلك evenNumbersستتكون القائمة من العناصر {2، 4، 6، 8}. وكما نتذكر، collectفإنه سيجمع كل العناصر في مجموعة معينة: في حالتنا، إلى List.

الطريقة مع المستهلك

إحدى الطرق Streamالتي تستخدم الواجهة الوظيفية Consumerهي peek. هذا ما سيبدو عليه Consumerمثالنا Stream:
public static void main(String[] args) {
   List<String> peopleGreetings = Stream.of("Elena", "John", "Alex", "Jim", "Sara")
           .peek(x -> System.out.println("Hello " + x + " !!!"))
           .collect(Collectors.toList());
}
إخراج وحدة التحكم:

Hello Elena !!!
Hello John !!!
Hello Alex !!!
Hello Jim !!!
Hello Sara !!!
ولكن بما أن الطريقة peekتعمل مع ، فلن يحدث Consumerتعديل على السلاسل ، بل سيعود بالعناصر الأصلية: نفس العناصر التي أتت بها. لذلك ستتكون القائمة من العناصر "إيلينا"، "جون"، "أليكس"، "جيم"، "سارة". هناك أيضًا طريقة شائعة الاستخدام ، وهي مشابهة للطريقة ، ولكن الفرق هو أنها نهائية.StreampeekStreampeopleGreetingsforeachpeek

الطريقة مع المورد

مثال على الطريقة Streamالتي تستخدم الواجهة الوظيفية Supplierهو generate، الذي يقوم بإنشاء تسلسل لا نهائي بناءً على الواجهة الوظيفية التي تم تمريرها إليها. لنستخدم مثالنا Supplierلطباعة خمسة أسماء عشوائية على وحدة التحكم:
public static void main(String[] args) {
   ArrayList<String> nameList = new ArrayList<>();
   nameList.add("Elena");
   nameList.add("John");
   nameList.add("Alex");
   nameList.add("Jim");
   nameList.add("Sara");

   Stream.generate(() -> {
       int value = (int) (Math.random() * nameList.size());
       return nameList.get(value);
   }).limit(5).forEach(System.out::println);
}
وهذا هو الإخراج الذي نحصل عليه في وحدة التحكم:

John
Elena
Elena
Elena
Jim
استخدمنا هنا الطريقة limit(5)لتعيين حد للطريقة generate، وإلا فسيقوم البرنامج بطباعة أسماء عشوائية على وحدة التحكم إلى أجل غير مسمى.

الطريقة مع الوظيفة

المثال النموذجي للطريقة التي تحتوي Streamعلى وسيطة Functionهي الطريقة mapالتي تأخذ عناصر من نوع واحد، وتفعل شيئًا بها وتمررها، ولكن يمكن أن تكون هذه العناصر بالفعل عناصر من نوع مختلف. كيف قد يبدو المثال Functionفي Stream:
public static void main(String[] args) {
   List<Integer> values = Stream.of("32", "43", "74", "54", "3")
           .map(x -> Integer.valueOf(x)).collect(Collectors.toList());
}
ونتيجة لذلك، نحصل على قائمة من الأرقام، ولكن في Integer.

الطريقة مع UnaryOperator

كطريقة تُستخدم UnaryOperatorكوسيطة، لنأخذ طريقة الفصل Stream- iterate. تشبه هذه الطريقة الطريقة generate: فهي تولد أيضًا تسلسلًا لا نهائيًا ولكن لها وسيطتان:
  • الأول هو العنصر الذي يبدأ منه توليد التسلسل؛
  • والثاني هو UnaryOperatorالذي يدل على مبدأ توليد عناصر جديدة من العنصر الأول.
وهذا ما سيبدو عليه مثالنا UnaryOperator، لكن في الطريقة iterate:
public static void main(String[] args) {
   Stream.iterate(9, x -> x * x)
           .limit(4)
           .forEach(System.out::println);
}
عندما نقوم بتشغيله، نحصل على الإخراج التالي إلى وحدة التحكم:

9
81
6561
43046721
أي أن كل عنصر من عناصرنا مضروب في نفسه، وهكذا بالنسبة للأعداد الأربعة الأولى. واجهات وظيفية في جافا - 4هذا كل شئ! سيكون من الرائع أن تكون بعد قراءة هذه المقالة على بعد خطوة واحدة من فهم وإتقان Stream API في Java!
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION