সরাসরি পর্বে যান
পর্ব ৩৪ · ৫৯-এর মধ্যে · মডিউল ৫

lambda, functional interface আর method reference — আচরণকে মানের মতো পাঠানো

Lambdas, Functional Interfaces and Method References — Target Typing, java.util.function, Composition, Capture Rules and How Lambdas Compile

Taught in Bangla, this lesson shows how Java passes behaviour as values: lambda syntax and target typing, the java.util.function family with primitive variants, composing Predicate and Function, the four kinds of method reference, capture rules and this inside lambdas, higher-order methods, checked exception problems, how lambdas compile with invokedynamic, and when a plain loop is better.

এই পর্বে যা শিখবেন

  • lambda কেন দরকার, আর target type দেখে compiler কীভাবে lambda-কে একটি interface method-এ বসায়
  • java.util.function-এর মূল interface-গুলো, primitive সংস্করণ, আর @FunctionalInterface
  • Predicate আর Function জুড়ে নিয়মের ইঞ্জিন বানানো, আর চার ধরনের method reference
  • capture-এর নিয়ম, lambda-তে this, collection-এর forEach/removeIf/computeIfAbsent, আর নিজের higher-order method
  • checked exception-এর সমস্যা, invokedynamic দিয়ে lambda কীভাবে চলে, আর কখন lambda না লিখে সাধারণ loop লিখবেন

১ · lambda মানে কী — আচরণকে মানের মতো পাঠানো

এ পর্যন্ত method-কে আপনি সবসময় ডেটা দিয়েছেন: একটি সংখ্যা, একটি String, একটি List. কিন্তু অনেক সময় method-কে ডেটার সাথে একটি নিয়ম বা কাজ-ও দিতে হয়। একটি অনলাইন দোকানের পণ্যের তালিকা ভাবুন। কেউ চান ৫০০ টাকার নিচের পণ্য, কেউ চান যা স্টকে আছে, কেউ চান নামে “Bag” আছে এমন পণ্য। প্রতিটির জন্য আলাদা method লিখলে দেখবেন, সব method প্রায় একই — একটি loop, একটি if, একটি নতুন list. পার্থক্য শুধু if-এর ভেতরের শর্তে।

static List<Product> cheap(List<Product> all) {
    List<Product> out = new ArrayList<>();
    for (Product p : all) if (p.price() < 500) out.add(p);    // শুধু এখানে পার্থক্য
    return out;
}
static List<Product> available(List<Product> all) {
    List<Product> out = new ArrayList<>();
    for (Product p : all) if (p.inStock()) out.add(p);       // আর এখানে
    return out;
}

সমাধান: একটিই method লিখুন, আর শর্তটা বাইরে থেকে পাঠান। “এই নিয়মে বাছাই করো” — নিয়মটাই একটি argument. পাঠানোর মতো এই ছোট আচরণের টুকরোকে বলে lambda:

static List<Product> filter(List<Product> all, Predicate<Product> rule) {
    List<Product> out = new ArrayList<>();
    for (Product p : all) if (rule.test(p)) out.add(p);
    return out;
}
// ব্যবহার — নিয়মটা ডাকার জায়গায় লেখা:
filter(items, p -> p.price() < 500);
filter(items, p -> p.inStock());
filter(items, p -> p.name().contains("Bag"));

একটি গার্মেন্টস কারখানার মান-যাচাই (QC) বিভাগ ভাবুন। ম্যানেজার প্রতিদিন পরিদর্শককে নতুন নির্দেশ লিখে দেন না যে “আজ এভাবে গুনবে, এভাবে বাছবে।” গোনা আর বাছার কাজ একই থাকে; বদলায় শুধু একটি ছোট কার্ড — “আজ সেলাই ছেঁড়া শার্ট বাদ”, কাল “রং মেলে না এমন শার্ট বাদ”। পরিদর্শক হলেন আপনার filter method, আর কার্ডটি lambda. কার্ড বদলালেই কাজ বদলায়, পরিদর্শককে নতুন করে প্রশিক্ষণ দিতে হয় না।

এখন প্রশ্ন: Java তো একটি টাইপ-কঠোর ভাষা। rule parameter-এর টাইপ কী? Java-তে আলাদা কোনো “function টাইপ” নেই। তার বদলে Java ব্যবহার করে পরিচিত interface — এমন interface যার মধ্যে ঠিক একটিই abstract method. পর্ব ২১-এ এর নাম শুনেছেন: functional interface. ওপরের Predicate<Product> এমনই একটি interface, যার একমাত্র abstract method হলো boolean test(Product p). lambda p -> p.price() < 500 আসলে সেই test method-এর শরীর।

মূল ধারণা

Java-তে lambda একা দাঁড়াতে পারে না — তার সবসময় একটি গন্তব্য টাইপ লাগে, যা একটি functional interface. একে বলে target type. compiler সেই interface-এর একমাত্র abstract method খুঁজে বের করে, আর lambda-কে সেই method-এর implementation বানায়। ফলে lambda-র ফল একটি সাধারণ object, যাকে ভ্যারিয়েবলে রাখা, method-এ পাঠানো আর ফেরত দেওয়া যায়।

পর্ব ২৭-এ দেখেছেন, Java 8-এর আগে একই কাজ করতে anonymous class লিখতে হতো — পাঁচ লাইনের আনুষ্ঠানিকতা, যার মধ্যে আসল কথা এক লাইন। lambda সেই আনুষ্ঠানিকতা ছেঁটে ফেলেছে। আর পর্ব ৩৩-এ Comparator লিখতে গিয়ে আপনি ইতিমধ্যে lambda দেখেছেন: (a, b) -> Double.compare(b.gpa(), a.gpa()). এই পর্বে সেই ধারণার পুরো ছবি।

২ · lambda-র রূপ আর target type

একটি lambda-র দুই অংশ: বাঁয়ে parameter, ডানে শরীর, মাঝে তীর ->. বাকি সব কিছু প্রসঙ্গ থেকে অনুমান করা হয়। লেখার কয়েকটি রূপ:

রূপউদাহরণকখন
parameter নেই() -> "Hello"শুধু কিছু তৈরি বা ফেরত দেয়
একটি parameterp -> p.price()বন্ধনী ঐচ্ছিক
একাধিক parameter(a, b) -> a + bবন্ধনী বাধ্যতামূলক
টাইপসহ(int a, int b) -> a + bদিলে সব parameter-এ দিতে হয়
var দিয়ে (Java 11 থেকে)(var a, var b) -> a + bparameter-এ annotation বসাতে
block শরীরx -> { int y = x * 2; return y + 1; }একাধিক statement; মান ফেরত দিলে return লাগবে

শরীর যদি একটি expression হয় (a + b), তার মানটাই ফেরত যায় — return বা { } লাগে না। শরীর যদি { } block হয়, তাহলে সাধারণ method-এর মতো return লিখতে হয়। parameter-এর টাইপ সাধারণত লেখা হয় না, কারণ compiler target type থেকে তা জেনে নেয় (type inference).

এক lambda, অনেক interface

একটি মজার ব্যাপার: lambda-র নিজের কোনো নির্দিষ্ট টাইপ নেই। একই লেখা ভিন্ন জায়গায় ভিন্ন interface-এর object হয়ে যায়:

Supplier<String> s = () -> "hi";             // String get()
Callable<String> c = () -> "hi";             // String call() throws Exception
Function<Integer, Integer> f = x -> x * 2;   // Integer apply(Integer)
UnaryOperator<Integer> u     = x -> x * 2;   // একই লেখা, অন্য interface
IntUnaryOperator i           = x -> x * 2;   // int applyAsInt(int) — boxing নেই

প্রতিটি লাইনে compiler বাঁ পাশের টাইপ দেখে, সেই interface-এর একমাত্র abstract method খোঁজে, আর মিলিয়ে দেখে: parameter-এর সংখ্যা মেলে কি না, শরীরের টাইপ method-এর return টাইপের সাথে মেলে কি না। মিললে বৈধ। তাই target type না থাকলে lambda লেখাই যায় না:

class Main {
    public static void main(String[] args) {
        var twice = x -> x * 2;
    }
}
Main.java:3: error: cannot infer type for local variable twice
        var twice = x -> x * 2;
            ^
  (lambda expression needs an explicit target-type)
1 error

var বলে “ডান পাশ দেখে টাইপ বের করো”, আর lambda বলে “বাঁ পাশ দেখে আমার টাইপ ঠিক করো” — দুজনই অন্যজনের দিকে তাকিয়ে আছে, তাই compiler হাল ছেড়ে দেয়। এবার পুরো প্রক্রিয়াটা ধাপে ধাপে দেখুন — compiler একটি lambda পেলে কী কী করে:

