Java 8 Stream API: ලේසියෙන් Collection Process කරන හැටි | SC Guide

Java 8 Stream API: ලේසියෙන් Collection Process කරන හැටි | SC Guide

කොහොමද යාලුවනේ! ඉතින් අද අපි කතා කරන්න යන්නේ Java programming වල තියෙන සුපිරිම feature එකක් ගැන. ඒ තමයි Java 8 එක්ක ආපු Stream API එක. Java developer කෙනෙක් විදිහට මේක නොදැන ඉන්න බැරිම දෙයක්. Stream API එක අපේ code එක ගොඩක් clean, readable වගේම efficient කරන්න උදව් වෙනවා. ඒ නිසා අපි බලමු මේක හරියටම මොකක්ද කියලා, කොහොමද පාවිච්චි කරන්නේ කියලා, ඒ වගේම අපේ project වලට මේකෙන් ගන්න පුළුවන් වාසි මොනවද කියලත්.

ඉස්සර අපි collection එකක් process කරන්න ගියාම (array, list වගේ) ගොඩක් වෙලාවට for-each loop, for loop වගේ ඒවා තමයි පාවිච්චි කළේ. උදාහරණයක් විදිහට, List එකක තියෙන numbers filter කරන්න, transform කරන්න, sum කරන්න ගියාම ගොඩක් lines of code ලියන්න වෙනවා. ඒ වගේම code එක කියවන්නත් ටිකක් අමාරුයි. හැබැයි Stream API එක ආවට පස්සේ මේ වැඩේ ගොඩක් ලේසි වුණා. අපි බලමු ඒ කොහොමද කියලා.

Stream API කියන්නේ මොකක්ද?

සරලව කිව්වොත් Stream එකක් කියන්නේ data sequence එකක්. මේක එක පාරටම processing operations ගණනාවක් කරන්න පුළුවන් pipeline එකක් වගේ. Collection එකක් වගේ data structure එකක් නෙවෙයි. Stream එකක් කියන්නේ collection එකක තියෙන elements වලට aggregate operations කරන්න පුළුවන් flow එකක් වගේ දෙයක්. මේකේ තියෙන ලොකුම වාසිය තමයි අපිට declarative programming style එකට code කරන්න පුළුවන් වීම. ඒ කියන්නේ "කොහොමද කරන්නේ?" (how to do) කියන එකට වඩා "මොකක්ද කරන්න ඕනේ?" (what to do) කියන එකට අවධානය යොමු කරන්න පුළුවන් වීම.

උදාහරණයක් විදිහට, List එකක තියෙන numbers වලින් 10ට වැඩි numbers විතරක් අරගෙන, ඒ හැම number එකක්ම දෙකෙන් වැඩි කරලා, අන්තිමට ඒ හැම එකක්ම print කරන්න ඕනේ කියලා හිතමු. සාමාන්‍ය විදිහට නම් අපි for loop එකක් ඇතුලේ if conditions දාලා, අලුත් List එකකට add කරගෙන වගේ තමයි මේක කරන්නේ. හැබැයි Stream API එකෙන් මේක එක line එකකින් වගේ ලියන්න පුළුවන්. මරු නේද?

Stream Operations: Intermediate vs. Terminal

Stream API එකේ operations වර්ග දෙකක් තියෙනවා. ඒ තමයි Intermediate operations සහ Terminal operations. මේ දෙක තේරුම් ගන්න එක Stream එකක් හරියට පාවිච්චි කරන්න අත්‍යවශ්‍යයි.

Intermediate Operations

