Java 8 Streams Collectors: Stream එකතුවක් බවට පත්කරමු!

Java 8 Streams Collectors: Stream එකතුවක් බවට පත්කරමු!

අයියේ, නංගියේ, කොහොමද ඉතින්? අද අපි කතා කරන්න යන්නේ Java වල ගොඩක්ම ප්‍රයෝජනවත්, ඒ වගේම ගොඩක් අය තාමත් සම්පූර්ණයෙන් පාවිච්චි නොකරන කෑල්ලක් ගැන - ඒ තමයි Java 8 Streams වල තියෙන Collectors.

Java ecosystem එකේ ඉන්න ඔයාලා හැමෝම Java 8 එක්ක ආපු Streams ගැන අහලා ඇති, සමහර විට දිනපතාම වගේ පාවිච්චි කරනවත් ඇති. Stream API එකෙන් data processing කොච්චර ලේසි වුණාද කියනවා නම්, කලින් for loops දාලා කරපු වැඩ, මේකට map, filter, reduce වගේ operations දාලා කොච්චර ලේසියෙන්, ඒ වගේම clean විදිහට කරන්න පුළුවන්ද කියලා හිතාගන්නත් බැහැ. ඒක හරියට අලුතින්ම ආපු sports car එකක් වගේ. වේගය වැඩියි, control කරන්නත් ලේසියි.

ඒත් ඔය stream එකෙන් වැඩේ ඉවර වුනාට පස්සේ, අන්තිමට ඒ data ටික collection එකකට (List, Set වගේ), Map එකකට, එහෙමත් නැත්නම් වෙනත් custom object එකකට ගන්න විදිහ තමයි මේ Collectors කියන්නේ. Stream එකක් පටන් ගත්තා වගේම ඉවර කරන්නත් ඕනනේ? ඒකට තමයි මේ Collectors. මේක හරියට, ඔයාලා එලවලු ටිකක් කපලා (Stream එකක් වගේ), රසට පිසලා (intermediate operations), අන්තිමට ඒ ටික සලාදයක් (Collection එකක්) හරි, රසවත් curry එකක් (Map එකක්) හරි හදනවා වගේ වැඩක්. තනිකරම data transform කරන වැඩක්. Java Application develop කරන ඔයාලා කවුරු වුණත් මේ Collectors ගැන හොඳටම දැනගෙන ඉන්න එක ඔයාලගේ productivity එක වැඩි කරනවා වගේම, ලියන code එකේ quality එකත් වැඩි කරනවා. ඉතින් අපි යමු මේ Collectors ලෝකේ ඇතුළට!

Collectors කියන්නේ මොනවද?

සරලවම කිව්වොත්, Stream එකක තියෙන elements ටික transform කරලා, ඒවා එකතු කරලා final result එකක් හදාගන්න උදව් වෙන method set එකක් තමයි මේ Collectors කියන්නේ. මේවා java.util.stream.Collectors class එකේ තියෙන factory methods විදිහට තමයි පාවිච්චි වෙන්නේ. මේ methods static methods, ඒ නිසා Collectors.methodName() විදිහට direct call කරන්න පුළුවන්. Stream එකක් .collect() method එකෙන් call කරනකොට තමයි මේ Collector එකක් supply කරන්නේ. collect() කියන්නේ terminal operation එකක්. ඒ කියන්නේ මේ method එක call කරාට පස්සේ Stream එක close වෙනවා. හරියට pipeline එකක් වගේ, දත්ත එහාට මෙහාට ගිහින් අන්තිම තැනට එනකොට final result එකක් විදිහට ලැබෙනවා. අපි මුලින්ම බලමු මේකේ සරලම use case එකක්.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class BasicCollectorExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Kamal", "Nimal", "Sampath", "Amara");

        // 'K' අකුරෙන් පටන් ගන්න නම් filter කරලා List එකකට collect කරනවා.
        List<String> filteredNames = names.stream()
                                          .filter(name -> name.startsWith("K"))
                                          .collect(Collectors.toList());

        System.out.println("Filtered Names: " + filteredNames); // Output: Filtered Names: [Kamal]
    }
}

මේ Collectors.toList() කියන්නේ අපි නිතරම වගේ පාවිච්චි කරන Collector එකක්. මේකෙන් Stream එකක තියෙන elements List එකක් විදිහට ආපහු දෙනවා.

Grouping (කණ්ඩායම් කිරීම) - Collectors.groupingBy()

data group කරන්න ඕන වුනාම, Collectors.groupingBy() කියන්නේ නියම option එකක්. මේකෙන් අපිට පුළුවන් Stream එකක තියෙන elements specific key එකක් අනුව group කරලා Map<K, List<V>> එකක් විදිහට ගන්න. මේක හරියට school එකක ළමයි, පන්ති අනුව වෙන් කරනවා වගේ වැඩක්.

