Java CompletableFuture Basics: Asynchronous Programming Sinhala Guide

Java CompletableFuture Basics: Asynchronous Programming Sinhala Guide

ඔබ Java Developer කෙනෙක් නම්, Applications Develop කරනකොට තත්පර ගණනක් Block වෙලා තියෙනවා දැකලා ඇති, නේද? සමහර වෙලාවට Data Fetch කරන වෙලාවට, නැත්නම් File එකක් Read කරන වෙලාවට වගේ අවස්ථාවලදී අපේ Application එක රිස්පොන්ස් කරන්නේ නැති වෙන්න පුළුවන්. ඒ කියන්නේ User Interface එක Freezed වෙලා වගේ දැනෙන්න පුළුවන්.

මේ වගේ අවස්ථාවලට විසඳුම් විදියට තමයි Asynchronous Programming කියන Concept එක එන්නේ. සාමාන්‍යයෙන් අපිට Java වල Threads පාවිච්චි කරන්න පුළුවන් වුණාට, ඒක හරියටම Manage කරන එක සහ ඒකෙන් එන Errors Handle කරන එක ලේසි වැඩක් නෙවෙයි. Java 5 වලදී හඳුන්වා දුන්නු Future Interface එකෙන් පොඩි පහසුවක් ලැබුණත්, ඒකත් Blocking Operation එකක් නිසා තවදුරටත් අපිට ඒක Improve කරන්න ඕන වුණා.

මේ ගැටලුවට Java 8 එක්ක ආපු සුපිරිම Solution එක තමයි CompletableFuture. මේකෙන් අපිට Non-blocking Operations ලේසියෙන් Manage කරන්න, Tasks Chain කරන්න, සහ Errors Elegant විදියට Handle කරන්න පුළුවන් වෙනවා.

අද මේ Guide එකෙන් අපි CompletableFuture කියන්නේ මොකක්ද, ඒක මොකටද පාවිච්චි කරන්නේ, සහ ඒක Practical විදියට අපේ Code වලට කොහොමද එකතු කරගන්නේ කියලා පියවරෙන් පියවර බලමු. එහෙනම්, අපි පටන් ගමු!

Future API එකේ සීමාවන් සහ CompletableFuture එකේ වාසි

මුලින්ම අපි බලමු Future Interface එකේ තිබ්බ පොඩි අඩුපාඩු ටිකක්. සාමාන්‍යයෙන් Future එකක් පාවිච්චි කරන්නේ Thread එකක Run වෙන Task එකක Result එකක් ගන්න. උදාහරණයක් විදියට:

import java.util.concurrent.*;

public class FutureExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        System.out.println("Starting a new task...");
        Future<String> future = executor.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(2); // Simulate long-running task
                return "Hello from the task!";
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            }
        });

        // Main thread might do other things here
        System.out.println("Main thread is doing something else...");

        // Blocking call - main thread waits until the result is available
        System.out.println("Waiting for the result...");
        String result = future.get(); 
        System.out.println("Result: " + result);

        executor.shutdown();
    }
}

මේ Code එකේ තියෙන ප්‍රධානම ගැටලුව තමයි future.get() කියන Call එක. මේක Synchronous, Blocking Call එකක්. ඒ කියන්නේ Task එක ඉවර වෙනකම් Main Thread එකට බලාගෙන ඉන්න වෙනවා. මේකෙන් වෙන්නේ Task එකේ Result එක එනකම් Application එකේ User Interface එක Freezed වෙන්න පුළුවන්, නැත්නම් වෙන Work ටිකක් කරන්න බැරිව යන්න පුළුවන්.

CompletableFuture මේ ගැටලුවට විසඳුම් දෙනවා. මේක Non-blocking. ඒ කියන්නේ Task එක ඉවර වෙනකම් බලාගෙන ඉන්න ඕනේ නැහැ. Task එක ඉවර වුණාම මොකද වෙන්න ඕනේ කියලා අපිට Callback Methods පාවිච්චි කරලා කියන්න පුළුවන්. ඒ වගේම CompletableFuture වලට තව Features ගොඩක් තියෙනවා:

  • Chaining Transformations: එක Task එකක් ඉවර වුණාට පස්සේ ඒකේ Result එක පාවිච්චි කරලා තව Task එකක් පටන් ගන්න පුළුවන්.
  • Combining Multiple Futures: එක එක Future වල Results එකට එකතු කරලා Process කරන්න පුළුවන්.
  • Error Handling: Errors ලේසියෙන් Handle කරන්න පුළුවන්.
  • Explicit Completion: අපිට Manual විදියට Future එකක Result එක Set කරන්න පුළුවන්.

