Lesson ১৫৮ মিনিট পড়া

ল্যাম্বডা এবং স্ট্রিমস (Lambdas & Streams)

আধুনিক জাভা — মার্জিত, সংক্ষিপ্ত, শক্তিশালী

ল্যাম্বডাস — ফাংশনের ভ্যালু হিসেবে কাজ করা (Lambdas — Functions as Values)

জাভা ৮ আসার আগে, যদি আপনি কোনো মেথডে সে কীভাবে কাজ করবে তা পাঠাতে চাইতেন, তবে আপনাকে পুরো একটি ক্লাস তৈরি করতে হতো। এটি অনেকটা একটি স্টিকি নোটের (sticky note) কাজ মেটানোর জন্য আস্ত একটা চিঠির পাতা লিখে ফেলার মতো। ল্যাম্বডাস (Lambdas) হলো সেই ছোট স্টিকি নোটের মতোই — অতি ক্ষুদ্র, নামহীন বা অ্যাননিমাস ফাংশন (anonymous functions), যাদের সহজে এক জায়গা থেকে আরেক জায়গায় পাঠানো যায়।

এর লেখার নিয়ম বা সিনট্যাক্স (syntax) বেশ সহজ: (parameters) -> expression

কিছু উদাহরণ:

  • (a, b) -> a + b — দুটি ভ্যালু গ্রহণ করে এবং তাদের যোগফল ফেরত দেয় বা রিটার্ন করে
  • (s) -> s.length() — একটি স্ট্রিং গ্রহণ করে এবং তার দৈর্ঘ্য (length) ফেরত দেয়
  • () -> System.out.println("Hello!") — কিছুই গ্রহণ করে না, শুধু hello প্রিন্ট করে

ল্যাম্বডা ফাংশনাল ইন্টারফেসগুলোর (functional interfaces) সাথে কাজ করে — এগুলো হলো এমন সব ইন্টারফেস, যাদের ঠিক একটি মাত্র অ্যাবস্ট্রাক্ট মেথড (abstract method) থাকে। জাভা আমাদের আগে থেকেই তৈরি করে রাখা এমন অনেকগুলো ফাংশনাল ইন্টারফেস দেয়। যেমন: Predicate (পরীক্ষা করার জন্য), Function (রূপান্তর করার জন্য), Consumer (কাজ করার জন্য) এবং Comparator (তুলনা করার জন্য)।

ল্যাম্বডার ব্যাসিকস এবং ফাংশনাল ইন্টারফেস (Lambda Basics & Functional Interfaces)

import java.util.ArrayList;
import java.util.Collections;
import java.util.Arrays;
import java.util.List;
public class Main {
// A functional interface — has exactly ONE abstract method
interface MathOperation {
int operate(int a, int b);
}
static int calculate(int a, int b, MathOperation op) {
return op.operate(a, b);
}
public static void main(String[] args) {
// Lambda as a variable
MathOperation add = (a, b) -> a + b;
MathOperation multiply = (a, b) -> a * b;
MathOperation max = (a, b) -> a > b ? a : b;
System.out.println("5 + 3 = " + calculate(5, 3, add));
System.out.println("5 * 3 = " + calculate(5, 3, multiply));
System.out.println("max(5,3) = " + calculate(5, 3, max));
System.out.println("---");
// Sorting with lambdas — so much cleaner!
List<String> names = new ArrayList<>(Arrays.asList(
"Sadia", "Anika", "Rafi", "Tariq"
));
// Sort alphabetically
Collections.sort(names, (a, b) -> a.compareTo(b));
System.out.println("Alphabetical: " + names);
// Sort by length
Collections.sort(names, (a, b) -> a.length() - b.length());
System.out.println("By length: " + names);
// forEach with lambda
System.out.print("Each: ");
names.forEach(n -> System.out.print(n + " "));
System.out.println();
}
}
Output
5 + 3 = 8
5 * 3 = 15
max(5,3) = 5
---
Alphabetical: [Anika, Rafi, Sadia, Tariq]
By length: [Rafi, Anika, Tariq, Sadia]
Each: Rafi Anika Tariq Sadia

স্ট্রিমস — পাইপলাইনের মতো ডেটা প্রসেস করা (Streams — Processing Data Like a Pipeline)

একটি স্ট্রিমকে (stream) কারখানার কনভেয়ার বেল্টের (conveyor belt) মতো ভাবতে পারেন। ডেটার আইটেমগুলো এক প্রান্ত দিয়ে ঢোকে, এরপর বিভিন্ন স্টেশনের (যেমন ফিল্টার করা, ট্রান্সফর্ম বা রূপান্তর করা, সাজানো ইত্যাদি) ভেতর দিয়ে পার হয় এবং শেষ পর্যন্ত একটি সম্পূর্ণ বা ফিনিশড প্রোডাক্ট (finished product) হিসেবে অন্য প্রান্ত থেকে বেরিয়ে আসে।

