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

واجهة Predicate ومُدمِجاتها

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

واجهة Predicate ومُدمِجاتها

الواجهة Predicate<T> هي واجهة وظيفية في حزمة java.util.function تمثّل اختبارًا واحدًا: عند إعطائها قيمة من النوع T، ترجع true أو false. إنّها الطريقة القياسية للتعبير عن شرط منطقي في الـ API الوظيفي لـ Java — تُستخدم كثيرًا في Streams API، وفي توابع المجموعات مثل removeIf، وفي أي API يحتاج إلى مرشّح.

import java.util.function.Predicate; Predicate<String> isLong = s -> s.length() > 10; System.out.println(isLong.test("hello")); // false System.out.println(isLong.test("hello, world!")); // true

التابع المجرّد الوحيد هو boolean test(T t). كل شيء آخر في Predicate — سواء كان and أو or أو negate أو not — هو تابع default أو static يتيح لك بناء شروط مركّبة دون كتابة كتل if متداخلة.

لماذا تهمّنا المسندات القابلة للتركيب؟

تخيّل أنّك تريد تصفية قائمة مستخدمين. بدون التركيب ستكتب إمّا تعبيرًا lambda ضخمًا يفعل كل شيء، أو حلقات for متعددة. بالتركيب تعرّف مسندات صغيرة مُسمّاة وتربطها معًا — كل جزء قابل للاختبار بمفرده، والنتيجة المركّبة تُقرأ كجملة مفهومة.

negate — عكس النتيجة

يرجع negate() Predicate جديدًا هو النفي المنطقي للأصل.

Predicate<Integer> isEven = n -> n % 2 == 0; Predicate<Integer> isOdd = isEven.negate(); System.out.println(isOdd.test(3)); // true System.out.println(isOdd.test(4)); // false

يوجد أيضًا مساعد ثابت هو Predicate.not(predicate) أُضيف في Java 11. إنّه مفيد جدًا داخل سلاسل التوابع حيث قد يستلزم استدعاء .negate() أقواسًا إضافية:

import java.util.List; var names = List.of("Alice", "", "Bob", " ", "Charlie"); // الإبقاء على الأسماء غير الفارغة فقط names.stream() .filter(Predicate.not(String::isBlank)) .forEach(System.out::println); // Alice // Bob // Charlie

and — يجب أن يتحقّق كلا الشرطين

يرجع and(other) Predicate جديدًا يعمل بالدارة القصيرة: إذا أرجع المسند الأول false فلن يُقيَّم الثاني — تمامًا مثل عامل &&.

Predicate<String> notEmpty = s -> !s.isEmpty(); Predicate<String> startsWithA = s -> s.startsWith("A"); Predicate<String> validAndStartsA = notEmpty.and(startsWithA); System.out.println(validAndStartsA.test("Alice")); // true System.out.println(validAndStartsA.test("Bob")); // false System.out.println(validAndStartsA.test("")); // false (دارة قصيرة، لا يُفحص startsWith)
تقييم الدارة القصيرة: يتوقف and عند أول نتيجة false، ويتوقف or عند أول نتيجة true. هذا يعكس تمامًا عاملَي && و|| في Java — الترتيب مهم إذا كان أحد المسندات يُنتج تأثيرًا جانبيًا أو يستهلك وقتًا في الحساب.

or — يكفي تحقّق أحد الشرطين

يرجع or(other) مسندًا يكون صحيحًا متى كان أحد الطرفين صحيحًا.

Predicate<Integer> isNegative = n -> n < 0; Predicate<Integer> isZero = n -> n == 0; Predicate<Integer> isNonPositive = isNegative.or(isZero); System.out.println(isNonPositive.test(-5)); // true System.out.println(isNonPositive.test(0)); // true System.out.println(isNonPositive.test(3)); // false

تركيب الثلاثة معًا

تظهر القوة الحقيقية عند سلسلة المُدمِجات المتعددة. إليك سيناريو تصفية واقعي لقائمة أسعار منتجات:

import java.util.List; import java.util.function.Predicate; public class PriceFilter { public static void main(String[] args) { List<Double> prices = List.of(0.0, 5.99, 12.50, 99.99, 250.00, -1.0); // القواعد: السعر يجب أن يكون موجبًا، لا يقل عن 5.00، وأقل من 100.00 Predicate<Double> isPositive = p -> p > 0; Predicate<Double> atLeastFive = p -> p >= 5.0; Predicate<Double> belowHundred = p -> p < 100.0; Predicate<Double> validPrice = isPositive.and(atLeastFive).and(belowHundred); prices.stream() .filter(validPrice) .forEach(p -> System.out.printf("$%.2f%n", p)); // $5.99 // $12.50 // $99.99 } }
سمّ مسنداتك. تعيين كل شرط إلى متغير باسم واضح كـ atLeastFive يجعل السلسلة تشرح نفسها. تجنّب كتابة تعبير lambda واحد متداخل بعمق — يصعب قراءته ولا يمكن إعادة استخدامه.

استخدام Predicate مع removeIf

Collection.removeIf(Predicate) تابع مريح يحذف كل عنصر يُحقّق المسند المُعطى. يقبل أي Predicate<T> بما في ذلك المركّبة منها:

import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; var numbers = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); Predicate<Integer> isEven = n -> n % 2 == 0; Predicate<Integer> isLarge = n -> n > 7; // احذف الأعداد الزوجية أو الأكبر من 7 numbers.removeIf(isEven.or(isLarge)); System.out.println(numbers); // [1, 3, 5, 7]
لا تعدّل قائمة أثناء معالجتها بـ stream. التابع removeIf آمن — صُمِّم للحذف في مكانه. أمّا stream().filter(...) فلا يُعدّل القائمة الأصلية بل ينتج stream جديدًا. اعرف أيّ السلوكَين تحتاجه قبل الاختيار بينهما.

الخلاصة

تحوّل Predicate<T> الشرط المنطقي إلى كيان من الدرجة الأولى. عملياتها الأربع الرئيسية هي:

  • test(t) — تقييم الشرط.
  • negate() / Predicate.not(...) — النفي المنطقي.
  • and(other) — الضرب المنطقي مع الدارة القصيرة.
  • or(other) — الجمع المنطقي مع الدارة القصيرة.

بناء مرشّحات معقّدة من مسندات صغيرة مُسمّاة هو أسلوب Java 8+ الاحترافي — قابل للتركيب، سهل القراءة، وسهل الاختبار. ستلتقي بهذا النمط كثيرًا حين نصل إلى درس Streams.