මේ උදාහරණ වලට අපි පොඩි Student class එකක් පාවිච්චි කරමු:

class Student {
    String name;
    int grade;

    public Student(String name, int grade) {
        this.name = name;
        this.grade = grade;
    }

    public int getGrade() { return grade; }
    public String getName() { return name; }

    @Override
    public String toString() {
        return name + " (Grade: " + grade + ")";
    }
}

දැන් අපි බලමු groupingBy කොහොමද පාවිච්චි කරන්නේ කියලා:

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

// Student class definition (as above)

public class GroupingByExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Sunil", 10),
            new Student("Kamala", 12),
            new Student("Ruwan", 10),
            new Student("Nandana", 11),
            new Student("Saman", 12) // තවත් student කෙනෙක් add කළා
        );

        Map<Integer, List<Student>> studentsByGrade = students.stream()
            .collect(Collectors.groupingBy(Student::getGrade));

        System.out.println("Students by Grade: " + studentsByGrade);
        // Output: Students by Grade: {10=[Sunil (Grade: 10), Ruwan (Grade: 10)], 11=[Nandana (Grade: 11)], 12=[Kamala (Grade: 12), Saman (Grade: 12)]}
    }
}

groupingBy වලට දෙවෙනි argument එකක් විදිහට තව Downstream Collector එකක් දෙන්නත් පුළුවන්. ඒකෙන් Group වුන data වලට තව operations කරන්න පුළුවන්. උදාහරණයක් විදිහට, අපිට පුළුවන් එකම grade එකේ ඉන්න Student ලා කී දෙනෙක් ඉන්නවද කියලා group කරන්න.

import java.util.Map;
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;

// Student class definition (as above)

public class GroupingByWithDownstreamCollector {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Sunil", 10),
            new Student("Kamala", 12),
            new Student("Ruwan", 10),
            new Student("Nandana", 11),
            new Student("Saman", 12)
        );

        Map<Integer, Long> studentCountByGrade = students.stream()
            .collect(Collectors.groupingBy(Student::getGrade, Collectors.counting()));

        System.out.println("Student Count by Grade: " + studentCountByGrade);
        // Output: Student Count by Grade: {10=2, 11=1, 12=2}
    }
}

Mapping & Reducing (සිතියම්කරණය සහ අඩු කිරීම)

Collectors.mapping()

groupingBy එක්ක හරි වෙන Collector එකක් එක්ක හරි, collect කරන්න කලින් elements transform කරන්න ඕන නම් Collectors.mapping() පාවිච්චි කරන්න පුළුවන්. මේකෙන් පුළුවන් data ටික map කරලා, ඊට පස්සේ ඒවා collector එකකට දෙන්න. උදාහරණයක් විදිහට, අපි Student ලා grade අනුව group කරලා, ඒ group එකේ ඉන්න Student ලගේ නම් විතරක් ගන්න බලමු.

import java.util.Map;
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;

// Student class definition (as above)

public class MappingCollectorExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Sunil", 10),
            new Student("Kamala", 12),
            new Student("Ruwan", 10),
            new Student("Nandana", 11),
            new Student("Saman", 12)
        );

        Map<Integer, List<String>> namesByGrade = students.stream()
            .collect(Collectors.groupingBy(Student::getGrade,
                                          Collectors.mapping(Student::getName, Collectors.toList())));

        System.out.println("Names by Grade: " + namesByGrade);
        // Output: Names by Grade: {10=[Sunil, Ruwan], 11=[Nandana], 12=[Kamala, Saman]}
    }
}

Collectors.reducing()

Collectors.reducing() කියන්නේ Stream එකේ elements ටික reduce කරලා, තනි result එකක් ගන්න පාවිච්චි කරන Collector එකක්. මේක Stream.reduce() method එකට සමාන වුනත්, Collector එකක් විදිහට පාවිච්චි කරන්න පුළුවන් වීම තමයි විශේෂත්වය. මේකට initial value එකක්, mapping function එකක් (element එක වෙනත් ආකාරයකට transform කරන්න), සහ binary operator එකක් (elements එකතු කරන්න) දෙන්න පුළුවන්.

import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;

// Student class definition (as above)

public class ReducingCollectorExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Sunil", 10),
            new Student("Kamala", 12),
            new Student("Ruwan", 10),
            new Student("Nandana", 11),
            new Student("Saman", 12)
        );

        // Student ලගේ grade වල එකතුව ගන්නවා
        Integer totalGradeSum = students.stream()
            .collect(Collectors.reducing(
                0, // Initial value
                Student::getGrade, // Mapper: Student object එක grade එකට convert කරනවා
                Integer::sum // Accumulator: grades එකතු කරනවා
            ));
        System.out.println("Total Grade Sum: " + totalGradeSum); // Output: Total Grade Sum: 55 (10+12+10+11+12)
    }
}

