واجهة التدفّقات

flatMap والتحويل

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

flatMap والتحويل

تعلمت سابقًا أن map() تحوّل كل عنصر في التدفق إلى شيء آخر — إدخال واحد، وإخراج واحد. لكن ماذا يحدث حين ينتج عنصر واحد عدة عناصر بطبيعته؟ هذه بالضبط المشكلة التي تحلّها flatMap(). إتقان متى تلجأ إلى flatMap بدلًا من map هو أحد أهم المهارات عند العمل مع Streams API.

المشكلة: الهياكل المتداخلة

تخيّل أن لديك قائمة من الجمل وتريد تدفقًا يحتوي على كل كلمة على حدة. باستخدام map() العادية ستحصل على Stream<String[]> — تدفق من المصفوفات — وليس تدفقًا مسطّحًا من النصوص:

List<String> sentences = List.of("hello world", "streams are powerful"); // map تُنتج Stream<String[]> — ليس ما تريده Stream<String[]> wrong = sentences.stream() .map(s -> s.split(" "));

كل جملة تحوّلت إلى مصفوفة، وأصبح لديك تدفق من المصفوفات. التكرار عليه يعطيك مصفوفات وليس كلمات. flatMap() تحلّ ذلك بتحويل كل عنصر إلى تدفق ثم دمج جميع تلك التدفقات الفرعية في تدفق واحد متصل.

النموذج الذهني: map هي علاقة واحد-لواحد. flatMap هي واحد-لكثير — كل عنصر مصدر ينتج تدفقًا من النتائج، وتلك التدفقات كلها تُدمج في تدفق الإخراج الواحد.

flatMap في العمل

List<String> sentences = List.of("hello world", "streams are powerful"); List<String> words = sentences.stream() .flatMap(s -> Arrays.stream(s.split(" "))) .collect(Collectors.toList()); System.out.println(words); // [hello, world, streams, are, powerful]

الـ lambda الممررة إلى flatMap يجب أن تُعيد Stream. هنا Arrays.stream(s.split(" ")) تُنتج تدفقًا صغيرًا لكل جملة. تقوم الـ API بدمج كل تلك التدفقات في تدفق واحد قبل أن تراه أي عملية لاحقة.

تسطيح قائمة من القوائم

حالة استخدام كلاسيكية هي المجموعات المتداخلة — مثلًا، كل قسم يحتوي على قائمة موظفين:

record Department(String name, List<String> employees) {} List<Department> departments = List.of( new Department("Engineering", List.of("Alice", "Bob", "Carol")), new Department("Marketing", List.of("Dave", "Eve")), new Department("Finance", List.of("Frank")) ); List<String> allEmployees = departments.stream() .flatMap(d -> d.employees().stream()) .sorted() .collect(Collectors.toList()); System.out.println(allEmployees); // [Alice, Bob, Carol, Dave, Eve, Frank]

مع map كنت ستحصل على Stream<List<String>>. مع flatMap تحصل على Stream<String> واحد — كل موظف في كل قسم، جاهزًا لعمليات إضافية كـ sorted() أو distinct().

الجمع بين flatMap وعمليات أخرى

بما أن flatMap تُنتج تدفقًا عاديًا، يمكنك تسلسل أي عملية لاحقة بعدها. نمط شائع هو التسطيح ثم التصفية ثم الجمع:

List<List<Integer>> matrix = List.of( List.of(1, 2, 3), List.of(4, 5, 6), List.of(7, 8, 9) ); // كل الأرقام الزوجية من المصفوفة بأكملها، مضاعفة List<Integer> result = matrix.stream() .flatMap(Collection::stream) .filter(n -> n % 2 == 0) .map(n -> n * 2) .collect(Collectors.toList()); System.out.println(result); // [4, 8, 12, 16]
استخدم Collection::stream كمرجع دالة بدلًا من كتابة list -> list.stream() حين يكون نوع العنصر مجموعة معروفة. فهو أكثر إيجازًا وبالقدر ذاته من الوضوح.

flatMap مقابل map: الاختيار الصحيح

  • إذا كانت الدالة تُعيد قيمة واحدة محوّلة، استخدم map.
  • إذا كانت الدالة تُعيد مجموعة أو تدفقًا من القيم ينبغي دمجها في التدفق الرئيسي، استخدم flatMap.
  • إذا استخدمت map بينما كنت تحتاج flatMap، سيصبح نوع العنصر Stream<Stream<T>> أو Stream<List<T>> — دلالة شائعة وقت التصريف على أنك اخترت الخاطئة.

المتغيرات العددية: flatMapToInt وflatMapToLong وflatMapToDouble

تمامًا كما أن لـ map نظيرًا mapToInt لتجنّب التغليف، لـ flatMap متغيرات متخصصة في الأنواع البدائية. إذا كان تدفقك الفرعي يُنتج قيم int، استخدم flatMapToInt للبقاء بدون تغليف:

List<int[]> arrays = List.of( new int[]{1, 2, 3}, new int[]{4, 5} ); int sum = arrays.stream() .flatMapToInt(Arrays::stream) // تُنتج IntStream .sum(); System.out.println(sum); // 15
لا تُسطّح إلى Stream<Integer> حين يكون لديك مصفوفات بدائية. سيُغلَّف كل int إلى Integer مما يضيف تخصيصات كومة غير ضرورية. فضّل flatMapToInt وابقَ على IntStream للعمليات العددية.

مثال واقعي: استخراج الوسوم

افترض منصة تدوين حيث كل منشور يحمل قائمة من الوسوم. تريد أعلى 5 وسوم الأكثر استخدامًا عبر جميع المنشورات:

record Post(String title, List<String> tags) {} List<Post> posts = List.of( new Post("Streams 101", List.of("java", "streams", "tutorial")), new Post("Lambda Guide", List.of("java", "lambdas", "tutorial")), new Post("OOP Patterns", List.of("java", "oop", "design")) ); posts.stream() .flatMap(p -> p.tags().stream()) .collect(Collectors.groupingBy(tag -> tag, Collectors.counting())) .entrySet().stream() .sorted(Map.Entry.<String, Long>comparingByValue().reversed()) .limit(5) .forEach(e -> System.out.println(e.getKey() + ": " + e.getValue())); // java: 3 // tutorial: 2 // ...

الخلاصة

flatMap هي الأداة المخصصة لتسطيح علاقات الواحد-لكثير في مسار التدفق. القاعدة الأساسية: يجب أن تُعيد الدالة الممررة Stream، وتُدمج كل تلك التدفقات الفرعية تلقائيًا في تدفق واحد. كلما وجدت نفسك تُطبّق map على مجموعة ثم تعاني من تدفق مكوّن من مجموعات، الجأ إلى flatMap. للعمل العددي، تُلغي flatMapToInt وflatMapToLong وflatMapToDouble تكلفة التغليف.