CompletableFuture මූලිකාංග: පටන් ගනිමු!

CompletableFuture එකක් හදාගන්න සහ ඒකේ Basic Operations ටිකක් කරගන්න ක්‍රම කීපයක් තියෙනවා. අපි එකින් එක බලමු.

1. CompletableFuture නිර්මාණය කිරීම (Creating CompletableFuture)

a. Result එකක් එක්ක Task එකක් Run කරන්න: supplyAsync()

මේ Method එක පාවිච්චි කරන්නේ Result එකක් Return කරන Task එකක් Asynchronously Run කරන්න. මේකට Supplier Interface එකක් දෙනවා.

import java.util.concurrent.*;

public class CompletableFutureBasics {
    public static void main(String[] args) throws InterruptedException {
        // supplyAsync - returns a result
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                System.out.println("Fetching data in a separate thread...");
                TimeUnit.SECONDS.sleep(3); // Simulate network call
                return "Data fetched successfully!";
            } catch (InterruptedException e) {
                throw new IllegalStateException(e);
            }
        });

        // Main thread continues execution
        System.out.println("Main thread is busy doing other stuff...");

        // We can do other things here without waiting
        // ...

        // To see the result, you can block for demonstration, but typically we'd use callbacks.
        // We will see callbacks next!
        try {
            System.out.println("Result (blocking just for demo): " + future.get());
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        // Give some time for async tasks to complete if not using .get()
        TimeUnit.SECONDS.sleep(4);
    }
}

b. Result එකක් නැතුව Task එකක් Run කරන්න: runAsync()

මේ Method එක පාවිච්චි කරන්නේ Result එකක් Return නොකරන Task එකක් Asynchronously Run කරන්න. මේකට Runnable Interface එකක් දෙනවා.

CompletableFuture<Void> futureAction = CompletableFuture.runAsync(() -> {
    System.out.println("Running some background process without returning a result.");
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("Background process finished.");
});
// You can wait for it if needed, but the point is not to block.
// futureAction.join(); // Blocks until completion

2. CompletableFuture එකක Result එකක් Process කිරීම (Processing Results)

CompletableFuture එකක Result එකක් ආවට පස්සේ ඒක Process කරන්න පාවිච්චි කරන පොදු Methods කීපයක් තියෙනවා:

a. Result එකක් Transform කරන්න: thenApply()

thenApply() කියන්නේ CompletableFuture එකක Result එකක් ගෙන, ඒක වෙනත් Type එකකට Transform කරලා, තවත් CompletableFuture එකක් Return කරන Method එකක්. මේකට Function එකක් දෙනවා.

CompletableFuture<String> initialFuture = CompletableFuture.supplyAsync(() -> {
    System.out.println("Getting raw data...");
    return "Hello World";
});

CompletableFuture<Integer> transformedFuture = initialFuture.thenApply(s -> {
    System.out.println("Transforming data...");
    return s.length(); // Get length of the string
});

transformedFuture.thenAccept(length -> {
    System.out.println("Length of the string: " + length);
});

// Add a sleep to ensure async tasks complete before main thread exits
TimeUnit.SECONDS.sleep(1);

b. Result එකක් Consume කරන්න: thenAccept()

මේක thenApply() වගේමයි, හැබැයි Result එකක් Transform කරන්නේ නැහැ. ඒ වෙනුවට, ලැබෙන Result එකක් මත පදනම්ව යම්කිසි Action එකක් විතරයි කරන්නේ. මේකට Consumer එකක් දෙනවා.

CompletableFuture.supplyAsync(() -> "කෑම එක ලෑස්තියි!")
    .thenAccept(message -> {
        System.out.println("අම්මාගෙන් ලැබුණු පණිවිඩය: " + message);
        System.out.println("දැන් කෑම කන්න යමු!");
    });