1interface Predicate<T> { boolean test(T t); }2Predicate<Product> isCheap = p -> p.price() < 500;34Product pen = new Product("Pen", 20, true);5boolean ok = isCheap.test(pen);
compiler আর JVM যা জানে
target type
যে method হবে
p-এর টাইপ
isCheap
  1. target type খোঁজা। compiler লাইন ২-এ lambda দেখল। lambda-র নিজের টাইপ নেই, তাই সে চারপাশে তাকায়: এটা কোথায় যাচ্ছে? বাঁ পাশের ভ্যারিয়েবলের টাইপ Predicate<Product> — এটাই target type. method-এর argument হলে সেই parameter-এর টাইপ, return হলে method-এর return টাইপ target হতো।
  2. একমাত্র abstract method. Predicate-এ abstract method একটিই — test. T এখানে Product, তাই method-টি হলো “Product নাও, boolean দাও”। Predicate-এ and, negate-এর মতো আরও method আছে, কিন্তু সেগুলো default method বা static — গোনায় ধরা হয় না।
  3. parameter অনুমান আর শরীর যাচাই। lambda-তে একটি parameter p — test-এও একটি। তাই p-এর টাইপ Product. এবার শরীর: p.price() < 500 একটি boolean, আর test-এর return টাইপও boolean. সব মিলেছে। শরীর যদি একটি String দিত, এখানেই compile error হতো।
  4. object তৈরি। প্রোগ্রাম চলার সময় লাইন ২ একটি object বানায়, যেটি Predicate<Product> implement করে, আর যার test method-এর শরীর হলো আপনার lambda. এর জন্য কোনো .class ফাইল ডিস্কে নেই — অংশ ৮-এ এর কৌশল দেখবেন।
  5. ডাকা। isCheap.test(pen) — একটি সাধারণ interface method call. ভেতরে lambda-র শরীর চলে: pen.price() হলো ২০, 20 < 500 সত্য, তাই ok হলো true. lambda লেখার সময় কিছুই চলে না; চলে শুধু যখন কেউ interface method ডাকে।

@FunctionalInterface — compiler-কে পাহারাদার বানানো

নিজের functional interface বানালে তার মাথায় @FunctionalInterface annotation লিখুন। এটা না লিখলেও lambda কাজ করবে — একটি abstract method থাকলেই যথেষ্ট। annotation-টি একটি প্রতিশ্রুতি: “এই interface-এ ঠিক একটি abstract method থাকবে।” পরে কেউ ভুলে আরেকটি abstract method যোগ করলে compiler সেখানেই থামাবে, আর যত জায়গায় এই interface-এর জন্য lambda লেখা আছে সেগুলো ভাঙার আগেই সমস্যা ধরা পড়বে:

@FunctionalInterface
interface Rule {
    boolean check(int price);
    String explain();
}

class Main {
    public static void main(String[] args) { }
}
Main.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
  Rule is not a functional interface
    multiple non-overriding abstract methods found in interface Rule
1 error

বার্তার শেষ লাইনটি পড়ুন: “multiple non-overriding abstract methods”. “non-overriding” কথাটি গুরুত্বপূর্ণ। Comparator-এ compare ছাড়াও boolean equals(Object) abstract হিসেবে ঘোষিত আছে, তবু সে functional interface — কারণ equals সব object-এর Object class থেকে আসে, তাই সেটা গোনায় ধরা হয় না। explain-কে default method বানালে (শরীর দিলে) error চলে যাবে।

৩ · java.util.function — তৈরি interface-এর পরিবার

প্রতিটি lambda-র জন্য নিজে interface বানাতে হলে বিরক্তিকর হতো। তাই Java 8 java.util.function package-এ চল্লিশটির বেশি সাধারণ-উদ্দেশ্যের functional interface দিয়েছে। মুখস্থ করার দরকার নেই — মূল পরিবারটা বুঝলে বাকিগুলোর নাম দেখেই অর্থ বোঝা যায়। প্রশ্ন দুটি: কী নেয়? আর কী ফেরত দেয়?

interfaceনেয়ফেরত দেয়methodউদাহরণ
Supplier<T>কিছু নাTget()() -> new ArrayList<>()
Consumer<T>Tকিছু নাaccept(t)m -> System.out.println(m)
Function<T,R>TRapply(t)s -> s.length()
Predicate<T>Tbooleantest(t)p -> p.price() < 500
BiFunction<T,U,R>T আর URapply(t, u)(price, qty) -> price * qty
UnaryOperator<T>Tএকই Tapply(t)s -> s.trim()
BinaryOperator<T>দুটি Tএকই Tapply(a, b)(a, b) -> a + b

সম্পর্কগুলো খেয়াল করুন। UnaryOperator<T> আসলে Function<T,T>-এর বিশেষ রূপ (সে Function-কে extend করে) — যখন ইনপুট আর আউটপুট একই টাইপ। BinaryOperator<T> তেমনি BiFunction<T,T,T>. “Bi” মানে দুটি ইনপুট: BiConsumer, BiPredicate-ও আছে — যেমন Map.forEach নেয় একটি BiConsumer, কারণ প্রতিটি entry-তে key আর value দুটি জিনিস। আর পুরোনো Runnable (কিছু নেয় না, কিছু দেয় না) আর Comparator-ও functional interface, যদিও তারা এই package-এর নয়।

Supplier — উৎপাদক

চাইলেই একটি মান দেয়। দেরিতে তৈরি করা (lazy), ডিফল্ট মান, নতুন container বানানো।

“দরকার হলে বানাব”

Consumer — ভোক্তা

একটি মান নিয়ে কিছু করে, ফেরত দেয় না — ছাপা, পাঠানো, সেভ করা। এর কাজই side effect.

“নিলাম, কাজ সারলাম”

Function — রূপান্তরক

একটি মানকে আরেকটিতে বদলায়: String থেকে সংখ্যা, পণ্য থেকে তার দাম।

“এটা দাও, ওটা নাও”

Predicate — বিচারক

হ্যাঁ বা না বলে। বাছাই, যাচাই, শর্ত।

“চলবে, নাকি বাদ?”

primitive সংস্করণ — boxing-এর খরচ এড়াতে

generics শুধু reference টাইপ নেয় (পর্ব ২৮), তাই Function<int, int> লেখা যায় না — লিখতে হয় Function<Integer, Integer>. এর মানে প্রতিটি ডাকে int-কে Integer object-এ মোড়ানো (autoboxing) আর আবার খোলা। দশটা সংখ্যায় কিছু যায় আসে না, কিন্তু কোটি সংখ্যার loop-এ বাড়তি object তৈরি আর garbage collection সময় খায়। তাই আছে primitive specialization:

boxed রূপprimitive রূপmethod
Predicate<Integer>IntPredicatetest(int)
Function<Integer,Integer>IntUnaryOperatorapplyAsInt(int)
BinaryOperator<Integer>IntBinaryOperatorapplyAsInt(int, int)
Function<String,Integer>ToIntFunction<String>applyAsInt(String)
Function<Integer,String>IntFunction<String>apply(int)
Supplier<Integer>IntSuppliergetAsInt()

নামের ধাঁচ: Int… মানে ইনপুট int, ToInt… মানে আউটপুট int. একই পরিবার Long আর Double-এর জন্যও আছে। সব primitive-এর জন্য নেই (যেমন CharPredicate নেই) — interface-এর সংখ্যা বিস্ফোরিত না করতে Java শুধু সবচেয়ে বেশি ব্যবহৃত তিনটি বেছে নিয়েছে। পর্ব ৩৫-এর IntStream এই interface-গুলোই ব্যবহার করে।

নিজে interface কখন বানাবেন

তৈরি interface দিয়ে কাজ চললে সেটাই ব্যবহার করুন — সবাই চেনে, আর and/andThen-এর মতো সুবিধা ফ্রি পাওয়া যায়। নিজে বানান তিনটি ক্ষেত্রে: নামটা আসলেই অর্থ বহন করে (DiscountRule বলে দেয় কী, Predicate<Order> বলে না), method-এর checked exception ছোঁড়া দরকার, অথবা দুটির বেশি parameter লাগে (তৈরি পরিবারে সর্বোচ্চ দুটি)।

৪ · ছোট নিয়ম জুড়ে বড় নিয়ম — composition

functional interface-গুলোর আসল শক্তি তাদের default method-এ। ছোট ছোট নিয়ম আলাদাভাবে লিখে, পরে সেগুলো জুড়ে নতুন নিয়ম বানানো যায় — কোনো নতুন lambda না লিখেই। একে বলে function composition.

methodকারঅর্থ
a.and(b)Predicateদুটিই সত্য হলে সত্য; a মিথ্যা হলে b চলেই না
a.or(b)Predicateযেকোনো একটি সত্য হলে সত্য; a সত্য হলে b চলে না
a.negate()Predicateউল্টো ফল
Predicate.not(a)Predicate (Java 11 থেকে)negate-এর static রূপ, method reference-এর সাথে কাজের
f.andThen(g)Functionআগে f, তার ফলের ওপর g
f.compose(g)Functionআগে g, তার ফলের ওপর f
c1.andThen(c2)Consumerএকই মানের ওপর দুটি কাজ পরপর

andThen আর compose নিয়ে নতুনরা প্রায়ই গুলিয়ে ফেলেন। ধরুন doubleIt = x -> x * 2 আর plus3 = x -> x + 3. doubleIt.andThen(plus3).apply(5) হলো (৫ × ২) + ৩ = ১৩ — লেখার ক্রমেই চলে। doubleIt.compose(plus3).apply(5) হলো (৫ + ৩) × ২ = ১৬ — গণিতের f(g(x))-এর মতো, ভেতরেরটা আগে। মনে রাখার সহজ উপায়: andThen পড়ুন “এটা, তারপর ওটা”।

পর্ব ৩৩-এর Comparator-ও একই ধারণায় তৈরি: Comparator.comparing(Product::price).thenComparing(Product::name).reversed() — ছোট তুলনা জুড়ে বড় তুলনা। এবার একটি সত্যিকারের প্রশ্ন: একটি ল্যাম্প আছে, দাম ৪৫০ টাকা, স্টকে নেই। নিয়ম “সস্তা এবং স্টকে আছে — এর উল্টো”, অর্থাৎ “এখন কেনার মতো নয়”। isCheap.and(inStock).negate() ল্যাম্পের জন্য কী দেবে? ধাপে ধাপে দেখুন:

