Java CompletableFuture Sinhala Guide | Asynchronous Programming Simplified

Java CompletableFuture Sinhala Guide | Asynchronous Programming Simplified

ආයුබෝවන් යාළුවනේ!

අද අපි කතා කරන්න යන්නේ Java වල Asynchronous Programming කියන ලෝකයේ තියෙන, වැඩිය කතා බහට ලක් නොවුණත්, බලවත්ම tool එකක් ගැනයි – ඒ තමයි CompletableFuture.

දැන් ඔබ හිතනවා ඇති මේ මොනවද කියලා. අපේ software applications, විශේෂයෙන්ම web applications හෝ microservices, හැදීමේදී speed එක සහ responsiveness එක කියන්නේ හරිම වැදගත් දෙයක්. සාම්ප්‍රදායිකව අපි වැඩ කරන විදිහට (Synchronous) එක වැඩක් ඉවර වෙනකම් ඊළඟ වැඩේ පටන් ගන්න බලාගෙන ඉන්න සිද්ධ වෙනවා. මේක ටිකක් හෙමින් වැඩ කරන system වලට ප්‍රශ්නයක් නෙවෙයි, ඒත් high-performance, real-time applications වලට මේක ලොකු බාධාවක්.

හිතන්නකෝ, ඔබගේ application එක database එකෙන් data ටිකක් ගන්නවා, ඊටපස්සේ තව external API එකකින් data ටිකක් ගන්නවා, පස්සේ ඒ data ටික processing කරලා user ට පෙන්නනවා කියලා. මේ හැමදේම එකින් එක synchronous විදියට කළොත්, මුළු process එකටම ගොඩක් වෙලා යන්න පුළුවන්. ඒක users ලාට නීරස අත්දැකීමක් වෙන්න පුළුවන්.

ඔන්න ඔය වගේ වෙලාවට තමයි Asynchronous Programming කියන concept එක අපිට උදව් වෙන්නේ. මේකෙන් පුළුවන් එක වැඩක් කරගෙන යන ගමන් තව වැඩ කිහිපයක් එකම වෙලාවේ පටන් අරගෙන, ඒවා ඉවර වුණාම result එක notify කරන්න. Java වලට මේ සඳහා Thread, ExecutorService, Callable වගේ දේවල් තිබුණත්, ඒවා ගොඩක් වෙලාවට complex සහ maintain කරන්න අමාරුයි. ඔන්න ඕකට නියම solution එකක් විදිහට තමයි Java 8 වලින් CompletableFuture හඳුන්වා දුන්නේ.

මේ tutorial එකෙන් අපි CompletableFuture කියන්නේ මොකක්ද, ඒක කොහොමද භාවිතා කරන්නේ, ඒකෙන් මොන වගේ වාසිද අපිට ලැබෙන්නේ වගේ දේවල් පියවරෙන් පියවර කතා කරමු. ඔබට මේක ඔබේ project වලට apply කරන්න අවශ්‍ය practical knowledge එක අපි ලබා දෙනවා. එහෙනම් පටන් ගමු!

Asynchronous Programming කියන්නේ මොකක්ද? (What is Asynchronous Programming?)

සරලව කිව්වොත්, Asynchronous Programming කියන්නේ tasks එකිනෙකට ස්වාධීනව (independently) run කරන්න පුළුවන් programming paradigm එකක්. මෙතනදී, ප්‍රධාන thread එක (main thread) task එකක් ආරම්භ කරලා, ඒක ඉවර වෙනකම් බලාගෙන ඉන්නේ නැතුව ඊළඟ task එකට යනවා. පස්සේ, පළවෙනි task එක ඉවර වුණාම, ඒක result එක notify කරනවා.

මේක හරියට restaurant එකකට ගියාම order කරනවා වගේ. ඔබ order එක දුන්නම, chef කෑම හදනකම් ඔබ බලාගෙන ඉන්නේ නැහැ. ඔබ වෙන වැඩක් කරනවා (ෆෝන් එක බලනවා, යාළුවෙක් එක්ක කතා කරනවා). කෑම එක ready වුණාම waiter ඇවිත් ඔබට ඒ බව දැනුම් දෙනවා. Asynchronous Programming කියන්නේ අන්න ඒ වගේ දෙයක් තමයි.

Synchronous විදිහට නම්, ඔබ order එක දීලා, chef කෑම එක හදලා, ඔබට කෑම එක දෙනකම් ඔබ ඇස් පියාගෙන, වෙන කිසිම දෙයක් නොකර, බලාගෙන ඉන්නවා වගේ වැඩක්.

මේ නිසා Asynchronous Programming වලදී, system එකේ resource utilization එක වැඩි වෙනවා වගේම, application එකේ responsiveness එකත් සැලකිය යුතු ලෙස වැඩි වෙනවා. විශේෂයෙන්ම I/O-bound operations (database calls, network requests) වලට මේක ඉතාමත්ම වැදගත්.

Future Interface එකේ සීමාවන් (Limitations of the Future Interface)

Java 5 වලදී java.util.concurrent.Future interface එක හඳුන්වා දුන්නා asynchronous operations handle කරන්න. මේකෙන් අපිට Callable task එකක් ExecutorService එකකට submit කරලා, ඒ task එකේ result එක Future object එකක් විදිහට ලබා ගන්න පුළුවන්.

ඒත්, Future interface එකට ලොකු සීමාවන් කිහිපයක් තිබුණා:

  1. Blocking get(): Future.get() method එක synchronous විදිහට වැඩ කරන්නේ. ඒ කියන්නේ result එක එනකම් main thread එක block වෙනවා. මේක Asynchronous Programming එකේ අරමුණටම පටහැනි.
  2. No Easy Chaining/Composition: එක asynchronous task එකක result එකක් අරගෙන, ඒකෙන් තව asynchronous task එකක් පටන් ගන්න (chaining) හෝ tasks කිහිපයක් එකට join කරන්න (composition) Future වලින් පහසුවෙන් කරන්න බැහැ. මේ සඳහා complex callbacks හෝ manual thread management කරන්න වෙනවා.
  3. No Exception Handling: Task එකක් fail වුණොත්, ඒක handle කරන්න direct mechanism එකක් Future වලට නැහැ. get() call එකේදී exception එකක් එනකම් බලාගෙන ඉන්න වෙනවා.
  4. No Completion Notification: Future එකක් complete වුණාම auto-notify කරන විදිහක් නැහැ. අපිට polling කරන්න සිද්ධ වෙනවා (isDone() method එකෙන්).

මේ සීමාවන් නිසා complex asynchronous flows build කරන්න Future එක එච්චර ප්‍රායෝගික වුණේ නැහැ. අන්න ඒකට විසඳුමක් විදිහට තමයි Java 8 වලදී CompletableFuture හඳුන්වා දුන්නේ.

CompletableFuture: Java වල අලුත් තරුණයා (CompletableFuture: The New Star in Java)

CompletableFuture කියන්නේ Future interface එකේ implementation එකක් වගේම, ඒකේ තියෙන සීමාවන් වලට විසඳුම් සපයන, වඩාත් flexible සහ powerful class එකක්. මේක අපිට non-blocking operations පහසුවෙන් manage කරන්න, tasks chain කරන්න, errors handle කරන්න සහ multiple asynchronous tasks එකට join කරන්න පහසුකම් සලසනවා.

CompletableFuture වල ප්‍රධාන වාසි මොනවද?

  • Non-blocking operations: Main thread එක block නොකර, result එක ready වුණාම ඒකට react කරන්න පුළුවන්.
  • Declarative Chaining: thenApply(), thenAccept(), thenCompose() වගේ methods වලින් tasks පහසුවෙන් chain කරන්න පුළුවන්. මේක code එක කියවන්න සහ maintain කරන්න පහසු කරනවා.
  • Robust Error Handling: exceptionally(), handle() වගේ methods වලින් asynchronous errors පහසුවෙන් manage කරන්න පුළුවන්.
  • Composition: allOf(), anyOf() වගේ methods වලින් tasks කිහිපයක් එකට එකතු කරන්න පුළුවන්.

CompletableFuture නිර්මාණය කරන හැටි (How to Create CompletableFutures)

CompletableFuture එකක් හදන්න ප්‍රධාන විදි 2ක් තියෙනවා:

  1. runAsync(): යම්කිසි ක්‍රියාවක් (action) Runnable එකක් විදිහට run කරන්න. මේකෙන් කිසිම result එකක් return කරන්නේ නැහැ.
  2. supplyAsync(): යම්කිසි ක්‍රියාවක් Supplier එකක් විදිහට run කරලා result එකක් return කරන්න.

මේ දෙකටම default විදිහට ForkJoinPool.commonPool() එක භාවිතා කරනවා. අවශ්‍ය නම්, අපිට වෙනත් Executor එකක් specify කරන්නත් පුළුවන්.

උදාහරණය (Example):

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class CompletableFutureCreation {

    public static void main(String[] args) {

        // 1. runAsync() - කිසිම result එකක් නැතිව task එකක් run කරන්න
        CompletableFuture<Void> future1 = CompletableFuture.runAsync(() -> {
            System.out.println("Runnable Task: Thread එක " + Thread.currentThread().getName() + " එකේ දුවනවා.");
            // යම්කිසි වෙලාවක් ගන්න වැඩක්
            try {
                Thread.sleep(1000); // milliseconds
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Runnable Task: වැඩේ ඉවරයි.");
        });

        // 2. supplyAsync() - result එකක් return කරන task එකක් run කරන්න
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            System.out.println("Supplier Task: Thread එක " + Thread.currentThread().getName() + " එකේ දුවනවා.");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return "Hello from CompletableFuture!";
        });

        // Custom Executor එකක් භාවිතා කිරීම
        ExecutorService customExecutor = Executors.newFixedThreadPool(2);
        CompletableFuture<Integer> future3 = CompletableFuture.supplyAsync(() -> {
            System.out.println("Custom Executor Task: Thread එක " + Thread.currentThread().getName() + " එකේ දුවනවා.");
            return 100;
        }, customExecutor);

        // Results බලාගෙන ඉන්න
        try {
            future1.get(); // runAsync වලට result එකක් නැති නිසා Void
            System.out.println("Future2 result: " + future2.get());
            System.out.println("Future3 result: " + future3.get());
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            customExecutor.shutdown(); // ExecutorService එක shutdown කරන්න අමතක කරන්න එපා
        }

        System.out.println("Main Thread එක තව වැඩ කරගෙන යනවා.");
    }
}

මේ code එක run කළාම ඔබට පෙනේවි Main Thread එක tasks ඉවර වෙනකම් block වෙන්නේ නැතිව අනිත් වැඩ ටිකත් කරගෙන යනවා කියලා. Tasks ටික වෙන threads වල run වෙනවා.

Async Operations එකතු කිරීම (Chaining Async Operations)

CompletableFuture වල තියෙන ලොකුම වාසියක් තමයි tasks chain කරන්න තියෙන පහසුව. Task එකක් ඉවර වුණාට පස්සේ ඊළඟ task එක automatically trigger කරන්න පුළුවන්. මේ සඳහා thenApply(), thenAccept(), thenRun() වගේ methods භාවිතා කරනවා.

  • thenApply(Function): කලින් task එකේ result එක අරගෙන, ඒක transform කරලා අලුත් result එකක් return කරනවා.
  • thenAccept(Consumer): කලින් task එකේ result එක අරගෙන, ඒකෙන් යම්කිසි ක්‍රියාවක් (side-effect) සිදු කරනවා, නමුත් result එකක් return කරන්නේ නැහැ.
  • thenRun(Runnable): කලින් task එක ඉවර වුණාට පස්සේ කිසිම result එකක් නැතිව task එකක් run කරනවා.

