এই পর্বে যা শিখবেন
- Java 5-এর আগের সমস্যা —
Object-এর তালিকা আর দূরে গিয়েClassCastException - Generic class (
Box<T>,Pair<K,V>,Cache<K,V>), generic method, type inference আর diamond<> - Bounded type parameter, শুধু reference টাইপ কেন চলে, আর boxing-এর খরচ
- Invariance বনাম array covariance, wildcard
? extends,? super,?আর PECS - Type erasure-এর পরিণতি, raw type, unchecked warning, heap pollution — আর JDK-র জটিল signature ধাপে ধাপে পড়া
১ · সমস্যা — সবকিছুর ঝুড়ি আর অনেক দূরে গিয়ে crash
পর্ব ১৩-এ array দেখেছেন: String[]-এ শুধু String রাখা যায়, অন্য কিছু রাখতে চাইলে compiler আপত্তি করে। কিন্তু array-এর আকার স্থির। দরকার মতো বড় হয় এমন তালিকা — ArrayList — Java-তে শুরু থেকেই ছিল। প্রশ্ন হলো, সেই তালিকা কোন টাইপের জিনিস রাখবে? String-এর জন্য একটি StringList, Integer-এর জন্য IntegerList, Student-এর জন্য StudentList — এভাবে লিখলে একই কোড শতবার কপি করতে হতো।
Java 5 (২০০৪)-এর আগে সমাধান ছিল একটিই: তালিকা রাখবে Object. Java-তে সব class-ই Object-এর subclass, তাই যেকোনো জিনিস ঢোকানো যায়। কিন্তু বের করার সময় আপনি পান শুধু একটি Object, আর সেটাকে নিজের হাতে আসল টাইপে cast করতে হয়। নিচের কোডটি আজকের Java-তেও compile হয় (একটি সতর্কবার্তাসহ), তাই পুরোনো ধাঁচটা নিজের চোখে দেখা যায়:
import java.util.*;
class Main {
public static void main(String[] args) {
List names = new ArrayList(); // কোন টাইপ? কেউ জানে না
names.add("Rahim");
names.add(42); // ভুল — কিন্তু compiler চুপ
for (Object o : names) {
String s = (String) o; // এখানে crash
System.out.println(s.length());
}
}
}Note: Main.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. 5 Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap') at Main.main(Main.java:8)
খেয়াল করুন তিনটি জিনিস। প্রথমত, compiler শুধু একটি অস্পষ্ট “Note” দিল, কোনো error নয় — প্রোগ্রাম তৈরি হয়ে গেল। দ্বিতীয়ত, প্রথম নাম “Rahim”-এর দৈর্ঘ্য ৫ ঠিকঠাক ছাপা হলো; ভুলটা ধরা পড়ল লুপের পরের ধাপে। তৃতীয়ত, stack trace আঙুল তুলছে লাইন ৮-এর দিকে, যেখানে cast হচ্ছে — অথচ আসল ভুল লাইন ৬-এ, যেখানে 42 ঢোকানো হয়েছিল। ছোট প্রোগ্রামে দুটো লাইন পাশাপাশি। বড় প্রোগ্রামে add হয়তো এক ফাইলে, এক সপ্তাহ আগে লেখা, আর crash হয় অন্য মডিউলে, গ্রাহকের সামনে।
একটি গার্মেন্টস কারখানার গুদাম ভাবুন। সব কার্টন একরকম, কোনো লেবেল নেই। কেউ শার্টের সারিতে ভুল করে প্যান্টের কার্টন রেখে দিল — তখন কেউ টের পেল না। টের পাওয়া গেল তিন সপ্তাহ পরে, যখন বিদেশি ক্রেতার কন্টেইনার খোলা হলো। এবার প্রতিটি তাকে লেবেল লাগান: “শুধু শার্ট”। এখন ভুল কার্টন রাখতে গেলেই গুদামের দারোয়ান সেই মুহূর্তে আটকাবেন। Generics হলো সেই লেবেল, আর compiler সেই দারোয়ান।
লেবেলসহ একই কোড — List<String> — লিখলে কী হয়, দেখুন:
List<String> names = new ArrayList<>(); // লাইন ৪
names.add("Rahim");
names.add(42); // লাইন ৬Main.java:6: error: incompatible types: int cannot be converted to String
names.add(42);
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
ভুলটা এখন ঠিক যেখানে করা হয়েছে, সেখানেই ধরা পড়ল — চলার সময় নয়, compile-এর সময়। আর বের করার সময় cast লাগবে না: String s = names.get(0); লিখলেই চলে, কারণ compiler জানে এই তালিকায় শুধু String আছে। এটাই generics-এর মূল কথা: একটি class বা method একবার লিখুন, অনেক টাইপের সাথে নিরাপদে ব্যবহার করুন, আর টাইপ মিলছে কি না সেটা compiler-কে যাচাই করতে দিন। পর্ব ২০-এ যে parametric polymorphism-এর নাম শুনেছিলেন, Java-তে তার রূপ এটাই।
২ · Generic class — টাইপ যখন একটি parameter
Method-এর parameter একটি মান-এর জায়গা ধরে রাখে: area(double r) — r কত, তা ডাকার সময় ঠিক হয়। Generic class-এ একই কাজ হয় টাইপ-এর জন্য। সবচেয়ে ছোট উদাহরণ — একটি বাক্স, যাতে একটি জিনিস রাখা যায়:
class Box<T> { // T = এখনো অজানা কোনো টাইপ
private T value;
void set(T value) { this.value = value; }
T get() { return value; }
}
Box<String> name = new Box<>(); // এখানে T = String
name.set("Ayesha");
String s = name.get(); // cast নেই
Box<Integer> roll = new Box<>(); // এখানে T = Integer
roll.set(17);Class-এর নামের পরে কৌণিক বন্ধনীর ভেতরের T হলো type parameter — একটি নাম, যা এখনো কোনো আসল টাইপ নয়। ব্যবহার করার সময় যে আসল টাইপ বসানো হয় (String, Integer), তাকে বলে type argument. ঠিক যেমন method-এ “parameter” আর “argument” আলাদা শব্দ। Box<T>-এর মতো class-কে বলে generic class. Box<String>-এ name.set(42) লিখলে আগের মতোই error: incompatible types: int cannot be converted to String.
দুটি type parameter — Pair আর Cache
একটি method থেকে দুটি মান ফেরত দিতে চান — যেমন পরীক্ষার নম্বরের সর্বনিম্ন আর সর্বোচ্চ। পর্ব ২৫-এর record-কেও generic বানানো যায়:
record Pair<A, B>(A first, B second) { }
Pair<Integer, Integer> range = new Pair<>(65, 88);
Pair<String, Double> item = new Pair<>("চা", 15.0);একই কাঠামোতে একটি ছোট, টাইপ-নিরাপদ cache — ভেতরে একটি HashMap, বাইরে নিজের নিয়ম (পরে এখানে সীমা, মেয়াদ বা লগ যোগ করা যাবে, বাকি কোড না বদলেই):
class Cache<K, V> {
private final Map<K, V> map = new HashMap<>();
V get(K key) { return map.get(key); }
void put(K key, V value) { map.put(key, value); }
}
Cache<String, Integer> prices = new Cache<>();
prices.put("tea", 15);
prices.put(15, "tea"); // লাইন ১১ — উল্টো ক্রমMain.java:11: error: incompatible types: int cannot be converted to String
prices.put(15, "tea");
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
1 error
Key আর value উল্টে ফেলা খুব সাধারণ ভুল, আর Object-ভিত্তিক cache-এ এটা নীরবে ঢুকে যেত — পরে get("tea") শুধু null দিত, কেউ বুঝত না কেন।
নামের রীতি — এক অক্ষর, বড় হাতের
Type parameter-এর নাম যেকোনো identifier হতে পারে, কিন্তু Java-র রীতি হলো একটি বড় হাতের অক্ষর — যাতে কোডে দেখেই বোঝা যায় এটা আসল class নয়, একটি parameter:
| নাম | অর্থ | JDK-তে কোথায় দেখবেন |
|---|---|---|
T | Type — সাধারণ যেকোনো টাইপ | Optional<T>, Comparable<T> |
E | Element — collection-এর উপাদান | List<E>, Set<E> |
K, V | Key, Value | Map<K, V> |
R | Return / Result — ফলের টাইপ | Function<T, R> |
S, U | দ্বিতীয়, তৃতীয় টাইপ | BiFunction<T, U, R> |
৩ · Generic method, type inference আর diamond
পুরো class generic না করেও একটিমাত্র method-কে generic করা যায়। Type parameter-টি বসে return টাইপের আগে:
static <T> T firstOrDefault(T[] arr, T def) {
return arr.length > 0 ? arr[0] : def;
}
String name = firstOrDefault(new String[0], "অতিথি"); // T = String → "অতিথি"
Integer roll = firstOrDefault(new Integer[]{7, 9}, 0); // T = Integer → 7<T> ঘোষণা করে “এই method-এর জন্য একটি টাইপের নাম T”; তারপর T ফেরত দেয়, আর parameter-ও T. এমন method-কে বলে generic method. ডাকার সময় আপনি কোথাও লেখেননি T = String — compiler argument-গুলো দেখে নিজেই বের করে নিয়েছে। একে বলে type inference. দরকার হলে স্পষ্ট করেও বলা যায়, নামের আগে: Main.<String>firstOrDefault(arr, "অতিথি"). বাস্তবে প্রায় কখনো লাগে না।
Generic method আসলে আপনি আগেই ব্যবহার করেছেন: List.of(72, 88, 65), Arrays.asList(...), Collections.max(...) — সবই generic method, আর প্রতিবার compiler টাইপ অনুমান করে নেয়।
Diamond <> — একই কথা দুবার নয়
Java 5-এ লিখতে হতো Map<String, List<Integer>> marks = new HashMap<String, List<Integer>>(); — বাঁ দিকে যা, ডান দিকেও হুবহু তাই। Java 7 থেকে ডান দিকে শুধু ফাঁকা কৌণিক বন্ধনী দিলেই চলে: new HashMap<>(). দেখতে হীরার মতো বলে নাম diamond operator; বাঁ দিকের ঘোষণা থেকে compiler টাইপ বসিয়ে নেয়। Java 10-এর var-এর সাথে মেশালে সাবধান: var list = new ArrayList<>(); লিখলে অনুমান করার মতো কিছু নেই, তাই টাইপ হয় ArrayList<Object> — প্রায় কখনোই যা চান তা নয়। var ব্যবহার করলে ডান দিকে টাইপ লিখুন: var list = new ArrayList<String>();.
৪ · Bounded type — “যেকোনো টাইপ নয়, শুধু যারা তুলনা করা যায়”
এবার একটি max() লিখতে চাই, যা পরীক্ষার নম্বর, পণ্যের দাম বা নামের তালিকা — সবার জন্য চলবে। প্রথম চেষ্টা:
static <T> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b; // লাইন ৩
}Main.java:3: error: cannot find symbol
return a.compareTo(b) >= 0 ? a : b;
^
symbol: method compareTo(T)
location: variable a of type T
where T is a type-variable:
T extends Object declared in method <T>max(T,T)
শেষ লাইনটি পড়ুন: T extends Object. Compiler-এর কাছে শুধু <T> মানে “যেকোনো object” — আর যেকোনো object-এর compareTo থাকে না। আমাদের দরকার শর্ত জুড়ে দেওয়া: “T এমন টাইপ, যাকে নিজের সাথে তুলনা করা যায়।” পর্ব ২৩-এর Comparable interface ঠিক এই প্রতিশ্রুতি দেয়:
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b; // এখন বৈধ
}<T extends Comparable<T>> হলো একটি bounded type parameter. এখানে extends শব্দটি class আর interface দুই ক্ষেত্রেই ব্যবহার হয় (implements নয়), আর অর্থ “এর subtype হতে হবে”। লাভ দুই দিকে: ভেতরে compareTo ডাকা যায়, আর বাইরে কেউ তুলনা করা যায় না এমন টাইপ দিলে compiler আটকায়। কয়েকটি রূপ:
<T extends Number>— ভেতরেdoubleValue(),intValue()ডাকা যায়;Integer,Double,Longসবাই চলে,Stringনয়।<T extends Number & Comparable<T>>— একাধিক শর্ত,&দিয়ে জোড়া। নিয়ম: class (যদি থাকে) প্রথমে, তারপর interface-গুলো।<T extends Comparable<T>>— টাইপটি নিজের নাম দিয়েই নিজের শর্ত লিখছে। একে বলে recursive bound; JDK-রEnum<E extends Enum<E>>-ও এমন।
এবার পুরো ছবি — একটি generic max(), যা তিন ধরনের তালিকায় চলে, আর একটি generic Pair, যা দিয়ে method দুটি মান ফেরত দেয়। চালিয়ে দেখুন:
import java.util.List;
record Pair<A, B>(A first, B second) { }
class Main {
static <T extends Comparable<T>> T max(List<T> items) {
T best = items.get(0);
for (T x : items)
if (x.compareTo(best) > 0) best = x;
return best;
}
static Pair<Integer, Integer> minMax(List<Integer> marks) {
int lo = marks.get(0), hi = marks.get(0);
for (int m : marks) { lo = Math.min(lo, m); hi = Math.max(hi, m); }
return new Pair<>(lo, hi);
}
public static void main(String[] args) {
List<Integer> marks = List.of(72, 88, 65);
System.out.println(max(marks));
System.out.println(max(List.of(120.5, 99.0, 250.0)));
System.out.println(max(List.of("Karim", "Rahim", "Ayesha")));
Pair<Integer, Integer> r = minMax(marks);
System.out.println("min=" + r.first() + ", max=" + r.second());
System.out.println(new Pair<>("চা", 15.0));
}
}আউটপুট: 88, 250.0, Rahim, min=65, max=88 আর Pair[first=চা, second=15.0]. একটিই max() তিনবার তিনটি আলাদা টাইপে (Integer, Double, String) চলল — String-এর ক্ষেত্রে বর্ণানুক্রমে সবচেয়ে পরের নামটি। কোথাও কোনো cast নেই, আর max(List.of(new Object())) লিখলে compile-ই হবে না (method max in class Main cannot be applied to given types), কারণ Object Comparable নয়। minMax একটি Pair ফেরত দিয়ে দুটি মান একসাথে পাঠিয়েছে — Java-তে একাধিক return value নেই, এটাই পরিচ্ছন্ন বিকল্প। (টীকা: অনলাইন runner ফাইলটিকে অন্য নামে compile করে, তাই এখানে কোনো class public নয়; নিজের প্রজেক্টে public class Main থাকে Main.java ফাইলে।)
শুধু reference টাইপ — List<int> কেন নেই
ওপরের কোডে List<Integer> লেখা, List<int> নয়। চেষ্টা করলে:
Main.java:4: error: unexpected type
List<int> marks = new ArrayList<>();
^
required: reference
found: int
1 error
Type argument হতে পারে শুধু reference type — class, interface, array বা record. int, double-এর মতো primitive type নয়। কারণটি অংশ ৭-এ পরিষ্কার হবে: compile-এর পর T হয়ে যায় Object, আর Object-এর জায়গায় একটি কাঁচা int বসতে পারে না। তাই primitive-এর জন্য তার wrapper class (Integer, Double) ব্যবহার হয়, আর পর্ব ০৬-এর autoboxing marks.add(72)-কে নিজে থেকে Integer.valueOf(72) বানিয়ে দেয়।
সুবিধার একটি দাম আছে। int[]-এ প্রতিটি সংখ্যা পাশাপাশি ৪ byte. List<Integer>-এ প্রতিটি উপাদান হলো একটি reference, যা heap-এ আলাদা একটি Integer object দেখায় — প্রতিটি object-এর নিজস্ব header থাকে। ফলে মেমরি কয়েক গুণ বেশি লাগে, আর বড় লুপে boxing-unboxing সময়ও খায়। কয়েকশো নম্বরের তালিকায় এটা কোনো ব্যাপার নয়; কয়েক কোটি সংখ্যার হিসাবে (কনটেস্ট, ডেটা প্রসেসিং) int[] বা IntStream ভাবুন। আরেকটি ফাঁদ: দুটি Integer-কে == দিয়ে তুলনা করলে মান নয়, reference তুলনা হয় — ছোট মানে (−128 থেকে 127) cache-এর কারণে কাকতালীয়ভাবে ঠিক আসে, বড় মানে সাধারণত false. সবসময় equals ব্যবহার করুন।
৫ · Invariance — List<Integer> একটি List<Number> নয়
Integer একটি Number — পর্ব ১৯-এর inheritance অনুযায়ী Number n = 5; সম্পূর্ণ বৈধ। তাহলে স্বাভাবিকভাবেই মনে হয়, পূর্ণসংখ্যার তালিকাও একটি সংখ্যার তালিকা। Compiler কিন্তু রাজি নয়:
List<Integer> ints = new ArrayList<>();
List<Number> nums = ints; // লাইন ৫Main.java:5: error: incompatible types: List<Integer> cannot be converted to List<Number>
List<Number> nums = ints;
^
1 error
অর্থাৎ Integer আর Number-এর মধ্যে যে সম্পর্ক, List<Integer> আর List<Number>-এর মধ্যে সেটা নেই। Type argument আলাদা হলেই দুটি generic টাইপ সম্পর্কহীন — এই নিয়মের নাম invariance. শুনতে বাড়াবাড়ি মনে হয়, কিন্তু অনুমতি দিলে কী হতো, ধাপে ধাপে দেখুন:
1List<Integer> ints = new ArrayList<>();2ints.add(72);3List<Number> nums = ints; // ধরি বৈধ4nums.add(3.14); // Number তো!5Integer x = ints.get(1); // ?6List<? extends Number> ro = ints;7Number n = ro.get(0);8ro.add(3.14);
| তালিকার ভেতরে | |
| ints দেখে | |
| দ্বিতীয় ভ্যারিয়েবল |
- পূর্ণসংখ্যার তালিকা।
ints-এ একটি72. এ পর্যন্ত সব স্বাভাবিক। - ধরুন Java অনুমতি দিত।
numsএকটি নতুন তালিকা নয় — একই object-এর আরেকটি নাম (পর্ব ১৩-এর aliasing). নামের টাইপList<Number>, কিন্তু ভেতরের জিনিস পূর্ণসংখ্যার তালিকা। - এখানেই ছিদ্র।
List<Number>-এর চোখে3.14(একটিDouble) ঢোকানো সম্পূর্ণ বৈধ — Double তো Number. ফলে Integer-এর তালিকায় একটি Double ঢুকে গেল, আর কেউ টের পেল না। - Crash হতো অন্য জায়গায়।
intsব্যবহারকারী জানে তালিকায় শুধু Integer, তাইints.get(1)-কে Integer-এ রাখে —ClassCastException. ঠিক অংশ ১-এর সমস্যা ফিরে আসত। তাই Java লাইন ৩-কেই বাতিল করে; এটাই invariance-এর কারণ। - শুধু পড়তে চাইলে উপায় আছে।
List<? extends Number>মানে “Number বা তার কোনো subtype-এর তালিকা — ঠিক কোনটা, জানি না।”List<Integer>,List<Double>সবাই এখানে বসে। যা-ই বের হোক, সেটা অন্তত একটিNumber— তাই পড়া নিরাপদ। - কিন্তু লেখা বন্ধ। Compiler জানে না তালিকাটা আসলে Integer-এর না Double-এর, তাই কোনো কিছুই ঢোকাতে দেয় না (শুধু
nullছাড়া)। Error-এ যেCAP#1দেখছেন, সেটা compiler-এর দেওয়া নাম — “সেই অজানা টাইপটি”। ছিদ্র বন্ধ, কিন্তু পড়ার নমনীয়তা রইল।
Array কেন অন্যরকম — covariance আর তার দাম
মজার ব্যাপার হলো, array-তে Java ঠিক উল্টো সিদ্ধান্ত নিয়েছিল। Integer[] একটি Number[] হিসেবে মেনে নেওয়া হয় — এর নাম array covariance (পর্ব ১৩)। ফলে ওপরের ছিদ্রটি array-তে সত্যিই আছে, আর তা ধরা পড়ে চলার সময়:
Number[] nums = new Integer[2]; // compile হয়
nums[0] = 3.14; // লাইন ৪Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double at Main.main(Main.java:4)
Array নিজের আসল টাইপ (Integer[]) চলার সময়েও মনে রাখে, তাই প্রতিটি লেখার সময় JVM পরীক্ষা করে আর ভুল হলে ArrayStoreException ছোড়ে। Generic তালিকা চলার সময় নিজের type argument মনে রাখে না (অংশ ৭), তাই এমন পরীক্ষা সম্ভব নয় — সেজন্য পুরো নিরাপত্তা compile-এর সময়েই নিশ্চিত করতে হয়। Java-র নকশাকারদের ভাষায়: array-এর ভুল ধরা পড়ে দেরিতে, generics-এর ভুল আগে।
৬ · Wildcard আর PECS — কে দেয়, কে নেয়
প্রশ্নচিহ্ন ?-কে বলে wildcard — “কোনো একটি টাইপ, নাম জানার দরকার নেই।” তিন রূপে আসে:
? extends T
Upper-bounded wildcard. T বা তার যেকোনো subtype-এর তালিকা। বের করলে অন্তত T পাওয়া যায় — পড়ার জন্য। ঢোকানো যায় না।
যে তালিকা আপনাকে জিনিস দেয় (producer)? super T
Lower-bounded wildcard. T বা তার যেকোনো supertype-এর তালিকা। T ঢোকানো সবসময় নিরাপদ — লেখার জন্য। বের করলে শুধু Object.
?
Unbounded wildcard. যেকোনো কিছুর তালিকা। শুধু টাইপ-নিরপেক্ষ কাজ — size(), isEmpty(), clear(), ছাপা।
? super T থেকে পড়তে গেলে কী হয়, সেটাও compiler স্পষ্ট বলে। List<? super Integer> sink = new ArrayList<Number>();-এ sink.add(5) চলে, কিন্তু Integer x = sink.get(0); লিখলে: error: incompatible types: CAP#1 cannot be converted to Integer — কারণ তালিকাটা List<Object>-ও হতে পারত। আর List<?>-এ add("x") দিলে একই ধাঁচের error: String cannot be converted to CAP#1. এই CAP#1-কে বলে wildcard capture — compiler প্রতিটি ?-কে একটি নামহীন, নির্দিষ্ট কিন্তু অজানা টাইপ হিসেবে ধরে।
Producer Extends, Consumer Super. যে parameter থেকে আপনার method শুধু জিনিস নেয় (সে আপনাকে দেয় — producer), তার টাইপ ? extends T. যে parameter-এ আপনার method শুধু জিনিস রাখে (সে আপনার কাছ থেকে নেয় — consumer), তার টাইপ ? super T. দুটোই করলে wildcard নয়, ঠিক T. Joshua Bloch তাঁর Effective Java বইয়ে এই সংক্ষিপ্ত নামটি জনপ্রিয় করেন। এর তত্ত্বের নাম: ? extends দেয় covariance, ? super দেয় contravariance.
JDK-র Collections.copy-এর signature ঠিক এই নিয়মে লেখা: static <T> void copy(List<? super T> dest, List<? extends T> src). আমাদের নিজের একটি ছোট copy দিয়ে দেখি — পূর্ণসংখ্যার তালিকা থেকে সাধারণ সংখ্যার তালিকায়:
- src
List<Integer>? extends T — producer - T item
T = Integerলুপের ভেতরে - dst
List<Number>? super T — consumer
static <T> void copy(List<? super T> dst,
List<? extends T> src)
ডাক: copy(all, marks)
marks : List<Integer> all : List<Number>
T = Integer
src : List<Integer> ✔ Integer extends Integer
dst : List<Number> ✔ Number super Integer
for (T item : src)
src-এর আসল টাইপ যা-ই হোক (Integer বা তার subtype),
বের হওয়া জিনিস অন্তত একটি T — পড়া নিরাপদ
dst.add(item);
dst-এর আসল টাইপ List<Integer>, List<Number> বা List<Object> —
তিনটিতেই একটি Integer রাখা বৈধ — লেখা নিরাপদ
List<? super T> src → পড়লে শুধু Object, T নয় ✗
List<? extends T> dst → add(item) বাতিল (CAP#1) ✗
তাই: Producer Extends, Consumer Super
- দুটি parameter, দুটি ভূমিকা।
srcথেকে method জিনিস নেবে — সে producer.dst-এ method জিনিস রাখবে — সে consumer. Signature-ই সেটা ঘোষণা করছে। - T অনুমান। Compiler দেখে
marksহলোList<Integer>, তাইT = Integer. তারপর যাচাই:NumberকিInteger-এর supertype? হ্যাঁ —allচলবে।List<Object>-ও চলত। - Producer থেকে পড়া।
? extends Tনিশ্চয়তা দেয়: যা-ই বের হোক, সেটা অন্তত একটিT. তাইT item-এ রাখা যায়, কোনো cast ছাড়াই। - Consumer-এ লেখা।
? super Tনিশ্চয়তা দেয়: তালিকাটা T-এর বা T-এর কোনো পূর্বপুরুষের। একটিIntegerসেই সবগুলোতেই বৈধ, তাইaddনিরাপদ। - উল্টো করলে দুই দিকই ভাঙে।
? superথেকে পড়লে শুধুObjectপাওয়া যায়;? extends-এ লিখতে গেলে compiler আটকায়। PECS মুখস্থ না করে এই যুক্তিটা মনে রাখুন: “বের করব তো extends, ঢোকাব তো super.”
এবার চালিয়ে দেখুন — একটি printAll, যা পূর্ণসংখ্যা, দশমিক বা মেশানো যেকোনো সংখ্যার তালিকা নেয়, আর ওপরের copy:
import java.util.ArrayList;
import java.util.List;
class Main {
static void printAll(List<? extends Number> nums) { // producer
double total = 0;
for (Number n : nums) total += n.doubleValue();
System.out.println(nums + " total=" + total);
}
static <T> void copy(List<? super T> dst, List<? extends T> src) {
for (T item : src) dst.add(item); // src পড়ি, dst-এ লিখি
}
public static void main(String[] args) {
List<Integer> marks = List.of(72, 88, 65);
List<Double> prices = List.of(120.5, 99.0);
printAll(marks);
printAll(prices);
List<Number> all = new ArrayList<>();
copy(all, marks);
copy(all, prices);
printAll(all);
List<Object> log = new ArrayList<>();
copy(log, marks);
System.out.println(log);
}
}আউটপুট চার লাইন: [72, 88, 65] total=225.0, [120.5, 99.0] total=219.5, [72, 88, 65, 120.5, 99.0] total=444.5 আর [72, 88, 65]. একটিই printAll তিন ধরনের তালিকা নিল। Parameter যদি List<Number> হতো, তাহলে printAll(marks)-এ error আসত: incompatible types: List<Integer> cannot be converted to List<Number>. copy একবার T = Integer, একবার T = Double হয়ে একই List<Number>-এ লিখল, আর শেষে List<Object>-এও — consumer হিসেবে supertype-এর যেকোনো তালিকা চলে।
কোথায় wildcard নয়
- Return টাইপে নয়।
List<? extends Number> load()ফেরত দিলে যে ডাকে, সে তালিকায় কিছু যোগ করতে পারে না, আর প্রতিটি ব্যবহারে wildcard টেনে বেড়াতে হয়। Wildcard হলো parameter-এর নমনীয়তা, ফলের নয় — ফেরত দিনList<Number>বাList<T>. - পড়া ও লেখা দুটোই করলে নয়। যে method একই তালিকা থেকে পড়ে আবার সেখানেই লেখে (যেমন সাজানো বা দুই ঘর অদলবদল), তার জন্য
List<T>. - সহজ নিয়ম: কোনো type parameter যদি method-এর ঘোষণায় মাত্র একবার আসে, তাকে wildcard দিয়ে বদলানো যায় —
<T extends Number> double sum(List<T> x)-এর চেয়েdouble sum(List<? extends Number> x)পড়তে সহজ।
৭ · Type erasure — চলার সময় generics আর থাকে না
এতক্ষণ যা দেখলেন, তার প্রায় সবটাই compiler-এর কাজ। javac সব টাইপ যাচাই করার পর generic তথ্যগুলো মুছে ফেলে, আর bytecode-এ রেখে যায় সাধারণ, Object-ভিত্তিক কোড। এই কৌশলের নাম type erasure. ধাপে ধাপে দেখুন Box<String>-এর কী হয়:
- source
Box<T>আপনার লেখা - যাচাই
javacটাইপ মেলানো - মোছা
T → Objecterasure - cast বসানো
checkcastব্যবহারের জায়গায় - JVM
BoxT-এর খবর নেই
class Box<T> {
private T value;
void set(T v) { value = v; }
T get() { return value; }
}
Box<String> b = new Box<>();
b.set("চা");
String s = b.get();
b.set("চা"); ✔ String → T=String
b.set(42); ✗ int cannot be converted to String
String s = b.get(); ✔ T=String → String
class Box {
private Object value;
void set(Object v) { value = v; }
Object get() { return value; }
}
(<T extends Number> হলে T → Number)
Box b = new Box();
b.set("চা");
String s = (String) b.get();
← আপনি লেখেননি, compiler বসিয়েছে
new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass() → true getClass().getName() → java.util.ArrayList একটিই class ফাইল, সব type argument-এর জন্য
- Source. আপনার কোডে
Tআছে,Box<String>আছে, আর কোথাও কোনো cast নেই। - সব নিরাপত্তা এখানেই।
javacপ্রতিটিsetআরgetমিলিয়ে দেখে।b.set(42)এই ধাপেই বাতিল — এর পরে আর কোনো generic যাচাই হবে না। - T মুছে
Object. Class-এর ভেতরে প্রতিটিT-এর জায়গায় বসে তার bound — bound না থাকলেObject,<T extends Number>হলেNumber. ফলেBoxclass-টি Java 1.4-এর যেকোনোObject-ভিত্তিক class-এর মতোই দেখতে। - Cast ফিরে আসে — কিন্তু compiler-এর হাতে। অংশ ১-এর যে cast আপনি নিজে লিখতেন, সেটা compiler বসায় (bytecode-এ checkcast নির্দেশ)। পার্থক্য হলো, compiler ধাপ ২-এ প্রমাণ করে নিয়েছে যে এই cast কখনো ব্যর্থ হবে না — যতক্ষণ কোথাও raw type বা চাপা দেওয়া warning নেই।
- JVM জানে না T কী ছিল।
List<String>আরList<Integer>চলার সময় একই class. দুটিরgetClass()তুলনা করে চালালে সত্যিই ছাপা হয়true, আর class-এর নাম শুধুjava.util.ArrayList. এই সিদ্ধান্তের কারণ ছিল পুরোনো কোডের সাথে মিল রাখা — ভাবনার প্রশ্ন ২-এ বিস্তারিত।
Erasure-এর পরিণতি — যা লেখা যায় না
চলার সময় T-এর পরিচয় নেই, তাই যে কাজে চলার সময় টাইপ জানা দরকার, তা generic কোডে নিষেধ। প্রতিটির আসল compiler বার্তা:
| যা লিখতে চাইলেন | javac বলে | কারণ ও বিকল্প |
|---|---|---|
new T() | unexpected typerequired: classfound: type parameter T | কোন class-এর object বানাবে, JVM জানে না। বিকল্প: Supplier<T> বা Class<T> parameter হিসেবে নিন। |
new T[10] | generic array creation | Array চলার সময় নিজের টাইপ মনে রাখে, কিন্তু T জানা নেই। বিকল্প: List<T>. (ArrayList নিজে ভেতরে Object[] রাখে।) |
obj instanceof List<String>( obj একটি Object) | Object cannot be safely cast to List<String> | চলার সময় শুধু List পরীক্ষা করা সম্ভব। লিখুন obj instanceof List<?>. |
void print(List<String> a) আর void print(List<Integer> b) একই class-এ | name clash: print(List<Integer>) and print(List<String>) have the same erasure | Erasure-এর পর দুটিই print(List). বিকল্প: আলাদা নাম — printNames, printMarks. |
static T shared; | non-static type variable T cannot be referenced from a static context | Static field সব object-এর জন্য একটিই, অথচ T প্রতিটি object-এ আলাদা হতে পারে। |
যে টাইপের পুরো তথ্য চলার সময়েও থাকে (String, int[], List<?>, raw List), তাকে বলে reifiable type; List<String> reifiable নয়। ওপরের প্রায় সব নিষেধের মূলে এই একটি পার্থক্য।
Raw type আর unchecked warning
অংশ ১-এর List names = new ArrayList(); — type argument ছাড়া generic টাইপের নাম — একে বলে raw type. এটি Java-তে রাখা হয়েছে শুধু ২০০৪-এর আগের কোড যাতে আজও compile হয়। নতুন কোডে কখনো নয়। ডিফল্টে compiler শুধু সেই দুই লাইনের “Note” দেয়; বিস্তারিত দেখতে -Xlint:unchecked দিয়ে compile করুন:
javac -Xlint:unchecked Main.javaMain.java:5: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
names.add("Rahim");
^
where E is a type-variable:
E extends Object declared in interface List
Main.java:6: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
names.add(42);
^
where E is a type-variable:
E extends Object declared in interface List
2 warnings
এমন সতর্কবার্তাকে বলে unchecked warning: “এখানে আমি টাইপ যাচাই করতে পারিনি — ধাপ ৪-এর cast যে নিরাপদ, সেই নিশ্চয়তা আর দিতে পারছি না।” Raw তালিকাকে generic ভ্যারিয়েবলে বসালেও একই সতর্কতা আসে: List<String> names = raw; → warning: [unchecked] unchecked conversion. প্রতিটি unchecked warning হলো একটি সম্ভাব্য ClassCastException-এর ঠিকানা। এগুলো ঠিক করুন; চাপা দেওয়ার কথা পরের অংশের AI টেবিলে।
Heap pollution আর @SafeVarargs (সংক্ষেপে)
যখন List<String> টাইপের কোনো ভ্যারিয়েবল আসলে এমন object দেখায় যাতে String নয় এমন জিনিস আছে, সেই অবস্থাকে বলে heap pollution. Raw type এর একটি পথ; আরেকটি পথ generic varargs. T... items আসলে একটি array, আর T-এর array চলার সময় তৈরি করা যায় না — তাই compiler Object[]-ধাঁচের array বানায় এবং সতর্ক করে:
Main.java:3: warning: [unchecked] Possible heap pollution from parameterized vararg type T
static <T> List<T> listOf(T... items) {
^
where T is a type-variable:
T extends Object declared in method <T>listOf(T...)
1 warning
Method যদি সেই array-তে কিছু না লেখে আর array-টি বাইরে না পাঠায়, তাহলে লেখক @SafeVarargs annotation দিয়ে প্রতিশ্রুতি দিতে পারেন: “এটা নিরাপদ।” এটি বসানো যায় শুধু static, final বা private method আর constructor-এ। মনে রাখবেন, @SafeVarargs একটি প্রতিশ্রুতি — compiler তা প্রমাণ করে না। List.of(...), Arrays.asList(...) এমন annotation-যুক্ত JDK method.
৮ · Generic interface আর JDK-র signature পড়া
Interface-ও generic হতে পারে, আর JDK-র সবচেয়ে ব্যবহৃত দুটি interface ঠিক তাই। Comparable<T> বলে “আমাকে একটি T-এর সাথে তুলনা করা যায়” — class Student implements Comparable<Student> লিখলে compareTo(Student other) লিখতে হয়, Object নিয়ে cast করার দরকার নেই। Comparator<T> হলো বাইরের একটি তুলনার নিয়ম: Comparator<Student> byGpa. দুটির পার্থক্য আর ব্যবহার বিস্তারিত পর্ব ৩৩-এ; এখানে শুধু এটুকু দেখুন যে টাইপ parameter থাকায় ভুল টাইপের সাথে তুলনা compile-ই হয় না। তালিকা নিয়ে আরও গভীরে যাবেন পর্ব ৩০-এ।
এবার সেই signature, যা দেখে অনেকেই প্রথমবার ঘাবড়ে যান — Collections.sort:
public static <T extends Comparable<? super T>> void sort(List<T> list)এক টুকরো করে পড়ি:
static— object ছাড়াইCollections.sort(...)ডাকা যায়।Collectionsএকটি utility class.<T— return টাইপের আগে কৌণিক বন্ধনী মানে এটি একটি generic method; এর type parameter-এর নাম T. প্রতিটি ডাকে compiler T অনুমান করবে।extends Comparable<…>— bound: T-কে তুলনা করা যায় এমন হতে হবে। না হলে সাজানো অর্থহীন। (>>দুটি বন্ধনী একসাথে বন্ধ হচ্ছে — ভেতরেরটা Comparable-এর, বাইরেরটা type parameter তালিকার।)? super T— সবচেয়ে সূক্ষ্ম অংশ। T-কে নিজের সাথে নয়, T বা তার কোনো পূর্বপুরুষের সাথে তুলনা করতে পারলেই চলবে। কেন?LocalDateনিজেকে তুলনা করে তার পূর্বপুরুষ-টাইপChronoLocalDate-এর সাথে — সে একটিComparable<ChronoLocalDate>,Comparable<LocalDate>নয়। শুধুComparable<T>হলেList<LocalDate>সাজানো যেত না। এটাই PECS:compareToT গ্রহণ করে — consumer, তাই super.void— কিছু ফেরত দেয় না; তালিকাটিকেই জায়গায় সাজায়।sort(List<T> list)— এখানে wildcard নেই, ঠিকList<T>. কারণ sort একই তালিকা থেকে পড়ে আর সেখানেই লেখে — পড়া ও লেখা দুটোই, তাই অংশ ৬-এর নিয়মে wildcard নয়।
? super T সত্যিই কতটা দরকারি, তা যাচাই করা যায়। অংশ ৪-এর কঠোর <T extends Comparable<T>> max(List<T>)-কে একটি List<LocalDate> দিলে:
Main.java:7: error: method max in class Main cannot be applied to given types;
max(days);
^
required: List<T>
found: List<LocalDate>
reason: inference variable T has incompatible equality constraints ChronoLocalDate,LocalDate
where T is a type-variable:
T extends Comparable<T> declared in method <T>max(List<T>)
1 error
Bound বদলে <T extends Comparable<? super T>> করলেই একই তালিকায় max কাজ করে, আর Collections.sort(days)-ও। আরও এক ধাপ জটিল উদাহরণ: Collections.max-এর signature হলো <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll). ওই আপাত-অর্থহীন Object & আছে erasure-এর কারণে: প্রথম bound Object হওয়ায় erasure-এর পর method-টি Object ফেরত দেয়, ঠিক Java 5-এর আগের max-এর মতো — ফলে পুরোনো compile করা কোড ভাঙেনি।
C-তে generics নেই; সাধারণ কোডের জন্য void* আর হাতে cast — অংশ ১-এর সমস্যার আরও বিপজ্জনক রূপ, কারণ ভুল cast-এ exception নয়, undefined behavior. C++-এর template দেখতে একই রকম (vector<int>), কিন্তু কৌশল উল্টো: compiler প্রতিটি টাইপের জন্য আলাদা কোড বানায়, তাই vector<int> সরাসরি চলে আর new T()-ও লেখা যায় — Java মোছে, C++ কপি করে। Python-এর list[int], TypeVar শুধু type hint; Python নিজে চলার সময় সেগুলো যাচাই করে না, mypy-এর মতো টুল করে — Java-তে যাচাই compiler-এর ভেতরেই বাধ্যতামূলক। তুলনার জন্য দেখুন C++-এর class template আর Python-এর type hint ও mypy পর্ব।
- Generics কেন দরকার? — compile-এর সময় টাইপ যাচাই, cast বাদ, আর একই কোড অনেক টাইপে; ভুল ধরা পড়ে
ClassCastExceptionহওয়ার আগেই। - Type erasure কী? — compile-এর পর generic তথ্য মুছে
Tহয়Objectবা তার bound, আর compiler cast বসায়; তাই চলার সময়List<String>আরList<Integer>একই class. List<Integer>কিList<Number>-এ বসানো যায়? — না, generics invariant; বসাতে চাইলেList<? extends Number>, যা থেকে শুধু পড়া যায়।- PECS কী? — Producer Extends, Consumer Super: যেখান থেকে পড়বেন সেখানে
? extends T, যেখানে লিখবেন সেখানে? super T. List<int>কেন হয় না? — type argument শুধু reference টাইপ হতে পারে;List<Integer>আর autoboxing ব্যবহার করুন।
৯ · AI-এর যুগে — unchecked warning চাপা নয়, পড়ুন
Generics-এ AI-এর ভুলগুলো চেনা যায় সহজে, কারণ প্রায় সবই একটি প্রবণতা থেকে আসে: “compile হলেই হলো।” সবচেয়ে বিপজ্জনকটি হলো সতর্কবার্তা চাপা দেওয়া। নিচের কোডটি কোনো warning ছাড়াই compile হয়, কারণ @SuppressWarnings("unchecked") সেটা লুকিয়ে ফেলেছে:
import java.util.*;
class Main {
@SuppressWarnings("unchecked")
static List<String> load() {
List raw = new ArrayList();
raw.add(7); // আসল ভুল — লাইন ৬
return raw;
}
public static void main(String[] args) {
List<String> names = load();
System.out.println(names.size());
String first = names.get(0); // crash — লাইন ১২
}
}1 Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap') at Main.main(Main.java:12)
লাইন ১২-তে কোনো cast লেখা নেই, তবু ClassCastException — এটা অংশ ৭-এর ধাপ ৪-এর লুকানো cast. size() ঠিকঠাক ১ দিল; তালিকাটা “দূষিত” অবস্থায় ঘুরে বেড়াল যতক্ষণ না কেউ ভেতরের জিনিস String হিসেবে বের করল। Stack trace দেখাচ্ছে লাইন ১২, ভুল লাইন ৬-এ — একটি @SuppressWarnings-এর দাম।
| AI যা করে | কেন সমস্যা | আপনি যা করবেন |
|---|---|---|
পুরোনো উদাহরণ থেকে raw type: List list = new ArrayList(); | compile-এর সময়ের সব নিরাপত্তা বন্ধ; crash পরে, অন্য জায়গায় | -Xlint:unchecked দিয়ে compile করুন; প্রতিটি raw type-এ type argument বসান |
পুরো class বা method-এ @SuppressWarnings("unchecked") | আসল bug-ও চাপা পড়ে যায় | আগে ঠিক করার চেষ্টা; একান্ত দরকারে শুধু একটি ভ্যারিয়েবলের ঘোষণায়, পাশে কেন নিরাপদ তার মন্তব্য |
Return টাইপে wildcard: List<? extends Item> getItems() | যে ডাকে সে কিছু যোগ করতে পারে না, wildcard সংক্রমিত হয় | wildcard শুধু parameter-এ; ফেরত দিন List<Item> |
PECS উল্টো: যেখানে লিখবে সেখানে ? extends | add compile হয় না; তখন AI raw type বা cast দিয়ে “ঠিক” করে | প্রতিটি parameter-এর পাশে লিখুন “পড়ি না লিখি”, তারপর extends/super মেলান |
<T extends Comparable<T>> সর্বত্র | LocalDate-এর মতো টাইপে চলে না | সাধারণ library কোডে Comparable<? super T> |
দরকার ছাড়াই <T> জোড়া, যেখানে T একবারই আসে | পড়া কঠিন, কোনো লাভ নেই | একবার এলে wildcard, একেবারে না লাগলে সাধারণ টাইপ |
Generic কোড লেখাতে বা রিভিউ করাতে এই কাঠামো ব্যবহার করুন — শেষের শর্তগুলোই AI-কে প্রতিটি টাইপ-সিদ্ধান্তের কারণ বলতে বাধ্য করে:
AI-এর generic কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Java ১৯৯৬ সালে generics ছাড়া এল, আর generics এল প্রায় আট বছর পরে। এগুলো কোথা থেকে এল, আর এত সময় লাগল কেন?
Java-র প্রথম সংস্করণ (১৯৯৬) ইচ্ছা করেই ছোট রাখা হয়েছিল। C++-এর template তখন জটিলতার জন্য কুখ্যাত — দুর্বোধ্য error বার্তা, ধীর compile — আর Java দল সেই জটিলতা আনতে চায়নি। কিন্তু অল্প দিনেই বোঝা গেল, Vector আর Hashtable-এ সবকিছু Object হিসেবে রাখা আর বারবার cast করা অংশ ১-এর মতো bug-এর কারখানা।
গবেষণাগার থেকে। ১৯৯৬-৯৭ সালে Martin Odersky আর Philip Wadler “Pizza” নামে একটি পরীক্ষামূলক ভাষা তৈরি করেন — Java-র ওপর generics, first-class function আর algebraic data type যোগ করে, যা compile হতো সাধারণ JVM bytecode-এ। Functional programming-এর দুনিয়ায় (ML, Haskell) parametric polymorphism তখন বহু পুরোনো, পরীক্ষিত ধারণা; Pizza দেখাল যে তা Java-তেও আনা যায়। এরপর Gilad Bracha, Odersky, David Stoutamire আর Wadler শুধু generics-এর ওপর মনোযোগ দিয়ে তৈরি করেন GJ (Generic Java), যার নকশা প্রকাশিত হয় ১৯৯৮ সালে। সেই গবেষণাপত্রের শিরোনামটিই পুরো দর্শন বলে দেয়: “Making the Future Safe for the Past” — ভবিষ্যৎকে অতীতের জন্য নিরাপদ করা।
Standard হতে সময়। GJ-কে ভিত্তি করে Java Community Process-এ JSR 14 শুরু হয় ১৯৯৯ সালে। একটি ভাষার মূল টাইপ-ব্যবস্থা বদলানো সহজ কাজ নয়: লক্ষ লক্ষ লাইনের পুরোনো কোড, পুরো standard library, আর সব JVM — কিছুই ভাঙা চলবে না। Wildcard-এর নকশা যোগ হয় পরে, আরেকটি গবেষণা থেকে। শেষ পর্যন্ত generics আসে J2SE 5.0-তে, ২০০৪ সালে — একই সংস্করণে autoboxing, enum, for-each আর annotation. Odersky নিজে পরে উল্লেখ করেছেন যে GJ-এর জন্য লেখা তাঁর compiler-ই Sun-এর javac-এর ভিত্তি হয়, generics চালু হওয়ার আগেই। পরে তিনি Scala তৈরি করেন, আর Wadler, Maurice Naftalin-এর সাথে Java Generics and Collections বইটি লেখেন।
শিক্ষা। আট বছর দেরি মানে ব্যর্থতা নয়; এটা ছিল একটি চলমান, বিশাল ecosystem-এ সাবধানে নতুন ধারণা ঢোকানোর দাম। Java-র প্রায় প্রতিটি বড় বৈশিষ্ট্য — lambda, module, record — একই ধাঁচে এসেছে: আগে গবেষণা আর পরীক্ষামূলক রূপ, তারপর দীর্ঘ আলোচনা, তারপর সামঞ্জস্য রেখে প্রবেশ।
মূল উপলব্ধি: Java generics কোনো কোম্পানির হঠাৎ সিদ্ধান্ত নয় — functional programming-এর তত্ত্ব, Pizza আর GJ-এর পরীক্ষা, আর পুরোনো কোড না ভাঙার কঠোর শর্ত মিলে তার আজকের চেহারা।
প্র ০২Java কেন type erasure বেছে নিল, যেখানে C# চলার সময়েও টাইপ মনে রাখে আর C++ প্রতিটি টাইপের জন্য আলাদা কোড বানায়?
Generic কোড বাস্তবায়নের তিনটি প্রধান কৌশল আছে, আর তিনটি বড় ভাষা তিনটি আলাদা পথ নিয়েছে।
C++ — কোড তৈরি। Template-এ vector<int> আর vector<string>-এর জন্য compiler দুটি সম্পূর্ণ আলাদা কোড তৈরি করে। লাভ: সর্বোচ্চ গতি, primitive সরাসরি, new T() লেখা যায়। দাম: বড় binary (একই কোডের অনেক কপি), ধীর compile, আর template-এর ভুলে কয়েক পাতার error বার্তা।
C# — চলার সময়েও টাইপ। Microsoft .NET 2.0 (২০০৫)-এ reified generics আনে; Microsoft Research-এর Andrew Kennedy আর Don Syme এর নকশায় বড় ভূমিকা রাখেন। সেখানে runtime নিজেই জানে একটি তালিকা List<int> না List<string>; value type-এর জন্য আলাদা বিশেষ কোড তৈরি হয়, তাই List<int>-এ boxing নেই, আর typeof(T), new T() (শর্তসাপেক্ষে) চলে। দাম: runtime বদলাতে হয়েছিল, আর পুরোনো non-generic collection আর নতুনগুলো দুটি আলাদা জগৎ হয়ে রইল।
Java — মুছে ফেলা। Java-র সামনে ছিল কঠিন শর্ত, যাকে বলা হয় migration compatibility: পুরোনো compile করা .class ফাইল নতুন library-র সাথে চলবে, নতুন generic কোড পুরোনো library ব্যবহার করতে পারবে, আর কোম্পানিগুলো একসাথে সব কোড না বদলে ধাপে ধাপে generics-এ যাবে। Erasure ঠিক এটাই দিল: ArrayList<String> আর পুরোনো ArrayList একই class, তাই raw type দিয়ে দুই জগৎ জোড়া লাগে। JVM-এর একটি নির্দেশও বদলাতে হয়নি; generic signature-এর তথ্য class ফাইলে metadata হিসেবে থাকে (reflection দিয়ে পড়া যায়), কিন্তু object নিজে তা বহন করে না।
দাম কে দিল। অংশ ৭-এর পুরো নিষেধ-টেবিল, List<int>-এর অনুপস্থিতি আর boxing-এর খরচ, heap pollution — সবই এই সিদ্ধান্তের ফল। আবার লাভটাও বিরাট: ২০০৪ সালে বিশ্বের বিপুল Java কোড কোনো দিন-রাত্রির রূপান্তর ছাড়াই generics পেল। Kotlin, Scala-র মতো JVM ভাষাগুলোও একই erasure-এর ওপর চলে, তাই তারা Java library অনায়াসে ব্যবহার করতে পারে।
মূল উপলব্ধি: কোনো কৌশল নিখুঁত নয় — C++ গতি কেনে আকার দিয়ে, C# ক্ষমতা কেনে runtime বদলে, আর Java পুরোনো কোডের সাথে সামঞ্জস্য কিনেছে চলার সময়ের টাইপ-তথ্য দিয়ে।
প্র ০৩List<int> কি কখনো Java-তে আসবে? Project Valhalla কী করতে চাইছে?
Java-র টাইপ-ব্যবস্থায় একটি পুরোনো ফাটল আছে: primitive (int, double) আর object. Primitive দ্রুত আর ঘন — মেমরিতে পাশাপাশি বসে। Object-এর পরিচয় (identity) আছে, header আছে, heap-এ আলাদা জায়গা নেয়, আর reference দিয়ে পৌঁছাতে হয়। Generics শুধু object-এর সাথে কাজ করে, তাই List<Integer>-এর প্রতিটি সংখ্যা আলাদা object — যে মেমরি আর গতির দাম অংশ ৪-এ দেখেছেন। আধুনিক CPU-তে এই “পয়েন্টার ধরে লাফানো” আরও ব্যয়বহুল, কারণ cache-এ পাশাপাশি থাকা ডেটা অনেক দ্রুত পড়া যায়।
Valhalla-র লক্ষ্য। Project Valhalla হলো OpenJDK-র একটি দীর্ঘমেয়াদি প্রকল্প, যার কথা Brian Goetz, John Rose প্রমুখ ২০১৪ সালের দিকে প্রকাশ্যে বলা শুরু করেন। মূল ধারণা: এমন class, যার object-এর identity নেই — শুধু মান। একটি Point(x, y) বা Money-এর ক্ষেত্রে “এটা কি সেই একই object?” প্রশ্নটার কোনো অর্থ নেই; দুটির মান সমান হলেই তারা সমান। Identity না থাকলে JVM এদের primitive-এর মতো পাশাপাশি, header ছাড়া রাখতে পারে। “Codes like a class, works like an int” — Valhalla দলের বহুল উদ্ধৃত স্লোগান।
Generics-এর সাথে সম্পর্ক। দ্বিতীয় ধাপে লক্ষ্য হলো generics-কে এমন value type আর primitive-এর ওপরও কাজ করানো, যাতে List<int>-এর মতো কিছু boxing ছাড়া দক্ষভাবে চলে, আর পুরোনো erasure-ভিত্তিক কোডও না ভাঙে। এটা অত্যন্ত কঠিন সমস্যা — আবারও সেই migration compatibility, এবার বিশ বছরের generic কোডের সাথে।
এখন কোথায় দাঁড়িয়ে — সাবধানে বলি। প্রকল্পটি বেশ কয়েকবার নকশা বদলেছে। Value class-এর প্রস্তাব (JEP 401) নিয়ে কাজ ও early-access build প্রকাশিত হয়েছে, কিন্তু এই লেখার সময় পর্যন্ত Java 21 বা পরের কোনো LTS সংস্করণে value class বা primitive-এর ওপর generics স্থায়ী বৈশিষ্ট্য হিসেবে আসেনি। কোনো তারিখের প্রতিশ্রুতি দেওয়া ঠিক হবে না; আগ্রহী হলে OpenJDK-র Valhalla পাতায় সর্বশেষ অবস্থা দেখে নিন।
আজ আপনার জন্য। পারফরম্যান্স-সংবেদনশীল কোডে int[], IntStream বা primitive-বিশেষায়িত library ব্যবহার করুন; বাকি জায়গায় List<Integer>-এর সুবিধা দামের চেয়ে বেশি।
মূল উপলব্ধি: List<int>-এর অনুপস্থিতি কোনো ভুল নয়, ২০০৪-এর erasure সিদ্ধান্তের ফল — আর সেটা সামঞ্জস্য না ভেঙে ঠিক করা এতটাই কঠিন যে একটি পুরো প্রকল্প এক দশকের বেশি সময় ধরে কাজ করছে।
প্র ০৪Wildcard নিয়ে এত অভিযোগ কেন? Kotlin আর Scala একই সমস্যা কীভাবে অন্যভাবে সমাধান করেছে?
Java-র wildcard একটি তাত্ত্বিকভাবে শক্তিশালী ধারণা থেকে এসেছে। Atsushi Igarashi আর Mirko Viroli-র “variant parametric types” গবেষণা, আর তার ওপর Mads Torgersen, Gilad Bracha, Neal Gafter, Peter von der Ahé প্রমুখের কাজ মিলে ২০০৪ সালে wildcard Java 5-এ ঢোকে। কিন্তু বাস্তব প্রোগ্রামারদের অভিজ্ঞতা ছিল মিশ্র। CAP#1-যুক্ত error বার্তা, <T extends Comparable<? super T>>-এর মতো signature, আর “কেন আমি এই তালিকায় কিছু যোগ করতে পারছি না” — এগুলো দীর্ঘদিন Java-র সবচেয়ে কঠিন অংশ বলে বিবেচিত। Joshua Bloch-এর PECS স্মৃতিসূত্রটি জনপ্রিয় হওয়াই প্রমাণ করে যে নিয়মটা স্বাভাবিকভাবে মাথায় আসে না।
মূল সমস্যা: use-site variance. Java-তে variance লিখতে হয় ব্যবহারের জায়গায় — প্রতিটি method-এর প্রতিটি parameter-এ আলাদা করে ? extends বা ? super. একটি টাইপ যদি স্বভাবগতভাবেই শুধু producer হয় (যেমন একটি অপরিবর্তনীয় তালিকা, যা থেকে শুধু পড়া যায়), তবু প্রতিটি ব্যবহারকারীকে সেটা বারবার লিখতে হয়। কেউ ভুলে গেলে তার API অকারণে অনমনীয় হয়ে যায়।
Declaration-site variance. Scala (২০০৪ থেকে) আর পরে Kotlin অন্য পথ নিল: variance লেখা হয় টাইপ ঘোষণার সময়, একবার। Scala-তে List[+A] মানে List covariant; Kotlin-এ interface Source<out T> মানে T শুধু বের হয়, আর Comparable<in T> মানে T শুধু ঢোকে। Compiler যাচাই করে যে out টাইপ সত্যিই কোনো parameter-এ ঢোকে না। ফলে Kotlin-এর read-only List<out E>-এ List<Int> সরাসরি List<Number> হিসেবে চলে — ব্যবহারকারীকে কিছুই লিখতে হয় না। একে বলে declaration-site variance. C# 4.0 (২০১০)-ও interface আর delegate-এর জন্য out/in আনে। Kotlin দরকার হলে use-site রূপও রাখে (type projection), কিন্তু সেটা ব্যতিক্রম।
Java কেন বদলায়নি? Java-র collection interface-গুলো (List) পড়া ও লেখা দুটোই দেয়, তাই তারা স্বভাবগতভাবে invariant; declaration-site variance সেখানে সরাসরি খাটে না। আর বিদ্যমান কোড না ভেঙে এমন বদল আনা কঠিন। তবে বিষয়টি নিয়ে Java সম্প্রদায়ে আলোচনা হয়েছে।
মূল উপলব্ধি: Wildcard ভুল নয়, কিন্তু খরচ চাপায় ব্যবহারকারীর ওপর; পরের ভাষাগুলো variance-কে টাইপের লেখকের দায়িত্বে সরিয়ে একই নিরাপত্তা অনেক কম ঝামেলায় দিয়েছে।
প্র ০৫২০১৬ সালে দেখানো হয় যে Java-র টাইপ-ব্যবস্থা “unsound”. এর মানে কী, আর তবু Java নিরাপদ থাকে কীভাবে?
একটি টাইপ-ব্যবস্থাকে sound বলা হয়, যদি compiler যে প্রোগ্রাম মেনে নেয়, তা চলার সময় কখনো টাইপ-ভুলে না পড়ে — সংক্ষেপে, “Well-typed programs cannot go wrong” (Robin Milner, ১৯৭৮)। Java-র ক্ষেত্রে এর একটি ব্যবহারিক অর্থ: কোনো cast বা raw type না লিখলে, কোনো unchecked warning না থাকলে, generics-এর কারণে ClassCastException হওয়ার কথা নয়।
Amin আর Tate-এর আবিষ্কার। ২০১৬ সালের OOPSLA সম্মেলনে Nada Amin আর Ross Tate “Java and Scala’s Type Systems are Unsound: The Existential Crisis of Null Pointers” নামে একটি গবেষণাপত্র উপস্থাপন করেন। তাঁরা একটি ছোট Java প্রোগ্রাম দেখান — কোনো cast নেই, কোনো raw type নেই, কোনো unchecked warning নেই — যা javac মেনে নেয়, অথচ চলার সময় ClassCastException দেয়। কৌশলটি জটিল: wildcard-এর lower আর upper bound, আর null মিলিয়ে compiler-কে “প্রমাণ” করানো যায় যে যেকোনো টাইপ অন্য যেকোনো টাইপের subtype. যুক্তির মূল ফাঁকটা হলো, null সব reference টাইপের মান হতে পারে, তাই অসম্ভব শর্তের “প্রমাণ” হিসেবে null পাঠানো যায়। একই ধরনের সমস্যা Scala-তেও দেখানো হয়। এর আগে সাধারণভাবে ধরে নেওয়া হতো যে wildcard-সহ এই টাইপ-ব্যবস্থা sound.
তবু Java নিরাপদ কেন। এখানেই অংশ ৭-এর erasure-এর একটি অপ্রত্যাশিত সুফল। Compiler যেখানে generic মান বের করে, সেখানে একটি সত্যিকারের checkcast বসায়, আর JVM সেটা চলার সময় পরীক্ষা করে। তাই টাইপ-ব্যবস্থার যুক্তিতে ফাঁক থাকলেও, ভুল object কখনো ভুল টাইপ হিসেবে ব্যবহার হয় না — তার আগেই ClassCastException. মেমরি নষ্ট হয় না, JVM-এর নিরাপত্তা ভাঙে না। C বা C++-এ একই ধরনের ফাঁক মানে হতো undefined behavior.
ব্যবহারিক গুরুত্ব। সাধারণ কোডে এমন প্রোগ্রাম কেউ ভুল করে লেখে না — উদাহরণটি ইচ্ছা করে বানানো। কিন্তু গবেষণাটি দেখায়, বৈশিষ্ট্যগুলো (wildcard, null, inference) আলাদাভাবে যুক্তিসঙ্গত হলেও একসাথে মিশলে অপ্রত্যাশিত ফল দিতে পারে; আর ভাষা-নকশায় আনুষ্ঠানিক প্রমাণ কেন জরুরি। Scala 3-এর তাত্ত্বিক ভিত্তি (DOT calculus) এমন প্রমাণের দিকেই গেছে।
মূল উপলব্ধি: Compiler-এর যাচাই আর JVM-এর চলার সময়ের যাচাই — দুই স্তরের নিরাপত্তা থাকায় তাত্ত্বিক ফাঁকও বাস্তবে নিরাপদ exception-এ পরিণত হয়; আর এখানে erasure-এর বসানো cast-ই শেষ পাহারাদার।
অনুশীলন
বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — ওপরের কোড বদলে দেখা আর ভাবাই আসল কাজ।
-
Compile হবে কি না বলুন: প্রতিটির জন্য বলুন compile হবে কি না, আর না হলে কেন। (ক)
List<Number> a = new ArrayList<Integer>();(খ)List<? extends Number> b = new ArrayList<Integer>();(গ) তার পরেb.add(5);(ঘ)List<? super Integer> c = new ArrayList<Number>(); c.add(5);(ক) না — invariance;
ArrayList<Integer>কোনোList<Number>নয়, compiler বলবেincompatible types. (খ) হ্যাঁ —? extends Numberযেকোনো Number-subtype-এর তালিকা নেয়। (গ) না —? extends-এ লেখা নিষেধ,int cannot be converted to CAP#1ধাঁচের error; compiler জানে না তালিকাটা আসলে Double-এর কি না। (ঘ) হ্যাঁ —? super Integerমানে Integer বা তার পূর্বপুরুষের তালিকা, যেখানে Integer রাখা সবসময় বৈধ (consumer). তবে সেখান থেকেInteger x = c.get(0);লিখলে আবার error — পড়লে শুধুObject. -
একটি generic method লিখুন:
lastOrDefaultলিখুন, যা যেকোনো টাইপেরListনেয় আর শেষ উপাদান ফেরত দেয়, তালিকা খালি হলে দেওয়া ডিফল্ট মান।lastOrDefault(List.of(72, 88), 0)দেবে88,lastOrDefault(List.of(), "নেই")দেবে"নেই".static <T> T lastOrDefault(List<? extends T> list, T def) { return list.isEmpty() ? def : list.get(list.size() - 1); }শুধু
List<T>দিলেও চলত।? extends Tদিলে তালিকাটা producer — যেমনList<Integer>থেকেNumberডিফল্টসহ (T = Number) ডাকা যায়।List.of()-এর ক্ষেত্রে compiler ডিফল্ট মান"নেই"দেখেT = Stringঅনুমান করে। কোথাও cast নেই, আর-Xlint:all-এ কোনো warning আসে না। -
Wildcard বেছে নিন: প্রতিটি parameter-এর জন্য
List<T>,List<? extends T>,List<? super T>বাList<?>— কোনটা? (ক)average(nums)— সংখ্যাগুলোর গড়। (খ)fillZeros(list, n)— তালিকায় n-টি0যোগ করে। (গ)countItems(list)— শুধুsize()ফেরত দেয়। (ঘ)swap(list, i, j)— দুটি ঘর অদলবদল।(ক)
List<? extends Number>— শুধু পড়ে, producer. (খ)List<? super Integer>— শুধু লেখে, consumer; তাহলেList<Integer>,List<Number>,List<Object>সবাই চলবে। (গ)List<?>— ভেতরের টাইপ নিয়ে কোনো কাজই নেই। (ঘ)List<T>(বা<T> void swap(List<T> list, ...)) — একই তালিকা থেকে পড়ে আবার সেখানেই লেখে, তাই wildcard নয়। মজার তথ্য: JDK-রCollections.swapবাইরে থেকেList<?>নেয়, আর ভেতরে সচেতনভাবে একটি raw type আর@SuppressWarningsব্যবহার করে — নিরাপদ, কারণ একই তালিকা থেকে নেওয়া জিনিসই সেখানে ফেরত রাখা হয়। এটা যুক্তিসহ, ছোট পরিসরে চাপা দেওয়ার উদাহরণ; অন্ধভাবে নয়। -
তারিখের তালিকায় max: অংশ ৪-এর চালানো যায় এমন প্রোগ্রামে
import java.time.LocalDate;যোগ করুন। তারপরList<LocalDate> days = List.of(LocalDate.of(2026, 12, 16), LocalDate.of(2026, 3, 26));লিখেmax(days)ছাপুন। কী হয়? Signature-এ ঠিক কোন একটি বদল করলে কাজ করবে, আর কেন?Compile error:
method max in class Main cannot be applied to given types, কারণ হিসেবেinference variable T has incompatible equality constraints ChronoLocalDate,LocalDate.LocalDate(তার interfaceChronoLocalDate-এর মাধ্যমে) একটিComparable<ChronoLocalDate>,Comparable<LocalDate>নয় — তাইT extends Comparable<T>শর্ত পূরণ হয় না। Bound বদলে<T extends Comparable<? super T>>করলে কাজ করে, আউটপুট2026-12-16.compareToএকটি T গ্রহণ করে — consumer — তাই PECS অনুযায়ী super. বাকি তিনটি ডাক (নম্বর, দাম, নাম) আগের মতোই চলবে। একটি সূক্ষ্ম ব্যাপার:List.of(...)সরাসরিmax(...)-এর ভেতরে লিখলে পুরোনো signature-ও compile হয়ে যায়, কারণ তখন compiler তালিকাটিকেইList<ChronoLocalDate>ধরে নেয় আরT = ChronoLocalDateঅনুমান করে।List<LocalDate>টাইপের ভ্যারিয়েবল দিলে সেই সুযোগ থাকে না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমJava-তে Box<String> ব্যবহার করা কোড লেখা থেকে চলা পর্যন্ত ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📝 প্রোগ্রামার
Box<String>লেখেন, কোথাও cast নেই - 🔍 javac প্রতিটি
set/get-এ type argument মিলিয়ে যাচাই করে - 🧽 erasure: class-এর ভেতরে
Tহয়ে যায়Object - 🩹 মান বের করার জায়গায় compiler
(String)cast বসায় - ⚙️ JVM শুধু
Boxclass চালায়, T কী ছিল জানে না
List<Integer> ints = new ArrayList<>(); List<Number> nums = ints; — Java-তে এই দুই লাইন compile করলে কী হবে?
- ঠিকঠাক compile হবে, কারণ Integer একটি Number
- compile error: incompatible types
- compile হবে, চলার সময় ArrayStoreException
- শুধু একটি unchecked warning আসবে
একটি method শুধু একটি তালিকার সংখ্যাগুলো পড়ে যোগফল বের করে, আর List<Integer>, List<Double> সবই নিতে চায়। parameter-এর সবচেয়ে উপযুক্ত টাইপ কোনটি?
- List<Number>
- List<? super Number>
- List<? extends Number>
- raw List
class Box<T extends Number> { T value; } — type erasure-এর পর bytecode-এ value field-এর টাইপ কী হয়?
- Number
- Object
- T, JVM চলার সময় ঠিক করে
- যে টাইপ দিয়ে Box বানানো হয়, প্রতিটির জন্য আলাদা
Java-তে List<int> marks = new ArrayList<>(); লিখলে কী হয়?
- autoboxing-এর কারণে স্বয়ংক্রিয়ভাবে List<Integer> হয়ে যায়
- boxing ছাড়া দ্রুত একটি primitive তালিকা তৈরি হয়
- compile হয়, প্রথম add-এ exception
- compile error: unexpected type, required: reference
নিচের কোনটি একটি generic class Box<T>-এর ভেতরে বা সাধারণ Java কোডে compile হয়?
- Box-এর ভেতরে T make() { return new T(); }
- Box-এর ভেতরে T[] items = new T[10];
- List<?> any = new ArrayList<String>();
- একই class-এ void print(List<String> a) আর void print(List<Integer> b)
List names = new ArrayList(); names.add("Rahim"); names.add(42); String s = (String) names.get(1); — Java 21-এ কী ঘটে?
- names.add(42) লাইনে compile error
- unchecked note দিয়ে compile হয়, চলার সময় ClassCastException
- s-এর মান হয় "42"
- raw type Java 21-এ বাতিল, তাই compile error
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Generics = টাইপ একটি parameter:
Box<T>,Pair<A,B>,<T> T firstOrDefault(...)— একবার লিখুন, অনেক টাইপে; ভুল টাইপ compile-এর সময়েই ধরা পড়ে, cast লাগে না। - Bound শর্ত দেয়:
<T extends Comparable<? super T>>মানে “তুলনা করা যায়”; type argument শুধু reference টাইপ, তাইList<Integer>আর boxing-এর খরচ। - Generics invariant:
List<Integer>কোনোList<Number>নয় — নইলে Double ঢুকে পড়ত। Array covariant, তাই সেখানেArrayStoreException. - PECS: পড়ার parameter
? extends T, লেখার parameter? super T, দুটোই হলেT; return টাইপে wildcard নয়। - Erasure: চলার সময়
Tনেই —new T(),new T[],instanceof List<String>, একই erasure-এর overload নিষেধ। Raw type আর unchecked warning মানে সম্ভাব্যClassCastException— চাপা নয়, ঠিক করুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৯ · exception — checked, unchecked আর try-with-resourcesপরবর্তী পর্বভুল হলে প্রোগ্রাম কীভাবে খবর দেয় আর সামলায়।
- Java প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- C++ · class templateগভীরে যানএকই ধারণা, উল্টো কৌশল — প্রতিটি টাইপের জন্য আলাদা কোড।
- C++ · function templateপাশের ট্র্যাকgeneric method-এর C++ রূপ আর type deduction.
- Python · type hint আর mypyতুলনাTypeVar, list[int] — যাচাই যখন compiler নয়, আলাদা টুল করে।