1Predicate<Product> isCheap = p -> p.price() < 500;2Predicate<Product> inStock = Product::inStock;3Predicate<Product> skip = isCheap.and(inStock).negate();4Product lamp = new Product("Lamp", 450, false);5skip.test(lamp);6skip.test(new Product("Bag", 950, true));
মূল্যায়নের অবস্থা
isCheap
inStock
and
negate → ফল
  1. জোড়া লাগানো মানে চালানো নয়। লাইন ৩ শুধু একটি নতুন Predicate object বানাল, যে ভেতরে মনে রাখে “আমি isCheap আর inStock-এর and-এর উল্টো”। কোনো পণ্য এখনো যাচাই হয়নি। and আর negate দুটিই নতুন object ফেরত দেয়; মূল isCheap বদলায় না।
  2. বাঁয়ের অংশ আগে। skip.test(lamp) ডাকা হলো। negate ভেতরের and-কে জিজ্ঞেস করে, আর and প্রথমে isCheap.test(lamp) চালায়: ৪৫০ < ৫০০, তাই সত্য।
  3. ডানের অংশ — শুধু দরকার হলে। বাঁ অংশ সত্য, তাই উত্তর এখনো অনিশ্চিত; and এবার inStock.test(lamp) চালায়: ল্যাম্প স্টকে নেই, মিথ্যা। সত্য && মিথ্যা = মিথ্যা।
  4. উল্টে দেওয়া। negate মিথ্যাকে সত্য বানাল। ফল true — ল্যাম্প “এখন কেনার মতো নয়”-এর তালিকায়। খেয়াল করুন, পুরো হিসাবটা একটি সাধারণ !(cheap && inStock)-এর মতোই, শুধু নিয়মগুলো আলাদা টুকরো হিসেবে নাম পেয়েছে।
  5. short-circuit. ব্যাগের দাম ৯৫০, তাই isCheap শুরুতেই মিথ্যা। and জানে “মিথ্যা && যেকোনো কিছু = মিথ্যা”, তাই inStock একবারও ডাকে না — ঠিক পর্ব ৭-এর &&-এর মতো। তাই দামি বা ধীর যাচাই (যেমন ডেটাবেস দেখা) পরে রাখুন।

এবার পুরো একটি ছোট “ছাড়ের নিয়ম ইঞ্জিন” চালিয়ে দেখুন। applyDiscount একটি নিয়ম (Predicate) আর একটি পরিবর্তন (UnaryOperator) নেয় — কোন পণ্যে আর কী ছাড়, দুটিই বাইরে থেকে আসে:

import java.util.*;
import java.util.function.*;

record Product(String name, int price, boolean inStock) { }

class Main {
    static List<Product> applyDiscount(List<Product> list, Predicate<Product> rule, UnaryOperator<Product> change) {
        List<Product> out = new ArrayList<>();
        for (Product p : list) out.add(rule.test(p) ? change.apply(p) : p);
        return out;
    }
    public static void main(String[] args) {
        List<Product> items = List.of(new Product("Pen", 20, true), new Product("Bag", 950, true),
                new Product("Lamp", 450, false), new Product("Mug", 180, true));
        Predicate<Product> isCheap = p -> p.price() < 500;
        Predicate<Product> inStock = Product::inStock;
        Predicate<Product> buyNow = isCheap.and(inStock);
        for (Product p : items) if (buyNow.test(p)) System.out.println("Buy: " + p.name());
        System.out.println("Lamp skip? " + buyNow.negate().test(items.get(2)));
        UnaryOperator<Product> tenOff = p -> new Product(p.name(), p.price() * 90 / 100, p.inStock());
        applyDiscount(items, isCheap.negate(), tenOff).forEach(System.out::println);
    }
}

আউটপুট সাত লাইন: Buy: Pen, Buy: Mug, Lamp skip? true, তারপর চারটি পণ্য — Product[name=Pen, price=20, inStock=true], Product[name=Bag, price=855, inStock=true], Product[name=Lamp, price=450, inStock=false], Product[name=Mug, price=180, inStock=true]. শুধু ব্যাগ দামি, তাই isCheap.negate() শুধু তাকেই বাছল, আর ৯৫০-এর ৯০% = ৮৫৫. লক্ষ করুন, Product একটি record — অপরিবর্তনীয়, তাই tenOff পুরোনো পণ্য বদলায় না, নতুন একটি ফেরত দেয়; মূল items অক্ষত থাকে। applyDiscount নিজে জানে না কোন নিয়ম বা কত ছাড় — কাল “স্টকে নেই এমন সব পণ্যে ২০% ছাড়” চাইলে শুধু দুটি lambda বদলাবেন, method নয়। Product::inStock আর System.out::println হলো method reference, পরের অংশে। (নিজের প্রজেক্টে main class সাধারণত public class Main; এখানে অনলাইন runner-এর জন্য public নেই।)

একই ধাঁচে ইনপুট যাচাই: ছোট ছোট যাচাই আলাদা নামে রাখুন, তারপর জুড়ুন। প্রতিটি নিয়ম আলাদাভাবে পরীক্ষা করা যায়, আর নতুন শর্ত যোগ করা মানে একটি .and(...):

Predicate<String> notBlank = s -> !s.isBlank();
Predicate<String> digitsOnly = s -> s.chars().allMatch(Character::isDigit);
Predicate<String> elevenLong = s -> s.length() == 11;
Predicate<String> validPhone = notBlank.and(digitsOnly).and(elevenLong);

validPhone.test("01712345678");   // true
validPhone.test("0171-234");      // false

৫ · method reference — চার ধরন

অনেক lambda শুধু একটি বিদ্যমান method ডাকে, আর কিছু করে না: s -> Integer.parseInt(s), m -> System.out.println(m). এখানে parameter-এর নাম (s, m) শুধু হাত বদলের জন্য। method reference সেটুকুও বাদ দেয়: Integer::parseInt, System.out::println. :: পড়ুন “এর এই method” হিসেবে। method reference চলে না — এটাও lambda-র মতো একটি functional interface-এর object বানায়, তাই এরও target type লাগে।

চার ধরনের method reference আছে, আর পার্থক্যটা একটাই প্রশ্নে: method-টি কোন object-এর ওপর চলবে, আর সেই object কোথা থেকে আসবে? চারটি ধরন lambda-র পাশে রেখে দেখুন:

  1. staticType::methodobject লাগে না
  2. boundobj::methodobject আগেই ঠিক
  3. unboundType::methodপ্রথম argument-ই object
  4. constructorType::newনতুন object
ধাপ ১static method
Function<String, Integer> parse = Integer::parseInt;
// সমান lambda:
Function<String, Integer> parse = s -> Integer.parseInt(s);
parse.apply("200")   // 200
ধাপ ২নির্দিষ্ট object-এর method (bound)
Consumer<String> print = System.out::println;
// সমান: m -> System.out.println(m)
String phone = "01712345678";
Predicate<String> hasPrefix = phone::startsWith;
// সমান: pre -> phone.startsWith(pre)
hasPrefix.test("017")   // true
ধাপ ৩যেকোনো object-এর method (unbound)
Function<String, String> up = String::toUpperCase;
// সমান: s -> s.toUpperCase()
BiFunction<String, String, Boolean> sw = String::startsWith;
// সমান: (s, pre) -> s.startsWith(pre)
up.apply("dhaka")   // DHAKA
ধাপ ৪constructor
Supplier<List<String>> make = ArrayList::new;
// সমান: () -> new ArrayList<>()
Function<Integer, List<String>> sized = ArrayList::new;
// সমান: n -> new ArrayList<>(n)
Function<String, Product> f = Product::new;  // যদি এমন constructor থাকে
  1. static method — Type::staticMethod. কোনো object লাগে না; lambda-র সব parameter সোজা method-এ যায়। Integer::parseInt = s -> Integer.parseInt(s). String-এর তালিকাকে সংখ্যায় বদলানোর সবচেয়ে পরিচিত রূপ এটাই। Math::max, Integer::sum (একটি BinaryOperator<Integer>) একই ধরনের।
  2. bound instance — object::method. object-টি আগে থেকেই ঠিক করা, reference তৈরির মুহূর্তে। System.out একটি নির্দিষ্ট PrintStream object; phone একটি নির্দিষ্ট String. lambda-র parameter যায় method-এর argument হিসেবে: phone::startsWith মানে “এই ফোন নম্বর কি অমুক দিয়ে শুরু?” সূক্ষ্ম কথা: object-টি reference তৈরির সময়েই মূল্যায়িত হয় — phone null হলে NullPointerException তখনই আসবে, ডাকার সময় নয়।
  3. unbound instance — Type::instanceMethod. দেখতে static-এর মতো, কিন্তু method-টি instance method. তাহলে কোন object-এর ওপর চলবে? উত্তর: lambda-র প্রথম parameter-ই সেই object. String::toUpperCase = s -> s.toUpperCase(). দুই parameter হলে প্রথমটি object, দ্বিতীয়টি argument: String::startsWith = (s, pre) -> s.startsWith(pre). Product::inStock, Product::price-ও এই ধরন।
  4. constructor — Type::new. lambda-র parameter যায় constructor-এ। কোন constructor? target type ঠিক করে। Supplier হলে খালি constructor, Function<Integer, …> হলে int নেওয়া constructor (ArrayList-এর ক্ষেত্রে প্রাথমিক ধারণক্ষমতা)। array-র জন্যও আছে: int[]::new = n -> new int[n], যেটা পর্ব ৩৫-এ toArray-এ দেখবেন।