Intermediate operations කියන්නේ stream එකක් තවත් stream එකක් බවට transform කරන operations. මේවා lazy evaluation කියන concept එකට අනුව වැඩ කරනවා. ඒ කියන්නේ මේ operations call කරපු ගමන් execute වෙන්නේ නැහැ. Terminal operation එකක් call කරනකම් මේවා queue එකක වගේ තියෙනවා. Stream එකක් අවසානයේදී Terminal operation එකක් call කරපුවාම තමයි මේ හැම intermediate operation එකක්ම එකට execute වෙන්නේ.

  • filter(Predicate<T> predicate): Stream එකක තියෙන elements වලින් දීපු condition එකට match වෙන elements විතරක් filter කරනවා.
  • map(Function<T, R> mapper): Stream එකක තියෙන හැම element එකක්ම වෙනත් type එකකට transform කරනවා. (e.g., List of objects to List of their names).
  • distinct(): Stream එකක තියෙන duplicate elements remove කරනවා.
  • sorted(): Stream එකේ elements sort කරනවා. (natural order or custom comparator).
  • limit(long maxSize): Stream එකේ මුල් elements ගණනක් විතරක් ගන්නවා.
  • skip(long n): Stream එකේ මුල් elements ගණනක් skip කරනවා.

උදාහරණයක්:

List<String> names = Arrays.asList("Alice", "Bob", "Anna", "Charlie", "David", "Anna");

// Intermediate operations example
List<String> filteredNames = names.stream()
    .filter(name -> name.startsWith("A")) // 'A' වලින් පටන් ගන්න නම්
    .distinct() // duplicate නම් අයින් කරනවා
    .sorted() // alphabetical order එකට sort කරනවා
    .collect(Collectors.toList()); // Terminal operation එක මෙතන
System.out.println(filteredNames); // Output: [Alice, Anna]

Terminal Operations

Terminal operations කියන්නේ stream එකේ process එක ඉවර කරන operations. මේවා call කරපුවම තමයි intermediate operations සෙට් එකත් එක්කම Stream pipeline එක execute වෙන්නේ. මේ operations වලින් අන්තිමට result එකක් return කරනවා (Collection, single value, void, etc.). Stream එකකට එක Terminal operation එකක් විතරයි තියෙන්න පුළුවන්. Terminal operation එකක් call කරාට පස්සේ ඒ Stream එක ආයෙත් පාවිච්චි කරන්න බැහැ.

  • forEach(Consumer<T> action): Stream එකේ හැම element එකක්ම iterate කරලා action එකක් apply කරනවා. (No return value - void).
  • collect(Collector<T, A, R> collector): Stream එකේ elements Collection එකකට (List, Set, Map) හෝ වෙනත් single result එකකට reduce කරනවා. (e.g., Collectors.toList(), Collectors.toSet()).
  • reduce(BinaryOperator<T> accumulator) or reduce(T identity, BinaryOperator<T> accumulator): Stream එකේ elements single result එකකට combine කරනවා.
  • count(): Stream එකේ elements ගණන return කරනවා.
  • min(Comparator<T> comparator) / max(Comparator<T> comparator): Stream එකේ minimum / maximum element එක return කරනවා. (Returns an Optional).
  • findFirst() / findAny(): Stream එකේ මුල්ම element එක හෝ ඕනෑම element එකක් return කරනවා. (Returns an Optional).
  • anyMatch(Predicate<T> predicate) / allMatch(Predicate<T> predicate) / noneMatch(Predicate<T> predicate): Stream එකේ elements දීපු condition එකට match වෙනවද කියලා check කරනවා. (Returns boolean).

උදාහරණයක්:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// forEach example
System.out.println("Numbers using forEach:");
numbers.stream()
    .filter(n -> n % 2 == 0) // Even numbers only
    .forEach(System.out::println); // Prints: 2, 4, 6, 8, 10

// Sum using reduce
int sum = numbers.stream()
    .reduce(0, (a, b) -> a + b); // Start with 0, add each number
System.out.println("Sum of numbers: " + sum); // Output: 55

// Check if any number is greater than 7
boolean anyGreaterThanSeven = numbers.stream()
    .anyMatch(n -> n > 7);
System.out.println("Any number greater than 7? " + anyGreaterThanSeven); // Output: true

Stream හදන හැටි (How to Create Streams)