TimeUnit.SECONDS.sleep(1); // Keep main thread alive

c. Result එකක් නැතුව Action එකක් කරන්න: thenRun()

thenRun() පාවිච්චි කරන්නේ කලින් CompletableFuture එක Successful විදියට Complete වුණාට පස්සේ, Result එකත් එක්ක වැඩක් නැතුව, වෙන Action එකක් කරන්න.

CompletableFuture.supplyAsync(() -> {
    System.out.println("Data download complete.");
    return "some_data";
})
.thenRun(() -> {
    System.out.println("Notification: Data processing has started!");
});

TimeUnit.SECONDS.sleep(1); // Keep main thread alive

CompletableFutures චේන් කිරීම සහ ඒකාබද්ධ කිරීම (Chaining and Combining)

CompletableFuture වල තියෙන ලොකුම වාසියක් තමයි අපිට මේවා Chain කරන්න සහ එකට එකතු කරන්න පුළුවන් වීම. මේකෙන් Complex Asynchronous Workflows හදාගන්න පුළුවන්.

1. Task Sequence එකක් හදන්න: thenCompose()

thenCompose() කියන්නේ එක CompletableFuture එකක Result එක පාවිච්චි කරලා, අලුත් CompletableFuture එකක් හදන්න පාවිච්චි කරන Method එකක්. මේක thenApply() වගේමයි, හැබැයි Function එක Return කරන්නේ CompletableFuture එකක්. මේකෙන් Nested Futures Flat කරන්න පුළුවන්.

CompletableFuture<String> fetchUserId = CompletableFuture.supplyAsync(() -> {
    System.out.println("Fetching User ID...");
    try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) {} // Simulate delay
    return "user123";
});

CompletableFuture<String> fetchUserDetails = fetchUserId.thenCompose(userId -> {
    System.out.println("Fetching details for user: " + userId);
    return CompletableFuture.supplyAsync(() -> {
        try { TimeUnit.MILLISECONDS.sleep(700); } catch (InterruptedException e) {} // Simulate another delay
        return "User Details for " + userId + ": Name=Nimal, [email protected]";
    });
});

fetchUserDetails.thenAccept(details -> {
    System.out.println("Final result: " + details);
});

TimeUnit.SECONDS.sleep(2); // Keep main thread alive

2. Futures දෙකක් Combine කරන්න: thenCombine()

thenCombine() පාවිච්චි කරන්නේ එකිනෙකට ස්වාධීන Futures දෙකක Results එකට එකතු කරලා Process කරන්න. මේකට BiFunction එකක් දෙනවා.

CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
    System.out.println("Getting price...");
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {} 
    return "Rs. 5000";
});

CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
    System.out.println("Getting delivery time...");
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) {} 
    return "2 days";
});

CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (price, deliveryTime) -> {
    return "Product Price: " + price + ", Estimated Delivery: " + deliveryTime;
});

combinedFuture.thenAccept(result -> {
    System.out.println("Combined Info: " + result);
});

TimeUnit.SECONDS.sleep(2); // Keep main thread alive

3. Futures ගොඩක් Complete වෙනකම් ඉන්න: allOf()

allOf() පාවිච්චි කරන්නේ Futures ගොඩක් Complete වෙනකම් බලාගෙන ඉන්න. හැබැයි මේකෙන් Results Return කරන්නේ නැහැ. ඒ වෙනුවට CompletableFuture<Void> එකක් Return කරනවා.

CompletableFuture<String> task1 = CompletableFuture.supplyAsync(() -> {
    try { TimeUnit.MILLISECONDS.sleep(800); } catch (InterruptedException e) {} 
    System.out.println("Task 1 Done"); return "Result 1";
});
CompletableFuture<String> task2 = CompletableFuture.supplyAsync(() -> {
    try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) {} 
    System.out.println("Task 2 Done"); return "Result 2";
});
CompletableFuture<String> task3 = CompletableFuture.supplyAsync(() -> {
    try { TimeUnit.MILLISECONDS.sleep(1200); } catch (InterruptedException e) {} 
    System.out.println("Task 3 Done"); return "Result 3";
});

CompletableFuture<Void> allTasks = CompletableFuture.allOf(task1, task2, task3);