method reference কখন lambda-র চেয়ে ভালো? যখন সে আরও পরিষ্কার পড়ায়। names.forEach(System.out::println) বা map(String::trim) নিজেই একটি বাক্যের মতো পড়া যায়। কিন্তু (a, b) -> a.compareToIgnoreCase(b)-এর জায়গায় String::compareToIgnoreCase লিখলে অনেক পাঠককে এক মুহূর্ত থেমে ভাবতে হয় — কে object, কে argument? আর lambda যদি method ডাকার আগে-পরে কিছু করে (s -> Integer.parseInt(s.trim())), method reference চলবে না। নিয়ম একটাই: যেটা পড়তে সহজ, সেটা লিখুন।

৬ · capture, this আর collection-এ lambda

বাইরের ভ্যারিয়েবল ধরা — effectively final

lambda তার চারপাশের local variable পড়তে পারে — যেমন বাজেট একটি ভ্যারিয়েবলে রেখে p -> p.price() <= budget. পর্ব ২৭-এ anonymous class-এর জন্য যে নিয়ম দেখেছেন, lambda-তেও তাই: lambda ভ্যারিয়েবলের মানের কপি রাখে (variable capture), তাই ভ্যারিয়েবলটি effectively final হতে হবে — ঘোষণার পর কখনো নতুন মান বসানো যাবে না। ধাপে ধাপে দেখুন:

1static Predicate<Product> under(int budget) {2    return p -> p.price() <= budget;3}4Predicate<Product> small = under(200);5small.test(new Product("Mug", 180, true));6// যদি লাইন ২-এর আগে লিখতেন: budget += 50;
মেমরিতে কী আছে
under-এর frame: budget
lambda object-এর কপি
small
  1. method চলছে। under(200) ডাকায় stack-এ একটি frame, যেখানে parameter budget = ২০০. parameter-ও একটি local variable — method শেষ হলে মুছে যাবে।
  2. lambda তৈরি — সাথে কপি। লাইন ২-এর lambda budget ব্যবহার করে, তাই lambda object তৈরির মুহূর্তে মানটি (২০০) object-এর ভেতরে কপি হয়ে যায়।
  3. method শেষ। under lambda ফেরত দিল, frame মুছে গেল। আসল budget আর নেই, কিন্তু lambda-র কাছে কপি আছে। এভাবে একটি method “নিয়ম বানানোর কারখানা” হতে পারে: under(200), under(1000) — প্রতিটি আলাদা বাজেট মনে রাখে।
  4. পরে ডাকা। small.test(mug) — শরীরে budget লেখা, কিন্তু পড়া হয় কপিটি: ১৮০ ≤ ২০০, সত্য।
  5. বদলানো গেলে? lambda-র আগে বা পরে budget += 50 লিখলে method দেখত ২৫০, আর lambda-র কপি কোন মানের — প্রশ্নটাই বিভ্রান্তিকর। Java তাই নিয়ম করেছে: ধরা ভ্যারিয়েবল বদলানো যাবে না, বদলালে compile হবে না।

সবচেয়ে সাধারণ ভুলটি হলো lambda দিয়ে যোগফল জমানোর চেষ্টা:

import java.util.*;

class Main {
    public static void main(String[] args) {
        int total = 0;
        List<Integer> prices = List.of(20, 950, 180);
        prices.forEach(p -> total += p);
        System.out.println(total);
    }
}
Main.java:7: error: local variables referenced from a lambda expression must be final or effectively final
        prices.forEach(p -> total += p);
                            ^
1 error

পর্ব ২৭-এ বার্তাটি ছিল “from an inner class”, এখানে “from a lambda expression” — নিয়ম একই। সমাধান “নিয়ম এড়ানোর কৌশল” নয় (int[] total = {0} লিখে array-র ভেতরের ঘর বদলানো — AI প্রায়ই এটা দেয়)। সমাধান হলো নকশা বদলানো: একটি সাধারণ for-each loop লিখুন, অথবা পর্ব ৩৫-এ দেখবেন prices.stream().mapToInt(Integer::intValue).sum() — যেখানে কিছুই বদলাতে হয় না, ফলটাই ফেরত আসে। নিয়মটির কারণ শুধু কপি নয়: lambda প্রায়ই অন্য thread-এ চলে, আর বদলানো যায় এমন local variable ভাগাভাগি হলে data race তৈরি হতো (প্রশ্ন ৫-এ আরও)।

lambda-তে this

পর্ব ২৭-এর পার্থক্যটি আবার মনে করুন: anonymous class একটি নতুন class, তাই তার ভেতরে this মানে নামহীন object নিজে। lambda নতুন class নয়, নতুন scope-ও নয় — তার ভেতরে this মানে যে object-এর method-এ lambda লেখা হয়েছে। ফলে একটি instance method-এর ভেতরে items.forEach(p -> this.total += p.price()) বৈধ: total একটি field, local variable নয়, আর lambda আসলে this-কে ধরে, যা নিজে বদলায় না। তবে এটাও বাইরের অবস্থা বদলানো — একাধিক thread-এ নিরাপদ নয়। একই কারণে lambda-র parameter বা ভেতরের ভ্যারিয়েবলের নাম বাইরের কোনো local variable-এর নামে রাখা যায় না; একই scope, তাই compiler আপত্তি করে।

collection-এ lambda — Java 8-এর নতুন method

lambda আসার সাথে Java 8 collection interface-গুলোতে default method যোগ করেছে, যেগুলো সরাসরি lambda নেয়। পর্ব ৩০-৩১-এর List আর Map এখন আরও সংক্ষেপে কাজ করে:

methodনেয়কাজউদাহরণ
forEachConsumerপ্রতিটি উপাদানে একটি কাজnames.forEach(System.out::println)
removeIfPredicateশর্ত মেলে এমন সব মুছে ফেলাcart.removeIf(p -> !p.inStock())
replaceAllUnaryOperatorপ্রতিটি উপাদান বদলে দেওয়াnames.replaceAll(String::trim)
Map.forEachBiConsumerপ্রতিটি key-value জোড়ায় কাজstock.forEach((k, v) -> ...)
computeIfAbsentFunctionkey না থাকলে মান বানিয়ে বসানোmap.computeIfAbsent(k, x -> new ArrayList<>())
mergeBiFunctionপুরোনো আর নতুন মান মেলানোcount.merge(word, 1, Integer::sum)

দুটি সতর্কতা। প্রথমত, removeIf আর replaceAll collection-টিকে বদলায়, তাই List.of(...)-এর মতো অপরিবর্তনীয় list-এ UnsupportedOperationException দেবে — আগে new ArrayList<>(...)-এ কপি করুন। দ্বিতীয়ত, পর্ব ১১-এ দেখেছেন for-each loop চলার সময় list থেকে মুছলে ConcurrentModificationException হয়; removeIf সেই কাজটাই নিরাপদে করে। আর computeIfAbsent হলো পর্ব ৩১-এর “key না থাকলে খালি list বসাও, তারপর যোগ করো” ধাঁচের এক লাইনের রূপ — পরের অংশের প্রোগ্রামে দেখবেন।

৭ · নিজের higher-order method আর checked exception-এর ঝামেলা

যে method আরেকটি আচরণ (function) parameter হিসেবে নেয় বা ফেরত দেয়, তাকে বলে higher-order function. filter, applyDiscount, under — সবই আপনি এর মধ্যে লিখেছেন। এই কৌশলের সবচেয়ে বড় লাভ: “কখন আর কীভাবে চালাতে হবে” — এই কাঠামো একবার লেখা, আর “কী চালাতে হবে” প্রতিবার আলাদা। নিচের প্রোগ্রামে দুটি বাস্তব উদাহরণ: নেটওয়ার্ক ব্যর্থ হলে আবার চেষ্টা করার retry, আর একটি ছোট নোটিফিকেশন dispatcher, যেখানে প্রতিটি ঘটনার জন্য কয়েকজন শ্রোতা (Consumer) নিবন্ধিত:

import java.util.*;
import java.util.function.*;
class Main {
    static <T> T retry(Supplier<T> task, int times) {
        RuntimeException last = new IllegalArgumentException("times must be > 0");
        for (int i = 1; i <= times; i++)
            try { return task.get(); }
            catch (RuntimeException e) { last = e; System.out.println("try " + i + ": " + e.getMessage()); }
        throw last;
    }
    public static void main(String[] args) {
        Iterator<String> net = List.of("timeout", "timeout", "200 OK").iterator();
        String reply = retry(() -> {
            String r = net.next();
            if (r.equals("timeout")) throw new IllegalStateException(r);
            return r;
        }, 3);
        System.out.println("got " + reply);
        Map<String, List<Consumer<String>>> subs = new TreeMap<>();
        subs.computeIfAbsent("order", k -> new ArrayList<>()).add(m -> System.out.println("SMS: " + m));
        subs.computeIfAbsent("order", k -> new ArrayList<>()).add(m -> System.out.println("Email: " + m));
        subs.computeIfAbsent("payment", k -> new ArrayList<>()).add(System.out::println);
        subs.forEach((topic, list) -> list.forEach(c -> c.accept(topic + " event")));
    }
}

