filter و map و forEach
يتكوّن كل pipeline للـ Stream من ثلاثة عناصر: مصدر (تناولناه في الدرس الثاني)، وصفر أو أكثر من العمليات الوسيطة التي تحوّل الـ Stream، وعملية طرفية واحدة بالضبط تُطلق التنفيذ وتُنتج نتيجة. يغطّي هذا الدرس العمليات الثلاث التي ستستخدمها في كل pipeline تقريبًا: filter وmap وforEach.
التقييم الكسول — لماذا يهمّ؟
العمليات الوسيطة كـfilter وmap لا تفعل شيئًا حتى تُستدعى عملية طرفية. الـ Stream هو وصف لما يجب فعله وليس حلقة تعمل بالفعل. هذا الكسل يتيح للـ JVM دمج العمليات وتجنّب العمل غير الضروري — فمثلًا عند استخدام filter مع findFirst()، بمجرد إيجاد أول تطابق لا يُلمَس باقي المصدر أبدًا.
وسيطة مقابل طرفية: filter وmap يُعيدان Stream جديدًا — فهما وسيطتان. forEach تُعيد void — فهي طرفية. يمكن استهلاك الـ Stream مرة واحدة فقط؛ بعد تنفيذ العملية الطرفية يُستنفَد الـ Stream.
filter — الاحتفاظ بالعناصر المطابقة للشرط
filter(Predicate<T> predicate) عملية وسيطة تُمرِّر فقط العناصر التي يُعيد فيها الـ predicate القيمة true.
import java.util.List;
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// الاحتفاظ بالأعداد الزوجية فقط وطباعتها
numbers.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println);
// الخرج: 2 4 6 8 10
الـ predicate هو أي تعبير يُعيد قيمة boolean. يمكنك تركيب عدة predicates باستخدام Predicate.and() وPredicate.or() وPredicate.negate() للحفاظ على وضوح الـ lambda:
import java.util.List;
import java.util.function.Predicate;
List<String> words = List.of("java", "stream", "api", "filter", "map", "go");
Predicate<String> longerThanThree = s -> s.length() > 3;
Predicate<String> startsWithF = s -> s.startsWith("f");
// كلمات أطول من 3 محارف أو تبدأ بـ 'f'
words.stream()
.filter(longerThanThree.or(startsWithF))
.forEach(System.out::println);
// الخرج: java stream filter
سلسل عدة filters بدلًا من predicate واحد معقّد حين يصعب القراءة. الـ JVM يدمج الـ filters المتتالية بكفاءة، لذا لا يوجد سبب للأداء لحشر المنطق في lambda واحدة.
map — تحويل كل عنصر
map(Function<T, R> mapper) عملية وسيطة تطبّق دالة على كل عنصر مُنتجةً Stream<R>. يمكن أن يتغيّر نوع الـ Stream — تحويل Stream<String> عبر String::length يُنتج Stream<Integer>.
import java.util.List;
List<String> names = List.of("alice", "bob", "charlie", "diana");
// كتابة الحرف الأول من كل اسم بالأحرف الكبيرة
names.stream()
.map(name -> name.substring(0, 1).toUpperCase() + name.substring(1))
.forEach(System.out::println);
// الخرج: Alice Bob Charlie Diana
النمط الأكثر شيوعًا في الواقع العملي هو استخراج حقل من كائن:
import java.util.List;
record Product(String name, double price) {}
List<Product> products = List.of(
new Product("Laptop", 1200.00),
new Product("Mouse", 25.00),
new Product("Monitor", 350.00)
);
// استخراج الأسماء فقط
products.stream()
.map(Product::name) // مرجع دالة — أوضح من p -> p.name()
.forEach(System.out::println);
// الخرج: Laptop Mouse Monitor
مراجع الدوال (مثل Product::name وString::toUpperCase) هي الاختصار الاصطلاحي لـ lambda ذات وسيط واحد تستدعي دالة بعينها. استخدمها حين تكون النية واضحة.
الجمع بين filter و map
تظهر القوة الحقيقية عند تسلسل العمليتين. تنفّذ العمليات عنصرًا بعنصر عبر الـ pipeline — يمرّ العنصر الأول عبر filter ثم map، ثم العنصر الثاني، وهكذا. لا توجد قائمة وسيطة:
import java.util.List;
record Product(String name, double price) {}
List<Product> products = List.of(
new Product("Laptop", 1200.00),
new Product("Mouse", 25.00),
new Product("Monitor", 350.00),
new Product("Keyboard", 75.00)
);
// أسماء المنتجات التي سعرها أكثر من 100، بأحرف كبيرة
products.stream()
.filter(p -> p.price() > 100)
.map(p -> p.name().toUpperCase())
.forEach(System.out::println);
// الخرج: LAPTOP MONITOR
forEach — استهلاك كل عنصر
forEach(Consumer<T> action) عملية طرفية تُنفّذ الإجراء على كل عنصر متبقٍّ في الـ Stream. تُعيد void وتُستخدم عادةً للآثار الجانبية — الطباعة، والتسجيل، والكتابة إلى نظام خارجي.
import java.util.List;
List<String> errors = List.of("NullPointerException", "IOException", "TimeoutException");
errors.stream()
.filter(e -> e.contains("Exception"))
.forEach(e -> System.out.println("[ERROR] " + e));
// الخرج:
// [ERROR] NullPointerException
// [ERROR] IOException
// [ERROR] TimeoutException
لا تستخدم forEach لبناء نتيجة. إن وجدت نفسك تُنشئ قائمة فارغة وتستدعي forEach وتضيف عناصر داخل الـ lambda، توقّف. هذا بالضبط ما صُمّم collect() من أجله (يُغطّى في الدرس الرابع). استخدم forEach للآثار الجانبية فقط — تعديل حالة مشتركة داخل lambda يُفسد نموذج الـ Stream ويُسبّب أخطاء مع الـ streams المتوازية.
forEachOrdered — حين يهمّ الترتيب
في الـ streams المتسلسلة يكون الترتيب محدّدًا. في الـ streams المتوازية، forEach لا تضمن ترتيب المواجهة. استخدم forEachOrdered حين يكون الترتيب مطلوبًا وتستخدم أيضًا parallel():
List.of("a", "b", "c", "d", "e")
.parallelStream()
.forEachOrdered(System.out::println); // تطبع دائمًا a b c d e بالترتيب
الجمع بين الثلاث معًا
إليك مثالًا متكاملًا يجمع العمليات الثلاث في سيناريو واقعي:
import java.util.List;
record Employee(String name, String department, double salary) {}
public class StreamDemo {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", "Engineering", 95_000),
new Employee("Bob", "Marketing", 55_000),
new Employee("Charlie", "Engineering", 110_000),
new Employee("Diana", "HR", 48_000),
new Employee("Eve", "Engineering", 88_000)
);
System.out.println("المهندسون الكبار (الراتب >= 90k):");
employees.stream()
.filter(e -> e.department().equals("Engineering"))
.filter(e -> e.salary() >= 90_000)
.map(e -> e.name() + " — $" + e.salary())
.forEach(System.out::println);
// الخرج:
// Alice — $95000.0
// Charlie — $110000.0
}
}
الخلاصة
filter(predicate) — وسيطة؛ تحتفظ بالعناصر التي يُعيد فيها الشرط true.
map(function) — وسيطة؛ تحوّل كل عنصر مع إمكانية تغيير النوع.
forEach(consumer) — طرفية؛ تُنفّذ إجراءً جانبيًا وتستنفد الـ Stream.
- العمليات الوسيطة كسولة: لا تنفّذ إلا عند استدعاء عملية طرفية.
- احتفظ بـ
forEach للآثار الجانبية؛ استخدم collect() حين تحتاج نتيجة.