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

واجهتا Function و BiFunction

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

واجهتا Function و BiFunction

في الدرس السابق تعاملت مع Predicate التي تُعيد دائمًا قيمة boolean. تُعمّم واجهة Function هذه الفكرة: تأخذ قيمةً من نوع معيّن وتُحوّلها إلى قيمة من نوع آخر محتمل. هذه الواجهة هي ما يقف خلف كل خطوة تعيين وتحليل وتحويل للبيانات تكتبها مع اللامدا.

واجهة Function

تملك java.util.function.Function<T, R> دالةً مجرّدةً واحدة:

@FunctionalInterface public interface Function<T, R> { R apply(T t); }

T هو نوع المدخل، وR هو نوع النتيجة. يمكن أن يكونا متماثلَين لكن لا يُشترط ذلك. أمثلة سريعة:

// String → Integer Function<String, Integer> length = s -> s.length(); System.out.println(length.apply("hello")); // 5 // Integer → String Function<Integer, String> intToStr = n -> "value=" + n; System.out.println(intToStr.apply(42)); // value=42 // String → String (نفس النوع دخلًا وخرجًا) Function<String, String> shout = s -> s.toUpperCase() + "!"; System.out.println(shout.apply("java")); // JAVA!
متى تستخدم Function بدلًا من دالة عادية؟ الجأ إلى Function حين تريد تمرير التحويل كمعامل، أو تخزينه في متغيّر، أو دمجه مع دوال أخرى. إن كنت تحتاج لتنفيذه مرّة واحدة في مكانه، فالدالة العادية أبسط.

التأليف باستخدام andThen

تملك Function دالة افتراضية andThen(Function after) تُسلسل دالتَين معًا: تُنفَّذ هذه الدالة أولًا ثم تستقبل after نتيجتها.

Function<String, String> trim = String::trim; Function<String, String> upper = String::toUpperCase; // أنبوب: حذف المسافات أولًا، ثم التحويل لأحرف كبيرة Function<String, String> normalise = trim.andThen(upper); System.out.println(normalise.apply(" hello world ")); // HELLO WORLD

يمكنك تسلسل عدد لا محدود من الدوال:

Function<String, Integer> countWords = ((Function<String, String>) String::trim) .andThen(s -> s.replaceAll("\\s+", " ")) .andThen(s -> s.split(" ")) .andThen(parts -> parts.length); System.out.println(countWords.apply(" one two three ")); // 3
اقرأ andThen كأنبوب بيانات: f.andThen(g) تعني "نفّذ f ثم مرّر ناتجها إلى g". تتدفق البيانات من اليسار إلى اليمين، وهو ما يطابق ترتيب القراءة الطبيعي ويجعل التحويلات متعددة الخطوات سهلة الفهم.

التأليف باستخدام compose

compose(Function before) هو عكس andThen: تُنفَّذ before أولًا ثم تستقبل هذه الدالة نتيجتها.

Function<Integer, Integer> doubleIt = x -> x * 2; Function<Integer, Integer> addTen = x -> x + 10; // compose: تُنفَّذ addTen أولًا، ثم doubleIt Function<Integer, Integer> addThenDouble = doubleIt.compose(addTen); System.out.println(addThenDouble.apply(5)); // (5+10)*2 = 30 // andThen: تُنفَّذ doubleIt أولًا، ثم addTen Function<Integer, Integer> doubleThenAdd = doubleIt.andThen(addTen); System.out.println(doubleThenAdd.apply(5)); // (5*2)+10 = 20
سهل الخلط بين andThen و compose: f.andThen(g) تعني f ثم g. أما f.compose(g) فتعني g ثم f (أي g تأتي أولًا). عمليًا يلجأ معظم المطوّرين إلى andThen لأن ترتيب القراءة يطابق ترتيب التنفيذ. احتفظ بـ compose للحالات التي تُزيّن فيها دالةً قائمة من الخارج.