আউটপুট: try 1: timeout, try 2: timeout, got 200 OK, SMS: order event, Email: order event, payment event. নেটওয়ার্কের আচরণ নকল করতে একটি Iterator ব্যবহার করা হয়েছে, যা পরপর দুবার “timeout” আর তৃতীয়বার সফল উত্তর দেয় — তাই আউটপুট প্রতিবার একই। চারটি জিনিস লক্ষ করুন। (১) retry জানে না কী কাজ — ব্যাংকের API, ফাইল ডাউনলোড, যেকোনো কিছু হতে পারে; সে শুধু জানে “ব্যর্থ হলে আবার”। (২) lambda net-কে ধরেছে, আর net ভ্যারিয়েবল কখনো বদলায়নি (effectively final), যদিও তার ভেতরের object-এর অবস্থা বদলাচ্ছে — নিয়মটা ভ্যারিয়েবলের, object-এর নয়। (৩) প্রথম computeIfAbsent “order”-এর জন্য খালি list বানাল, দ্বিতীয়টি আগের list-ই ফেরত দিল, তাই দুই শ্রোতা একই list-এ। (৪) TreeMap হওয়ায় topic-গুলো বর্ণানুক্রমে আসে; Map.forEach-এর lambda নেয় দুটি parameter — একটি BiConsumer.

checked exception — lambda-র সবচেয়ে বড় কাঁটা

ধরুন কয়েকটি ফাইলের লেখা ছাপতে চান। Files.readString (পর্ব ৩৭) একটি checked exception — IOException — ছুঁড়তে পারে। main-এ throws IOException লিখে দিলেও:

import java.io.IOException;
import java.nio.file.*;
import java.util.*;

class Main {
    public static void main(String[] args) throws IOException {
        List<Path> files = List.of(Path.of("a.txt"), Path.of("b.txt"));
        files.forEach(p -> System.out.println(Files.readString(p)));
    }
}
Main.java:8: error: unreported exception IOException; must be caught or declared to be thrown
        files.forEach(p -> System.out.println(Files.readString(p)));
                                                              ^
1 error

কেন? lambda-র শরীর আসলে Consumer.accept-এর শরীর, main-এর নয়। আর accept-এর ঘোষণায় কোনো throws নেই। main-এর throws এখানে কাজে আসে না, কারণ lambda কখন চলবে তা main জানে না — forEach চালাবে, হয়তো পরে, হয়তো অন্য thread-এ। তিনটি সমাধান, পছন্দের ক্রমে:

  • সাধারণ loop লিখুন। for (Path p : files) System.out.println(Files.readString(p)); — এখানে main-এর throws কাজ করে। checked exception থাকলে প্রায়ই এটাই সবচেয়ে পরিষ্কার।
  • lambda-র ভেতরে ধরে unchecked-এ মুড়ে দিন। JDK এই কাজের জন্যই UncheckedIOException দিয়েছে; মূল exception-টি cause হিসেবে থেকে যায়, তাই কারণ হারায় না:
files.forEach(p -> {
    try { System.out.println(Files.readString(p)); }
    catch (IOException e) { throw new UncheckedIOException(e); }   // কারণ সাথে থাকে
});
  • বারবার লাগলে একটি সহকারী বানান। নিজের functional interface, যার method throws IOException, আর একটি method যা তাকে সাধারণ Function-এ মুড়ে দেয়:
@FunctionalInterface
interface IOFunction<T, R> { R apply(T t) throws IOException; }

static <T, R> Function<T, R> unchecked(IOFunction<T, R> f) {
    return t -> {
        try { return f.apply(t); }
        catch (IOException e) { throw new UncheckedIOException(e); }
    };
}
// ব্যবহার: files.stream().map(unchecked(Files::readString)).forEach(System.out::println);

যা কখনো করবেন না: catch (IOException e) { } — ফাঁকা catch দিয়ে compiler-কে চুপ করানো। ফাইল না থাকলে প্রোগ্রাম নীরবে কিছুই ছাপবে না, আর কেউ জানবে না কেন। পর্ব ২৯-এর swallowed exception lambda-র ভেতরে আরও বিপজ্জনক, কারণ লুকানো ছোট কোডে চোখ এড়িয়ে যায়। e.printStackTrace() দিয়ে এগিয়ে যাওয়াও প্রায় সমান খারাপ — ভুল ফল নিয়ে প্রোগ্রাম চলতে থাকে।

৮ · পর্দার আড়ালে, আর কখন lambda না লিখবেন

পর্ব ২৭-এ দেখেছেন, প্রতিটি anonymous class compile হয়ে আলাদা Main$1.class ফাইল হয়। lambda হয় না। javac lambda-র শরীরকে বাইরের class-এর ভেতরেই একটি private synthetic method বানায় (নাম অনেকটা lambda$main$0), আর lambda যেখানে লেখা সেখানে বসায় একটি invokedynamic নির্দেশ। সেই নির্দেশ প্রথমবার চলার সময় JVM একটি bootstrap method ডাকে — LambdaMetafactory — যে চলার সময়েই functional interface-এর একটি implementation তৈরি করে, যার method ওই private method-কে ডাকে। ফলটি সেই জায়গার সাথে বাঁধা থাকে, তাই পরের বার এই খরচ আর লাগে না।

বিষয়anonymous classlambda
ডিস্কে আলাদা ফাইলহ্যাঁ, Main$1.classনা
শরীর কোথায়নতুন class-এর method-এবাইরের class-এর private method-এ
implementation কখন তৈরিcompile-এর সময়প্রথমবার চলার সময়
প্রতিবার নতুন object?হ্যাঁ, প্রতিটি new-একিছু না ধরলে সাধারণত একটিই object পুনর্ব্যবহার হয়
stack trace-এ নামMain$1.runlambda$main$0 ধরনের

শেষ সারিটি বাস্তবে গুরুত্বপূর্ণ: lambda-র ভেতরে exception হলে stack trace-এ lambda$main$0 আর JDK-র ভেতরের কয়েকটি লাইন দেখবেন — কোন lambda, সেটা বুঝতে লাইন নম্বরের ওপর ভরসা করতে হয়। পাঁচটি lambda এক লাইনে জুড়লে debug করা কঠিন হয়ে পড়ে। আর lambda-র object-এর identity-র ওপর নির্ভর করবেন না (== দিয়ে দুটি lambda তুলনা, বা তাকে map-এর key বানানো) — JVM সেটা পুনর্ব্যবহার করবে কি না, তা নির্দিষ্টভাবে প্রতিশ্রুত নয়।

কখন lambda নয়

lambda একটি সরঞ্জাম, লক্ষ্য নয়। নিচের পরিস্থিতিতে সাধারণ loop বা নাম দেওয়া method সাধারণত পরিষ্কার:

পরিস্থিতিকেন lambda অসুবিধারভালো বিকল্প
শরীর তিন-চার লাইনের বেশিডাকার জায়গাটা পড়া যায় নানাম দেওয়া private method + method reference
checked exceptiontry-catch-এ মুড়তে হয়সাধারণ for loop
মাঝপথে থামা (break), একাধিক ফল জমানোforEach-এ break নেই, বাইরের ভ্যারিয়েবল বদলানো যায় নাloop, বা পর্ব ৩৫-এর stream
lambda-র ভেতরে lambda-র ভেতরে lambdaকোন x কার, বোঝা কঠিনপ্রতিটি স্তরকে নাম দিন
নিয়মটির একটি ব্যবসায়িক নাম আছেp -> p.price() > 5000 && p.vip() কিছু বলে নাPredicate<Order> needsApproval = ... বা method needsApproval(o)
ছোট, এক কাজের lambda — হ্যাঁ।
লম্বা হলে নাম দিন।
C/C++/Python জানলে

C-তে আচরণ পাঠানোর উপায় function pointer — কোনো ভ্যারিয়েবল ধরতে পারে না, প্রসঙ্গ পাঠাতে void* লাগে। C++-এর lambda [=] বা [&] দিয়ে কপি বা reference ধরে, আর std::function একটি সত্যিকারের function টাইপ; Java-তে আছে শুধু interface, আর ধরা সবসময় কপি। Python-এর lambda শুধু একটি expression নিতে পারে, কিন্তু সেখানে function নিজেই object, তাই target type লাগে না। তুলনার জন্য দেখুন C++-এর lambda আর Python-এর lambda ও functools পর্ব।

পরীক্ষা ও ইন্টারভিউতে
  • functional interface কী? — ঠিক একটি abstract method-ওয়ালা interface (default, static আর Object-এর method গোনায় ধরা হয় না); lambda এরই object.
  • Function, Predicate, Consumer, Supplier-এর পার্থক্য? — T নিয়ে R দেয়; T নিয়ে boolean দেয়; T নেয়, কিছু দেয় না; কিছু নেয় না, T দেয়।
  • চার ধরনের method reference? — static (Integer::parseInt), bound (System.out::println), unbound (String::length), constructor (ArrayList::new).
  • lambda কেন বাইরের local variable বদলাতে পারে না? — সে মানের কপি রাখে; ভ্যারিয়েবল effectively final হতে হয়, যাতে কপি আর মূল কখনো আলাদা না হয়।
  • lambda কি anonymous class-এর মতো আলাদা .class ফাইল বানায়? — না; শরীর একটি private method হয়, আর invokedynamic + LambdaMetafactory চলার সময় implementation তৈরি করে।

৯ · AI-এর যুগে — লম্বা lambda আর লুকানো exception ধরুন

AI lambda লিখতে খুব ভালোবাসে। “একটি ফাইল পড়ে লাইনগুলো যাচাই করো” বললে প্রায়ই পাবেন একটি forEach-এর ভেতরে পনেরো লাইনের lambda, তার ভেতরে আরেকটি forEach, আর মাঝে একটি catch (Exception e) { }. কোড compile হয়, ছোট টেস্টে চলেও। কিন্তু ভুল ফাইলে প্রোগ্রাম নীরবে খালি ফল দেয়, আর তিন মাস পরে কেউ বুঝতে পারে না কোন lambda কী করছে। এমন ধাঁচগুলো চিনে রাখুন:

