مرحبًا! في مهمة Java Syntax Pro، درسنا تعبيرات lambda وقلنا إنها ليست أكثر من مجرد تطبيق لطريقة وظيفية من واجهة وظيفية. بمعنى آخر، هذا هو تنفيذ بعض الفئات المجهولة (غير المعروفة)، وطريقتها غير المحققة. وإذا بحثنا في محاضرات الدورة في التلاعب بتعبيرات لامدا، فسننظر الآن، إذا جاز التعبير، في الجانب الآخر: أي هذه الواجهات ذاتها. قدم الإصدار الثامن من Java مفهوم الواجهات الوظيفية . ما هذا؟ تعتبر الواجهة التي تحتوي على طريقة واحدة (مجردة) غير منفذة وظيفية. وتندرج العديد من الواجهات المبتكرة تحت هذا التعريف، مثل الواجهة التي تمت مناقشتها مسبقًا فاعل
مستهلك
المورد
وظيفة
UnaryOperator
Comparator
. وأيضا الواجهات التي نقوم بإنشائها بأنفسنا مثل:
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
}
لدينا واجهة تتمثل مهمتها في تحويل كائنات من نوع ما إلى كائنات من نوع آخر (نوع من المحول). التعليق التوضيحي @FunctionalInterface
ليس شيئًا معقدًا أو مهمًا جدًا، حيث أن الغرض منه هو إخبار المترجم أن هذه الواجهة وظيفية ويجب ألا تحتوي على أكثر من طريقة واحدة. إذا كانت الواجهة التي تحتوي على هذا التعليق التوضيحي تحتوي على أكثر من طريقة (مجردة) غير منفذة، فلن يتخطى المترجم هذه الواجهة، لأنه سيعتبرها رمزًا خاطئًا. يمكن اعتبار الواجهات التي لا تحتوي على هذا التعليق التوضيحي وظيفية وستعمل، ولكن @FunctionalInterface
هذا ليس أكثر من تأمين إضافي. دعونا نعود إلى الصف Comparator
. إذا نظرت إلى الكود الخاص به (أو الوثائق )، يمكنك أن ترى أن لديه أكثر من طريقة واحدة. ثم تسأل: كيف إذن يمكن اعتبارها واجهة وظيفية؟ يمكن أن تحتوي الواجهات المجردة على طرق لا تقع ضمن نطاق طريقة واحدة:
- ثابتة
@FunctionalInterface
public interface Converter<T, N> {
N convert(T t);
static <T> boolean isNotNull(T t){
return t != null;
}
}
بعد تلقي هذه الطريقة، لم يشتكي المترجم، مما يعني أن واجهتنا لا تزال تعمل.
- الأساليب الافتراضية
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
وهذا يعني أن طريقتنا تعمل بشكل صحيح.
واجهات 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);
}
كمثال، لنأخذ ، الذي يحول الأرقام من تنسيق السلسلة ( ) إلى تنسيق الأرقام ( ): Function
String
Integer
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. كيف تسأل؟ بحيث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
تعديل على السلاسل ، بل سيعود بالعناصر الأصلية: نفس العناصر التي أتت بها. لذلك ستتكون القائمة من العناصر "إيلينا"، "جون"، "أليكس"، "جيم"، "سارة". هناك أيضًا طريقة شائعة الاستخدام ، وهي مشابهة للطريقة ، ولكن الفرق هو أنها نهائية.Stream
peek
Stream
peopleGreetings
foreach
peek
الطريقة مع المورد
مثال على الطريقة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
أي أن كل عنصر من عناصرنا مضروب في نفسه، وهكذا بالنسبة للأعداد الأربعة الأولى. هذا كل شئ! سيكون من الرائع أن تكون بعد قراءة هذه المقالة على بعد خطوة واحدة من فهم وإتقان Stream API في Java!
GO TO FULL VERSION