Modern Java
Records and Modern Data Models
আপনি একটি free preview lesson দেখছেন।
Lesson Overview
Java application-এ অনেক class থাকে যেগুলোর কাজ খুব simple:
কিছু data hold করা
constructor দিয়ে initialize করা
accessor provide করা
equals() implement করা
hashCode() implement করা
toString() implement করা
Example:
final class CourseSummary {
private final String code;
private final String title;
private final long priceInPaisa;
CourseSummary(
String code,
String title,
long priceInPaisa
) {
this.code =
code;
this.title =
title;
this.priceInPaisa =
priceInPaisa;
}
String code() {
return code;
}
String title() {
return title;
}
long priceInPaisa() {
return priceInPaisa;
}
@Override
public boolean equals(
Object other
) {
...
}
@Override
public int hashCode() {
...
}
@Override
public String toString() {
...
}
}
এই ধরনের value-oriented class-এর জন্য অনেক boilerplate লাগে।
Modern Java provides:
record
একই model:
record CourseSummary(
String code,
String title,
long priceInPaisa
) {
}
অনেক concise।
কিন্তু record-এর purpose শুধু code ছোট করা নয়।
এটি একটি design statement:
এই type মূলত তার data values দিয়ে define হয়।
এই lesson-এ আমরা শিখব:
recordকী- Record components
- Generated constructor
- Generated accessors
equals()hashCode()toString()- Record immutability
- Shallow immutability
- Compact constructor
- Validation
- Normal canonical constructor
- Defensive copying
- Records as value objects
- Record methods
- Static members
- Record vs normal class
- Record vs entity
- DTO, command, result, key, value object
- Records কোথায় useful
- Records কোথায় inappropriate
- Common mistakes
What Is a Record?
A record হলো Java-র একটি special class form যা data-oriented types conciseভাবে define করতে সাহায্য করে।
Example:
record CourseSummary(
String code,
String title,
long priceInPaisa
) {
}
এই declaration থেকে Java automatically important members generate করে।
Conceptually:
components
constructor
accessors
equals()
hashCode()
toString()
Record Components
এই অংশ:
String code,
String title,
long priceInPaisa
হলো record components।
Full declaration:
record CourseSummary(
String code,
String title,
long priceInPaisa
) {
}
Components define করে record-এর state।
Generated Constructor
এই record:
record CourseSummary(
String code,
String title,
long priceInPaisa
) {
}
use করতে পারি:
CourseSummary summary =
new CourseSummary(
"JAVA",
"Java Foundation",
300_000
);
Java automatically একটি canonical constructor provide করে যা সব components receive করে।
Generated Accessors
Normal JavaBean-style getter নয়:
getCode()
getTitle()
getPriceInPaisa()
Record accessor names component names-এর মতো।
Example:
summary.code();
summary.title();
summary.priceInPaisa();
Accessor Example
CourseSummary summary =
new CourseSummary(
"JAVA",
"Java Foundation",
300_000
);
System.out.println(
summary.code()
);
System.out.println(
summary.title()
);
System.out.println(
summary.priceInPaisa()
);
Records Are Final
A record implicitly final।
অর্থাৎ সাধারণভাবে:
class SpecialCourseSummary
extends CourseSummary
এভাবে record extend করা যায় না।
Record value-oriented closed representation হিসেবে design করা।
Records Cannot Extend Arbitrary Classes
Record-এর superclass structure language-defined।
তাই record অন্য normal class extend করতে পারে না।
কিন্তু record interfaces implement করতে পারে।
Example:
record CourseCode(
String value
) implements Comparable<CourseCode> {
@Override
public int compareTo(
CourseCode other
) {
return value.compareTo(
other.value
);
}
}
Generated toString()
Example:
CourseSummary summary =
new CourseSummary(
"JAVA",
"Java Foundation",
300_000
);
System.out.println(
summary
);
Output conceptually:
CourseSummary[code=JAVA, title=Java Foundation, priceInPaisa=300000]
Exact component values visible থাকে।
Debugging-এর জন্য useful।
Generated equals()
Records value-based equality provide করে।
Example:
CourseSummary first =
new CourseSummary(
"JAVA",
"Java Foundation",
300_000
);
CourseSummary second =
new CourseSummary(
"JAVA",
"Java Foundation",
300_000
);
Then:
first.equals(
second
)
returns:
true
কারণ component values equal।
This Is Different from Identity Equality
These are different objects:
first == second
will normally be:
false
কিন্তু:
first.equals(
second
)
can be:
true
because records value semantics use করে।
Generated hashCode()
যেহেতু records automatically compatible equals() এবং hashCode() provide করে, তারা HashMap এবং HashSet keys হিসেবে খুব useful হতে পারে।
Example:
record EnrollmentKey(
long learnerId,
String courseCode
) {
}
Then:
Set<EnrollmentKey> enrollments =
new HashSet<>();
Composite Key Example
EnrollmentKey key =
new EnrollmentKey(
1001,
"JAVA"
);
Another:
EnrollmentKey sameKey =
new EnrollmentKey(
1001,
"JAVA"
);
Because component values same:
key.equals(
sameKey
)
is true।
এবং compatible hash code automatically generated।
Records and Value Semantics
Record-এর strongest use case:
value-like data
Examples:
CourseCode
EmailAddress
Money
DateRange
Coordinate
CourseSummary
EnrollmentKey
SearchCriteria
Command
Result
যেখানে object-এর identity-এর চেয়ে component values বেশি important।
Record Does Not Automatically Validate Data
Consider:
record CourseCode(
String value
) {
}
This is valid:
new CourseCode(
null
);
Unless we explicitly validate।
Record concise হলেও domain validation automatically আসে না।
Compact Constructor
Record validation-এর জন্য concise syntax আছে:
record CourseCode(
String value
) {
CourseCode {
if (
value == null
|| value.isBlank()
) {
throw new IllegalArgumentException(
"Course code is required."
);
}
}
}
এটিকে বলা হয়:
compact constructor
Why Is It Called Compact?
Notice আমরা লিখিনি:
CourseCode(
String value
)
again।
শুধু:
CourseCode {
...
}
Record header already component list জানে।
Automatic Assignment
Compact constructor-এ normally এই assignment manually লিখতে হয় না:
this.value =
value;
Validation/body complete হওয়ার পরে record components-এর assignment automatically handled হয়।
Normalization Inside Compact Constructor
Suppose Course code:
" java "
কে normalize করে:
"JAVA"
করতে চাই।
record CourseCode(
String value
) {
CourseCode {
if (
value == null
|| value.isBlank()
) {
throw new IllegalArgumentException(
"Course code is required."
);
}
value =
value.strip()
.toUpperCase();
}
}
The normalized parameter value component হিসেবে stored হবে।
Use Locale When Case Normalization Matters
For machine/domain identifiers:
value.toUpperCase()
default locale-dependent হতে পারে।
More deliberate:
value =
value.strip()
.toUpperCase(
Locale.ROOT
);
Import:
import java.util.Locale;
Strong CourseCode Value Object
import java.util.Locale;
record CourseCode(
String value
) {
CourseCode {
if (
value == null
|| value.isBlank()
) {
throw new IllegalArgumentException(
"Course code is required."
);
}
value =
value.strip()
.toUpperCase(
Locale.ROOT
);
if (
!value.matches(
"[A-Z0-9-]+"
)
) {
throw new IllegalArgumentException(
"Invalid course code: "
+ value
);
}
}
}
Now:
new CourseCode(
" java-basics "
);
stores:
JAVA-BASICS
Validation Creates an Invariant
Once CourseCode construction succeeds, downstream code can assume:
non-null
nonblank
normalized
valid format
This is much stronger than passing raw:
String
throughout the application।
Record as a Value Object
Compare:
String courseCode
with:
CourseCode courseCode
The second communicates domain meaning।
Method:
void enroll(
String value
)
What is value?
Could be:
course code
email
title
token
Better:
void enroll(
CourseCode courseCode
)
Type system now carries domain meaning।
Primitive Obsession
Using only:
String
long
int
boolean
for every domain concept can lead to:
Primitive Obsession
Example:
void enroll(
long learnerId,
String courseCode,
long amount
)
Possible stronger types:
void enroll(
LearnerId learnerId,
CourseCode courseCode,
Money amount
)
Records make small value types inexpensive to create।
ID Record Example
record LearnerId(
long value
) {
LearnerId {
if (
value <= 0
) {
throw new IllegalArgumentException(
"Learner ID must be positive."
);
}
}
}
Now method:
findLearner(
LearnerId learnerId
)
cannot accidentally receive a raw Course ID without an explicit conversion/type mismatch।
Different IDs Should Be Different Types
Suppose both are long:
long learnerId;
long courseId;
Method:
enroll(
courseId,
learnerId
);
compiler may not detect reversed arguments if signature also uses two longs।
With:
LearnerId
CourseId
compiler helps।
Email Value Object
record EmailAddress(
String value
) {
EmailAddress {
if (
value == null
) {
throw new IllegalArgumentException(
"Email is required."
);
}
value =
value.strip()
.toLowerCase(
Locale.ROOT
);
if (
value.isBlank()
|| value.contains(
" "
)
|| !value.contains(
"@"
)
) {
throw new IllegalArgumentException(
"Invalid email address."
);
}
}
}
This is a simplified email validation example।
The key lesson is:
validation belongs close to the value invariant
Custom Methods in a Record
Records can contain methods।
Example:
record Money(
long amountInPaisa
) {
Money {
if (
amountInPaisa < 0
) {
throw new IllegalArgumentException(
"Amount cannot be negative."
);
}
}
Money add(
Money other
) {
return new Money(
amountInPaisa
+ other.amountInPaisa
);
}
boolean isZero() {
return amountInPaisa
== 0;
}
}
Records Are Not Just Dumb DTOs
A record can have meaningful behavior।
Example:
money.add(
otherMoney
);
This can be better than external utility:
MoneyUtils.add(
first,
second
);
if behavior naturally belongs to the value type।
Static Methods in Records
Records can also contain static methods।
Example:
record CourseCode(
String value
) {
static CourseCode of(
String value
) {
return new CourseCode(
value
);
}
}
Use:
CourseCode code =
CourseCode.of(
"JAVA"
);
Static Constants
Possible:
record Money(
long amountInPaisa
) {
static final Money ZERO =
new Money(
0
);
}
Instance Fields Beyond Components
Records are intended for state defined by their components।
You cannot freely add arbitrary mutable instance state like a normal class।
That restriction reinforces:
record state is the record components
Static fields are fine।
Records Are Shallowly Immutable
This is a critical point।
Suppose:
record Course(
String title,
List<String> topics
) {
}
The record component reference:
topics
cannot be reassigned inside the record after construction।
But the referenced List may still be mutable।
Example
List<String> topics =
new ArrayList<>();
topics.add(
"Java"
);
Course course =
new Course(
"Java Foundation",
topics
);
topics.add(
"Backend"
);
Now:
course.topics()
may also contain:
Backend
because both reference the same mutable List।
Record Did Not Become Deeply Immutable
The record itself prevents replacing the component reference।
It does not automatically copy or freeze mutable objects stored inside।
This is why we call it:
shallow immutability
Defensive Copying
If we want Course topics protected:
record Course(
String title,
List<String> topics
) {
Course {
if (
title == null
|| title.isBlank()
) {
throw new IllegalArgumentException(
"Title is required."
);
}
topics =
List.copyOf(
topics
);
}
}
Why List.copyOf()?
Suppose caller passes mutable:
ArrayList
Record constructor stores a defensive copy।
Later caller mutates original list:
original.add(...)
record-এর internal list change হবে না।
Accessor Safety
List.copyOf() result unmodifiable।
So:
course.topics()
.add(
"Something"
);
will fail rather than mutate record state।
Validate Before Copy
Better:
topics =
Objects.requireNonNull(
topics,
"topics must not be null"
);
topics =
List.copyOf(
topics
);
Or:
topics =
List.copyOf(
Objects.requireNonNull(
topics
)
);
Elements Can Still Be Mutable
Even:
List.copyOf(...)
only protects the List structure।
Suppose:
List<MutableLesson>
contains mutable objects।
Those objects may still change।
So deep immutability requires all contained values to be suitably immutable too।
Prefer Immutable Components for Value Records
Records work especially well with:
String
primitive values
other immutable records
LocalDate
Instant
enum
List.copyOf(...) of immutable elements
Set.copyOf(...)
Map.copyOf(...)
Defensive Set Copy
record Course(
String title,
Set<String> tags
) {
Course {
tags =
Set.copyOf(
tags
);
}
}
Defensive Map Copy
record Metadata(
Map<String, String> values
) {
Metadata {
values =
Map.copyOf(
values
);
}
}
Canonical Constructor
Instead of compact constructor, full canonical constructor লিখতে পারি।
Example:
record CourseCode(
String value
) {
CourseCode(
String value
) {
if (
value == null
) {
throw new IllegalArgumentException(
"Value is required."
);
}
this.value =
value.strip();
}
}
এখানে parameter list explicitly লেখা এবং component field explicitly assign করা হয়।
Compact Constructor Is Often Cleaner
Same:
record CourseCode(
String value
) {
CourseCode {
if (
value == null
) {
throw new IllegalArgumentException(
"Value is required."
);
}
value =
value.strip();
}
}
For validation/normalization, compact form usually elegant।
Additional Constructors
Records can define additional constructors, but they must eventually initialize through the canonical representation।
Example:
record Money(
long amountInPaisa
) {
Money(
int amountInTaka
) {
this(
amountInTaka * 100L
);
}
}
Now:
new Money(
500
);
can represent:
50000 paisa
Be Careful with Ambiguous Units
Though overloaded constructors can be convenient, financial types should make units extremely clear।
Potentially clearer:
static Money fromTaka(
long taka
) {
return new Money(
taka * 100
);
}
than relying on:
new Money(
500
)
where unit might be unclear।
Record Equality Includes All Components
Consider:
record Course(
long id,
String title
) {
}
Two records:
new Course(
1,
"Java"
)
and:
new Course(
1,
"Backend"
)
are not equal because title differs।
Record equality includes record components।
This Matters for Domain Entities
Suppose Course is an entity whose identity is:
CourseId
and title can change।
Should equality depend on:
id + title + description + status + lessons
?
Maybe not।
This is one reason not every domain entity should automatically become a record।
Value Object vs Entity
This distinction is very important।
Value Object
Defined mainly by its values।
Example:
CourseCode("JAVA")
Money(500000)
DateRange(start, end)
Two values with same components are logically same।
Records are excellent fit।
Entity
Has continuing identity over time।
Example:
Course ID 100
may remain the same Course even if:
title changes
status changes
lessons change
Entity identity and lifecycle can make a normal class more appropriate।
Example Entity
final class Course {
private final CourseCode code;
private String title;
private CourseStatus status;
Course(
CourseCode code,
String title
) {
this.code =
code;
this.title =
title;
this.status =
CourseStatus.DRAFT;
}
void changeTitle(
String title
) {
...
}
void publish() {
...
}
}
This has:
lifecycle
state transitions
encapsulated mutation
business behavior
A normal class may communicate that model better।
Record Does Not Mean "Immutable Domain Entity"
Records naturally suit immutable state representation।
If an object has meaningful state transitions:
DRAFT
→ PUBLISHED
→ ARCHIVED
and behavior controls those transitions, forcing it into a record can make design awkward।
Records for DTOs
A DTO or summary is a strong use case।
Example:
record CourseSummary(
String code,
String title,
long priceInPaisa,
CourseStatus status
) {
}
It simply describes a snapshot of data।
Records for Commands
Example:
record CreateCourseCommand(
String code,
String title,
long priceInPaisa
) {
}
A command captures input data for an operation।
Validation can live in command or deeper domain types depending on architecture।
Records for Results
Example:
record EnrollmentResult(
long enrollmentId,
String courseCode,
String learnerEmail
) {
}
Useful immutable output representation।
Records for Keys
Excellent:
record EnrollmentKey(
LearnerId learnerId,
CourseCode courseCode
) {
}
Because value equality + hashing automatically align with components।
Records for Events
Example:
record CoursePublished(
CourseCode courseCode,
Instant publishedAt
) {
}
Represents an immutable fact।
Very natural fit।
Records with Interfaces
Example:
interface DomainEvent {
Instant occurredAt();
}
Record:
record CoursePublished(
CourseCode courseCode,
Instant occurredAt
) implements DomainEvent {
}
Records can participate in abstractions।
Record as Map Key
Suppose we need uniqueness by:
learner + course
record EnrollmentKey(
long learnerId,
String courseCode
) {
}
Then:
Map<EnrollmentKey, Enrollment> enrollments =
new HashMap<>();
Lookup:
EnrollmentKey key =
new EnrollmentKey(
learnerId,
courseCode
);
Enrollment enrollment =
enrollments.get(
key
);
No manual equals()/hashCode() implementation required।
Records and Hashing
Recall hashing rule:
equals() true
→ hashCode() same
Records generate component-based equality/hash behavior consistently।
This makes them strong candidates for immutable hash keys।
Still:
components themselves should have stable equality
for safe use।
Mutable Component as Hash Key Risk
Suppose:
record Key(
List<String> values
) {
}
and mutable List is used directly।
If list contents change after key goes into HashMap, its hash/equality behavior can effectively change।
Even though outer record cannot reassign values, inner object is mutable।
Therefore:
record alone does not make a safe hash key
Defensive copy matters।
Better
record Key(
List<String> values
) {
Key {
values =
List.copyOf(
values
);
}
}
Now collection structure is stable।
Overriding Accessors
Records allow explicit accessor methods, but use carefully।
Example:
record Name(
String value
) {
@Override
public String value() {
return value;
}
}
This adds nothing।
Avoid unnecessary overrides।
Do Not Surprise the Caller
Bad idea conceptually:
record Price(
long amount
) {
public long amount() {
return amount * 2;
}
}
Now accessor no longer simply represents stored component semantics।
That defeats record transparency।
If transformation needed, use another method:
long doubledAmount() {
return amount * 2;
}
Record Component Names Matter
Because accessor names come directly from component names:
record Course(
String c,
String t
) {
}
produces:
c()
t()
Poor API।
Prefer:
record CourseSummary(
String code,
String title
) {
}
Then:
code()
title()
communicate meaning।
Records and Null
Records do not automatically reject null।
Example:
record Person(
String name
) {
}
This compiles:
new Person(
null
);
If null invalid:
record Person(
String name
) {
Person {
name =
Objects.requireNonNull(
name,
"name must not be null"
);
}
}
Normalize Strings at Construction Boundary
Suppose code should always be trimmed।
Better:
record CourseCode(
String value
) {
CourseCode {
value =
Objects.requireNonNull(
value
).strip();
if (
value.isEmpty()
) {
throw new IllegalArgumentException(
"Course code is required."
);
}
}
}
Now downstream code does not repeatedly call:
strip()
Validate Once, Trust the Type
This is a powerful value-object principle:
Validate at construction
↓
valid object created
↓
downstream code trusts invariant
Instead of:
if (
courseCode != null
&& !courseCode.isBlank()
)
everywhere।
Strong Types Reduce Repeated Validation
Raw String version:
void publish(
String courseCode
) {
if (
courseCode == null
|| courseCode.isBlank()
) {
...
}
}
Another method repeats same validation।
With:
CourseCode
constructor validates once।
Method:
void publish(
CourseCode courseCode
) {
...
}
can trust code validity।
Record vs Normal Class
Use a record when object is primarily:
a value
a snapshot
a result
a command
a key
a small immutable data model
Use a normal class when object needs:
complex lifecycle
controlled mutable state
identity semantics different from all fields
rich encapsulation
inheritance from a class
internal state not represented directly by constructor data
Record Is Not "Better Class"
It is a different design tool।
Bad reasoning:
Records are modern
therefore every class should become a record.
Correct reasoning:
Does this type have value-oriented semantics?
Example: Good Record
record DateRange(
LocalDate start,
LocalDate end
) {
DateRange {
Objects.requireNonNull(
start
);
Objects.requireNonNull(
end
);
if (
end.isBefore(
start
)
) {
throw new IllegalArgumentException(
"End date cannot be before start date."
);
}
}
boolean contains(
LocalDate date
) {
return !date.isBefore(
start
)
&& !date.isAfter(
end
);
}
}
This is an excellent value object।
Why DateRange Fits Record Well
Its identity is exactly:
start
end
Two ranges with same dates are logically equal।
It can be immutable।
Behavior derives naturally from its values।
Example: Good Result Record
record CourseStatistics(
long totalCourses,
long publishedCourses,
long archivedCourses
) {
}
Pure immutable result।
Example: Good Request Record
record RegisterLearnerRequest(
String name,
String email
) {
}
Though boundary validation still needs appropriate design।
Example: Maybe Not a Record
Course
with:
addLesson()
changeTitle()
publish()
archive()
If these state transitions are core domain behavior, normal class may be more expressive।
Entity Snapshot Can Still Be a Record
The entity itself may be a normal class:
Course
but read model:
record CourseSummary(...)
can be a record।
This separation is common and useful।
Example
Domain entity:
final class Course {
...
}
Projection:
record CourseSummary(
String code,
String title,
CourseStatus status,
int lessonCount
) {
}
The summary is a snapshot, not the mutable lifecycle owner।
Records and Serialization
Many frameworks and libraries support records well, but framework compatibility is a separate concern।
Do not assume every old library automatically understands every modern Java feature।
In plain Java code, records are first-class language types।
At external integration boundaries, verify framework support when needed।
Records and Persistence
Do not choose:
record vs class
solely based on database mapping convenience।
First decide domain semantics।
Then adapt persistence boundary appropriately।
Record with Nested Records
record CourseSummary(
CourseCode code,
String title,
Money price
) {
}
This is often stronger than:
record CourseSummary(
String code,
String title,
long price
) {
}
because domain concepts remain typed।
Complete Example
import java.util.List;
import java.util.Locale;
import java.util.Objects;
public class Main {
public static void main(String[] args) {
CourseCode code =
new CourseCode(
" java-foundation "
);
CourseSummary first =
new CourseSummary(
code,
"Java Foundation",
300_000,
List.of(
"Java",
"OOP"
)
);
CourseSummary second =
new CourseSummary(
new CourseCode(
"JAVA-FOUNDATION"
),
"Java Foundation",
300_000,
List.of(
"Java",
"OOP"
)
);
System.out.println(
first
);
System.out.println(
first.equals(
second
)
);
System.out.println(
first.code()
);
}
record CourseCode(
String value
) {
CourseCode {
value =
Objects.requireNonNull(
value,
"Course code is required."
)
.strip()
.toUpperCase(
Locale.ROOT
);
if (
value.isBlank()
|| !value.matches(
"[A-Z0-9-]+"
)
) {
throw new IllegalArgumentException(
"Invalid course code: "
+ value
);
}
}
@Override
public String toString() {
return value;
}
}
record CourseSummary(
CourseCode code,
String title,
long priceInPaisa,
List<String> topics
) {
CourseSummary {
Objects.requireNonNull(
code,
"Course code is required."
);
title =
Objects.requireNonNull(
title,
"Course title is required."
).strip();
if (
title.isBlank()
) {
throw new IllegalArgumentException(
"Course title is required."
);
}
if (
priceInPaisa < 0
) {
throw new IllegalArgumentException(
"Price cannot be negative."
);
}
topics =
List.copyOf(
Objects.requireNonNull(
topics,
"Topics are required."
)
);
}
}
}
What This Example Demonstrates
CourseCode:
normalizes itself
validates itself
has value equality
can safely represent the domain concept
CourseSummary:
is a snapshot
has immutable component references
defensively copies topics
gets equals/hashCode/toString automatically
This is a strong record use case।
Common Mistake 1 — Assuming Record Means Deeply Immutable
Wrong।
record Something(
List<String> values
) {
}
does not make values immutable automatically।
Use defensive copying when required।
Common Mistake 2 — Using Record for Every Entity
A lifecycle-rich mutable entity may be better as a normal class।
Do not choose record solely because it is concise।
Common Mistake 3 — No Validation
record CourseCode(
String value
) {
}
allows invalid values unless validation added।
Records do not automatically enforce domain rules।
Common Mistake 4 — Exposing Mutable Collections
record Course(
List<String> topics
) {
}
with caller-owned mutable List can break value stability।
Consider:
List.copyOf(...)
Common Mistake 5 — Poor Component Names
Avoid:
record X(
String a,
int b
) {
}
when this is public/domain code।
Component names become API names।
Common Mistake 6 — Treating Record Accessors Like Bean Getters
Record:
record Course(
String title
) {
}
accessor:
course.title()
not automatically:
course.getTitle()
Common Mistake 7 — Overriding Generated Methods Without Need
Generated:
equals()
hashCode()
toString()
are usually exactly why record is useful।
Override only when semantics genuinely require it and remain consistent with design।
Common Mistake 8 — Making Equality Semantics Wrong for an Entity
If object identity is only:
id
but record components include many mutable/changeable attributes, component-based equality may not match entity semantics।
Use a normal class when appropriate।
Common Mistake 9 — Mutable Record Components as Hash Keys
Outer record may appear immutable, but inner mutable collection/object can change equality/hash behavior।
Use stable immutable components for hash keys।
Common Mistake 10 — Using Raw Primitives When Strong Types Help
Instead of:
String email
String courseCode
long learnerId
consider small validated record value objects when they reduce ambiguity and repeated validation।
Practice 1 — Basic Record
Create a record:
Point
x
y
Solution
record Point(
int x,
int y
) {
}
Practice 2 — Accessor
Given:
Point point =
new Point(
10,
20
);
How do you access x?
Answer
point.x()
Practice 3 — Equality
new Point(
10,
20
).equals(
new Point(
10,
20
)
)
Result?
Answer
true
because record component values are equal।
Practice 4 — Validation
Create positive CourseId।
Solution
record CourseId(
long value
) {
CourseId {
if (
value <= 0
) {
throw new IllegalArgumentException(
"Course ID must be positive."
);
}
}
}
Practice 5 — Defensive Copy
Given:
record Tags(
List<String> values
)
protect collection structure।
Solution
record Tags(
List<String> values
) {
Tags {
values =
List.copyOf(
values
);
}
}
Practice 6 — Record or Class?
Type:
Money
amount
currency
immutable value semantics
Answer
Record is a strong candidate।
Practice 7 — Record or Class?
Type:
Course
has mutable lifecycle
addLesson()
publish()
archive()
changeTitle()
Answer
A normal class is likely a better starting point।
Practice 8 — Record or Class?
Type:
CourseSummary
code
title
price
lessonCount
Read-only snapshot।
Answer
Record is an excellent fit।
Practice 9 — Null Validation
Does:
record Email(
String value
) {
}
automatically reject null?
Answer
No।
Explicit validation required।
Practice 10 — Shallow Immutability
Is this deeply immutable?
record Data(
ArrayList<String> values
) {
}
Answer
No।
The ArrayList itself can still mutate।
Practice 11 — Map Key
Why can:
record EnrollmentKey(
long learnerId,
String courseCode
) {
}
be convenient as a HashMap key?
Answer
Because record automatically provides component-based:
equals()
hashCode()
which aligns naturally with value-key semantics।
Practice 12 — Component Names
Which is better?
record Course(
String c,
String t
)
or:
record CourseSummary(
String code,
String title
)
Answer
The second।
Record component names become part of the API through accessors।
Practice 13 — Strong Type
Why might:
CourseCode
be better than raw:
String
Answer
It can encode:
domain meaning
validation
normalization
type safety
once, instead of repeating them throughout the codebase।
Practice 14 — Compact Constructor
What does this do?
record Name(
String value
) {
Name {
value =
value.strip();
}
}
Answer
Constructor parameter value normalize করে এবং normalized value record component হিসেবে stored হয়।
Practice 15 — Value Object or Entity?
Two objects with same values should logically be interchangeable।
What concept is this closer to?
Answer
Value Object
Records fit this style well।
True or False
- A record is a special form of Java class.
- Records automatically generate accessors.
- Record accessors are normally named
getX(). - Records automatically provide value-based
equals(). - Records automatically provide compatible
hashCode(). - Records automatically reject null values.
- Records can contain validation.
- Compact constructors are useful for validation and normalization.
- Record components are deeply immutable automatically.
List.copyOf()can help protect collection structure.- Records are excellent candidates for many value objects.
- Every domain entity should be a record.
- Records can implement interfaces.
- Records can contain methods.
- Record component names are part of the public API.
Answers
1. True
2. True
3. False
4. True
5. True
6. False
7. True
8. True
9. False
10. True
11. True
12. False
13. True
14. True
15. True
Knowledge Check
Question 1
record কী ধরনের problem solve করে?
Question 2
Record component কী?
Question 3
Record automatically কোন common methods provide করে?
Question 4
Compact constructor কেন useful?
Question 5
Record কেন deeply immutable নয়?
Question 6
Defensive copying কী?
Question 7
Value Object কী?
Question 8
Entity এবং Value Object-এর মূল conceptual difference কী?
Question 9
কেন record composite Map key হিসেবে useful?
Question 10
কেন required domain validation record constructor-এর মধ্যে রাখা useful হতে পারে?
Question 11
কখন normal class record-এর চেয়ে better?
Question 12
Strong domain record raw primitive type-এর চেয়ে কী advantage দিতে পারে?
Knowledge Check Answers
Answer 1
Value-oriented data class-এর repetitive boilerplate কমায় এবং component-based state/equality conciseভাবে express করে।
Answer 2
Record header-এর declared state values।
Example:
record Course(
String code,
String title
)
এখানে:
code
title
record components।
Answer 3
Record সাধারণভাবে component-based:
canonical constructor
accessors
equals()
hashCode()
toString()
provide করে।
Answer 4
Compact constructor full constructor parameter list এবং field assignments repeat না করে validation এবং normalization করতে সাহায্য করে।
Answer 5
Record component references stable হলেও referenced objects mutable হতে পারে।
Example:
List<String>
component-এর list contents change হতে পারে যদি defensive copy না করা হয়।
Answer 6
Caller-provided mutable object-এর independent copy store করা, যাতে caller পরে original object mutate করলেও internal value change না হয়।
Example:
List.copyOf(
values
)
Answer 7
এমন domain object যার logical identity তার values দ্বারা define হয়।
Same values সাধারণত same logical value represent করে।
Answer 8
Value Object:
value দ্বারা identity
Entity:
continuing identity থাকে
attributes সময়ের সাথে change করতে পারে
Answer 9
কারণ record automatically consistent component-based equals() এবং hashCode() provide করে।
Composite key semantics-এর সাথে এটি naturally fit করে।
Answer 10
একবার valid record construct হলে downstream code invariant trust করতে পারে এবং repeated validation কমে।
Answer 11
যখন object-এর:
mutable lifecycle
controlled state transitions
identity semantics
complex encapsulation
important।
Answer 12
Strong record type:
meaning encode করে
invalid state reject করতে পারে
normalization centralize করে
wrong argument type mix-up কমায়
Practical Record Selection Guide
Use a record for:
Value Object
DTO
Request
Command
Result
Summary
Composite Key
Immutable Event
Configuration snapshot
Small immutable model
Consider a normal class for:
Rich domain entity
Mutable lifecycle
State transitions
Identity independent of all fields
Complex internal invariants
Encapsulated mutable collections
Long-lived behavioral object
Value Object Examples
record CourseCode(
String value
) {
}
record LearnerId(
long value
) {
}
record EmailAddress(
String value
) {
}
record DateRange(
LocalDate start,
LocalDate end
) {
}
record EnrollmentKey(
LearnerId learnerId,
CourseCode courseCode
) {
}
Record Design Checklist
একটি নতুন record বানানোর আগে জিজ্ঞেস করুন:
এই type কি তার values দিয়ে define হয়?
সব components কি constructor সময়েই known?
State কি ideally immutable?
Component-based equality কি correct?
Validation construction-এর সময় করা যায়?
Mutable collections defensive copy করা হয়েছে?
Component names clear কি?
এই type-এর lifecycle mutation দরকার কি?
এটি entity না value object?
Strong type বানালে primitive ambiguity কমবে কি?
Core Mental Model
Record-কে শুধু:
short class
হিসেবে ভাববেন না।
Better:
Record declares:
"This type is primarily a value
defined by these components."
Example:
record CourseCode(
String value
) {
}
means:
CourseCode-এর meaning তার value।
আর:
record CourseSummary(
CourseCode code,
String title,
long priceInPaisa
) {
}
means:
This is a data snapshot
described by these components.
কিন্তু:
A Course that changes title,
adds lessons,
publishes,
archives
একটি lifecycle-rich entity।
সেটিকে normal class হিসেবে model করা অনেক সময় better।
Lesson Summary
এই lesson-এ আমরা Java record এবং modern data modeling-এর foundation শিখেছি।
আমরা শিখেছি:
recordvalue-oriented class declaration concise করে- Record components type-এর state define করে
- Java canonical constructor generate করতে পারে
- Record accessor component name ব্যবহার করে
- Records automatically
equals(),hashCode(), এবংtoString()provide করে - Record equality component values-এর উপর ভিত্তি করে
- Records
HashMap/HashSetkeys হিসেবে useful হতে পারে - Records automatically validate data করে না
- Compact constructor validation এবং normalization-এর জন্য useful
- Small value objects record হিসেবে excellent fit
- Strong domain types raw primitives-এর ambiguity কমাতে পারে
- Records methods এবং static members রাখতে পারে
- Records interfaces implement করতে পারে
- Records shallowly immutable
- Mutable components এখনও mutate হতে পারে
List.copyOf(),Set.copyOf(),Map.copyOf()defensive copying-এ useful- Value Object এবং Entity একই concept নয়
- Value Object-এর identity values দিয়ে define হয়
- Entity-এর continuing identity এবং lifecycle থাকে
- DTO, command, result, summary, key, event-এর জন্য records strong fit
- Lifecycle-rich mutable entity-এর জন্য normal class often better
- Record component names API design-এর অংশ
- Record modern বলে সব class record করা উচিত নয়
- Type-এর semantics design choice determine করবে
সবচেয়ে important principle:
Record ব্যবহার করুন
যখন object মূলত একটি value।
Normal class ব্যবহার করুন
যখন object মূলত একটি lifecycle এবং behavior-এর owner।
Next Lesson
পরবর্তী lesson:
Introduction to Concurrency and Thread Safety
আমরা শিখব:
- Process এবং Thread
- Concurrency কী
- Parallelism কী
- Concurrency এবং Parallelism-এর difference
- Shared mutable state
- Race condition
- Thread safety
- Atomic-looking code কেন সবসময় atomic নয়
synchronized- Critical section
- Visibility
volatile-এর basic concept- Immutability এবং thread safety
- Stateless code
- Thread confinement
- কেন concurrency bugs reproduce করা কঠিন
- Safe concurrency design-এর foundation