AI যা করেকেন সমস্যাআপনি যা করবেন
দশ-পনেরো লাইনের multi-line lambdaডাকার জায়গায় মূল যুক্তি হারিয়ে যায়, টেস্ট করা কঠিনশরীরকে নাম দেওয়া method-এ সরিয়ে method reference
lambda-র ভেতরে lambda-র ভেতরে lambdaকোন parameter কোন স্তরের, বোঝা যায় না; stack trace অস্পষ্টপ্রতিটি স্তরকে নাম দেওয়া ভ্যারিয়েবল বা method বানান
checked exception-এ catch (Exception e) { }ভুল চাপা পড়ে, প্রোগ্রাম ভুল ফল নিয়ে চলেUncheckedIOException-এ মোড়ানো, বা সাধারণ loop
hot path-এ Function<Integer,Integer>প্রতিটি ডাকে boxing, অনেক বাড়তি objectIntUnaryOperator, IntPredicate, ToIntFunction
effectively final এড়াতে int[] sum = {0}নকশার সমস্যা চাপা পড়ে, thread-এ data raceloop বা stream-এর sum(), ফল ফেরত দেওয়া নকশা
অকারণে নিজের interface Checkerপরিচিত interface আর and/andThen হারায়আগে java.util.function দেখুন

AI-কে lambda-ভিত্তিক কোড লিখতে বা রিভিউ করতে বলার সময় এই কাঠামো ব্যবহার করুন:

আমি Java 21 শিখছি; lambda, functional interface আর method reference-এর মূল ধারণা জানি। কাজ: [যেমন — পণ্যের তালিকা থেকে বাজেটের মধ্যে আর স্টকে থাকা পণ্য বাছাই, তারপর একটি ছাড়ের নিয়ম প্রয়োগ] শর্ত: ১. কোনো lambda তিন লাইনের বেশি হবে না; বড় হলে নাম দেওয়া private method বানিয়ে method reference দেবে। ২. lambda-র ভেতরে আরেকটি lambda নেস্ট করবে না; দরকার হলে প্রতিটি নিয়মকে নাম দেওয়া Predicate বা Function ভ্যারিয়েবলে রাখো। ৩. প্রতিটি lambda বা method reference-এর পাশে মন্তব্যে লেখো এটা কোন functional interface (Predicate, Function…)। ৪. checked exception কখনো ফাঁকা catch দিয়ে চাপা দেবে না; হয় সাধারণ loop, নয়তো UncheckedIOException-এ মুড়ে কারণসহ ছুঁড়বে। ৫. int বা long নিয়ে বারবার চলা জায়গায় IntPredicate, IntUnaryOperator, ToIntFunction-এর মতো primitive interface ব্যবহার করবে। ৬. বাইরের ভ্যারিয়েবল বদলাতে int[] বা AtomicInteger-এর কৌশল ব্যবহার করবে না। ৭. শেষে বলো: কোন জায়গায় lambda-র চেয়ে সাধারণ loop বেশি পরিষ্কার হতো, আর কেন।

AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১“lambda” নামটি কোথা থেকে এলো? ১৯৩০-এর দশকের একটি গাণিতিক ধারণা কীভাবে Lisp হয়ে ২০১৪ সালে Java-তে পৌঁছাল?

১৯৩০-এর দশকের শুরুতে মার্কিন গণিতবিদ Alonzo Church গণিতের ভিত্তি নিয়ে কাজ করতে গিয়ে একটি ক্ষুদ্র আনুষ্ঠানিক ব্যবস্থা তৈরি করেন, যার নাম lambda calculus. এতে আছে শুধু তিনটি জিনিস: ভ্যারিয়েবল, function তৈরি (গ্রিক অক্ষর λ দিয়ে লেখা, যেমন λx. x + 1 — “x নাও, x + 1 দাও”), আর function প্রয়োগ। আশ্চর্যের বিষয়, এই তিনটি দিয়েই যেকোনো গণনা প্রকাশ করা যায়। ১৯৩৬ সালে Church এটি ব্যবহার করে দেখান যে কিছু গাণিতিক প্রশ্নের উত্তর কোনো যান্ত্রিক পদ্ধতিতে বের করা সম্ভব নয়; প্রায় একই সময়ে Alan Turing তাঁর Turing machine দিয়ে একই সিদ্ধান্তে পৌঁছান। পরে প্রমাণিত হয়, দুটি মডেল সমান ক্ষমতার।

Lisp-এ প্রবেশ। ১৯৫৮ সালে MIT-তে John McCarthy যখন Lisp নকশা করেন, তিনি Church-এর ধারণা সরাসরি নিয়েছিলেন — Lisp-এ নামহীন function লেখা হতো LAMBDA শব্দ দিয়ে। Lisp-এ function প্রথম শ্রেণির মান: ভ্যারিয়েবলে রাখা, argument হিসেবে পাঠানো, ফেরত দেওয়া যায়। এই ধারা আর lambda calculus-এর অনুপ্রেরণায় পরে Scheme, ML, Haskell-এর মতো functional ভাষা জন্মেছে।

মূলধারায় আসা। বহু দশক এই ধারণা মূলত গবেষণা আর বিশেষ ভাষায় সীমাবদ্ধ ছিল। কিন্তু ২০০০-এর দশকে JavaScript, Ruby, Python জনপ্রিয় হলো, যেখানে function-কে মান হিসেবে পাঠানো স্বাভাবিক; C# 2007 সালে lambda পেল, C++ পেল C++11-এ। Java ছিল সবচেয়ে বড় ভাষাগুলোর মধ্যে প্রায় শেষ — anonymous class দিয়ে কাজ চালাচ্ছিল। শেষে মার্চ ২০১৪-তে Java 8 lambda আনল, JSR 335-এর মাধ্যমে, OpenJDK-র Project Lambda-র ফল হিসেবে, যার স্পেসিফিকেশনের নেতৃত্বে ছিলেন Brian Goetz.

তত্ত্ব আর বাস্তবের পার্থক্য। Church-এর lambda শুধু function — কোনো টাইপ নেই, কোনো object নেই। Java-র lambda একটি functional interface-এর object, যার target type লাগে, যে checked exception মানে, আর যে বাইরের ভ্যারিয়েবল বদলাতে পারে না। নাম একই, কিন্তু Java ধারণাটিকে নিজের টাইপ ব্যবস্থার ছাঁচে ঢেলে নিয়েছে।

মূল উপলব্ধি: আপনি যখন p -> p.price() < 500 লেখেন, আপনি প্রায় নব্বই বছরের পুরোনো একটি গাণিতিক ধারণা ব্যবহার করছেন — যা গণনার সীমা বোঝার জন্য জন্মেছিল, আর আজ তালিকা থেকে সস্তা পণ্য বাছে।

প্র ০২C# ২০০৭ সালে lambda পেল, Java পেল ২০১৪ সালে। মাঝের বছরগুলোতে Java-তে closure নিয়ে কী বিতর্ক চলেছিল, আর এত দেরি কেন হলো?

২০০৬ সালের দিকে Java সম্প্রদায়ে দাবি জোরালো হয়: anonymous class খুব বেশি আনুষ্ঠানিক, Java-তে closure দরকার। পরের দুই বছরে তিনটি প্রধান প্রস্তাব আলোচনায় আসে, প্রতিটি ভিন্ন দর্শনের।

BGGA. নামটি চার লেখকের নামের আদ্যক্ষর — Gilad Bracha, Neal Gafter, James Gosling আর Peter von der Ahé. এটি ছিল সবচেয়ে উচ্চাভিলাষী: সত্যিকারের function টাইপ, বাইরের ভ্যারিয়েবল বদলানোর সুযোগ, আর এমন closure যার ভেতরের return বা break বাইরের method-কে প্রভাবিত করতে পারে — যাতে প্রোগ্রামাররা নিজেরাই নতুন control structure বানাতে পারেন। সমালোচকরা বললেন, এটা ভাষাকে অনেক জটিল করবে।

CICE. Bob Lee, Doug Lea আর Joshua Bloch-এর “Concise Instance Creation Expressions” ছিল উল্টো প্রান্তে: শুধু একটি method-ওয়ালা interface-এর জন্য anonymous class লেখাকে সংক্ষিপ্ত করা — নতুন টাইপ নেই, নতুন অর্থ নেই।

FCM. Stephen Colebourne আর Stefan Schulz-এর “First-Class Methods” মাঝামাঝি পথ নিয়েছিল — method-কে সরাসরি মান হিসেবে ব্যবহার, আজকের method reference-এর পূর্বসূরি ধারণা।

অচলাবস্থা আর সমাধান। কোনো প্রস্তাবে ঐকমত্য হয়নি, আর Java 7-এর কাজও দীর্ঘায়িত হচ্ছিল। ২০০৯ সালের শেষে Sun-এর Mark Reinhold ঘোষণা করেন Java-তে closure আসবে, আর OpenJDK-তে Project Lambda শুরু হয়। কিন্তু ২০১০ সালে বোঝা গেল সব একসাথে তৈরি হবে না; তথাকথিত “Plan B” অনুযায়ী Java 7 (২০১১) lambda ছাড়াই বের হলো, আর lambda গেল Java 8-এ। চূড়ান্ত নকশা অনেকটা CICE-এর আত্মা নিয়েছে (functional interface), FCM থেকে method reference, আর BGGA-র উচ্চাভিলাষী অংশগুলো — বদলানো যায় এমন capture, non-local return — বাদ দিয়েছে।