واجهة BiFunction

أحيانًا يحتاج التحويل إلى مدخلَين. تُغطّي java.util.function.BiFunction<T, U, R> هذه الحالة:

@FunctionalInterface public interface BiFunction<T, U, R> { R apply(T t, U u); }

مثال عملي — تنسيق الاسم الكامل من اسم أول واسم أخير منفصلَين:

BiFunction<String, String, String> fullName = (first, last) -> first + " " + last; System.out.println(fullName.apply("Ada", "Lovelace")); // Ada Lovelace

مثال آخر — دمج تسمية مع قيمة رقمية للعرض:

BiFunction<String, Integer, String> label = (name, score) -> name + ": " + score + " pts"; System.out.println(label.apply("Alice", 95)); // Alice: 95 pts

تملك BiFunction أيضًا andThen (لكن ليس compose)، ما يتيح لك معالجة النتيجة لاحقًا:

BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b; Function<Integer, String> format = n -> "Result: " + n; var multiplyAndFormat = multiply.andThen(format); System.out.println(multiplyAndFormat.apply(6, 7)); // Result: 42

UnaryOperator و BinaryOperator — تخصيصات مريحة

حين يكون نوعا المدخل والمخرج متماثلَين، يوفّر Java واجهتَين مختصرتَين:

  • UnaryOperator<T> تمتدّ من Function<T, T> — مدخل واحد بنفس نوع المخرج.
  • BinaryOperator<T> تمتدّ من BiFunction<T, T, T> — مدخلان بنفس نوع المخرج.
import java.util.function.UnaryOperator; import java.util.function.BinaryOperator; UnaryOperator<String> exclaim = s -> s + "!"; System.out.println(exclaim.apply("wow")); // wow! BinaryOperator<Integer> add = (a, b) -> a + b; System.out.println(add.apply(3, 4)); // 7

استخدم هاتَين الواجهتَين بدلًا من Function<T, T> أو BiFunction<T, T, T> — فهما أكثر تعبيرًا، وبعض واجهات برمجة التطبيقات كـ List.replaceAll تتوقّعهما صراحةً.

تجميع المفاهيم — أنبوب تحويل قابل لإعادة الاستخدام

إليك مثالًا واقعيًا مصغّرًا: أنبوب تطبيع بيانات بسيط مبنيٌّ من نُسَخ Function قابلة للتأليف.

import java.util.List; import java.util.function.Function; public class PipelineDemo { static <T, R> List<R> mapList(List<T> items, Function<T, R> transform) { return items.stream().map(transform).toList(); } public static void main(String[] args) { Function<String, String> trim = String::trim; Function<String, String> lower = String::toLowerCase; Function<String, String> slug = s -> s.replace(" ", "-"); Function<String, String> toSlug = trim.andThen(lower).andThen(slug); List<String> titles = List.of(" Java Lambdas ", " Functional Style", "Clean Code "); List<String> slugs = mapList(titles, toSlug); slugs.forEach(System.out::println); // java-lambdas // functional-style // clean-code } }

لاحظ أن كل خطوة متغيّر مسمّى، ما يجعل الأنبوب موثّقًا بذاته. يمكنك إعادة استخدام trim وlower وslug بصورة مستقلة في أنابيب أخرى — هذا هو المردود الحقيقي للقابلية على التأليف.

الخلاصة

  • Function<T, R> تُحوّل قيمةً من النوع T إلى قيمة من النوع R عبر apply.
  • andThen(g) يُسلسل: هذه الدالة أولًا ثم g (من اليسار لليمين).
  • compose(g) يُسلسل: g أولًا ثم هذه الدالة (من اليمين لليسار).
  • BiFunction<T, U, R> تقبل مدخلَين وتُعيد نتيجة واحدة.
  • UnaryOperator<T> وBinaryOperator<T> اختصارات ملائمة حين تكون الأنواع متماثلة.