Stream එකක් හදාගන්න විවිධ ක්‍රම තියෙනවා. අපි වැඩියෙන්ම පාවිච්චි කරන ක්‍රම ටිකක් බලමු.

  • From Collections: Collection එකක (List, Set) තියෙන elements වලින් Stream එකක් හදාගන්න stream() හෝ parallelStream() method එක පාවිච්චි කරන්න පුළුවන්.
  • From Arrays: Array එකකින් Stream එකක් හදාගන්න Arrays.stream() method එක පාවිච්චි කරන්න පුළුවන්.
  • From Individual Elements: Stream.of() method එක පාවිච්චි කරලා individual elements වලින් Stream එකක් හදාගන්න පුළුවන්.
  • Using Stream.iterate(): Infinite Stream එකක් හදන්න මේක පාවිච්චි කරන්න පුළුවන්. (careful with infinite streams, always use limit()).
  • Using Stream.generate(): Supplier එකක් පාවිච්චි කරලා Stream එකක් generate කරන්න පුළුවන්. (Also infinite, use limit()).
// Generate 5 random numbers
Stream.generate(Math::random)
      .limit(5)
      .forEach(System.out::println);
    
// First 10 even numbers
Stream.iterate(0, n -> n + 2)
      .limit(10)
      .forEach(System.out::println);
    
Stream<String> stream3 = Stream.of("hello", "world");
    
String[] myArray = {"x", "y", "z"};
Stream<String> stream2 = Arrays.stream(myArray);
    
List<String> myList = Arrays.asList("a", "b", "c");
Stream<String> stream1 = myList.stream();
    

Practical Examples and Use Cases

Stream API එකේ බලය තේරුම් ගන්න හොඳම ක්‍රමය තමයි real-world scenarios වලදී මේක කොහොමද පාවිච්චි කරන්නේ කියලා බලන එක.

උදාහරණ 1: Student object List එකක් filter කරලා, transform කරලා, collect කරන එක

අපිට Student class එකක් තියෙනවා කියලා හිතමු, ඒකේ name, age, grade වගේ fields තියෙනවා.

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

class Student {
    private String name;
    private int age;
    private char grade;

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

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

    @Override
    public String toString() {
        return "Student{" + "name='" + name + '\'' + ", age=" + age + ", grade=" + grade + '}';
    }
}

List<Student> students = Arrays.asList(
    new Student("Kasun", 22, 'A'),
    new Student("Nimal", 20, 'B'),
    new Student("Amara", 23, 'A'),
    new Student("Sunil", 21, 'C'),
    new Student("Dilini", 20, 'A')
);

// Grade 'A' තියෙන, වයස 21ට වැඩි Student ලාගේ නම් විතරක් List එකකට ගන්න.
List<String> topStudentsNames = students.stream()
    .filter(s -> s.getGrade() == 'A' && s.getAge() > 21) // Grade 'A' and age > 21
    .map(Student::getName) // Student object එකෙන් name විතරක් ගන්නවා
    .collect(Collectors.toList()); // Result එක List එකකට collect කරනවා

System.out.println("Top Students (Grade A & > 21 years old): " + topStudentsNames);
// Output: Top Students (Grade A & > 21 years old): [Kasun, Amara]

මෙතනදී බලන්න, අපි loop එකක් දාලා if conditions ගණනාවක් ලියනවා වෙනුවට, එක pipeline එකකින් වැඩේ කරලා තියෙනවා. Code එක කොච්චර clean ද කියලා.

උදාහරණ 2: Data Grouping සහ Summarizing

Collectors.groupingBy() සහ Collectors.summarizingInt() වගේ දේවල් Streams එක්ක පාවිච්චි කරලා data summarize කරන්න පුළුවන්.

import java.util.IntSummaryStatistics;
import java.util.Map;
import java.util.stream.Collectors;

// ... Student class as above (assume it's defined or imported)

Map<Character, List<Student>> studentsByGrade = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade)); // Grade එක අනුව group කරනවා

System.out.println("Students grouped by Grade: " + studentsByGrade);
/* Output will be something like:
Students grouped by Grade: {A=[Student{name='Kasun', age=22, grade=A}, Student{name='Amara', age=23, grade=A}, Student{name='Dilini', age=20, grade=A}], B=[Student{name='Nimal', age=20, grade=B}], C=[Student{name='Sunil', age=21, grade=C}]}
*/

// Each grade category එකේ students ලගේ age statistics ගන්න
Map<Character, IntSummaryStatistics> gradeAgeStatistics = students.stream()
    .collect(Collectors.groupingBy(Student::getGrade,
        Collectors.summarizingInt(Student::getAge)));

System.out.println("Grade Age Statistics: " + gradeAgeStatistics);
/* Output will be something like:
Grade Age Statistics: {A=IntSummaryStatistics{count=3, sum=65, min=20, average=21.666667, max=23}, B=IntSummaryStatistics{count=1, sum=20, min=20, average=20.000000, max=20}, C=IntSummaryStatistics{count=1, sum=21, min=21, average=21.000000, max=21}}
*/

මෙතනදී බලන්න, එක code block එකකින් grade එක අනුව students ලා group කරලා, ඒ හැම group එකකම students ලගේ වයස් වල statistics (count, sum, min, max, average) ගන්න පුළුවන් වෙලා තියෙනවා. පට්ට නේද? කලින් නම් මේකට loops ගොඩක් දාලා data structures ගොඩක් හදන්න වෙනවා.

Streams පාවිච්චි කරන එකේ වාසි

Stream API එක අපේ code base එකට ගොඩක් වටිනවා. මේක පාවිච්චි කරන එකේ ප්‍රධාන වාසි ටිකක් තමයි:

  • Readable and Concise Code: Stream pipeline එකක් කියවන්න ගොඩක් ලේසියි. මොකද මේක declarative නිසා අපිට මොකක්ද කරන්නේ කියලා පැහැදිලිව තේරෙනවා. Code lines ගණනත් අඩුවෙනවා.
  • Parallelism: Stream API එක built-in parallelism support කරනවා. parallelStream() method එක පාවිච්චි කරලා අපිට collection එකක් multi-core processors වල parallel process කරන්න පුළුවන්. Data processing වලදී performance වැඩි කරගන්න මේක ගොඩක් උදව් වෙනවා.
  • Reduced Boilerplate: Loops, if-else statements වගේ පුන පුනා එන code රටා අඩු කරන්න පුළුවන්.
  • Functional Programming Style: Java වලට functional programming elements ගේන්න Stream API එක ගොඩක් උදව් වුණා. Lambda expressions, method references වගේ ඒවා එක්ක මේකේ වැඩ කරන්න පුළුවන්.
  • Lazy Evaluation: Intermediate operations lazy evaluation කරන නිසා, අනවශ්‍ය calculations eliminate කරන්න පුළුවන්. මේක performance වලට ගොඩක් හොඳයි.

අවසන් වශයෙන්

Java 8 Stream API එක Java programming වල game changer එකක්. Collection process කරන විදිහ මුළුමනින්ම වෙනස් කළා කිව්වොත් හරි. මේක අපේ code එක cleaner, more readable, scalable, වගේම maintainable කරන්න උදව් වෙනවා. මුලින් ටිකක් අලුත් වගේ දැනුනත්, පුරුදු වුණාම මේක නැතුව code කරන්න බැරි වෙයි. දැන් ඉතින් පුරුදු වෙන්න කාලේ හරි!

ඉතින් යාලුවනේ, මේ blog post එකෙන් Java Stream API එක ගැන මූලික අදහසක් ගන්න පුළුවන් වුණා කියලා හිතනවා. ඔයාලට මේ ගැන තවත් විස්තර දැනගන්න ඕනේ නම්, නැත්නම් මොකක් හරි ගැටලුවක් තියෙනවනම් පහළින් comment එකක් දාන්න. අපි උදව් කරන්න ලෑස්තියි! Java Stream API පාවිච්චි කරලා ඔයාලගේ අත්දැකීම් මොනවද කියලත් comment කරන්න අමතක කරන්න එපා. එහෙනම් ආයෙත් දවසක තවත් සුපිරි article එකකින් හම්බවෙමු! තෙරුවන් සරණයි!