স্ট্রিম যেকোনো মূল ডেটা বা আসল ডেটাকে পরিবর্তন করে না — তারা একটি নতুন ফলাফল তৈরি করে। তাছাড়া এরা খুবই অলস প্রকৃতির বা লেজি (lazy) হয়ে থাকে: যতক্ষণ না আপনি একটি টার্মিনাল অপারেশন (terminal operation) কল করছেন (যেমন: collect(), forEach(), অথবা count()), ততক্ষণ এরা কোনো কাজই শুরু করে না।

স্ট্রিমের সবচেয়ে পরিচিত কিছু অপারেশন বা কাজ:

  • filter() — যেসব আইটেম শর্তের সাথে মিলে যায়, কেবল তাদেরই রেখে দেয়
  • map() — প্রতিটি আইটেমকে অন্য কোনো রূপে ট্রান্সফর্ম বা রূপান্তর করে
  • sorted() — আইটেমগুলোকে সুন্দরভাবে সাজিয়ে রাখে
  • reduce() — সবগুলো আইটেমকে একটি ফলাফলে একত্রিত করে ফেলে
  • collect() — ফলাফলগুলোকে একটি লিস্ট, সেট, অথবা অন্য কোনো কালেকশনের (collection) ভেতর জড়ো করে

ব্যাবহারিক ক্ষেত্রে স্ট্রিমস (Streams in Action)

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Filter: keep only even numbers
List<Integer> evens = numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println("Evens: " + evens);
// Map: square each number
List<Integer> squares = numbers.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println("Squares: " + squares);
// Chain them: filter + map + sort
List<Integer> result = numbers.stream()
.filter(n -> n > 3) // keep 4,5,6,7,8,9,10
.map(n -> n * 2) // double them
.filter(n -> n < 16) // keep under 16
.sorted() // sort
.collect(Collectors.toList());
System.out.println("Chained: " + result);
// Reduce: sum all numbers
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
System.out.println("Sum: " + sum);
// Count
long count = numbers.stream()
.filter(n -> n > 5)
.count();
System.out.println("Numbers > 5: " + count);
}
}
Output
Evens: [2, 4, 6, 8, 10]
Squares: [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
Chained: [8, 10, 12, 14]
Sum: 55
Numbers > 5: 5

স্ট্রিং এবং মেথড রেফারেন্সসহ স্ট্রিমস (Streams with Strings & Method References)

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class Main {
public static void main(String[] args) {
List<String> words = Arrays.asList(
"hello", "world", "java", "streams", "are", "awesome", "hi"
);
// Method reference :: — shorthand for simple lambdas
// Instead of: s -> s.toUpperCase()
List<String> upper = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
System.out.println("Upper: " + upper);
// Filter words longer than 3 chars, sort, join into one string
String sentence = words.stream()
.filter(w -> w.length() > 3)
.sorted()
.collect(Collectors.joining(", "));
System.out.println("Long words: " + sentence);
// Find first match
String first = words.stream()
.filter(w -> w.startsWith("a"))
.findFirst()
.orElse("none found");
System.out.println("First 'a' word: " + first);
// Check conditions
boolean anyLong = words.stream().anyMatch(w -> w.length() > 5);
boolean allShort = words.stream().allMatch(w -> w.length() < 10);
System.out.println("Any word > 5 chars? " + anyLong);
System.out.println("All words < 10 chars? " + allShort);
// Get distinct lengths
List<Integer> lengths = words.stream()
.map(String::length)
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println("Distinct lengths: " + lengths);
}
}
Output
Upper: [HELLO, WORLD, JAVA, STREAMS, ARE, AWESOME, HI]
Long words: awesome, hello, java, streams, world
First 'a' word: are
Any word > 5 chars? true
All words < 10 chars? true
Distinct lengths: [2, 3, 4, 5, 7]
Note: মেথড রেফারেন্সগুলো (::) মূলত ছোট এবং সাধারণ ল্যাম্বডার একটি সংক্ষিপ্ত রূপ বা শর্টহ্যান্ড (shorthand) মাত্র। String::toUpperCase এবং s -> s.toUpperCase() আসলে একই জিনিস। যখন ল্যাম্বডা মাত্র একটি মেথডকে কল করে তখন এগুলো ব্যবহার করবেন — এতে আপনার কোডকে দেখতে স্বাভাবিক ইংরেজির মতো মনে হয়। যেমন System.out::println, Integer::parseInt, Math::max — এগুলো দেখতে একদম পরিষ্কার ও পরিচ্ছন্ন।
চ্যালেঞ্জ

ছোট কুইজ

ল্যাম্বডা এক্সপ্রেশন (lambda expression) কী?
File I/O