allTasks.thenRun(() -> {
    System.out.println("All tasks completed!");
    // If you need results, you have to get them individually after allOf completes
    try {
        System.out.println("Task 1 result: " + task1.join());
        System.out.println("Task 2 result: " + task2.join());
        System.out.println("Task 3 result: " + task3.join());
    } catch (CompletionException e) {
        System.err.println("One or more tasks failed: " + e.getMessage());
    }
});

TimeUnit.SECONDS.sleep(3); // Keep main thread alive

දෝෂ හැසිරවීම (Error Handling)

Asynchronous Operations කරනකොට Errors එන එක සාමාන්‍ය දෙයක්. CompletableFuture වලට මේ Errors Elegant විදියට Handle කරන්න පහසුකම් සලසලා තියෙනවා.

1. Exception එකකින් Recover වෙන්න: exceptionally()

මේ Method එක පාවිච්චි කරන්නේ CompletableFuture එකක් Exception එකක් එක්ක Complete වුණොත්, ඒක Recover කරලා Default Value එකක් Return කරන්න.

CompletableFuture<String> futureWithError = CompletableFuture.supplyAsync(() -> {
    System.out.println("Task with potential error...");
    if (Math.random() < 0.5) {
        throw new RuntimeException("Simulated network error!");
    }
    return "Success Data";
});

futureWithError.exceptionally(ex -> {
    System.err.println("Error occurred: " + ex.getMessage());
    return "Fallback Data"; // Return a default value on error
}).thenAccept(data -> {
    System.out.println("Processed Data: " + data);
});

TimeUnit.SECONDS.sleep(1); // Keep main thread alive

2. සාමාන්‍ය සහ දෝෂ සහිත Result දෙකම Handle කරන්න: handle()

handle() කියන්නේ CompletableFuture එකක Successful Result එකක් වුණත්, Exception එකක් වුණත් දෙකම Handle කරන්න පුළුවන් Method එකක්. මේකට BiFunction එකක් දෙනවා, ඒකේ පළවෙනි Parameter එක Result එකටත්, දෙවෙනි Parameter එක Exception එකටත් පාවිච්චි කරනවා.

CompletableFuture<String> futureWithHandle = CompletableFuture.supplyAsync(() -> {
    System.out.println("Another task with error potential...");
    if (Math.random() < 0.7) {
        throw new IllegalArgumentException("Invalid input!");
    }
    return "Valid Output";
});

futureWithHandle.handle((result, ex) -> {
    if (ex != null) {
        System.err.println("Handled error: " + ex.getMessage());
        return "Error fallback value";
    } else {
        System.out.println("Handled success: " + result);
        return result + " (processed)";
    }
}).thenAccept(finalResult -> {
    System.out.println("Final Result from handle: " + finalResult);
});

TimeUnit.SECONDS.sleep(1); // Keep main thread alive

නිගමනය (Conclusion)

Java 8 එක්ක ආපු CompletableFuture කියන්නේ Asynchronous Programming ලෝකයේ අපිට ලැබුණු ලොකු තල්ලුවක්. මේකෙන් අපේ Java Applications වල Performance සහ Responsiveness වැඩි කරගන්න පුළුවන්. Blocking Calls වෙනුවට Non-blocking Operations පාවිච්චි කරන්න, Complex Workflows Chain කරන්න, සහ Errors ලස්සනට Handle කරන්න CompletableFuture අපිට උදව් කරනවා.

මතක තියාගන්න, Concurrency කියන්නේ පොඩ්ඩක් Complex Topic එකක්. ඒ නිසා මේ Concepts හොඳට තේරුම් අරගෙන, Practice කරන එක ගොඩක් වැදගත්. ඔබේ Project වලට මේවා එකතු කරලා බලන්න. ඒකෙන් ලැබෙන වාසි ඔබටම තේරේවි!

මේ Guide එක ඔයාට CompletableFuture ගැන හොඳ අවබෝධයක් ලබාදෙන්න ඇති කියලා හිතනවා. ඔබත් CompletableFuture පාවිච්චි කරලා තියෙනවද? නැත්නම් මේ ගැන අලුතෙන් ඉගෙන ගත්තු දෙයක් තියෙනවද? පහළින් Comment එකක් දාගෙන යන්න!