দেরির কারণ। Java-র কোটি কোটি লাইনের বিদ্যমান কোড আর পুরোনো সংস্করণের সাথে সামঞ্জস্য রাখা বাধ্যতামূলক। একটি ভুল ভাষা-বৈশিষ্ট্য চিরকাল থেকে যায়। তাই Java দল ধীরে, সাবধানে এগিয়েছে — আর সাথে collection-এ default method ও stream-এর মতো লাইব্রেরি পরিবর্তন একসাথে নকশা করেছে।

মূল উপলব্ধি: Java-র lambda একটি সমঝোতার ফল — সবচেয়ে শক্তিশালী নকশা নয়, কিন্তু বিদ্যমান কোড না ভেঙে লক্ষ লক্ষ প্রোগ্রামারের কাছে নিরাপদে পৌঁছানোর মতো নকশা।

প্র ০৩Kotlin বা C#-এ (Int) -> Boolean-এর মতো function টাইপ আছে। Java কেন আলাদা function টাইপ না এনে functional interface বেছে নিল — লাভ আর দাম কী?

Project Lambda-র নকশাকারীদের সামনে দুটি পথ ছিল। এক, ভাষায় নতুন ধরনের টাইপ যোগ করা — function type, যেমন “int নেয়, boolean দেয়”। দুই, বিদ্যমান টাইপ ব্যবস্থা ব্যবহার করা — একটি abstract method-ওয়ালা interface. Brian Goetz-এর “State of the Lambda” নথিতে দ্বিতীয় পথের যুক্তিগুলো বিস্তারিত আছে।

পুরোনো লাইব্রেরি নিজে থেকেই প্রস্তুত। সবচেয়ে বড় লাভ। Java-তে ইতিমধ্যে হাজার হাজার API ছিল যারা Runnable, Comparator, Callable, ActionListener-এর মতো একটি method-ওয়ালা interface নেয়। functional interface পথে এই সব API এক লাইনও না বদলে lambda গ্রহণ করতে শুরু করল — Collections.sort(list, (a, b) -> ...) Java 8-এ সরাসরি চলল। function টাইপ আনলে প্রতিটি API-র নতুন overload লাগত।

JVM আর generics-এর সীমা। JVM-এ কোনো function টাইপ নেই; সবকিছু class আর interface. নতুন টাইপ মানে হয় JVM বদলানো, নয়তো compiler-এর স্তরে লুকানো রূপান্তর। আবার Java-র generics type erasure দিয়ে কাজ করে আর primitive নেয় না (পর্ব ২৮), তাই (int) -> boolean-এর মতো টাইপকে generics-এর সাথে পরিষ্কারভাবে মেলানো কঠিন হতো।

নামের মূল্য। interface-এর নাম আর Javadoc থাকে। Comparator শুধু “দুটি T নিয়ে int দেয়” নয় — তার চুক্তি আছে (ঋণাত্মক, শূন্য, ধনাত্মক মানে কী), আর reversed, thenComparing-এর মতো default method. একটি খালি function টাইপে এসব রাখার জায়গা নেই।

দাম। প্রথমত, interface-এর বিস্ফোরণ: primitive-এর জন্য আলাদা সংস্করণ মিলিয়ে java.util.function-এ চল্লিশটির বেশি interface, আর তবুও তিন parameter-এর কিছু নেই। দ্বিতীয়ত, দুটি interface-এর গঠন হুবহু এক হলেও একে অন্যের জায়গায় বসে না — একটি Predicate<String>-কে সরাসরি একটি নিজের Checker<String> ভ্যারিয়েবলে রাখা যায় না, p::test দিয়ে রূপান্তর করতে হয়। তৃতীয়ত, checked exception: Function.apply-এর ঘোষণায় throws নেই, তাই অংশ ৭-এর ঝামেলা।

মূল উপলব্ধি: Java “নতুন কিছু না এনে পুরোনোকে নতুন অর্থ দেওয়া”-র পথ বেছে নিয়েছে — এতে বিদ্যমান কোড তাৎক্ষণিক উপকৃত হলো, বিনিময়ে কিছু মার্জিত ভাব হারাল।

প্র ০৪invokedynamic নির্দেশটি Java 7-এ এসেছিল JRuby বা Groovy-র মতো dynamic ভাষার জন্য। Java-র নিজের lambda কীভাবে সেটা কাজে লাগাল, আর এতে লাভ কী হলো?

JVM-এর প্রথম পনেরো বছরে method ডাকার নির্দেশ ছিল চারটি — invokestatic, invokevirtual, invokeinterface, invokespecial. চারটিতেই compile-এর সময় ঠিক থাকে কোন class-এর কোন method, কোন টাইপের parameter. Java-র জন্য এটা স্বাভাবিক। কিন্তু JRuby, Jython, Groovy-র মতো ভাষা, যেখানে টাইপ চলার সময় ঠিক হয়, JVM-এ চালাতে গেলে এই কাঠামো বাধা হয়ে দাঁড়াত; তাদের reflection আর নানা ঘুরপথ ব্যবহার করতে হতো, যা ধীর।

JSR 292. সমাধান হিসেবে John Rose-এর নেতৃত্বে Da Vinci Machine প্রকল্প থেকে JSR 292 জন্ম নেয়, যা ২০১১ সালে Java 7-এ যুক্ত হয়: পঞ্চম নির্দেশ invokedynamic, আর java.lang.invoke package (MethodHandle, CallSite). invokedynamic-এর বিশেষত্ব: কোন কোড চলবে, তা compiler ঠিক করে না। নির্দেশটি প্রথমবার চলার সময় JVM একটি bootstrap method ডাকে, যে একটি CallSite ফেরত দেয়; সেটাই ঠিক করে ওই জায়গায় কী চলবে। JIT compiler সেই সিদ্ধান্তকে সাধারণ method call-এর মতোই অপ্টিমাইজ করতে পারে।

lambda-র জন্য পুনর্ব্যবহার। Java 8-এর দল দেখল, এটাই lambda-র জন্য আদর্শ। javac lambda-র শরীরকে একটি private method বানায় আর lambda-র জায়গায় একটি invokedynamic বসায়, যার bootstrap হলো LambdaMetafactory.metafactory. প্রথমবার চলার সময় সেটি একটি implementation class তৈরি করে (সাম্প্রতিক JDK-তে একটি hidden class). bytecode-এ শুধু লেখা থাকে “এখানে এই interface-এর একটি object দরকার, শরীর অমুক method”; কীভাবে বানানো হবে, সেটা JDK-র ভেতরের ব্যাপার।

লাভ। এক, ডিস্কে হাজার হাজার ছোট class ফাইল নেই, jar ছোট। দুই, কৌশলটি compile করা কোডে আটকে নেই — JDK নিজের LambdaMetafactory উন্নত করলে পুরোনো jar-ও recompile ছাড়া লাভবান হয়, আর বাস্তবে পরের JDK-গুলোতে অভ্যন্তরীণ কৌশল বদলেছে। তিন, একই যন্ত্র পরে অন্য জায়গায় কাজে লেগেছে: Java 9 থেকে String-এর + জোড়া (JEP 280), আর Java 16-এর record-এর toString, equals, hashCode.

মূল উপলব্ধি: অন্য ভাষার জন্য তৈরি একটি সাধারণ যন্ত্র Java-র নিজের সবচেয়ে বড় ভাষা-পরিবর্তনের ভিত্তি হলো — ভালো নিম্নস্তরের বিমূর্ততা প্রায়ই তার নকশাকারীদের কল্পনার বাইরেও কাজে লাগে।

প্র ০৫functional programming-এর মূল ধারণা — অপরিবর্তনীয়তা, pure function, function-কে মান হিসেবে ব্যবহার। Java এগুলো কতটা গ্রহণ করেছে, আর কোথায় থেমেছে?

functional programming-এর কেন্দ্রে কয়েকটি ধারণা। প্রথমত, pure function: একই input-এ সবসময় একই output, আর বাইরের কিছু বদলায় না। দ্বিতীয়ত, immutable ডেটা: মান একবার তৈরি হলে বদলায় না, বদল মানে নতুন মান। তৃতীয়ত, function প্রথম শ্রেণির মান, আর ছোট function জুড়ে বড় function. এর সুফল: কোড বোঝা আর টেস্ট করা সহজ, আর ভাগ করা অবস্থা না থাকায় একাধিক thread-এ নিরাপদ।

Java যা নিয়েছে। Java 8 দিয়েছে lambda, method reference, java.util.function, stream (পর্ব ৩৫) আর Optional (পর্ব ৩৬). Java 9 দিয়েছে List.of, Map.of-এর মতো অপরিবর্তনীয় collection. Java 16-এ স্থায়ী হওয়া record অপরিবর্তনীয় ডেটা বানানো সহজ করেছে, আর Java 21-এ স্থায়ী record pattern ও switch-এর pattern matching functional ভাষার “ডেটা ভেঙে মেলানো” ধাঁচ এনেছে — OpenJDK-র Project Amber এগুলোর পেছনে। effectively final নিয়মও আসলে অপরিবর্তনীয়তার দিকে একটি ঠেলা।

যেখানে থেমেছে। Java মূলত একটি object-oriented, imperative ভাষাই রয়ে গেছে। ভ্যারিয়েবল আর field ডিফল্টভাবে বদলানো যায়; final লিখতে হয়। compiler pure function যাচাই করে না — একটি Predicate চাইলে ডেটাবেসে লিখতে পারে। tail-call optimization নেই, তাই গভীর recursion-এ StackOverflowError. JDK-তে persistent (কাঠামো ভাগ করে নতুন সংস্করণ বানানো) collection নেই — List.of শুধু বদলানো নিষেধ করে। checked exception আর lambda-র টানাপোড়েন রয়েই গেছে।

