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

record — ডেটার জন্য ছোট class

Java Records — Immutable Data Carriers, Compact Constructors and Defensive Copies

Taught in Bangla, this lesson introduces records: replacing boilerplate data classes, what the compiler generates from one line, accessor naming like amount(), validation in a compact constructor, extra constructors and static factories, record limits, shallow immutability with List.copyOf, records as map keys, local records, and when to prefer a class or Lombok.

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

  • Data carrier class কী, আর হাতে লেখা ডেটা class-এ boilerplate কেন ভুলের উৎস
  • এক লাইনের record থেকে compiler ঠিক কী কী তৈরি করে, আর accessor-এর নাম কেন amount(), getAmount() নয়
  • Compact constructor দিয়ে যাচাই ও মান ঠিক করা, অন্য constructor, method আর static factory যোগ করা
  • record-এর সীমা, shallow immutability-র ফাঁদ আর List.copyOf দিয়ে defensive copy
  • Map/Set-এ record, local record, record pattern-এর ঝলক, আর কখন record ব্যবহার করবেন না

১ · যে class শুধু ডেটা বহন করে — আর boilerplate-এর বোঝা

পর্ব ১৫ থেকে আপনি যত class লিখেছেন, সেগুলোকে দুই ভাগে ফেলা যায়। এক ভাগের class-এর ভেতরে আসল কাজ আছে — Account টাকা জমা নেয়, নিয়ম পাহারা দেয়, ব্যালান্স বদলায়। আরেক ভাগের class কোনো কাজই করে না; শুধু কয়েকটি মান একসাথে বেঁধে এক জায়গা থেকে আরেক জায়গায় নিয়ে যায়। একটি bKash লেনদেনের রেকর্ড (আইডি, টাকার অঙ্ক, ধরন), মানচিত্রের একটি বিন্দু (অক্ষাংশ, দ্রাঘিমাংশ), একজন পরীক্ষার্থীর ফল (রোল, GPA), কোনো API থেকে আসা একটি উত্তর। এদের বলে data carrier — ডেটার বাহক।

bKash-এ টাকা পাঠালে একটি SMS আসে: “TrxID ABC123, Tk 500 sent to 017…”. এই রসিদের কাজ শুধু তথ্য জানানো। রসিদ নিজে কিছু করে না, আর পরে কেউ এটা বদলাতেও পারে না — বদলালে সেটা আর আসল রসিদ থাকে না। দুটি রসিদের সব তথ্য হুবহু এক হলে আমরা বলি “এ দুটো একই লেনদেনের রসিদ”। record ঠিক এমন জিনিসের জন্য: অপরিবর্তনীয়, স্বচ্ছ, আর মান দিয়ে চেনা যায়।

সমস্যা হলো, Java 16-এর আগে এমন একটি সাধারণ রসিদ লিখতেও অনেক কোড লাগত। তিনটি field-এর একটি লেনদেন class “ঠিকভাবে” লিখলে দেখতে এরকম:

import java.util.Objects;

public final class Transaction {
    private final String id;
    private final long amount;
    private final String type;

    public Transaction(String id, long amount, String type) {
        this.id = id;
        this.amount = amount;
        this.type = type;
    }

    public String getId()   { return id; }
    public long getAmount() { return amount; }
    public String getType() { return type; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Transaction)) return false;
        Transaction other = (Transaction) o;
        return amount == other.amount
                && Objects.equals(id, other.id)
                && Objects.equals(type, other.type);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, amount, type);
    }

    @Override
    public String toString() {
        return "Transaction[id=" + id + ", amount=" + amount
                + ", type=" + type + "]";
    }
}

প্রায় ৪০ লাইন, অথচ এর মধ্যে নতুন তথ্য মাত্র একটি: “একটি লেনদেনে আছে id, amount আর type”. বাকি সব হলো boilerplate — একই ছাঁচে বারবার লেখা কোড। আর boilerplate শুধু ক্লান্তিকর নয়, বিপজ্জনকও। পর্ব ২৩-এ দেখেছেন equals আর hashCode-এর চুক্তি: দুটি object equals হলে তাদের hashCode সমান হতেই হবে। এখন কেউ class-এ চতুর্থ field fee যোগ করলেন, equals-এ যোগ করলেন, hashCode-এ ভুলে গেলেন — কোড compile হবে, কিন্তু HashMap-এ অদ্ভুত bug আসবে। ৪০ লাইনের ভেতরে এই ভুল চোখে পড়ে না।

Java 16-এ একই জিনিস লেখা যায় এক লাইনে:

record Transaction(String id, long amount, String type) {}

এটাই record. বন্ধনীর ভেতরের তিনটি অংশকে বলে record component. এই এক লাইনের ভেতরে ওপরের ৪০ লাইনের প্রায় সবকিছু আছে — আর যেহেতু compiler লেখে, equals আর hashCode কখনো একে অপরের সাথে অমিল হয় না। নতুন component যোগ করলে সব method নিজে থেকেই হালনাগাদ হয়।

মূল ধারণা

record শুধু “কম টাইপ করার শর্টকাট” নয়। এটা একটি ঘোষণা: “এই টাইপটি শুধু এই কয়টি মানের স্বচ্ছ, অপরিবর্তনীয় বাহক; এর state মানেই এর component, এর বাইরে কিছু লুকানো নেই।” এই প্রতিশ্রুতির কারণেই compiler নিরাপদে constructor, accessor, equals, hashCode আর toString বানিয়ে দিতে পারে। Java-র নকশাকারীদের ভাষায় record হলো “transparent carrier for immutable data”.

record এসেছে Oracle-এর Project Amber থেকে, যেখানে Brian Goetz-এর নেতৃত্বে Java ভাষার ছোট ছোট উন্নতি নিয়ে কাজ হয়। প্রথমে JDK 14-এ (২০২০ সালের মার্চ) JEP 359 হিসেবে preview feature, তারপর JDK 15-এ JEP 384 হিসেবে দ্বিতীয় preview, আর শেষে JDK 16-এ (২০২১ সালের মার্চ) JEP 395 হিসেবে স্থায়ী। অর্থাৎ Java 17 আর Java 21 — দুটি LTS সংস্করণেই record পুরোপুরি ব্যবহার করা যায়। ভাবনার প্রশ্ন ১-এ এই যাত্রার বিস্তারিত।

২ · এক লাইনের ভেতরে কী আছে — compiler যা তৈরি করে

record Transaction(String id, long amount, String type) {} লিখলে javac আসলে একটি পূর্ণাঙ্গ class তৈরি করে। Reflection দিয়ে compile হওয়া class-টির ভেতরটা দেখলে (OpenJDK 21-এ যাচাই করা) যা পাওয়া যায়, ধাপে ধাপে দেখুন:

ধাপ ১আপনি যা লিখেছেন
record Transaction(String id, long amount, String type) {}

একটি নাম + তিনটি component (নাম ও টাইপ)
শরীর {} খালি — তবু নিচের সব তৈরি হবে
ধাপ ২class-এর মাথা আর field
final class Transaction extends java.lang.Record {
    private final String id;
    private final long   amount;
    private final String type;
প্রতিটি component থেকে একটি private final field
class নিজে final — কেউ extends করতে পারবে না
ধাপ ৩canonical constructor
    Transaction(String id, long amount, String type) {
        this.id = id;
        this.amount = amount;
        this.type = type;
    }
সব component, একই ক্রমে — এর নাম canonical constructor
access record-এর মতোই (এখানে package-private)
ধাপ ৪accessor method
    public String id()     { return id; }
    public long   amount() { return amount; }
    public String type()   { return type; }
নাম component-এর নামেই — getAmount() নয়
setter একটিও নেই
ধাপ ৫equals, hashCode, toString
    public final boolean equals(Object o)  // সব component তুলনা
    public final int hashCode()            // সব component থেকে
    public final String toString()         // নাম=মান তালিকা
}
Transaction[id=TX101, amount=500, type=SEND]   ← toString
  1. শুরু এক লাইনে। record-এর হেডারে থাকে নাম আর component-এর তালিকা, ঠিক method-এর parameter-এর মতো দেখতে। এই তালিকাকে বলে record-এর state description — record-এর সব অবস্থা এখানেই ঘোষিত।
  2. Field আর class-এর মাথা। প্রতিটি component-এর জন্য একটি private final field. Class-টি নিজে final, আর তার superclass java.lang.Record — আপনি লেখেননি, compiler বসিয়েছে। Transaction.class.isRecord() ডাকলে true পাওয়া যায়।
  3. Canonical constructor. canonical constructor সব component একই ক্রমে নেয় আর field-এ বসায়। এটি ছাড়া record-এর object বানানোর অন্য কোনো পথ নেই — আপনি অন্য constructor লিখলেও শেষ পর্যন্ত এটিকেই ডাকতে হবে (অংশ ৪)।
  4. Accessor. প্রতিটি component-এর জন্য একটি public accessor method, নাম হুবহু component-এর নামে: t.amount(). Setter নেই, কারণ field final.
  5. তিনটি Object method. equals সব component তুলনা করে, hashCode সব component থেকে হিসাব করে, toString “নাম[component=মান, …]” আকারে লেখা দেয়। Reflection-এ এই তিনটি public final হিসেবে দেখা যায়। দরকার হলে আপনি নিজে override করতে পারেন, কিন্তু প্রায় কখনো দরকার হয় না।

getAmount() নয়, amount() — JavaBeans-এর সাথে পার্থক্য

পর্ব ১৭-এ getter-এর নাম রেখেছিলেন getBalance() — এটা JavaBeans রীতি। record এই রীতি মানে না; accessor-এর নাম component-এর নামেই। পুরোনো অভ্যাসে t.getAmount() লিখলে javac বলে:

Main.java:5: error: cannot find symbol
        System.out.println(t.getAmount());
                            ^
  symbol:   method getAmount()
  location: variable t of type Transaction
1 error

কেন এই সিদ্ধান্ত? কারণ record-এর দর্শন হলো “component আর accessor একই জিনিস”। হেডারে যা লিখেছেন, ঠিক সেই নামেই মান ফেরত পাবেন — কোনো নামবদল নেই। এর একটি ব্যবহারিক মূল্যও আছে: অংশ ৮-এর record pattern-এ Transaction(String id, long amount, String type) লিখে যে নামে ভাঙা হয়, constructor-এ সেই নামে বানানো হয় আর accessor-ও সেই নামে পড়ে। তিন জায়গায় এক নাম।

বিষয়হাতে লেখা ডেটা classrecord
Fieldনিজে লিখতে হয়; final দিতে ভুলে যাওয়া যায়নিজে থেকেই private final
Constructorনিজে লিখতে হয়canonical constructor তৈরি হয়
পড়ার methodgetAmount() (JavaBeans)amount()
equals / hashCodeহাতে লেখা, অমিলের ঝুঁকিসব component ধরে, সবসময় মিলে
toStringলিখতে ভুলে গেলে Transaction@1b6d3586-এর মতো অর্থহীন লেখাTransaction[id=TX101, amount=500, type=SEND]
Inheritanceextends করা যায় (final না দিলে)নিজে থেকেই final, অন্য class extends করতে পারে না

toString নিয়ে একটি ছোট কথা। পর্ব ২৩-এ সতর্ক করা হয়েছিল, Object-এর ডিফল্ট toString (ClassName@hash) প্রতিবার চালালে আলাদা হতে পারে, তাই সেটা ছাপা ঠিক নয়। record-এর toString সে রকম নয় — এটা শুধু component-এর মান দিয়ে তৈরি, তাই একই মান মানে একই লেখা। লগে আর ডিবাগে নিশ্চিন্তে ছাপতে পারেন। তবে Record-এর Javadoc বলে এর সঠিক ফরম্যাট implementation-এর বিষয় এবং ভবিষ্যতে বদলাতে পারে; তাই toString-এর লেখা parse করে ডেটা বের করার কোড লিখবেন না — ডেটা চাইলে accessor ডাকুন।

C/C++/Python জানলে

C-র struct-ও কয়েকটি মান একসাথে রাখে, কিন্তু তুলনা (==) বা ছাপার কোনো ব্যবস্থা নেই, আর সব সদস্য বদলানো যায়। C++20-এ bool operator==(const P&) const = default; লিখে সদস্য ধরে তুলনা পাওয়া যায় — C++-এর enum ও struct পর্বে দেখুন। Java record-এর সবচেয়ে কাছের আত্মীয় Python-এর @dataclass(frozen=True): সেখানেও __init__, __eq__, __repr__ নিজে থেকে তৈরি হয়, আর যাচাই হয় __post_init__-এ — Java-তে সেই জায়গা compact constructor. পার্থক্য: Python dataclass ডিফল্টে বদলানো যায়, Java record কখনো না। বিস্তারিত তুলনা Python-এর property ও dataclass পর্বে।

৩ · compact constructor — যাচাই আর মান ঠিক করা

record বানানো সহজ, কিন্তু একটি প্রশ্ন থেকে যায়: ভুল মান ঢুকলে কী হবে? bKash-এ ঋণাত্মক টাকার লেনদেন হয় না। মানচিত্রে অক্ষাংশ −৯০ থেকে ৯০-এর বাইরে যেতে পারে না। একটি পরিসরের শুরু তার শেষের চেয়ে বড় হতে পারে না। পর্ব ১৬-এ শিখেছেন, এমন নিয়ম (invariant) পাহারা দেওয়ার জায়গা constructor. record-এ এর জন্য আছে একটি বিশেষ ছোট রূপ — compact constructor:

record Location(double lat, double lon) {
    Location {                                       // বন্ধনী নেই, parameter তালিকা নেই
        if (lat < -90 || lat > 90)
            throw new IllegalArgumentException("lat out of range: " + lat);
        if (lon < -180 || lon > 180)
            throw new IllegalArgumentException("lon out of range: " + lon);
    }                                                // এখানে this.lat = lat; ... নিজে থেকে বসে
}

খেয়াল করুন তিনটি জিনিস। এক, নামের পরে কোনো বন্ধনী নেই — parameter তালিকা হেডার থেকেই জানা। দুই, ভেতরে lat আর lon হলো parameter, field নয়। তিন, শরীরের শেষে compiler নিজে this.lat = lat; this.lon = lon; বসায়। ফলে আপনার কোডে শুধু নিয়মটুকু থাকে, বাকি সব নিয়মমাফিক।

Compact constructor শুধু যাচাই নয়, মান ঠিক করতেও (normalization) পারে। parameter-এ নতুন মান বসালে, শেষে field-এ সেই নতুন মানই যায়। লেনদেনের ধরন কেউ “send”, কেউ “SEND” লিখলে সবগুলোকে বড় হাতের অক্ষরে আনা যায়। পুরো ঘটনাটি ধাপে ধাপে দেখুন:

1record Transaction(String id, long amount, String type) {2  Transaction {3    if (amount <= 0)4      throw new IllegalArgumentException(...);5    type = type.toUpperCase(Locale.ROOT);6  }  // লুকানো: this.id = id; this.amount = amount; this.type = type;7}8new Transaction("TX101", 500, "send");9new Transaction("TX102", -50, "cash-out");
অবস্থা
parameter type
parameter amount
field this.type
field this.amount
  1. ডাক এলো। new Transaction("TX101", 500, "send") — JVM object-এর জন্য জায়গা নেয়, তারপর canonical constructor ডাকে। এই record-এ canonical constructor-টি আপনার লেখা compact রূপ।
  2. Parameter এলো, field এখনো ফাঁকা। শরীরের ভেতরে id, amount, type নামগুলো parameter-কে বোঝায়। Field-এ এখনো কিছু বসেনি — তাই এখানে this.amount পড়ারও মানে নেই।
  3. যাচাই। amount <= 0? 500, তাই না — এগিয়ে চলুন। নিয়ম ভাঙলে এখানেই থামত, কোনো ভুল object কখনো জন্মাত না।
  4. মান ঠিক করা। type = type.toUpperCase(Locale.ROOT) — এটা parameter-এ নতুন মান বসানো, field-এ নয়। Locale.ROOT দেওয়া হয়েছে যাতে কম্পিউটারের ভাষা-সেটিং যা-ই হোক, “send” সবসময় “SEND” হয়।
  5. শেষ বন্ধনী — field বসল। শরীর শেষ হতেই compiler-এর বসানো তিনটি লাইন চলে: parameter-গুলোর তখনকার মান field-এ যায়। তাই type field-এ যায় “SEND”, “send” নয়। এরপর আর কেউ field বদলাতে পারবে না।
  6. নিয়ম ভাঙলে। দ্বিতীয় ডাকে amount −50. যাচাই ব্যর্থ, exception ছোড়া হলো, constructor শেষ হলো না। যে ডেকেছিল সে exception পায়, কোনো object নয়। record-এর প্রতিটি জীবিত object তাই নিশ্চিতভাবে নিয়ম মেনে চলে।

এবার চালিয়ে দেখুন — record বানানো, accessor, equals, HashSet আর যাচাই, সব একসাথে:

import java.util.*;

record Transaction(String id, long amount, String type) {
    Transaction {                                   // compact constructor
        if (amount <= 0) throw new IllegalArgumentException("amount must be positive: " + amount);
        type = type.toUpperCase(Locale.ROOT);       // normalization
    }
}

class Main {
    public static void main(String[] args) {
        var t1 = new Transaction("TX101", 500, "send");
        var t2 = new Transaction("TX101", 500, "SEND");
        System.out.println(t1);
        System.out.println(t1.amount() + " " + t1.type());
        System.out.println(t1.equals(t2) + " " + (t1 == t2));
        Set<Transaction> seen = new HashSet<>(List.of(t1));
        System.out.println(seen.contains(t2));
        try {
            new Transaction("TX102", -50, "cash-out");
        } catch (IllegalArgumentException e) {
            System.out.println("rejected: " + e.getMessage());
        }
    }
}

আউটপুট পাঁচ লাইন: Transaction[id=TX101, amount=500, type=SEND], 500 SEND, true false, true, আর rejected: amount must be positive: -50. প্রথম লাইনে toString — “send” লিখে দেওয়া হয়েছিল, কিন্তু compact constructor তাকে “SEND” বানিয়েছে। দ্বিতীয় লাইনে accessor amount() আর type(). তৃতীয় লাইন গুরুত্বপূর্ণ: t1 আর t2 দুটি আলাদা object (== তাই false), কিন্তু সব component সমান, তাই equals true — একটিতে “send”, অন্যটিতে “SEND” দেওয়া হলেও normalization-এর পরে দুটি একই। চতুর্থ লাইনে HashSet-এ শুধু t1 রাখা হয়েছিল, তবু t2 খুঁজলে পাওয়া যায় — কারণ hashCode আর equals দুটিই component থেকে (অংশ ৭)। শেষ লাইনে −50 টাকার লেনদেন compact constructor-এ আটকে গেছে। record আর Main দুটোই public ছাড়া লেখা, কারণ অনলাইন runner ফাইলটিকে অন্য নামে compile করে; নিজের প্রজেক্টে সাধারণত public record Transaction থাকে নিজের Transaction.java ফাইলে।

compact constructor-এ field ছোঁয়া যায় না

নতুনদের একটি সাধারণ ভুল: compact constructor-এর ভেতরে পুরোনো অভ্যাসে this.start = start; লেখা। Field বসানোর কাজ compiler নিজে করবে, তাই আপনার হাতে সেটা নিষিদ্ধ:

record Range(int start, int end) {
    Range {
        this.start = start;
    }
}
Main.java:3: error: cannot assign a value to final variable start
        this.start = start;
            ^
1 error

সমাধান সহজ: লাইনটি মুছে দিন। মান বদলাতে চাইলে parameter-এ বসান (start = Math.max(0, start);), field-এ নয়। আর যদি সত্যিই সব field নিজে বসাতে চান, তাহলে compact রূপের বদলে পুরো canonical constructor লিখুন — Range(int start, int end) { this.start = start; this.end = end; } — তখন সব field নিজেকেই বসাতে হবে। প্রায় সব ক্ষেত্রে compact রূপই পরিষ্কার।

৪ · আরও constructor, method আর static factory

record-এর শরীর খালি রাখতে হবে এমন কোনো কথা নেই। সাধারণ class-এর মতো এখানে method, static field, static method, এমনকি অতিরিক্ত constructor-ও লেখা যায়। একটি পরিসর (Range) দিয়ে সব দেখা যাক:

record Range(int start, int end) {
    static final Range EMPTY = new Range(0, 0);          // static field — চলে

    Range {                                              // compact: নিয়ম পাহারা
        if (start > end)
            throw new IllegalArgumentException("start > end: " + start + " > " + end);
    }
    Range(int end) { this(0, end); }                     // অতিরিক্ত constructor → canonical-কে ডাকে

    int length() { return end - start; }                 // সাধারণ method
    boolean contains(int x) { return start <= x && x < end; }

    static Range of(int a, int b) {                      // static factory: ক্রম নিজে ঠিক করে
        return new Range(Math.min(a, b), Math.max(a, b));
    }
}

এই record-এ new Range(5) দেয় Range[start=0, end=5], যার length() হলো 5; আর Range.of(9, 3) দেয় Range[start=3, end=9] (OpenJDK 21-এ চালিয়ে যাচাই করা)। দুটি নিয়ম লক্ষ করুন।

এক, অতিরিক্ত constructor-কে প্রথম লাইনেই অন্য constructor ডাকতে হবে। Range(int end) নিজে field বসাতে পারে না; সে this(0, end) দিয়ে শেষ পর্যন্ত canonical constructor-এ যায়। এতে নিশ্চিত হয় যে compact constructor-এর যাচাই কখনো বাদ পড়ে না — record-এর প্রতিটি object একই দরজা দিয়ে ঢোকে। এই নিয়ম ভাঙলে:

record Range(int start, int end) {
    Range(int end) {
        System.out.println("x");
    }
}
Main.java:2: error: constructor is not canonical, so its first statement must invoke another constructor of class Range
    Range(int end) {
    ^
1 error

দুই, static factory method নাম দিয়ে উদ্দেশ্য বোঝায়। Range.of(9, 3) ভুল ক্রমে দেওয়া মান নিজে সাজিয়ে নেয়, আর new Range(9, 3) সরাসরি exception দেয়। দুটোই রাখা যায় — কঠোর দরজা আর সুবিধাজনক দরজা। JDK-র List.of, Map.of একই ধারণা।

instance field নিষেধ — state শুধু হেডারে

record-এর শরীরে static field চলে, কিন্তু নতুন instance field চলে না। ধরুন কেউ লেনদেনে “কতবার পাঠানোর চেষ্টা হলো” রাখতে চাইলেন:

record Transaction(String id, long amount, String type) {
    private int retries;
}
Main.java:2: error: field declaration must be static
    private int retries;
                ^
  (consider replacing field with record component)
1 error

javac নিজেই পরামর্শ দিচ্ছে: field-টিকে component বানান। কারণ record-এর প্রতিশ্রুতি — “সব state হেডারে ঘোষিত”। হেডারের বাইরে লুকানো field থাকলে equals, hashCode আর toString সেটা দেখত না, আর “দুটি record-এর component সমান মানে record সমান” কথাটি মিথ্যা হয়ে যেত। একই কারণে record-এর method field বদলাতে পারে না:

record Transaction(String id, long amount, String type) {
    void refund() { amount = 0; }
}
Main.java:2: error: cannot assign a value to final variable amount
    void refund() { amount = 0; }
                    ^
1 error

তাহলে “বদলানো” কীভাবে হয়? নতুন record বানিয়ে। Transaction refunded() { return new Transaction(id, 0, "REFUND"); } — পুরোনোটা যেমন ছিল তেমনই থাকে, নতুন মানের জন্য নতুন object. পর্ব ০৮-এর String-এর মতো: toUpperCase() আগেরটা বদলায় না, নতুন String দেয়। এভাবে একটি component বদলে নতুন কপি দেওয়া method-কে প্রচলিত ভাষায় “wither” বলে (withAmount(...))। Java 21-এ record নিজে থেকে wither বানায় না, প্রয়োজন হলে হাতে লিখতে হয়। ভবিষ্যতের জন্য JEP 468 (“derived record creation”) নামে একটি প্রস্তাব আছে, যা with দিয়ে কপি বানানোর syntax আনতে চায়; এই কোর্সের Java 21-এ সেটা নেই।

৫ · record-এর সীমা — final, extends নেই, interface আছে

অংশ ২-এর stepper-এ দেখেছেন, record নিজে থেকে final আর তার superclass java.lang.Record. এর দুটি ফল।

কেউ record extends করতে পারে না। পর্ব ১৯-এর final class-এর মতো:

record Point(int x, int y) {}
class Point3D extends Point { }
Main.java:2: error: cannot inherit from final Point
class Point3D extends Point { }
                      ^
Main.java:2: error: constructor Point in record Point cannot be applied to given types;
class Point3D extends Point { }
^
  required: int,int
  found:    no arguments
  reason: actual and formal argument lists differ in length
2 errors

প্রথম error-টিই আসল কথা: Point final, তাই তার উত্তরসূরি হয় না। দ্বিতীয়টি পর্ব ১৯-এর পরিচিত লুকানো super() — Point3D-এর default constructor parameter ছাড়া Point-এর constructor খোঁজে, অথচ record-এর canonical constructor চায় দুটি int. প্রথমটি সারানোর কোনো উপায় নেই, তাই দ্বিতীয়টি নিয়ে মাথা ঘামানোর দরকারও নেই।

record নিজেও কিছু extends করতে পারে না — তার parent ইতিমধ্যে ঠিক করা (Record), আর Java-তে parent একটিই। হেডারের পরে extends লিখলে:

class Base {}
record Point(int x, int y) extends Base {}
Main.java:2: error: '{' expected
record Point(int x, int y) extends Base {}
                          ^
1 error

কেন এত কড়াকড়ি? কারণ subclass এলে record-এর প্রতিশ্রুতি ভাঙে। Point3D একটি z যোগ করলে Point(1, 2) আর Point3D(1, 2, 9) কি সমান? Point-এর equals বলবে হ্যাঁ, Point3D-এরটা বলবে না — equals-এর symmetry ভেঙে যায়, যা পর্ব ২৩-এর সবচেয়ে কঠিন সমস্যাগুলোর একটি। record এই প্রশ্ন পুরোপুরি বন্ধ করে দিয়েছে।

কিন্তু interface implement করা যায়, যত খুশি। এতে record-কে সাজানো যায়, অন্য টাইপের অংশ বানানো যায়। একটি পরীক্ষার ফলকে GPA অনুযায়ী সাজাতে:

record ExamResult(int roll, double gpa) implements Comparable<ExamResult> {
    public int compareTo(ExamResult o) {
        int byGpa = Double.compare(o.gpa, gpa);          // বেশি GPA আগে
        return byGpa != 0 ? byGpa : Integer.compare(roll, o.roll);
    }
}

লক্ষ করুন, record-এর ভেতরে o.gpa লিখে সরাসরি field পড়া যায় (একই class), আর বাইরে থেকে o.gpa(). পর্ব ২৬-এ দেখবেন record আর sealed interface একসাথে কী শক্তিশালী — sealed interface Shape permits Circle, Square, যেখানে Circle আর Square দুটিই record.

record-এ যা চলেrecord-এ যা চলে না
instance ও static method, static field, static initializerনতুন instance field (state শুধু হেডারে)
compact বা পূর্ণ canonical constructor, অতিরিক্ত constructor (প্রথমে this(...))instance initializer block { ... }
যত খুশি interface implementঅন্য class extends (parent সবসময় Record)
generic type parameter: record Pair<A, B>(A first, B second)record-কে abstract করা বা অন্য কাউকে record extends করতে দেওয়া
accessor নিজে লেখা (public, একই return type)accessor-এর নাম বদলে getX() বানানো (সেটা নতুন একটি method হবে মাত্র)
nested record (নিজে থেকে static), local recordfield-এর মান বদলানো (সব final)

৬ · shallow immutability — ভেতরের List আর array-এর ফাঁদ

record-এর field final, তাই একবার বসলে আর বদলায় না। কিন্তু পর্ব ১৮-এ দেখেছেন, final reference মানে reference-টা বদলায় না — reference যে object-কে দেখায়, তার ভেতরটা বদলাতে পারে। record-এর ক্ষেত্রেও একই কথা। একে বলে shallow immutability — অগভীর অপরিবর্তনীয়তা। component যদি String, int বা অন্য record হয়, সমস্যা নেই; কিন্তু List বা array হলে record আসলে immutable নয়।

একটি ব্যাচের রোল নম্বরের তালিকা রাখা record ভাবুন। যে কোড record বানিয়েছে, তার হাতে এখনো মূল ArrayList-এর reference আছে। সে তালিকায় কিছু যোগ করলে কী হয়? ধাপে ধাপে দেখুন:

ধাপ ১LeakyBatch b = new LeakyBatch("A", list)
list ─────────────┐
                  ▼
           ┌──────────────┐
b.rolls ──▶│ [101, 102]   │   ArrayList (heap)
           └──────────────┘
record-এর field আর বাইরের list — একই object-কে দেখায়
ধাপ ২list.add(999)
list ─────────────┐
                  ▼
           ┌───────────────────┐
b.rolls ──▶│ [101, 102, 999]  │
           └───────────────────┘
b.rolls() → [101, 102, 999]
record-এর field বদলায়নি, কিন্তু তার দেখানো তালিকা বদলেছে
ধাপ ৩Batch { rolls = List.copyOf(rolls); }
list ──────▶ ┌───────────────────┐
             │ [101, 102, 999]   │   বাইরের ArrayList
             └───────────────────┘
             ┌───────────────────┐
s.rolls ───▶ │ [101, 102]        │   নতুন, unmodifiable কপি
             └───────────────────┘
কপি নেওয়া হয়েছিল record বানানোর মুহূর্তে — পরে list বদলালেও এটা অটল
ধাপ ৪s.rolls().add(5)
s.rolls().add(5)
   ✘ UnsupportedOperationException
accessor যে তালিকা ফেরত দেয়, সেটা দিয়েও ভেতরটা বদলানো যায় না
ঢোকার পথ আর বের হওয়ার পথ — দুটোই বন্ধ
  1. একই object, দুটি reference. new LeakyBatch("A", list) — canonical constructor শুধু reference কপি করে, তালিকা নয়। এখন বাইরের list আর record-এর rolls একই ArrayList দেখায় (পর্ব ১৩-এর aliasing).
  2. বাইরের বদল ভেতরে। বাইরের কোড list.add(999) করল। record-এর field একটুও বদলায়নি — এখনো একই reference. কিন্তু সেই reference-এর ওপাশের তালিকা বদলেছে, তাই b.rolls() এখন তিনটি রোল দেখায়। “immutable” record-এর মান বদলে গেল, আর এটি যদি HashSet-এ থাকত, তার hashCode-ও বদলে যেত।
  3. সমাধান — defensive copy. Compact constructor-এ rolls = List.copyOf(rolls);. defensive copy মানে ঢোকার মুহূর্তে নিজের একটি কপি রাখা। List.copyOf একটি নতুন unmodifiable list দেয় — বাইরের list-এর সাথে আর কোনো যোগ নেই।
  4. বের হওয়ার পথও বন্ধ। List.copyOf-এর তালিকা নিজেই বদলানো যায় না, তাই accessor-এ আলাদা কপি লাগে না: s.rolls().add(5) exception দেয়। (মনে রাখবেন, List.copyOf null উপাদান মানে না — তালিকায় null থাকলে NullPointerException দেয়, যা প্রায়ই চাওয়া আচরণই।)

চালিয়ে মিলিয়ে নিন — সাথে অংশ ৭-এর একটি local record-ও আছে:

import java.util.*;

record LeakyBatch(String name, List<Integer> rolls) {}
record Batch(String name, List<Integer> rolls) {
    Batch { rolls = List.copyOf(rolls); }           // defensive copy
}

class Main {
    public static void main(String[] args) {
        var list = new ArrayList<>(List.of(101, 102));
        var leaky = new LeakyBatch("A", list);
        var safe = new Batch("A", list);
        list.add(999);                              // বাইরে থেকে বদল
        System.out.println(leaky.rolls() + " " + safe.rolls());
        try { safe.rolls().add(5); }
        catch (UnsupportedOperationException e) { System.out.println("safe list cannot change"); }

        record Result(int roll, double gpa) {}      // local record
        var results = new ArrayList<>(List.of(new Result(103, 4.5), new Result(101, 5.0), new Result(102, 4.5)));
        results.sort(Comparator.comparingDouble(Result::gpa).reversed().thenComparingInt(Result::roll));
        results.forEach(System.out::println);
    }
}

আউটপুট: [101, 102, 999] [101, 102], safe list cannot change, তারপর তিনটি Result — Result[roll=101, gpa=5.0], Result[roll=102, gpa=4.5], Result[roll=103, gpa=4.5]. প্রথম লাইনে LeakyBatch বাইরের list.add(999) দেখতে পেয়েছে, Batch পায়নি — কারণ Batch বানানোর মুহূর্তে List.copyOf নিজের কপি রেখেছিল। দ্বিতীয় লাইনে accessor-এর ফেরত দেওয়া তালিকায় add করতে গিয়ে UnsupportedOperationException — বের হওয়ার পথও বন্ধ। শেষ অংশে main-এর ভেতরে ঘোষিত local record Result: প্রথমে GPA বড় থেকে ছোট (reversed()), সমান GPA হলে রোল ছোট থেকে বড় (thenComparingInt). তাই 4.5 পাওয়া দুজনের মধ্যে 102 আগে, 103 পরে। Result::gpa — accessor-কে method reference হিসেবে দেওয়া যায় (পর্ব ৩৩ ও ৩৪)।

array-এর দুটি সমস্যা

array component আরও ঝামেলার। প্রথমত, array-এর জন্য List.copyOf-এর মতো unmodifiable রূপ নেই, তাই ঢোকার সময় marks = marks.clone(); আর accessor-কেও নিজে লিখে কপি ফেরত দিতে হয় — public int[] marks() { return marks.clone(); }. দ্বিতীয়ত, record-এর equals প্রতিটি component তুলনা করে সেই component-এর নিজের equals দিয়ে, আর array-এর equals হলো reference তুলনা (পর্ব ১৩)। ফলে record Scores(int[] marks)-এ একই নম্বরের দুটি আলাদা array দিলে দুটি record সমান নয় — OpenJDK 21-এ চালালে false আসে। তাই সাধারণ নিয়ম: record-এ array নয়, List রাখুন, আর compact constructor-এ List.copyOf.

৭ · Map-এর key, Set আর local record

record-এর সবচেয়ে বড় সুবিধাগুলোর একটি হলো HashMap-এর key বা HashSet-এর উপাদান হিসেবে এটা প্রথম দিন থেকেই ঠিকঠাক কাজ করে। কারণ equals আর hashCode দুটিই component থেকে তৈরি, আর দুটি একে অপরের সাথে মিলে থাকে। হাতে লেখা class-এ equals বা hashCode ভুলে গেলে কী হয়, আর record-এ কী হয় — পাশাপাশি দেখুন:

ধাপ ১class PointC { int x, y; } — equals/hashCode নেই
set.add(new PointC(1, 2));

বালতি ক:  [ PointC(1,2) #১ ]
hashCode আসে Object থেকে — object-এর পরিচয় থেকে, মান থেকে নয়
ধাপ ২set.contains(new PointC(1, 2))
নতুন object #২ → অন্য hashCode → (প্রায় সবসময়) অন্য বালতি
বালতি খ:  [ ]      → খুঁজে পেল না
একই বালতিতে পড়লেও equals = reference তুলনা → #১ ≠ #২
ফল: false   — একই মান, তবু “নেই”
ধাপ ৩record Point(int x, int y) — set.add(new Point(1, 2))
hashCode = f(x=1, y=2)   — শুধু component থেকে
বালতি গ:  [ Point[x=1, y=2] ]
new Point(1, 2) আবার বানালে → একই hashCode → একই বালতি গ
ধাপ ৪set.contains(new Point(1, 2))
বালতি গ-তে গিয়ে equals:
  x: 1 == 1 ✔   y: 2 == 2 ✔
ফল: true
map.get(new Point(1, 2)) → "shop"   (একই নিয়মে)
  1. equals/hashCode ছাড়া class. HashSet প্রথমে hashCode দেখে ঠিক করে কোন “বালতিতে” রাখবে। Override না করলে hashCode আসে Object থেকে, যা প্রতিটি আলাদা object-এর জন্য সাধারণত আলাদা — মানের সাথে এর কোনো সম্পর্ক নেই। (সংখ্যাটা কত, তা চালানোভেদে বদলায়, তাই এখানে দেখানো হলো না।)
  2. একই মান, তবু পাওয়া গেল না। নতুন new PointC(1, 2) একটি আলাদা object, তাই প্রায় নিশ্চিতভাবে অন্য বালতি। আর কাকতালীয়ভাবে একই বালতিতে পড়লেও Object-এর equals reference তুলনা করে। contains বলে false — পর্ব ২৩-এর সেই ক্লাসিক bug.
  3. record — মান থেকে hash. record-এর hashCode শুধু component থেকে হিসাব হয়। তাই একই মানের দুটি Point সবসময় একই hashCode পায়, একই বালতিতে যায়। সঠিক সূত্রটা Javadoc-এ নির্দিষ্ট নয় — শুধু নিশ্চয়তা আছে যে সমান record-এর hashCode সমান।
  4. equals মিলিয়ে দেখে। বালতিতে পৌঁছে equals প্রতিটি component তুলনা করে — সব মিলেছে, তাই true. HashMap<Point, String>-এ map.get(new Point(1, 2))-ও একই কারণে কাজ করে। অংশ ৩-এর runnable কোডে seen.contains(t2) ঠিক এটাই দেখিয়েছিল।

record-কে HashMap-এর key বানালে ভেতরের component যেন সত্যিই immutable হয়। অংশ ৬-এর LeakyBatch-কে key বানিয়ে পরে বাইরের list বদলালে তার hashCode বদলে যাবে, আর map সেটাকে আর খুঁজে পাবে না — map-এর ভেতরে থেকেও “হারিয়ে যাওয়া” key. defensive copy শুধু সৌন্দর্য নয়, collection-এর নিরাপত্তার শর্ত।

local record — একটি method-এর ভেতরের সাময়িক ডেটা

অনেক সময় একটি method-এর ভেতরেই কিছু মান সাময়িকভাবে জোড়া বাঁধা দরকার হয় — যেমন রোল আর GPA একসাথে সাজাতে। এর জন্য আলাদা ফাইলে class বানানো বাড়াবাড়ি, আবার দুটি আলাদা array রেখে একসাথে সাজানো ঝামেলার। Java 16 থেকে method-এর ভেতরেই record ঘোষণা করা যায় — local record. অংশ ৬-এর runnable কোডে record Result(int roll, double gpa) {} ঠিক এটাই: main-এর ভেতরে জন্ম, main-এর বাইরে কেউ জানে না। local record নিজে থেকে static, তাই বাইরের method-এর local variable ব্যবহার করতে পারে না — এটা শুধু ডেটার একটি ছাঁচ, কোনো লুকানো সংযোগ নেই। Stream-এর মাঝখানের ধাপে (পর্ব ৩৫) এর ব্যবহার সবচেয়ে বেশি।

৮ · কখন record নয় — Lombok, class, entity আর record pattern

record দেখে প্রথমে মনে হয়, “সব class-ই তো record বানিয়ে ফেলি!” না। record-এর প্রতিশ্রুতি — “মানই পরিচয়, আর মান কখনো বদলায় না” — যেখানে খাটে না, সেখানে record ভুল হাতিয়ার।

record মানানসই

লেনদেনের রসিদ, GPS বিন্দু, টাকার অঙ্ক + মুদ্রা, পরীক্ষার ফল, API-র request/response DTO, method থেকে একাধিক মান ফেরত, Map-এর যৌগিক key.

মানই পরিচয়

সাধারণ class ভালো

ব্যাংক account (ব্যালান্স বদলায়), শপিং কার্ট, চলমান খেলার অবস্থা — যার একটি পরিচয় আছে আর সময়ের সাথে অবস্থা বদলায়।

পরিচয় + জীবনচক্র

JPA entity — record নয়

ডেটাবেসের টেবিলের সারির জন্য JPA/Hibernate entity-কে খালি constructor, বদলযোগ্য field আর subclass proxy লাগে — record তিনটিই দেয় না।

পর্ব ৪৯–৫১

পার্থক্যটা পরিচয় (identity) নিয়ে। দুটি Location(23.81, 90.41) সব দিক থেকে একই বিন্দু — আলাদা করার কোনো কারণ নেই। কিন্তু একই নামে, একই ব্যালান্সের দুজন গ্রাহকের দুটি account আলাদা account; একটিতে টাকা জমা হলে অন্যটিতে হয় না। account-এর পরিচয় তার মানের বাইরে (account নম্বর, জীবনকাল), আর তার মান সময়ের সাথে বদলায়। এমন জিনিস record দিয়ে লিখলে প্রতিটি জমার পর নতুন object বানাতে হবে, আর পুরোনো reference যারা ধরে আছে তারা পুরোনো ব্যালান্স দেখবে।

record বনাম Lombok @Value বনাম হাতে লেখা class

record আসার আগে Java জগতে boilerplate কমানোর জনপ্রিয় উপায় ছিল Lombok — একটি তৃতীয় পক্ষের লাইব্রেরি, যার @Value annotation একটি class-কে final, field-গুলোকে private final বানায়, আর getter, equals, hashCode, toString তৈরি করে।

দিকrecordLombok @Valueহাতে লেখা class
কোথা থেকেভাষার অংশ, Java 16+বাইরের লাইব্রেরি, build-এ যোগ করতে হয়নিজে লেখা
পড়ার methodamount()getAmount()যা খুশি
অন্য class extendsপারে নাপারে (তবে নিজে final)পারে
অতিরিক্ত instance fieldনাহ্যাঁহ্যাঁ
record pattern-এ ভাঙাহ্যাঁ (Java 21)নানা
wither / builderনিজে লিখতে হয়@With, @Builder দিয়েনিজে লিখতে হয়

নতুন কোডে সরল ডেটা বাহকের জন্য record-ই প্রথম পছন্দ — বাইরের নির্ভরতা নেই, আর ভাষা নিজে এর অর্থ জানে। Lombok এখনো কাজে লাগে যেখানে JavaBeans নামের getter বাধ্যতামূলক (কিছু পুরোনো framework), বা বদলযোগ্য class-এ boilerplate কমাতে হয়।

record pattern — একটি ঝলক

record স্বচ্ছ — বাইরে থেকে জানা যায় এর ভেতরে কী কী আছে। এই স্বচ্ছতা কাজে লাগিয়ে Java 21-এ স্থায়ী হয়েছে record pattern: একটি object record কি না যাচাই আর তার component বের করা, এক ধাপে।

Object o = new Point(3, 4);
if (o instanceof Point(int x, int y)) {     // Point হলে x আর y সরাসরি পাওয়া যায়
    System.out.println(x + y);              // 7
}

এখানে cast নেই, p.x()-এর ডাক নেই — pattern matching এক ধাপে ভেঙে দিয়েছে (OpenJDK 21-এ চালিয়ে 7 পাওয়া গেছে)। switch-এর ভেতরে nested record pattern আর sealed interface মিলিয়ে এর আসল শক্তি দেখবেন পরের পর্বে, পর্ব ২৬-এ।

Jackson, Spring আর record

বাস্তব প্রজেক্টে record-এর সবচেয়ে বড় ব্যবহার API-র ডেটা আনা-নেওয়ায় (DTO). JSON-কে Java object-এ রূপ দেওয়ার জনপ্রিয় লাইব্রেরি Jackson-এর আধুনিক সংস্করণ (2.12 থেকে) record সরাসরি বোঝে — component-এর নাম দেখে JSON-এর key মেলায় আর canonical constructor দিয়ে object বানায়। Spring Boot-এর আধুনিক সংস্করণেও REST controller-এর request/response আর configuration properties-এ record ব্যবহার করা যায়। এগুলো JDK-র অংশ নয়, তাই এখানে চালানো যাবে না; পর্ব ৩৯ আর ৫১-এ হাতে-কলমে দেখবেন। একটি কথা মনে রাখুন: যে framework-কে খালি constructor আর setter দিয়ে object বানাতে হয় (পুরোনো ধাঁচের কিছু লাইব্রেরি, JPA entity), সেখানে record চলবে না।

পরীক্ষা ও ইন্টারভিউতে

record কোন সংস্করণে স্থায়ী? Java 16 (JEP 395); Java 14 ও 15-এ preview ছিল।
record P(int x)-এর accessor-এর নাম? x() — getX() নয়।
record কি অন্য class extends করতে পারে? না; এর superclass সবসময় java.lang.Record আর এটা নিজে final. তবে interface implement করতে পারে।
record কি সম্পূর্ণ immutable? অগভীরভাবে — field final, কিন্তু List/array component-এর ভেতরটা বদলাতে পারে; compact constructor-এ List.copyOf দিয়ে defensive copy নিতে হয়।
compact constructor কী? parameter তালিকা ছাড়া লেখা canonical constructor, যেখানে যাচাই/মান ঠিক করা হয় আর শেষে field নিজে থেকে বসে।

৯ · AI-এর যুগে — কখন record, কখন নয়, তা যাচাই আপনার কাজ

AI-কে “একটি bKash-ধাঁচের লেনদেনের মডেল Java-তে লিখে দাও” বললে অনেক সময় এখনো ৪০-৫০ লাইনের getter-setter-ভরা class আসে — কারণ AI যে কোটি লাইনের কোড থেকে শিখেছে, তার বেশিরভাগই record আসার আগের। আবার উল্টো ভুলও হয়: “আধুনিক Java” চাইলে AI সব কিছু record বানিয়ে দেয়, এমনকি ব্যালান্স-বদলানো account-ও। আর সবচেয়ে নীরব ভুল — List component-এ defensive copy না নেওয়া, যা কোনো টেস্টে সহজে ধরা পড়ে না।

AI যা করেকেন সমস্যাআপনি যা করবেন
সরল ডেটা বাহকের জন্য লম্বা POJO, setter-সহboilerplate, equals/hashCode অমিলের ঝুঁকি, অকারণে বদলযোগ্য“Java 21, ডেটা বাহকের জন্য record” — স্পষ্ট বলুন
বদলানো অবস্থার জিনিসকে (account, cart, entity) record বানায়প্রতিটি বদলে নতুন object, পুরোনো reference পুরোনো মান দেখে; JPA-তে চলেই নাপ্রশ্ন করুন: “এর পরিচয় কি মানের বাইরে? অবস্থা কি বদলায়?”
List/array component-এ defensive copy নেইবাইরে থেকে ভেতরের ডেটা বদলায়, HashMap key হারায়compact constructor-এ List.copyOf চান; array থাকলে List-এ বদলাতে বলুন
যাচাই ছাড়া recordঋণাত্মক টাকা, ভুল অক্ষাংশ — অবৈধ object তৈরি হয়প্রতিটি component-এর নিয়ম লিখে দিন, compact constructor-এ বসাতে বলুন
record-এ getAmount() যোগ করে বা কলার কোডে ডাকেদুটি নাম একই কাজে; বা সরাসরি compile erroraccessor শুধু amount() — নিশ্চিত করুন

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

আমি Java 21 ব্যবহার করছি। নিচের ডেটাগুলোর জন্য টাইপ নকশা করে দাও: [যেমন — bKash-ধাঁচের লেনদেন, গ্রাহকের account, GPS অবস্থান] শর্ত: ১. প্রতিটি টাইপের জন্য আগে বলো: এটা কি শুধু মানের বাহক (মানই পরিচয়, বদলায় না), নাকি এর আলাদা পরিচয় আছে আর অবস্থা বদলায়? প্রথমটি হলে record, দ্বিতীয়টি হলে সাধারণ class. ২. record-এ setter বা getX() নামের method দেবে না; accessor component-এর নামেই। ৩. প্রতিটি component-এর বৈধতার নিয়ম compact constructor-এ যাচাই করো, ভুল হলে IllegalArgumentException. ৪. List বা Map component থাকলে compact constructor-এ List.copyOf / Map.copyOf দিয়ে defensive copy নাও; array ব্যবহার কোরো না। ৫. সুবিধাজনক তৈরির জন্য দরকার হলে static factory method দাও, কিন্তু সব পথ যেন canonical constructor দিয়ে যায়। ৬. কোনো টাইপ JPA entity হলে record বানাবে না, আর কারণ বলো। ৭. শেষে প্রতিটি record-এর জন্য একটি ছোট main লিখে দেখাও: সমান মানের দুটি object equals কি না, আর অবৈধ মান দিলে কী হয়।

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

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

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

প্র ০১record সরাসরি স্থায়ী না হয়ে কেন দুই দফা preview (JEP 359, JEP 384) পার হয়ে Java 16-এ JEP 395 হিসেবে এলো? Preview-র সময় কী বদলেছিল?

Java-র সবচেয়ে বড় প্রতিশ্রুতি backward compatibility: একবার ভাষায় কিছু স্থায়ী হলে সেটা আর ফেরত নেওয়া যায় না, কারণ কোটি লাইনের কোড তার ওপর দাঁড়িয়ে যায়। পর্ব ১৯-এ দেখেছেন, Stack extends Vector-এর মতো ভুল তিন দশক ধরে টিকে আছে। তাই JDK 12-এর আশপাশ থেকে OpenJDK একটি নিয়ম চালু করে: বড় ভাষাগত বৈশিষ্ট্য আগে preview হিসেবে আসবে — পুরোপুরি তৈরি, কিন্তু --enable-preview flag ছাড়া চলবে না, আর মতামতের ভিত্তিতে বদলাতে পারবে। switch expression, text block, record, sealed class — সবাই এই পথে এসেছে।

JEP 359 — JDK 14 (২০২০)। প্রথম preview-তে record-এর মূল কাঠামো ছিল: হেডারে component, compiler-এর বানানো field, canonical constructor, accessor, equals, hashCode, toString, আর compact constructor. এর পেছনে ছিল Brian Goetz-এর ২০১৯ সালের নকশা-দলিল “Data Classes and Sealed Types for Java”, যেখানে তিনি স্পষ্ট বলেন — লক্ষ্য শুধু boilerplate কমানো নয়, একটি অর্থপূর্ণ ধারণা আনা: “ডেটা, শুধুই ডেটা”।

JEP 384 — JDK 15. দ্বিতীয় preview-তে প্রথম দফার মতামত থেকে নিয়মগুলো আরও পাকা করা হয়। accessor আর canonical constructor নিজে লিখলে কী কী মানতে হবে, compact constructor-এর ভেতরে কী করা যায় আর কী যায় না — এমন খুঁটিনাটি পরিষ্কার হয়, যাতে “parameter ঠিক করো, field বসাবে compiler” মডেলটি (অংশ ৩) দ্ব্যর্থহীন থাকে। সবচেয়ে চোখে পড়ার মতো সংযোজন ছিল local record — সাথে local enum ও local interface — যাতে method-এর ভেতরে সাময়িক ডেটা-ছাঁচ লেখা যায়।

JEP 395 — JDK 16 (২০২১)। স্থায়ী সংস্করণে একটি গুরুত্বপূর্ণ শিথিলতা আসে: inner class-এর ভেতরে static member রাখার পুরোনো নিষেধ তুলে দেওয়া হয়, যাতে inner class-এর ভেতরেও record (যা নিজে থেকে static) ঘোষণা করা যায়। এভাবে record-এর জন্য একটি ভাষা-নিয়মই বদলে গেল। এরপর record pattern এসেছে আলাদা পথে — JDK 19 আর 20-এ preview, JDK 21-এ JEP 440 হিসেবে স্থায়ী।

এই প্রক্রিয়ার সুফল হলো, record যখন স্থায়ী হলো, তখন হাজারো ডেভেলপার আর IDE, Jackson-এর মতো লাইব্রেরি এক বছর ধরে এটা পরীক্ষা করে ফেলেছে। Java 17 LTS-এ যারা প্রথম record পেলেন, তাঁরা একটি পরিণত বৈশিষ্ট্য পেলেন।

মূল উপলব্ধি: যে ভাষা কখনো কিছু ফেরত নিতে পারে না, তাকে ধীরে আর সাবধানে এগোতে হয় — preview হলো “স্থায়ী হওয়ার আগে বাস্তবে পরীক্ষা”, আর record সেই পদ্ধতির একটি সফল উদাহরণ।

প্র ০২JEP 395 বলে record-কে “nominal tuple” ভাবা যায়। Python বা Kotlin-এর মতো নামহীন tuple বা Pair<A, B> না দিয়ে Java নাম-ওয়ালা tuple বেছে নিল কেন?

Tuple মানে কয়েকটি মানের একটি ক্রমযুক্ত গুচ্ছ — Python-এ (101, 4.5). Tuple দুই ধরনের হতে পারে। Structural tuple-এর টাইপ নির্ধারিত হয় শুধু গঠন দিয়ে: “একটি int আর একটি double” — নাম নেই। Nominal tuple-এর একটি নাম থাকে, আর প্রতিটি অংশেরও নাম থাকে: ExamResult(int roll, double gpa). JEP 395-এর লেখাতেই আছে, record-কে “nominal tuple” হিসেবে ভাবা যায় — এবং এটা সচেতন সিদ্ধান্ত।

নামের মূল্য। ধরুন একটি method ফেরত দেয় (int, double). এটা কি রোল আর GPA, নাকি বয়স আর ওজন? Structural টাইপে দুটোই একই টাইপ, তাই compiler কখনো ধরবে না যে আপনি একটি GPA-র জায়গায় ওজন পাঠিয়েছেন। Nominal টাইপে ExamResult আর BodyStat আলাদা টাইপ — গঠন একই হলেও মেলানো যায় না। Brian Goetz বিভিন্ন লেখা ও আলোচনায় এই যুক্তি দিয়েছেন: Java-র টাইপ ব্যবস্থা শুরু থেকেই nominal (class-এর নাম দিয়ে চলে), আর নাম কোড পড়ার জন্য সবচেয়ে মূল্যবান ডকুমেন্টেশন। result.gpa() পড়া সহজ; pair.getSecond() পড়ে কিছুই বোঝা যায় না।

Pair কেন JDK-তে নেই। বহু বছর ধরে মানুষ java.util.Pair চেয়েছে, আর JDK দল ইচ্ছা করেই দেয়নি (JavaFX-এ একটি Pair ছিল, Apache Commons-এ আছে)। যুক্তি ছিল: Pair সহজ, কিন্তু কোড থেকে অর্থ মুছে দেয় — Pair<String, Pair<Integer, Double>> দেখতে কেমন, ভাবুন। record এসে সমস্যাটা সমাধান করল এমনভাবে, যাতে নাম রাখার খরচ প্রায় শূন্য: এক লাইনে record RollGpa(int roll, double gpa) {}, তাও method-এর ভেতরে local record হিসেবে। অর্থাৎ structural tuple-এর সুবিধা (কম কোড) আর nominal টাইপের নিরাপত্তা একসাথে।

অন্য ভাষায় কী আছে। Kotlin-এ আছে Pair আর Triple, কিন্তু Kotlin-এর নিজস্ব নির্দেশনাও অর্থপূর্ণ data class ব্যবহার করতে উৎসাহিত করে। Python-এ সাধারণ tuple আছে, সাথে NamedTuple আর dataclass — অর্থাৎ Python-ও শেষ পর্যন্ত নাম যোগ করার পথ রেখেছে। C#-এ নামহীন tuple (int, double) আর record দুটোই আছে।

মূল উপলব্ধি: টাইপের নাম শুধু লেবেল নয়, একটি নিরাপত্তা-বেষ্টনী আর ডকুমেন্টেশন। Java নামহীন tuple-এর সহজতা নিয়েছে, কিন্তু নাম ছাড়েনি — record হলো “নাম রাখার খরচ এত কমানো, যাতে কেউ নামহীন tuple চাইবে না”।

প্র ০৩Brian Goetz ২০২২ সালে “Data-Oriented Programming in Java” নামে একটি প্রবন্ধ লেখেন। এই ধারণাটি কী, আর record, sealed type ও pattern matching কীভাবে এর তিনটি খুঁটি?

পর্ব ১৫ থেকে ২১ পর্যন্ত আপনি object-oriented programming শিখেছেন: ডেটা আর তার ওপর কাজ একই class-এ বাঁধা, ভেতরের অবস্থা লুকানো, আচরণ polymorphism দিয়ে বিভক্ত। এটা খুব ভালো কাজ করে যখন একটি বড় system-এর ভেতরে জটিল অবস্থা সামলাতে হয় — একটি ব্যাংক account, একটি থ্রেড পুল। কিন্তু আধুনিক software-এর বড় একটি অংশ হলো সীমানায় ডেটা আনা-নেওয়া: API থেকে JSON আসে, তা যাচাই হয়, অন্য service-এ যায়। এখানে ডেটা লুকানোর কিছু নেই; ডেটাই আসল জিনিস।

Data-oriented programming নিয়ে Goetz-এর প্রবন্ধটি প্রকাশিত হয় InfoQ-এ, ২০২২ সালে। সেখানে তিনি কয়েকটি নীতির কথা বলেন, যার সারকথা: ডেটাকে ডেটা হিসেবেই মডেল করুন — স্বচ্ছ ও অপরিবর্তনীয়; ডেটা অপরিবর্তনীয় রাখুন; সম্ভাব্য সব রূপ টাইপ দিয়ে স্পষ্ট করুন; আর অবৈধ অবস্থা যেন প্রকাশই করা না যায় (“make illegal states unrepresentable”).

তিনটি খুঁটি। এক, record — product type: “একটি লেনদেনে আছে id এবং amount এবং type”. দুই, sealed interface (পর্ব ২৬) — sum type: “একটি পেমেন্ট হয় Card, অথবা Bkash, অথবা Cash — এর বাইরে কিছু না”. তিন, pattern matching — switch-এ প্রতিটি রূপকে ভেঙে তার component বের করা, আর compiler নিশ্চিত করে কোনো রূপ বাদ পড়েনি। এই তিনটি মিলিয়ে ML, Haskell, Scala, Rust-এর মতো ভাষার “algebraic data type” Java-তে চলে এসেছে।

OOP-এর সাথে বিরোধ নয়। Goetz নিজেই জোর দিয়েছেন, এটা OOP বাদ দেওয়া নয়। ভেতরের জটিল অবস্থা — যেখানে নিয়ম পাহারা দিতে হয়, অবস্থা বদলায় — সেখানে encapsulation-ওয়ালা class-ই ঠিক। আর সীমানায় চলাচলকারী সরল ডেটার জন্য record + sealed + pattern. পর্ব ২০-এ দেখা “expression problem”-এর দুই দিক: নতুন টাইপ যোগ করা সহজ চাইলে polymorphism, নতুন কাজ যোগ করা সহজ চাইলে sealed + switch. Java এখন দুটোই দেয়, আর বেছে নেওয়া আপনার নকশার সিদ্ধান্ত।

বাংলাদেশি উদাহরণে: একটি মোবাইল-পেমেন্ট গেটওয়ে প্রতিদিন লাখো লেনদেন-বার্তা নেয়, যাচাই করে, ভাগ করে, পাঠায়। এই বার্তাগুলোকে record বানালে প্রতিটি বার্তা বানানোর মুহূর্তেই যাচাই হয়ে যায়, কেউ মাঝপথে বদলাতে পারে না, আর একাধিক thread নিশ্চিন্তে একই বার্তা পড়তে পারে (পর্ব ৪০)।

মূল উপলব্ধি: সব কিছুকে “আচরণসহ object” ভাবার দরকার নেই। যেখানে ডেটাই মূল, সেখানে স্বচ্ছ, অপরিবর্তনীয় record আর সম্পূর্ণ তালিকাভুক্ত sealed টাইপ কোডকে সরল আর নিরাপদ করে।

প্র ০৪Java serialization-কে বহু বছর ধরে নিরাপত্তার দুর্বলতার উৎস বলা হয়। record-এর serialization কীভাবে আলাদা — deserialization কেন canonical constructor দিয়ে যায়, আর এতে লাভ কী?

Serialization মানে object-কে byte-এ রূপান্তর করা — ফাইলে রাখা বা নেটওয়ার্কে পাঠানোর জন্য; উল্টো কাজটি deserialization. Java ১৯৯৭ সালের JDK 1.1 থেকে Serializable interface দিয়ে এটা দিয়েছে। কিন্তু সাধারণ class-এর deserialization একটি অদ্ভুত কাজ করে: সে class-এর নিজের constructor ডাকে না। JVM একটি “খালি” object বানায় আর byte-স্রোত থেকে সরাসরি field-এ মান বসিয়ে দেয়। অর্থাৎ আপনার constructor-এর সব যাচাই — “amount ঋণাত্মক নয়” — পুরোপুরি এড়িয়ে যাওয়া যায়। কেউ হাতে বানানো byte-স্রোত পাঠালে এমন object তৈরি হতে পারে, যা স্বাভাবিক পথে কখনো তৈরি হতো না।

এই “লুকানো constructor”-এর সমস্যা Java-র ইতিহাসে বহু গুরুতর নিরাপত্তা-দুর্বলতার জন্ম দিয়েছে — deserialization gadget chain-এর মতো আক্রমণে অবিশ্বস্ত ডেটা deserialize করে দূর থেকে কোড চালানো পর্যন্ত সম্ভব হয়েছে। Brian Goetz serialization-কে প্রকাশ্যে Java-র একটি বড় ভুল নকশা বলেছেন, আর Joshua Bloch-এর Effective Java-তেও serialization নিয়ে কঠোর সতর্কতা আছে।

record-এর ভিন্ন পথ। JEP 395 আর Java Object Serialization Specification অনুযায়ী, একটি Serializable record-এর serialization শুধু তার component-গুলো লেখে, আর deserialization-এর সময় JVM byte-স্রোত থেকে component-এর মান পড়ে canonical constructor ডেকে object বানায়। ফলে compact constructor-এর সব যাচাই আর defensive copy deserialization-এর সময়ও চলে। ভুল ডেটা এলে constructor exception দেয়, অবৈধ object তৈরিই হয় না। record-এ readObject, writeObject-এর মতো কাস্টমাইজেশন method লিখলেও সেগুলো উপেক্ষিত হয় — কোনো পাশের দরজা নেই।

কেন record-এ এটা সম্ভব। সাধারণ class-এর ভেতরের অবস্থা লুকানো, তাই serialization-কে লুকিয়ে field ছুঁতে হতো। record স্বচ্ছ — তার পুরো অবস্থা হেডারেই ঘোষিত, আর সেই অবস্থা থেকে object বানানোর একটি সরকারি পথ আছে (canonical constructor). তাই serialization সরকারি পথ দিয়েই ভাঙতে আর জোড়া দিতে পারে। এই ধারণা JSON লাইব্রেরিগুলোতেও খাটে: Jackson record-কে canonical constructor দিয়েই বানায় (পর্ব ৩৯)।

মূল উপলব্ধি: নিরাপত্তা আসে “একটি মাত্র দরজা” থেকে। record-এর সব object — new দিয়ে হোক, deserialization দিয়ে হোক, JSON থেকে হোক — একই canonical constructor পার হয়, তাই একবার লেখা যাচাই সব পথে পাহারা দেয়।

প্র ০৫Kotlin-এর data class, Scala-র case class আর C#-এর record — Java record-এর সাথে এদের মিল-অমিল কী? Java কেন ইচ্ছা করে কিছু সুবিধা (copy, বদলযোগ্য property) দেয়নি?

“ডেটার জন্য ছোট class”-এর ধারণা Java-তে নতুন হলেও অন্য ভাষায় পুরোনো। তুলনা করলে Java-র নকশার দর্শন পরিষ্কার হয়।

Scala case class (Scala-র শুরুর দিক থেকে)। case class Point(x: Int, y: Int) লিখলে equals, hashCode, toString, একটি copy method আর pattern matching-এ ভাঙার সুবিধা পাওয়া যায়। Java record-এর ওপর এর প্রভাব স্পষ্ট — বিশেষ করে pattern matching-এর সাথে জুটি। পার্থক্য: case class-এ চাইলে var field রাখা যায়, আর সাধারণ class থেকে extend করতে পারে।

Kotlin data class (Kotlin 1.0, ২০১৬)। data class Transaction(val id: String, val amount: Long) — equals, hashCode, toString, copy(amount = 0) আর component1()-এর মতো method দিয়ে destructuring. কিন্তু Kotlin data class-এ var property রাখা পুরোপুরি বৈধ, তাই data class মানেই immutable নয়; আর হেডারের বাইরে শরীরে property রাখা যায়, যা equals-এ ধরা হয় না। Android-এ Kotlin data class অত্যন্ত জনপ্রিয়, তাই Java থেকে Kotlin-এ গেলে এই পার্থক্য মাথায় রাখুন।

C# record (C# 9, ২০২০)। record Point(int X, int Y); — মূল্য-ভিত্তিক equality, সুন্দর ToString, আর with expression: p with { X = 5 }. C# record inheritance সমর্থন করে (record থেকে record), আর C# 10-এ এসেছে record struct. Java record অনেক বেশি সংযত।

Java কেন কম দিল। Java-র নকশাকারীরা record-কে “শুধু সংক্ষিপ্ত syntax” হিসেবে দেখেননি, দেখেছেন একটি অর্থগত ধারণা হিসেবে — “স্বচ্ছ, অপরিবর্তনীয় ডেটা বাহক”। এই অর্থ রক্ষা করতে যা ভাঙে, তা বাদ: বদলযোগ্য field নেই (তাহলে HashMap key নিরাপদ নয়), হেডারের বাইরে state নেই (তাহলে equals মিথ্যা বলে), inheritance নেই (তাহলে equals-এর symmetry ভাঙে)। আর এই কঠোরতার পুরস্কার: compiler নিশ্চিত জানে record-এর পুরো অবস্থা কী, তাই record pattern আর নিরাপদ serialization সম্ভব। copy/with-এর মতো সুবিধা বাদ পড়েছে কারণ সেটা ঠিকভাবে নকশা করা কঠিন; অংশ ৪-এর JEP 468 প্রস্তাব সেই কাজই করছে, সতর্কভাবে।

মূল উপলব্ধি: একই নামের বৈশিষ্ট্য সব ভাষায় একই প্রতিশ্রুতি দেয় না। Kotlin data class আর C# record “সুবিধা”-কেন্দ্রিক, Java record “প্রতিশ্রুতি”-কেন্দ্রিক — কম স্বাধীনতা, কিন্তু যা আছে তার ওপর পুরো ভরসা করা যায়।

অনুশীলন

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

  1. Defensive copy সরিয়ে দেখুন: অংশ ৬-এর চালানো যায় এমন প্রোগ্রামে Batch-এর compact constructor থেকে rolls = List.copyOf(rolls); মুছে দিন (শুধু Batch { } রাখুন)। না চালিয়ে বলুন প্রথম লাইন আর দ্বিতীয় লাইনে কী ছাপা হবে, তারপর চালিয়ে মিলিয়ে নিন।

    প্রথম লাইন হবে [101, 102, 999] [101, 102, 999] — এখন Batch-ও বাইরের ArrayList-এর reference রাখে, তাই list.add(999) দুটি record-এই দেখা যায়। দ্বিতীয় লাইনে “safe list cannot change” আর ছাপা হবে না, কারণ safe.rolls().add(5) এখন একটি সাধারণ ArrayList-এ 5 যোগ করে, কোনো exception হয় না — নীরবে record-এর “অপরিবর্তনীয়” মান আবার বদলে গেল। এটাই shallow immutability: field final, তালিকা নয়।

  2. ৪০ লাইন থেকে এক লাইন: অংশ ১-এর হাতে লেখা Transaction class-টি আবার পড়ুন। এর কোন কোন অংশ record সরাসরি বদলে দেয়? এমন কিছু কি আছে, যা record-এ রূপান্তরের পর বদলাবে এবং কলার কোড ভেঙে দেবে?

    তিনটি private final field, constructor, equals, hashCode, toString আর class-এর final — সবই record নিজে বানায়। যা বদলায়: পড়ার method-এর নাম। আগে ছিল getId(), getAmount(), getType(); record-এ id(), amount(), type(). যে কোড t.getAmount() ডাকে, সেটা compile হবে না (“cannot find symbol”). পুরোনো কোড রক্ষা করতে চাইলে record-এ অস্থায়ীভাবে public long getAmount() { return amount; } যোগ করা যায়, কিন্তু দীর্ঘমেয়াদে কলার কোড বদলানোই পরিষ্কার। আরেকটি সূক্ষ্ম পার্থক্য: হাতে লেখা constructor ছিল public; top-level record-কে public না করলে তার canonical constructor-ও package-private হবে।

  3. নিজে লিখুন — পরীক্ষার হলের সময়সূচি: একটি record TimeSlot(int startMin, int endMin) লিখুন, যেখানে মিনিট গোনা হয় দিনের শুরু থেকে (০ থেকে ১৪৪০)। compact constructor-এ যাচাই করুন: দুটি মানই ০–১৪৪০-এর মধ্যে, আর শুরু শেষের আগে। একটি boolean overlaps(TimeSlot other) method যোগ করুন, আর main-এ (৯:০০–১১:০০) আর (১০:৩০–১২:০০) পরীক্ষা করুন।

    একটি সম্ভাব্য সমাধান:

    record TimeSlot(int startMin, int endMin) {
        TimeSlot {
            if (startMin < 0 || endMin > 1440 || startMin >= endMin)
                throw new IllegalArgumentException("bad slot: " + startMin + "-" + endMin);
        }
        boolean overlaps(TimeSlot o) {
            return startMin < o.endMin && o.startMin < endMin;
        }
    }
    // main-এ:
    var a = new TimeSlot(540, 660);   // 9:00-11:00
    var b = new TimeSlot(630, 720);   // 10:30-12:00
    System.out.println(a.overlaps(b)); // true

    শর্ত startMin < o.endMin && o.startMin < endMin দুটি পরিসরের মিল খোঁজার প্রচলিত সূত্র: একটি শুরু হয় অন্যটি শেষ হওয়ার আগে, আর উল্টোটাও। ১১:০০-এ শেষ আর ১১:০০-এ শুরু হলে মিল নেই, কারণ < ব্যবহার করা হয়েছে। খেয়াল করুন, startMin >= endMin যাচাইয়ের ফলে শূন্য দৈর্ঘ্যের সময়ও বাতিল।

  4. record, না class? প্রতিটির জন্য এক লাইনে সিদ্ধান্ত আর কারণ দিন: (ক) একটি bKash লেনদেনের লগ-এন্ট্রি; (খ) একজন গ্রাহকের ওয়ালেট, যার ব্যালান্স প্রতিটি লেনদেনে বদলায়; (গ) মেট্রো রেলের একটি স্টেশনের GPS অবস্থান; (ঘ) Spring Boot-এ একটি REST API-র JSON response; (ঙ) Hibernate দিয়ে ডেটাবেসে রাখা Customer entity.

    (ক) record — লগ-এন্ট্রি একবার লেখা হয়, আর বদলায় না; সব তথ্য এক হলে একই এন্ট্রি। (খ) class — ওয়ালেটের একটি পরিচয় (নম্বর) আছে আর ব্যালান্স সময়ের সাথে বদলায়; প্রতিটি লেনদেনে নতুন ওয়ালেট object বানানো অর্থহীন। (গ) record — Location(lat, lon), মানই পরিচয়, compact constructor-এ সীমা যাচাই। (ঘ) record — DTO হলো সীমানা পার হওয়া সরল ডেটা; আধুনিক Spring Boot আর Jackson record সমর্থন করে। (ঙ) class — JPA entity-কে খালি constructor, বদলযোগ্য field আর proxy-র জন্য subclass করার সুযোগ লাগে; record তিনটিই নিষেধ করে। তবে entity থেকে বাইরে পাঠানোর জন্য একটি record DTO বানানো খুব প্রচলিত।

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

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

ক্রমcompact constructor-ওয়ালা record-এ new Transaction("TX1", 500, "send") লিখলে ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📞 canonical constructor ডাকা হয়, argument-গুলো parameter হিসেবে আসে
  2. 🔍 compact constructor-এর শরীরে যাচাই চলে (amount > 0 কি না)
  3. ✏️ parameter-এ নতুন মান বসিয়ে মান ঠিক করা হয় (type → "SEND")
  4. 📥 শরীর শেষে compiler-এর বসানো লাইনগুলো parameter-এর মান final field-এ বসায়
  5. ✅ object তৈরি, accessor দিয়ে মান পড়া যায় — আর কখনো বদলায় না

record Point(int x, int y) {} — একটি Point p-এর x মান পড়ার সঠিক উপায় কোনটি?

  • p.getX()
  • p.x
  • p.x()
  • p.get("x")

record Transaction(String id, long amount) { ... } — শরীরের ভেতরে নিচের কোনটি লিখলে compile error হবে?

  • static final long LIMIT = 50000;
  • private int retries;
  • boolean isLarge() { return amount > 10000; }
  • static Transaction of(String id) { return new Transaction(id, 0); }

Java record-এর compact constructor-এ field-এ মান কখন বসে?

  • শরীর চলার আগে, ঢোকার সাথে সাথে
  • প্রোগ্রামার নিজে this.x = x লিখে বসান
  • প্রথমবার accessor ডাকলে
  • শরীর শেষ হওয়ার পর, compiler নিজে বসায়

record Batch(List<Integer> rolls) {} — কোনো compact constructor নেই। একটি ArrayList দিয়ে Batch বানানোর পর সেই ArrayList-এ বাইরে থেকে একটি উপাদান যোগ করলে কী হয়?

  • batch.rolls()-এও নতুন উপাদানটি দেখা যায়
  • record নিজে থেকে কপি রাখে, তাই কোনো প্রভাব পড়ে না
  • বাইরের add() UnsupportedOperationException দেয়
  • compile error হয়

Java record সম্পর্কে কোন কথাটি সঠিক?

  • record যেকোনো class extends করতে পারে
  • অন্য class একটি record extends করতে পারে
  • record নিজে final, তবে interface implement করতে পারে
  • record কোনো interface implement করতে পারে না

নিচের কোনটির জন্য Java record সবচেয়ে কম উপযুক্ত?

  • একটি GPS অবস্থান (lat, lon)
  • একটি ব্যাংক account, যার ব্যালান্স প্রতিটি জমা-তোলায় বদলায়
  • একটি REST API-র JSON response DTO
  • একজন পরীক্ষার্থীর ফল (roll, gpa)

শব্দকোষ

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

মনে রাখুন

  1. record = স্বচ্ছ, অপরিবর্তনীয় ডেটা বাহক (Java 16 থেকে স্থায়ী)। এক লাইনে compiler বানায় private final field, canonical constructor, component-এর নামে accessor (amount()), আর সব component ধরে equals, hashCode, toString.
  2. Compact constructor-এ যাচাই আর মান ঠিক করা: parameter আসে → আপনার কোড → শেষে field নিজে থেকে বসে। this.x = ... লেখা নিষেধ; অতিরিক্ত constructor-কে প্রথমেই this(...) ডাকতে হয়।
  3. record নিজে final, superclass java.lang.Record; কিছু extends করে না, তবে interface implement করে। শরীরে method, static field, static factory চলে — নতুন instance field চলে না।
  4. Shallow immutability: List/array component-এর ভেতরটা বদলাতে পারে। compact constructor-এ List.copyOf, আর array-এর বদলে List.
  5. record Map-এর key আর Set-এর উপাদান হিসেবে প্রথম দিন থেকেই ঠিক; method-এর ভেতরে local record চলে। কিন্তু পরিচয় আর বদলানো অবস্থার জিনিস (account, JPA entity) — সাধারণ class.

পরের পর্বে: একটি পেমেন্ট হয় Card, অথবা bKash, অথবা Cash — এর বাইরে কিছু না। এমন “এই কয়টির যেকোনো একটি” টাইপ কীভাবে লিখবেন, আর switch-এ record pattern দিয়ে প্রতিটি রূপ ভেঙে compiler-কে দিয়ে নিশ্চিত করাবেন যে কোনোটি বাদ পড়েনি — sealed class আর pattern matching.

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

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