දැන් ඔයාලට පේනවා ඇති reducing() කොච්චර powerful ද කියලා. Complex aggregations මේකෙන් පහසුවෙන් කරන්න පුළුවන්.

Common Collectors (සාමාන්‍යයෙන් භාවිතා වන Collectors)

මේවා තමයි අපි නිතරම වගේ පාවිච්චි කරන, ඒ වගේම ගොඩක්ම ප්‍රයෝජනවත් Collectors ටික:

  • Collectors.toList(): Stream එකේ elements ටික List එකකට convert කරනවා. (අපි මුලින්ම දැක්කා)
  • Collectors.toSet(): Stream එකේ elements ටික Set එකකට convert කරනවා. Duplicates (පුනරාවර්තන) නැති කරන්න නියමයි.
  • Collectors.toMap(keyMapper, valueMapper): Stream එකේ elements ටික Map එකකට convert කරනවා. Key එකක් සහ Value එකක් හදාගන්න විදිහ specify කරන්න ඕන. හැබැයි මේකේ පොඩි ගැටලුවක් එන්න පුළුවන්. ඒ තමයි එකම Key එකට values දෙකක් තියෙනවා නම්. ඒ වගේ වෙලාවට IllegalStateException එකක් එන්න පුළුවන්. ඒක වළක්වා ගන්න, අපිට තුන්වෙනි argument එකක් විදිහට merge function එකක් දෙන්න පුළුවන්. ඒකෙන් පුළුවන් එකම Key එකට values දෙකක් එනකොට ඒ values කොහොම handle කරන්නද කියලා කියන්න.
  • Collectors.joining(): Stream එකක තියෙන CharSequence elements ටික එක string එකකට join කරනවා. මේකට delimiter එකක්, prefix එකක් සහ suffix එකක් දෙන්නත් පුළුවන්.
  • Collectors.counting(): Stream එකේ elements ගාන count කරනවා. (අපි කලින් groupingBy එක්ක දැක්කා)
  • Collectors.summingInt/Long/Double(), Collectors.averagingInt/Long/Double(): Stream එකක තියෙන numeric values sum කරන්න සහ average එක ගන්න පාවිච්චි කරනවා.
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;

// Student class definition (as above)

public class AveragingCollectorExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Sunil", 10),
            new Student("Kamala", 12),
            new Student("Ruwan", 10),
            new Student("Nandana", 11),
            new Student("Saman", 12)
        );

        Double averageGrade = students.stream()
            .collect(Collectors.averagingDouble(Student::getGrade));
        System.out.println("Average Grade: " + averageGrade); // Output: Average Grade: 10.75
    }
}
import java.util.List;
import java.util.Arrays;
import java.util.stream.Collectors;

// Student class definition (as above)

public class JoiningCollectorExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Sunil", 10),
            new Student("Kamala", 12),
            new Student("Ruwan", 10)
        );

        String allNames = students.stream()
            .map(Student::getName) // Student name එක ගන්නවා
            .collect(Collectors.joining(", ", "Names: [", "]")); // comma and space වලින් join කරනවා
        System.out.println(allNames); // Output: Names: [Sunil, Kamala, Ruwan]
    }
}
import java.util.List;
import java.util.Map;
import java.util.Arrays;
import java.util.stream.Collectors;

// Student class definition (as above)

public class ToMapExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Sunil", 10),
            new Student("Kamala", 12),
            new Student("Ruwan", 10) // Ruwan also has grade 10
        );

        // සාමාන්‍ය toMap, key duplicate වුනොත් error එනවා.
        // Map<String, Integer> studentGradeMap = students.stream()
        //     .collect(Collectors.toMap(Student::getName, Student::getGrade));

        // Key එක duplicate වුනොත් existing value එක තියාගන්න merge function එකක් එක්ක.
        Map<String, Integer> studentGradeMapMerged = students.stream()
            .collect(Collectors.toMap(Student::getName, Student::getGrade,
                                (existingValue, newValue) -> existingValue)); // Keep existing value
        System.out.println("Student Grade Map: " + studentGradeMapMerged);
        // Output: Student Grade Map: {Sunil=10, Kamala=12, Ruwan=10}
    }
}
import java.util.List;
import java.util.Set;
import java.util.Arrays;
import java.util.stream.Collectors;

// Student class definition (as above)