বাস্তব প্রভাব। তবু প্রভাব বিশাল। আধুনিক Java কোডে তালিকা প্রক্রিয়ার বেশিরভাগ stream দিয়ে লেখা, Spring-এর মতো framework callback আর lambda-ভিত্তিক API ব্যবহার করে, আর “অপরিবর্তনীয় record + pure method” এখন ভালো নকশার স্বীকৃত ধাঁচ। যাঁরা আরও গভীর functional ধারা চান, তাঁদের জন্য একই JVM-এ Scala, Clojure আর অনেকাংশে Kotlin আছে।

মূল উপলব্ধি: Java functional programming-কে বদলে ফেলার দর্শন হিসেবে নয়, দরকারি সরঞ্জামের বাক্স হিসেবে নিয়েছে — কোথায় অপরিবর্তনীয়তা আর pure function ব্যবহার করবেন, সেই সিদ্ধান্ত এখনো আপনার।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — ওপরের কোড বদলে দেখা আর ভাবাই আসল কাজ।

  1. interface চিনুন: প্রতিটি lambda-র জন্য সবচেয়ে মানানসই java.util.function interface বলুন — (ক) () -> LocalDate.now(), (খ) s -> s.isEmpty(), (গ) (a, b) -> a * b যেখানে দুটিই int, (ঘ) order -> sendSms(order), (ঙ) s -> s.length(), যা কোটিবার চলবে।

    (ক) Supplier<LocalDate> — কিছু নেয় না, একটি মান দেয়। (খ) Predicate<String> — হ্যাঁ/না। (গ) IntBinaryOperator — দুটি int নিয়ে int দেয়, boxing ছাড়া; boxed রূপ BinaryOperator<Integer>. (ঘ) Consumer<Order> — নেয়, কাজ করে, কিছু ফেরত দেয় না (যদি sendSms void হয়)। (ঙ) ToIntFunction<String> — ফল int, তাই কোটিবার Integer object বানাতে হয় না; method reference-এ String::length.

  2. রূপান্তর করুন: প্রতিটি lambda-কে method reference-এ লিখুন আর ধরনটি বলুন — (ক) s -> Double.parseDouble(s), (খ) p -> p.name(), (গ) x -> list.add(x), (ঘ) () -> new HashMap<String, Integer>(), (ঙ) s -> s.trim().toUpperCase().

    (ক) Double::parseDouble — static. (খ) Product::name — unbound, lambda-র প্রথম parameter-ই object. (গ) list::add — bound, একটি নির্দিষ্ট list object; reference তৈরির সময়েই list মূল্যায়িত হয়। (ঘ) HashMap::new — constructor; target type (Supplier<Map<String, Integer>>) থেকে generic টাইপ অনুমান হয়। (ঙ) এক method reference-এ সম্ভব নয়, কারণ দুটি method পরপর ডাকা হচ্ছে। চাইলে Function<String, String> f = String::trim; f.andThen(String::toUpperCase) — কিন্তু এখানে lambda-টাই বেশি পরিষ্কার।

  3. ফল অনুমান করুন: Function<Integer, Integer> tax = x -> x + x * 15 / 100; আর Function<Integer, Integer> off = x -> x - 100; দেওয়া আছে। tax.andThen(off).apply(1000) আর tax.compose(off).apply(1000) কত? দোকানের রসিদে কোনটা ন্যায্য, ভাবুন।

    andThen: আগে ভ্যাট, পরে ছাড় — ১০০০ + ১৫০ = ১১৫০, তারপর − ১০০ = ১০৫০. compose: আগে off, পরে tax — ১০০০ − ১০০ = ৯০০, তারপর ৯০০ + ১৩৫ = ১০৩৫. ক্রমভেদে ১৫ টাকার পার্থক্য! সাধারণত ছাড়ের পর দামের ওপর ভ্যাট হয় (দ্বিতীয়টি), তবে নিয়মটা ব্যবসা আর আইনের ব্যাপার, প্রোগ্রামিং-এর নয়। শিক্ষা: composition-এর ক্রম আসল হিসাব বদলায়; নিয়ম জোড়ার সময় ক্রমটা কোডে স্পষ্ট রাখুন, প্রয়োজনে afterDiscount.andThen(addVat)-এর মতো নাম দিয়ে।

  4. Error অনুমান ও ঠিক করুন: একটি method-এ লেখা: int count = 0; names.forEach(n -> { if (n.startsWith("A")) count++; }); System.out.println(count); — compiler কী বলবে? AI প্রস্তাব দিল int[] count = {0};. এর চেয়ে ভালো দুটি সমাধান বলুন।

    compiler বলবে local variables referenced from a lambda expression must be final or effectively final, কারণ count++ lambda-র ভেতর থেকে একটি ধরা ভ্যারিয়েবল বদলাচ্ছে। int[] কৌশল compile হবে, কিন্তু নকশার সমস্যা ঢেকে দেয় আর একাধিক thread-এ (যেমন parallel stream) ভুল গণনা দেবে। ভালো সমাধান: (১) সাধারণ for-each loop — for (String n : names) if (n.startsWith("A")) count++; — এখানে count ধরা হচ্ছে না, তাই নিয়ম প্রযোজ্য নয়। (২) পর্ব ৩৫-এর stream — long count = names.stream().filter(n -> n.startsWith("A")).count(); — কিছুই বদলায় না, ফলটা ফেরত আসে। দ্বিতীয়টি functional ধাঁচ, প্রথমটি সবচেয়ে সরল; দুটিই int[]-এর চেয়ে ভালো।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমJava-তে Predicate<Product> isCheap = p -> p.price() < 500; লেখা থেকে শরীরটি চলা পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 🎯 compiler target type খুঁজে পায়: Predicate<Product>
  2. 🔍 interface-এর একমাত্র abstract method বের করে: boolean test(Product)
  3. 🏷️ p-এর টাইপ Product অনুমান করে আর শরীরের টাইপ boolean কি না যাচাই করে
  4. 📦 শরীরটি একটি private method হয়, আর সেই জায়গায় invokedynamic বসে
  5. ⚙️ প্রথমবার চলার সময় LambdaMetafactory implementation object তৈরি করে
  6. ✅ isCheap.test(pen) ডাকলে lambda-র শরীর চলে

Java-তে functional interface বলতে কী বোঝায়?

  • যে interface-এর সব method default
  • যে interface-এর মাথায় @FunctionalInterface লেখা থাকতেই হবে
  • যে interface-এ ঠিক একটি abstract method আছে
  • শুধু java.util.function package-এর interface-গুলো

একটি String নিয়ে boolean ফেরত দেয় এমন lambda রাখার জন্য java.util.function-এর কোন interface সবচেয়ে মানানসই?

  • Consumer<String>
  • Predicate<String>
  • Supplier<Boolean>
  • UnaryOperator<String>

Java-তে String::toUpperCase method reference-টি নিচের কোন lambda-র সমান?

  • s -> String.toUpperCase(s)
  • () -> toUpperCase()
  • () -> str.toUpperCase(), যেখানে str একটি নির্দিষ্ট String
  • s -> s.toUpperCase()

Function<Integer,Integer> doubleIt = x -> x * 2; আর Function<Integer,Integer> plus3 = x -> x + 3; হলে doubleIt.compose(plus3).apply(5)-এর মান কত?

  • 16
  • 13
  • 10
  • compile error

একটি method-এ int total = 0; তারপর list.forEach(p -> total += p); লেখা। compile করলে কী হবে?

  • compile হবে, total-এ সব মানের যোগফল থাকবে
  • compile হবে, কিন্তু total ০-ই থাকবে
  • compile error: ভ্যারিয়েবলটি final বা effectively final নয়
  • চলার সময় ConcurrentModificationException

list.forEach-এর lambda-র ভেতরে Files.readString(p) ডাকলে, বাইরের method-এ throws IOException লেখা থাকলেও compile error কেন হয়?

  • lambda-র ভেতরে static method ডাকা নিষেধ
  • lambda-র শরীর Consumer.accept-এর শরীর, আর accept-এর ঘোষণায় IOException নেই
  • IOException একটি unchecked exception, তাই ঘোষণা করা যায় না
  • forEach কোনো exception বের হতে দেয় না

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. lambda = পাঠানো যায় এমন আচরণ: method-কে ডেটার সাথে নিয়মও দেওয়া — কাঠামো একবার লেখা, “কী করতে হবে” প্রতিবার আলাদা।
  2. lambda সবসময় একটি functional interface-এর object: target type দেখে compiler ঠিক করে সে কোন method-এর শরীর; target ছাড়া (var f = x -> ...) compile হয় না। নিজের interface-এ @FunctionalInterface.
  3. মূল পরিবার: Supplier (দেয়), Consumer (নেয়), Function (বদলায়), Predicate (বিচার করে), UnaryOperator/BinaryOperator; গরম loop-এ IntPredicate-এর মতো primitive সংস্করণ। and/or/negate/andThen/compose দিয়ে জোড়া।
  4. method reference চার ধরন: static (Integer::parseInt), bound (System.out::println), unbound (String::toUpperCase), constructor (ArrayList::new) — যেটা পড়তে সহজ, সেটা লিখুন।
  5. সীমা: ধরা ভ্যারিয়েবল effectively final; checked exception মুড়তে হয় বা loop লিখতে হয়; lambda-তে this বাইরের object; আলাদা .class নেই, invokedynamic চলার সময় implementation বানায়।

পরের পর্বে: Stream API. এই পর্বের Predicate, Function আর Consumer এবার একসাথে কাজ করবে — filter, map, collect দিয়ে loop না লিখেই তালিকা থেকে বাছাই, রূপান্তর আর হিসাব।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান