تعابير لامدا والواجهات الوظيفيّة

Consumer و Supplier

15 دقيقة الدرس 6 من 13

Consumer و Supplier

في الدروس السابقة تعرّفت على Predicate (اختبار شيء وإعادة قيمة منطقية) وFunction (تحويل قيمة إلى أخرى). ثمّة واجهتان وظيفيتان مدمجتان أخريان تُكمّلان مجموعة الأدوات اليومية: Consumer وSupplier. إنّهما يقعان في طرفَي نقيض من طيف تدفق البيانات — الأولى تأخذ البيانات فقط دون أن تُعيد شيئًا، والثانية تُنتج البيانات دون أن تأخذ شيئًا.

Consumer — التصرف بناءً على قيمة دون إعادة شيء

تقبل java.util.function.Consumer<T> وسيطًا واحدًا من النوع T ولا تُعيد شيئًا (void). الدالة المجرّدة الوحيدة فيها هي:

void accept(T t);

استخدم Consumer كلّما أردت تنفيذ تأثير جانبي — طباعة، تسجيل، كتابة في قاعدة بيانات، تحديث واجهة مستخدم — دون إنتاج قيمة جديدة ترجع إلى المُستدعي.

import java.util.function.Consumer; public class ConsumerDemo { public static void main(String[] args) { Consumer<String> print = message -> System.out.println(">> " + message); print.accept("Hello, Consumer!"); // >> Hello, Consumer! print.accept("Side effects only"); // >> Side effects only } }

يُترجَم التعبير اللامبدي message -> System.out.println(...) مباشرةً إلى Consumer<String> لأنّه يأخذ وسيطًا واحدًا ولا يُعيد شيئًا. ومرجع الدالة System.out::println يعمل بالطريقة ذاتها وكثيرًا ما يكون أكثر وضوحًا.

تسلسل Consumer عبر andThen

توفّر Consumer دالةً افتراضية andThen(Consumer<? super T> after) تربط بين مستهلكَين كي يعملا على نفس المدخل بالتتابع — مفيد حين تريد التسجيل والحفظ معًا مثلًا.

Consumer<String> log = s -> System.out.println("[LOG] " + s); Consumer<String> notify = s -> System.out.println("[NOTIFY] " + s); Consumer<String> logAndNotify = log.andThen(notify); logAndNotify.accept("Order shipped"); // [LOG] Order shipped // [NOTIFY] Order shipped
فكرة محورية: على عكس Function::andThen التي تُمرّر القيمة المُحوَّلة من دالة إلى التالية، فإنّ Consumer::andThen تُغذّي المستهلكَين بـنفس القيمة الأصلية. لا يحدث أي تحويل — كلاهما يعمل على مدخل متطابق.

BiConsumer — مدخلان بلا مخرج

حين تحتاج للتصرف على قيمتين معًا، استخدم BiConsumer<T, U>:

import java.util.function.BiConsumer; BiConsumer<String, Integer> printWithScore = (name, score) -> System.out.printf("%s scored %d%n", name, score); printWithScore.accept("Alice", 95); // Alice scored 95 printWithScore.accept("Bob", 82); // Bob scored 82

الموقع الأكثر شيوعًا لمصادفة BiConsumer في المكتبة القياسية هو Map::forEach التي تُمرّر كل مفتاح وقيمة إلى تعبيرك اللامبدي.

import java.util.Map; Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 82, "Carol", 88); scores.forEach((name, score) -> System.out.printf("%-8s %d%n", name, score) );

Supplier — إنتاج قيمة بشكل كسول

لا تأخذ java.util.function.Supplier<T> أيّ وسيطات وتُعيد قيمة من النوع T. الدالة المجرّدة الوحيدة فيها هي:

T get();

يُعبّر التوقيع عن النية بوضوح: هذا الكائن هو مصنع أو عملية حسابية مؤجَّلة. تُمرّر Supplier وتستدعي get() فقط حين تحتاج القيمة فعلًا — هذا ما يعنيه التقييم الكسول.

import java.util.function.Supplier; import java.time.LocalDateTime; public class SupplierDemo { public static void main(String[] args) { // Supplier يُغلّف عملية حسابية قد تكون مكلفة Supplier<LocalDateTime> now = () -> LocalDateTime.now(); System.out.println("Before call"); // لم يُستدعَ LocalDateTime.now() بعد LocalDateTime timestamp = now.get(); // يُستدعى هنا تحديدًا System.out.println("Captured: " + timestamp); } }
متى تختار Supplier بدلًا من قيمة مباشرة: مرّر Supplier حين تكون القيمة مكلفة حسابيًا وربّما لن تُحتاج (كرسالة سجلّ احتياطية)، أو حين تريد قيمة جديدة في كل استدعاء لـget() (كمعرّف فريد أو نسخة جديدة من كائن).

مثال عملي — orElseGet مقابل orElse في Optional

تُوضح دالتا الاحتياط في Optional أهمية Supplier على الأداء:

import java.util.Optional; Optional<String> maybe = Optional.empty(); // orElse: القيمة الاحتياطية تُحسَب بشكل استباقي — حتى لو كان Optional يحوي قيمة String a = maybe.orElse(expensiveDefault()); // orElseGet: لا يُستدعى Supplier إلا حين يكون Optional فارغًا — تقييم كسول String b = maybe.orElseGet(() -> expensiveDefault());

إذا كانت expensiveDefault() تستعلم من قاعدة بيانات أو تستدعي خدمة خارجية، فالفارق بين النهجين قد يكون كبيرًا. فضّل orElseGet مع Supplier كلّما كانت القيمة الاحتياطية غير تافهة.

خطأ شائع: لا تمتلك Supplier ذاكرة. كل استدعاء لـget() يُعيد تنفيذ جسم اللامبدا. إن أردت تخزين النتيجة مؤقتًا، احسبها مرة واحدة واحفظها، أو استخدم غلافًا مخصصًا للتخزين — لا تفترض أن Supplier تُخزّن تلقائيًا.

حقن السلوك في دالة

تتألّق كلتا الواجهتين حين تُضخّ السلوكيات في دالة عبر المعاملات — وهو سمة أسلوب البرمجة الوظيفية:

import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; public class BehaviourInjection { // Consumer لفصل الإجراء عن هذه الدالة static void processAll(List<String> items, Consumer<String> action) { for (String item : items) { action.accept(item); } } // Supplier لتأجيل إنشاء الكائن static void printIfAbsent(String value, Supplier<String> fallback) { System.out.println(value != null ? value : fallback.get()); } public static void main(String[] args) { processAll( List.of("apple", "banana", "cherry"), item -> System.out.println(item.toUpperCase()) ); printIfAbsent(null, () -> "default-" + System.currentTimeMillis()); } }

مرجع سريع

  • Consumer<T> — تأخذ T، لا تُعيد شيئًا. للتأثيرات الجانبية.
  • Consumer::andThen — تسلسل مستهلكَين على نفس المدخل.
  • BiConsumer<T, U> — تأخذ T وU، لا تُعيد شيئًا.
  • Supplier<T> — لا تأخذ شيئًا، تُنتج T. للقيم الكسولة أو المؤجَّلة.
  • فضّل orElseGet(Supplier) على orElse(value) للقيم الاحتياطية غير التافهة.

في الدرس القادم ستستكشف مراجع الدوال — اختصار موجز يحوّل الدوال الموجودة مباشرةً إلى أيٍّ من هذه الواجهات الوظيفية.