මේ methods වලට Async suffix එක එකතු කිරීමෙන් (thenApplyAsync(), thenAcceptAsync(), thenRunAsync()) පුළුවන් ඊළඟ task එක වෙනම thread එකක run කරන්න. නැත්නම්, කලින් task එක run වුණ thread එකේම ඊළඟ task එක run වෙන්න පුළුවන් (ඒක thread pool එකේ availability එක අනුව තීරණය වෙනවා).

උදාහරණය (Example):

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class CompletableFutureChaining {

    public static void main(String[] args) {
        CompletableFuture<String> welcomeMessage = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1); // තත්පර 1ක් බලා සිටී
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Step 1: Data fetched from DB. " + Thread.currentThread().getName());
            return "Hello, Devs!";
        })
        .thenApply(name -> { // thenApply - result එක transform කරනවා
            System.out.println("Step 2: Transforming data. " + Thread.currentThread().getName());
            return name + " Welcome to our tutorial.";
        })
        .thenAccept(finalMessage -> { // thenAccept - result එක accept කරලා side-effect එකක් කරනවා
            System.out.println("Step 3: Displaying final message. " + Thread.currentThread().getName());
            System.out.println(finalMessage);
        })
        .thenRun(() -> { // thenRun - result එකක් නැතිව ක්‍රියාවක් කරනවා
            System.out.println("Step 4: All operations completed. " + Thread.currentThread().getName());
        });

        // Main thread එකට future එක complete වෙනකම් බලාගෙන ඉන්න
        // මේක නැත්නම් main thread එක ඉවර වෙලා program එක shut down වෙන්න පුළුවන්
        try {
            welcomeMessage.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("Main Thread done its work.");
    }
}

මේ උදාහරණයෙන් පෙනෙනවා එක task එකක result එක අරගෙන, ඒකෙන් තව task එකක් කොහොමද පටන් ගන්නේ කියලා. මේකෙන් code එක readability වැඩි වෙනවා වගේම, callback hell වගේ ප්‍රශ්න වලින් මිදෙන්නත් පුළුවන්.

දෙකක් එකට (Combining Two CompletableFutures)

සමහර වෙලාවට අපිට asynchronous tasks දෙකක results එකට අවශ්‍ය වෙනවා ඊළඟ task එක පටන් ගන්න. මේ සඳහා thenCombine() සහ thenCompose() භාවිතා කරනවා.

  • thenCombine(otherFuture, BiFunction): ස්වාධීන CompletableFuture දෙකක් parallel run කරලා, ඒවා දෙකම complete වුණාට පස්සේ ඒවායේ results combine කරලා අලුත් result එකක් හදනවා.
  • thenCompose(Function): මේක chaining වලටත් යම් සමානකමක් තියෙනවා. එක CompletableFuture එකක result එකක් අරගෙන, ඒ result එකෙන් තව CompletableFuture එකක් return කරනවා (flattening nested Futures). මේක thenApply වලට වඩා වෙනස් වෙන්නේ thenCompose එකෙන් CompletableFuture එකක් return කරන නිසා.