public class ToSetExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
            new Student("Sunil", 10),
            new Student("Kamala", 12),
            new Student("Ruwan", 10) // Ruwan also has grade 10
        );

        Set<Integer> uniqueGrades = students.stream()
            .map(Student::getGrade) // Student object එකෙන් grade එක ගන්නවා
            .collect(Collectors.toSet());
        System.out.println("Unique Grades: " + uniqueGrades); // Output: Unique Grades: [10, 12] (Order might vary)
    }
}

Custom Collectors (ඔබේම Collectors)

සමහර වෙලාවට අපිට මේ default Collectors වලින් කරන්න බැරි custom collection logic එකක් අවශ්‍ය වෙන්න පුළුවන්. ඒ වගේ වෙලාවට අපිට පුළුවන් Collector.of() method එක පාවිච්චි කරලා අපේම Custom Collector එකක් හදාගන්න. මේකට supplier, accumulator, combiner, සහ finisher කියන functional interfaces ටික දෙන්න ඕන.

මේක ටිකක් advanced topic එකක්. ඒත් ඔයාලට අවශ්‍ය නම් මෙන්න මේ වගේ format එකක් පාවිච්චි කරන්න පුළුවන්:

import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

public class CustomCollectorDefinition {
    public static <T, A, R> Collector<T, A, R> createCustomCollector() {
        return Collector.of(
            (Supplier<A>) () -> {
                // Creates a new mutable result container
                // e.g., new ArrayList<>();
                return null; // Replace with actual supplier logic
            },
            (BiConsumer<A, T>) (container, element) -> {
                // Adds an element to the result container
                // e.g., container.add(element);
            },
            (BinaryOperator<A>) (container1, container2) -> {
                // Combines two result containers (for parallel streams)
                // e.g., container1.addAll(container2); return container1;
                return null; // Replace with actual combiner logic
            },
            (Function<A, R>) container -> {
                // Performs final transformation on the result container
                // e.g., Collections.unmodifiableList(container);
                return null; // Replace with actual finisher logic
            },
            Collector.Characteristics.IDENTITY_FINISH // Or other characteristics
        );
    }
}

මේක තවත් article එකකට කතා කරන්න පුළුවන් තරම් ලොකු topic එකක්. ඒත් දැනට මෙහෙම එකක් හදන්න පුළුවන් බව දැනගෙන ඉන්න එක වටිනවා. මේකෙන් ඔයාලට Streams වලින් කරන්න පුළුවන් දේවල් වල සීමාව ගොඩක් දුරට වැඩි වෙනවා.

අවසාන වශයෙන්...

ඉතින් යාලුවනේ, Java 8 Streams එක්ක වැඩ කරනකොට මේ Collectors ගැන හොඳ අවබෝධයක් තියෙන එක ඔයාලගේ code එක ගොඩක් clean, effective, ඒ වගේම කියවන්න ලේසි කරන්න උපකාරී වෙනවා. Stream එකක data ටික process කරලා, අන්තිමට අවශ්‍ය විදිහට format කරලා ගන්න මේ Collectors නැතුවම බැහැ. මේවා Spring Boot වගේ frameworks වලදී data manipulate කරන්න ගොඩක් පාවිච්චි වෙනවා. Project වලදී complex business logic implement කරනකොට මේ Collectors පාවිච්චි කරලා code එකේ readability එක වැඩි කරගන්නත් පුළුවන්. ඒ වගේම performance wise බලනකොටත් Stream API එක optimized නිසා මේක ගොඩක් හොඳ විසඳුමක්.

මේක practice කරන එක තමයි හොඳම දේ. පුළුවන් නම් ඔයාලගේ project එකක පොඩි තැනකින් හරි මේ Collectors පාවිච්චි කරලා බලන්න. එතකොට තේරෙයි මේකේ පහසුව. මුලදී ටිකක් අමාරු වගේ පෙනුනත්, ටිකෙන් ටික පුරුදු වෙනකොට මේක ඔයාලට නැතුවම බැරි දෙයක් වෙයි. Java development වල ඉස්සරහට යන්න මේ Streams සහ Collectors කියන්නේ අනිවාර්යයෙන්ම දැනගෙන ඉන්න ඕන දෙයක්.

මේ ගැන ඔයාලට මොනවා හරි ප්‍රශ්න තියෙනවා නම්, එහෙමත් නැත්නම් ඔයාලා දන්න collectors tricks මොනවා හරි තියෙනවා නම් පහලින් comment එකක් දාගෙන යන්න. ඔයාලගේ අදහස් අපිට ගොඩක් වටිනවා. ඒ වගේම මේ article එක ඔයාලට වැදගත් වුණා නම් share කරන්නත් අමතක කරන්න එපා. තවත් මේ වගේම වැදගත් Java topic එකක් අරගෙන ඉක්මනටම හම්බවෙමු! තෙරුවන් සරණයි!