thenCombine() උදාහරණය:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class CompletableFutureCombine {

    public static void main(String[] args) {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return "Hello";
        });

        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1); // මේක කලින් ඉවර වෙනවා
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            return "World";
        });

        // future1 සහ future2 දෙකම complete වුණාට පස්සේ ඒවායේ results combine කරනවා
        CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> {
            System.out.println("Combining results. " + Thread.currentThread().getName());
            return result1 + " " + result2 + "!";
        });

        try {
            System.out.println("Combined result: " + combinedFuture.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

මේකේදී future1 සහ future2 parallel run වෙනවා. future2 එක කලින් ඉවර වුණත්, thenCombine block එක run වෙන්නේ future1 එකත් ඉවර වුණාට පස්සේ.

thenCompose() උදාහරණය:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class CompletableFutureCompose {

    public static void main(String[] args) {
        // පළමු task එක: User ID එකක් ගන්නවා
        CompletableFuture<String> userIdFuture = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.MILLISECONDS.sleep(500);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Fetched user ID: " + Thread.currentThread().getName());
            return "user123";
        });

        // දෙවන task එක: User ID එකෙන් user details ගන්නවා
        // මේක Compose කරන්නේ, දෙවන task එක පටන් ගන්න පළමු task එකේ result එක ඕන නිසා
        CompletableFuture<String> userDetailsFuture = userIdFuture.thenCompose(userId ->
            CompletableFuture.supplyAsync(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(700);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("Fetched details for " + userId + ": " + Thread.currentThread().getName());
                return "Details for " + userId + ": Name=Alice, [email protected]";
            })
        );

        try {
            System.out.println("User details: " + userDetailsFuture.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

thenCompose එක thenApply එකට වඩා වෙනස් වෙන්නේ, thenCompose එකේ Function එක CompletableFuture එකක් return කරන නිසා. thenApply එකෙන් සාමාන්‍ය Object එකක් return කරනවා. thenCompose භාවිතා කරන්නේ dependent asynchronous operations chain කරන්න.

දෝෂ කළමනාකරණය (Error Handling)

Asynchronous operations වලදී errors handle කරන එකත් හරිම වැදගත්. CompletableFuture මේ සඳහාත් හොඳ පහසුකම් සලසනවා:

  • exceptionally(Function): කලින් stage එකේදී exception එකක් ආවොත්, මේ method එකෙන් ඒක catch කරලා default value එකක් return කරන්න හෝ recovery logic එකක් run කරන්න පුළුවන්. මේක Java catch block එකට සමානයි.
  • handle(BiFunction): මේක exceptionally() වලට වඩා flexible. Task එක successful වුණත්, fail වුණත් මේ method එක run වෙනවා. Successful නම් result එකත්, fail නම් exception එකත් ලබා දෙනවා. මේක Java finally block එකට සමානයි.

උදාහරණය (Example):

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class CompletableFutureErrorHandling {

    public static void main(String[] args) {

        // exceptionally() උදාහරණය
        CompletableFuture<String> errorFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("Task 1: Thread " + Thread.currentThread().getName());
            if (true) { // දෝෂයක් ඇති කරනවා
                throw new RuntimeException("Data fetching failed!");
            }
            return "Some Data";
        }).exceptionally(ex -> { // Exception එකක් ආවොත් මේ block එක run වෙනවා
            System.out.println("Caught exception in exceptionally(): " + ex.getMessage() + " by Thread " + Thread.currentThread().getName());
            return "Default Data due to error"; // Default value එකක් return කරනවා
        });

        try {
            System.out.println("Exceptionally result: " + errorFuture.get());
        } catch (Exception e) {
            e.printStackTrace(); // මෙතනට එන්නේ exceptionally() වලින් exception එක handle නොකර ඉඳියොත් විතරයි
        }

        System.out.println("--------------------");

        // handle() උදාහරණය
        CompletableFuture<String> handleFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("Task 2: Thread " + Thread.currentThread().getName());
            // if (true) { // මෙතනත් දෝෂයක් ඇති කළොත් handle() එක ඒකත් අල්ලනවා
            //     throw new RuntimeException("Another failure!");
            // }
            return "Successfully fetched data";
        }).handle((result, ex) -> { // result එකක් ආවත්, exception එකක් ආවත් මේ block එක run වෙනවා
            if (ex != null) {
                System.out.println("Caught exception in handle(): " + ex.getMessage() + " by Thread " + Thread.currentThread().getName());
                return "Handled Error Data";
            } else {
                System.out.println("Handled success in handle(): " + result + " by Thread " + Thread.currentThread().getName());
                return "Handled Success: " + result;
            }
        });

        try {
            System.out.println("Handle result: " + handleFuture.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

මේ methods වලින් අපිට asynchronous code එකේ errors robust විදිහට handle කරන්න පුළුවන්.

සියලුම/ඕනෑම එකක් සම්පූර්ණ කිරීම (Completing All/Any Of)

සමහර වෙලාවට අපිට asynchronous tasks කිහිපයක් එකම වෙලාවේ run කරලා, ඒ සියල්ලම complete වෙනකම් හෝ ඒ අතරින් එකක් හෝ complete වෙනකම් බලාගෙන ඉන්න වෙනවා. මේ සඳහා allOf() සහ anyOf() methods භාවිතා කරනවා.

  • CompletableFuture.allOf(CompletableFuture...): දී ඇති CompletableFuture සියල්ලම complete වන තෙක් බලා සිටිනවා. මේකෙන් CompletableFuture<Void> එකක් return කරන්නේ. ඒ කියන්නේ individual results ලබා ගන්න වෙනම get() call කරන්න වෙනවා.
  • CompletableFuture.anyOf(CompletableFuture...): දී ඇති CompletableFuture අතරින් ඕනෑම එකක් complete වන තෙක් බලා සිටිනවා. මේකෙන් CompletableFuture<Object> එකක් return කරන්නේ.

උදාහරණය (Example):

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.Arrays;
import java.util.List;

public class CompletableFutureAllOfAnyOf {

    public static void main(String[] args) {

        CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Task 1 Done.");
            return "Result from Task 1";
        });

        CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Task 2 Done.");
            return "Result from Task 2";
        });

        CompletableFuture<String> task3 = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1); // මේක කලින්ම ඉවර වෙනවා
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Task 3 Done.");
            return "Result from Task 3";
        });

        // allOf() උදාහරණය
        CompletableFuture<Void> allTasks = CompletableFuture.allOf(task1, task2, task3);

        allTasks.thenRun(() -> {
            System.out.println("\nAll tasks are completed!");
            // individual results ලබා ගන්න
            // මෙතනදී get() call කරන එක safe, මොකද tasks ඔක්කොම ඉවරයි
            try {
                System.out.println("Task 1 result: " + task1.get());
                System.out.println("Task 2 result: " + task2.get());
                System.out.println("Task 3 result: " + task3.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        // allTasks.get() call කරන එක main thread එක block කරනවා
        // ඒත් demonstration එකට අපිට results print කරන්න ඕන නිසා මෙතනදී get() පාවිච්චි කරනවා
        // production code වලදී මේ වගේ අවස්ථාවක join() use කරන්න පුළුවන්.
        try {
            allTasks.get();
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("\n--------------------");

        // anyOf() උදාහරණය
        CompletableFuture<Object> anyOneTask = CompletableFuture.anyOf(task1, task2, task3);

        anyOneTask.thenAccept(result -> {
            System.out.println("Any one task completed! Result: " + result);
        });

        try {
            anyOneTask.get(); // මේක first completed task එකේ result එක return කරනවා
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

මේ methods වලින් අපිට parallel processing patterns පහසුවෙන් implement කරන්න පුළුවන්.

ප්‍රායෝගික උදාහරණයක්: Online Order Process එකක් (A Practical Example: Online Order Process)

හිතන්න, ඔබ online order එකක් place කරනවා. මේ order එක process කරන්න tasks කිහිපයක් එකම වෙලාවේ run වෙන්න පුළුවන්:

  1. Order එක Database එකට save කරනවා.
  2. Inventory එක update කරනවා.
  3. Payment Process කරනවා (external service).
  4. Customer ට Email එකක් යවනවා (external service).

මේවා එකින් එක synchronous විදිහට කළොත්, මුළු process එකටම ගොඩක් වෙලා යයි. CompletableFuture භාවිතයෙන් අපිට මේවා parallel run කරන්න පුළුවන්.

උදාහරණය (Example):

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class OrderProcessor {

    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        CompletableFuture<Void> saveOrder = CompletableFuture.runAsync(() -> {
            System.out.println("Saving Order to DB... " + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(2); // Simulate DB call
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Order Saved.");
        });

        CompletableFuture<String> updateInventory = CompletableFuture.supplyAsync(() -> {
            System.out.println("Updating Inventory... " + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(3); // Simulate API call
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Inventory Updated.");
            return "Inventory_Update_Success";
        });

        CompletableFuture<String> processPayment = CompletableFuture.supplyAsync(() -> {
            System.out.println("Processing Payment... " + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1); // Simulate external payment gateway
                // throw new RuntimeException("Payment Failed!"); // Payment fail කරන්න පුළුවන්
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Payment Processed.");
            return "Payment_Success";
        }).exceptionally(ex -> {
            System.err.println("Error processing payment: " + ex.getMessage());
            return "Payment_Failed";
        });

        // සියලු tasks සම්පූර්ණ වන තෙක් බලා සිටීම
        CompletableFuture<Void> allFutures = CompletableFuture.allOf(saveOrder, updateInventory, processPayment);

        // සියල්ල ඉවර වූ පසු email යැවීම
        CompletableFuture<Void> finalStage = allFutures.thenRunAsync(() -> {
            System.out.println("\nAll core tasks completed. Sending confirmation email... " + Thread.currentThread().getName());
            try {
                TimeUnit.SECONDS.sleep(1); // Simulate email sending
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            System.out.println("Confirmation Email Sent.");
            
            // Results පරීක්ෂා කිරීම (optional)
            try {
                String inventoryStatus = updateInventory.get();
                String paymentStatus = processPayment.get();
                System.out.println("Final Status Check: Inventory = " + inventoryStatus + ", Payment = " + paymentStatus);
            } catch (Exception e) {
                e.printStackTrace();
            }

        });

        try {
            finalStage.get(); // Final stage complete වන තෙක් main thread එක බලා සිටිනවා
        } catch (Exception e) {
            e.printStackTrace();
        }

        long endTime = System.currentTimeMillis();
        System.out.println("\nOrder Processing Completed in " + (endTime - startTime) + " ms.");
    }
}

මේ උදාහරණයෙන් පෙනෙනවා synchronous විදිහට කළා නම් තත්පර 1 + 2 + 3 + 1 = 7ක් විතර යන වැඩක් (longest running task එක තත්පර 3ක් නිසා) තත්පර 4-5ක් වැනි අඩු කාලයකින් ඉවර කරන්න පුළුවන් විදිහ. මේක main thread එක block නොකර කරන්නේ. ඒක applications වල responsiveness එකට සහ scalability එකට ඉතාම වැදගත්.

නිගමනය (Conclusion)

CompletableFuture කියන්නේ Java ecosystem එකේ Asynchronous Programming සඳහා තියෙන හරිම බලවත් tool එකක්. Future interface එකේ තිබුණ සීමාවන් මඟ හරවමින්, මේක අපිට non-blocking operations පහසුවෙන් handle කරන්න, tasks chain කරන්න, errors manage කරන්න සහ multiple asynchronous tasks effective විදිහට combine කරන්න උදව් කරනවා.

අද අපි runAsync(), supplyAsync() වලින් CompletableFuture නිර්මාණය කරන හැටි, thenApply(), thenAccept(), thenRun() වලින් tasks chain කරන හැටි, thenCombine(), thenCompose() වලින් tasks combine කරන හැටි, exceptionally(), handle() වලින් errors manage කරන හැටි, වගේම allOf(), anyOf() වලින් tasks එකතු කරන හැටිත් ඉගෙන ගත්තා. ඒ වගේම real-world order processing example එකකින් මේවා කොහොමද ප්‍රායෝගිකව භාවිතා කරන්නේ කියලාත් අපි බැලුවා.

ඔබගේ Java applications වල performance එක සහ responsiveness එක වැඩි දියුණු කරගන්නට CompletableFuture ගැන මේ ඉගෙන ගත්තු දේවල් අනිවාර්යයෙන්ම උදව් වේවි. මේක තවදුරටත් ගවේෂණය කරන්න, ඔබේ projects වලට apply කරන්න උත්සාහ කරන්න. මේ ගැන ඔබට අදහස්, ප්‍රශ්න හෝ අත්දැකීම් තියෙනවා නම් පහලින් comment කරන්න. අපි ඊළඟ tutorial එකකින් නැවත හමුවෙමු!