Generics, Collections, and Core Data Structures
Designing Classes That Own Collections
আপনি একটি free preview lesson দেখছেন।
Lesson Overview
একটি class-এর মধ্যে collection field রাখা খুব common।
Examples:
Course has lessons
Module has lesson IDs
Learner has completed content IDs
Instructor has courses
Order has items
Team has members
কিন্তু শুধু collection field declare করলেই design complete হয় না।
Weak design:
public final class Course {
public List<Lesson> lessons =
new ArrayList<>();
}
এখানে application-এর যেকোনো code:
nulllesson add করতে পারে- Duplicate lesson add করতে পারে
- Published course clear করতে পারে
- Lesson order arbitrarily change করতে পারে
- Maximum lesson count bypass করতে পারে
Strong design-এ class collection-এর ownership নেয়।
এর মানে:
- কে element add করতে পারবে class decide করবে
- কোন element valid class validate করবে
- Duplicate allowed কি না class enforce করবে
- Order কীভাবে change হবে class control করবে
- Internal mutable collection direct expose করা হবে না
- Constructor input defensively copy করা হবে
- Read operations safe snapshot return করবে
এই lesson-এ আমরা একটি complete Course aggregate design করব, যা ordered lessons safely own করে।
Learning Objectives
এই lesson শেষে আপনি পারবেন:
- Collection ownership explain করতে
- Aggregate object identify করতে
- Internal mutable collection encapsulate করতে
- Constructor input defensively copy করতে
- Read-only snapshot return করতে
- Controlled add, remove, rename এবং reorder operations লিখতে
- Duplicate এবং maximum-size invariants enforce করতে
- Parent-child ownership model করতে
- Collection mutation এবং element mutation-এর difference বুঝতে
- Leaked mutable state detect করতে
What Does It Mean to Own a Collection?
Suppose:
public final class Course {
private final List<Lesson> lessons;
}
Course lessons own করলে সাধারণত বোঝায়:
- Course lesson sequence control করে
- Course lesson addition rules enforce করে
- Course lesson removal rules enforce করে
- Outside code internal list direct mutate করতে পারে না
- Lessons course context-এর অংশ হিসেবে managed হয়
Ownership সবসময় database ownership বোঝায় না।
এটি object-design responsibility বোঝায়।
Aggregate Object
একটি aggregate object multiple related objectsকে একটি consistent boundary-এর মধ্যে manage করে।
Example:
Course
└── Lessons
Caller ideally courseকে bypass করে lesson collection modify করবে না।
Weak:
course.getLessons()
.add(
lesson
);
Strong:
course.addLesson(
lesson
);
এখন Course operation validate করতে পারে।
Why Direct Collection Exposure Is Dangerous
public final class Course {
private final List<Lesson> lessons =
new ArrayList<>();
public List<Lesson> getLessons() {
return lessons;
}
}
Caller can do:
course.getLessons()
.add(
null
);
course.getLessons()
.clear();
course.getLessons()
.add(
duplicateLesson
);
course.getLessons()
.remove(
0
);
Class-এর own methods এবং validations bypass হয়ে যায়।
Private field alone যথেষ্ট নয়, যদি getter same mutable reference return করে।
Leaked Mutable State
Internal mutable object-এর reference বাইরে দিয়ে দিলে তাকে mutable state leak বলা যায়।
return lessons;
Field private হলেও caller same list object পায়।
Conceptually:
Course ───────┐
├── Same mutable List
Caller ───────┘
যে কেউ list change করলে Course state change হয়।
Return a Read-Only Snapshot
public List<Lesson> getLessons() {
return List.copyOf(
lessons
);
}
Caller:
course.getLessons()
.clear();
Runtime-এ fail করবে:
UnsupportedOperationException
Internal list unchanged থাকবে।
Snapshot vs Live View
List.copyOf() current structure-এর snapshot তৈরি করে।
Suppose:
List<Lesson> snapshot =
course.getLessons();
Then course later নতুন lesson add করল।
Existing snapshot automatically update হবে না।
Snapshot creation time-এর elementsই থাকবে
এটি predictable behavior।
Snapshot Is Shallow
List.copyOf(
lessons
)
Lesson objects deep-copy করে না।
Both lists same lesson references রাখতে পারে।
Internal list ─┐
├── Same Lesson object
Snapshot ──────┘
If Lesson mutable:
snapshot.get(0)
.rename(
"Changed"
);
may still affect the lesson observed by course।
Therefore collection protection এবং element protection separate concerns।
Prefer Immutable Child Objects When Possible
A lesson title এবং ID যদি creation-এর পরে change করার দরকার না হয়:
public final class Lesson {
private final long id;
private final String title;
}
Immutable children aggregate reasoning সহজ করে।
যদি lesson update করতে হয়, course operation-এর মাধ্যমে replacement করা যেতে পারে।
Example:
course.renameLesson(
lessonId,
"New Title"
);
Course new Lesson object তৈরি করে old object replace করতে পারে।
Constructor Input Aliasing
Weak constructor:
public Course(
List<Lesson> lessons
) {
this.lessons =
lessons;
}
Caller and course same list share করে।
List<Lesson> lessons =
new ArrayList<>();
Course course =
new Course(
lessons
);
lessons.clear();
Course-এর internal stateও clear হয়ে যায়।
এটিকে aliasing problem বলা যায়।
Defensive Copying
public Course(
List<Lesson> lessons
) {
if (lessons == null) {
throw new IllegalArgumentException(
"Lessons are required."
);
}
this.lessons =
new ArrayList<>(
lessons
);
}
Now caller’s list এবং course internal list different structures।
Caller list → One ArrayList
Course list → Another ArrayList
Elements still shared references হতে পারে।
Immutable Internal Collection
If course creation-এর পরে lesson structure change হবে না:
this.lessons =
List.copyOf(
lessons
);
Then internal field type can remain:
private final List<Lesson> lessons;
No add/remove operations possible through that structure।
But if editing required, internal ArrayList appropriate।
Validate Before Copying
public Course(
List<Lesson> lessons
) {
if (lessons == null) {
throw new IllegalArgumentException(
"Lessons are required."
);
}
for (
Lesson lesson
: lessons
) {
if (lesson == null) {
throw new IllegalArgumentException(
"Lessons cannot contain null."
);
}
}
this.lessons =
new ArrayList<>(
lessons
);
}
Copying invalid data does not make it valid।
Collection Invariants
An invariant হলো এমন rule যা valid object state-এর জন্য সবসময় true থাকতে হবে।
Course lesson invariants হতে পারে:
No null lesson
No duplicate lesson ID
Maximum 100 lessons
Lesson order is meaningful
Published course cannot change lesson structure
Class-এর every constructor এবং mutation method এই rules preserve করবে।
Design the Lesson Value
public final class Lesson {
private final long id;
private final String title;
public Lesson(
long id,
String title
) {
if (id <= 0) {
throw new IllegalArgumentException(
"Lesson ID must be positive."
);
}
if (
title == null
|| title.isBlank()
) {
throw new IllegalArgumentException(
"Lesson title is required."
);
}
this.id = id;
this.title = title.strip();
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public Lesson rename(
String newTitle
) {
return new Lesson(
id,
newTitle
);
}
@Override
public String toString() {
return id
+ ": "
+ title;
}
}
Lesson immutable।
rename() current object modify না করে new object return করে।
Why Not Put Course Position Inside Lesson?
Possible design:
private int position;
But position may belong to course-lesson relationship, not lesson identity।
একই lesson অন্য learning path-এ different position-এ থাকতে পারে।
Current simple model list order দিয়ে position represent করছে।
Index 0 → First lesson
Index 1 → Second lesson
Controlled Addition
public boolean addLesson(
Lesson lesson
) {
if (published) {
return false;
}
if (lesson == null) {
return false;
}
if (
lessons.size()
>= MAX_LESSON_COUNT
) {
return false;
}
if (
containsLessonId(
lesson.getId()
)
) {
return false;
}
lessons.add(
lesson
);
return true;
}
Every rule add operation-এর আগে checked।
Duplicate Detection
private boolean containsLessonId(
long lessonId
) {
for (
Lesson lesson
: lessons
) {
if (
lesson.getId()
== lessonId
) {
return true;
}
}
return false;
}
The list allows duplicate objects technically।
Course domain rule duplicates reject করছে।
Why Use List If Duplicates Are Rejected?
Because lesson order matters।
Set duplicate prevention built-in দেয়, but typical hash-based set index বা sequence দেয় না।
Current requirements:
Order required
Duplicate IDs forbidden
A List plus explicit duplicate validation is reasonable।
If both frequent ID lookup and ordering become important, another design may use:
LinkedHashMap<Long, Lesson>
But start with the simplest structure matching current operations।
Adding at a Specific Position
public boolean addLessonAt(
int index,
Lesson lesson
) {
if (published) {
return false;
}
if (lesson == null) {
return false;
}
if (
index < 0
|| index > lessons.size()
) {
return false;
}
if (
lessons.size()
>= MAX_LESSON_COUNT
) {
return false;
}
if (
containsLessonId(
lesson.getId()
)
) {
return false;
}
lessons.add(
index,
lesson
);
return true;
}
For insertion, valid index range:
0 through size()
Index size() means append at end।
Finding a Lesson
public Lesson findLessonById(
long lessonId
) {
for (
Lesson lesson
: lessons
) {
if (
lesson.getId()
== lessonId
) {
return lesson;
}
}
return null;
}
For a small ordered collection, linear search is acceptable।
If collection becomes large and lookup frequent, a Map may be better।
Removing by Domain Identity
public boolean removeLesson(
long lessonId
) {
if (published) {
return false;
}
for (
int index = 0;
index < lessons.size();
index++
) {
Lesson lesson =
lessons.get(
index
);
if (
lesson.getId()
== lessonId
) {
lessons.remove(
index
);
return true;
}
}
return false;
}
Caller does not need list index জানতে।
It supplies domain identity:
lessonId
This creates a more stable API।
Why Avoid Public Index-Based Removal?
Weak:
course.removeLessonAt(
4
);
Caller must know current internal ordering।
If order changes between reading and removal, wrong lesson remove হতে পারে।
Stronger:
course.removeLesson(
lessonId
);
Use index-based API only when position itself is the domain intent।
Renaming an Immutable Lesson
public boolean renameLesson(
long lessonId,
String newTitle
) {
if (published) {
return false;
}
if (
newTitle == null
|| newTitle.isBlank()
) {
return false;
}
for (
int index = 0;
index < lessons.size();
index++
) {
Lesson lesson =
lessons.get(
index
);
if (
lesson.getId()
== lessonId
) {
lessons.set(
index,
lesson.rename(
newTitle
)
);
return true;
}
}
return false;
}
The old lesson object is replaced by a new valid Lesson।
Reordering Lessons
Suppose lessonকে one position থেকে another position-এ move করতে হবে।
public boolean moveLesson(
long lessonId,
int targetIndex
) {
if (published) {
return false;
}
if (
targetIndex < 0
|| targetIndex >= lessons.size()
) {
return false;
}
int currentIndex =
findLessonIndex(
lessonId
);
if (currentIndex < 0) {
return false;
}
if (currentIndex == targetIndex) {
return false;
}
Lesson lesson =
lessons.remove(
currentIndex
);
lessons.add(
targetIndex,
lesson
);
return true;
}
A Reordering Subtlety
Suppose:
[A, B, C, D]
Move B from index 1 to index 3।
After removing B:
[A, C, D]
Then add at index 3:
[A, C, D, B]
This implementation interprets targetIndex as final index in the resulting list।
For many simple cases this works clearly।
More complex UI contracts should define whether target index refers to:
- Before removal state
- After removal state
- Insert before another lesson
- Insert after another lesson
Index semantics should be documented।
Find a Lesson Index
private int findLessonIndex(
long lessonId
) {
for (
int index = 0;
index < lessons.size();
index++
) {
if (
lessons.get(
index
).getId()
== lessonId
) {
return index;
}
}
return -1;
}
-1 means not found।
Maximum Size Rule
private static final int MAX_LESSON_COUNT =
100;
Before add:
if (
lessons.size()
>= MAX_LESSON_COUNT
) {
return false;
}
Named constant communicates business limit।
Avoid unexplained magic number:
if (
lessons.size()
>= 100
)
Publishing Locks Structure
public boolean publish() {
if (published) {
return false;
}
if (lessons.isEmpty()) {
return false;
}
published = true;
return true;
}
All structural mutation methods check:
if (published) {
return false;
}
This preserves:
Published course lesson structure cannot change
If future domain allows revisions, model may need versioning or draft/published snapshots।
Do not simply remove the invariant without considering behavior।
Constructor with Initial Lessons
Sometimes course created with existing lessons।
public Course(
long id,
String title,
List<Lesson> initialLessons
) {
validateId(
id
);
validateTitle(
title
);
if (initialLessons == null) {
throw new IllegalArgumentException(
"Initial lessons are required."
);
}
if (
initialLessons.size()
> MAX_LESSON_COUNT
) {
throw new IllegalArgumentException(
"Too many lessons."
);
}
validateLessons(
initialLessons
);
this.id = id;
this.title = title.strip();
this.lessons =
new ArrayList<>(
initialLessons
);
this.published = false;
}
Validate Duplicate IDs in Constructor Input
private static void validateLessons(
List<Lesson> lessons
) {
Set<Long> lessonIds =
new HashSet<>();
for (
Lesson lesson
: lessons
) {
if (lesson == null) {
throw new IllegalArgumentException(
"Lessons cannot contain null."
);
}
boolean added =
lessonIds.add(
lesson.getId()
);
if (!added) {
throw new IllegalArgumentException(
"Duplicate lesson ID: "
+ lesson.getId()
);
}
}
}
Temporary Set duplicate detection সহজ করে।
Final storage still List, because order matters।
Why Validate in Every Creation Path?
If one constructor validates but another does not, invalid object তৈরি হতে পারে।
All constructors should preserve same invariants।
Possible approach:
public Course(
long id,
String title
) {
this(
id,
title,
List.of()
);
}
The smaller constructor delegates to the full constructor।
Constructor Delegation
public Course(
long id,
String title
) {
this(
id,
title,
List.of()
);
}
This reduces duplicated validation and initialization logic।
Complete Course Implementation
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class Course {
private static final int MAX_LESSON_COUNT =
100;
private final long id;
private final String title;
private final List<Lesson> lessons;
private boolean published;
public Course(
long id,
String title
) {
this(
id,
title,
List.of()
);
}
public Course(
long id,
String title,
List<Lesson> initialLessons
) {
if (id <= 0) {
throw new IllegalArgumentException(
"Course ID must be positive."
);
}
if (
title == null
|| title.isBlank()
) {
throw new IllegalArgumentException(
"Course title is required."
);
}
if (initialLessons == null) {
throw new IllegalArgumentException(
"Initial lessons are required."
);
}
if (
initialLessons.size()
> MAX_LESSON_COUNT
) {
throw new IllegalArgumentException(
"Course cannot contain more than "
+ MAX_LESSON_COUNT
+ " lessons."
);
}
validateLessons(
initialLessons
);
this.id = id;
this.title = title.strip();
this.lessons =
new ArrayList<>(
initialLessons
);
this.published = false;
}
public boolean addLesson(
Lesson lesson
) {
return addLessonAt(
lessons.size(),
lesson
);
}
public boolean addLessonAt(
int index,
Lesson lesson
) {
if (published) {
return false;
}
if (lesson == null) {
return false;
}
if (
index < 0
|| index > lessons.size()
) {
return false;
}
if (
lessons.size()
>= MAX_LESSON_COUNT
) {
return false;
}
if (
containsLessonId(
lesson.getId()
)
) {
return false;
}
lessons.add(
index,
lesson
);
return true;
}
public boolean removeLesson(
long lessonId
) {
if (published) {
return false;
}
int index =
findLessonIndex(
lessonId
);
if (index < 0) {
return false;
}
lessons.remove(
index
);
return true;
}
public boolean renameLesson(
long lessonId,
String newTitle
) {
if (published) {
return false;
}
if (
newTitle == null
|| newTitle.isBlank()
) {
return false;
}
int index =
findLessonIndex(
lessonId
);
if (index < 0) {
return false;
}
Lesson currentLesson =
lessons.get(
index
);
lessons.set(
index,
currentLesson.rename(
newTitle
)
);
return true;
}
public boolean moveLesson(
long lessonId,
int targetIndex
) {
if (published) {
return false;
}
if (
targetIndex < 0
|| targetIndex >= lessons.size()
) {
return false;
}
int currentIndex =
findLessonIndex(
lessonId
);
if (
currentIndex < 0
|| currentIndex == targetIndex
) {
return false;
}
Lesson lesson =
lessons.remove(
currentIndex
);
lessons.add(
targetIndex,
lesson
);
return true;
}
public Lesson findLessonById(
long lessonId
) {
int index =
findLessonIndex(
lessonId
);
if (index < 0) {
return null;
}
return lessons.get(
index
);
}
public boolean publish() {
if (published) {
return false;
}
if (lessons.isEmpty()) {
return false;
}
published = true;
return true;
}
public List<Lesson> getLessons() {
return List.copyOf(
lessons
);
}
public int getLessonCount() {
return lessons.size();
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public boolean isPublished() {
return published;
}
private boolean containsLessonId(
long lessonId
) {
return findLessonIndex(
lessonId
) >= 0;
}
private int findLessonIndex(
long lessonId
) {
for (
int index = 0;
index < lessons.size();
index++
) {
if (
lessons.get(
index
).getId()
== lessonId
) {
return index;
}
}
return -1;
}
private static void validateLessons(
List<Lesson> lessons
) {
Set<Long> lessonIds =
new HashSet<>();
for (
Lesson lesson
: lessons
) {
if (lesson == null) {
throw new IllegalArgumentException(
"Lessons cannot contain null."
);
}
if (
!lessonIds.add(
lesson.getId()
)
) {
throw new IllegalArgumentException(
"Duplicate lesson ID: "
+ lesson.getId()
);
}
}
}
}
Using the Course
public class Main {
public static void main(
String[] args
) {
Course course =
new Course(
1L,
"Java and OOP Foundation"
);
course.addLesson(
new Lesson(
101L,
"Introduction to Generics"
)
);
course.addLesson(
new Lesson(
102L,
"Working with List"
)
);
course.addLesson(
new Lesson(
103L,
"Working with Set"
)
);
boolean duplicateAdded =
course.addLesson(
new Lesson(
102L,
"Duplicate Lesson"
)
);
course.renameLesson(
103L,
"Working with Set and Equality"
);
course.moveLesson(
103L,
1
);
printLessons(
course
);
System.out.println(
"Duplicate added: "
+ duplicateAdded
);
System.out.println(
"Published: "
+ course.publish()
);
System.out.println(
"Can remove after publish: "
+ course.removeLesson(
101L
)
);
}
private static void printLessons(
Course course
) {
List<Lesson> lessons =
course.getLessons();
for (
int index = 0;
index < lessons.size();
index++
) {
System.out.println(
(index + 1)
+ ". "
+ lessons.get(
index
).getTitle()
);
}
}
}
Possible output:
1. Introduction to Generics
2. Working with Set and Equality
3. Working with List
Duplicate added: false
Published: true
Can remove after publish: false
Collection Structure vs Child Lifecycle
Current Course protects collection structure।
But suppose caller obtains:
Lesson lesson =
course.findLessonById(
101L
);
If Lesson mutable, caller could change it directly।
Our Lesson immutable, তাই safe।
If child objects are mutable, consider:
- Return immutable child
- Return defensive copy
- Expose child through read-only interface
- Perform updates through aggregate methods
- Accept that child is independently owned
The correct choice depends on ownership semantics।
Does Course Own the Lesson Object?
There are two possible models।
Exclusive Ownership
Lesson exists only inside one Course
Course should strongly control lesson lifecycle।
Shared Reference
Same Lesson can appear in multiple learning paths
Course may only own the relationship/order, not the lesson object।
Do not assume ownership without understanding the domain।
Removing a Child
When:
course.removeLesson(
lessonId
);
What happens to the Lesson object?
In memory, if no references remain, it becomes eligible for garbage collection।
In a database-backed application, additional decisions exist:
Delete lesson row?
Detach from course only?
Archive lesson?
Reject removal if progress exists?
Collection removal alone does not define persistence behavior।
Enforcing Minimum Size
Current course can become empty before publication।
Suppose rule:
A module must always contain at least one lesson
Then removal needs check:
if (
lessons.size()
<= 1
) {
return false;
}
Invariants depend on lifecycle।
Possible rule:
Draft course may be empty
Published course must contain at least one lesson
Current implementation reflects this distinction।
Returning Counts Instead of Full Collections
Sometimes caller only needs count।
Use:
public int getLessonCount() {
return lessons.size();
}
Do not force caller to retrieve a copied list just to call:
size()
Expose focused queries।
Returning a Specific Element
public Lesson getLessonAt(
int index
) {
if (
index < 0
|| index >= lessons.size()
) {
return null;
}
return lessons.get(
index
);
}
Use only if positional access is meaningful।
Alternative:
findLessonById(...)
is often more stable।
Do Not Expose Mutation Through an Iterator
Weak:
public Iterator<Lesson> iterator() {
return lessons.iterator();
}
Caller may use:
iterator.remove();
and bypass domain rules।
If aggregate should remain protected, return:
List.copyOf(
lessons
).iterator();
or simply expose a snapshot list।
Unmodifiable View vs Snapshot
Java also supports an unmodifiable view:
Collections.unmodifiableList(
lessons
)
A view cannot be modified directly, but reflects later internal changes।
List.copyOf() creates an immutable structural snapshot।
Difference:
Unmodifiable view → Later internal mutations visible
Immutable copy → Later internal mutations not visible
For simple APIs, snapshots are often easier to reason about।
Avoid Overexposing Collection Operations
Do not blindly wrap every List method:
add()
addAll()
set()
clear()
remove()
removeAll()
retainAll()
sort()
replaceAll()
Expose only domain-meaningful operations:
addLesson()
removeLesson()
renameLesson()
moveLesson()
publish()
This keeps the aggregate contract focused।
Batch Addition
If multiple lessons add করতে হয়:
public boolean addLessons(
List<Lesson> newLessons
)
Questions:
- One invalid lesson হলে সব reject?
- Valid ones add, invalid ones skip?
- Duplicates current list-এর সঙ্গে কীভাবে handled?
- Maximum count exceed হলে কী হবে?
- Return boolean, count, না detailed result?
Batch operations need explicit atomicity semantics।
Simple safe policy:
Validate everything first
Then add everything
Atomic Batch Addition
public boolean addLessons(
List<Lesson> newLessons
) {
if (published) {
return false;
}
if (newLessons == null) {
return false;
}
if (
lessons.size()
+ newLessons.size()
> MAX_LESSON_COUNT
) {
return false;
}
Set<Long> newIds =
new HashSet<>();
for (
Lesson lesson
: newLessons
) {
if (lesson == null) {
return false;
}
if (
containsLessonId(
lesson.getId()
)
) {
return false;
}
if (
!newIds.add(
lesson.getId()
)
) {
return false;
}
}
lessons.addAll(
newLessons
);
return true;
}
No state changes until all validation passes।
Why Validate First?
Weak:
for (
Lesson lesson
: newLessons
) {
if (valid) {
lessons.add(
lesson
);
} else {
return false;
}
}
If third lesson invalid:
First two already added
Method returns false
Caller may assume nothing changed।
Validation-first avoids partial mutation।
Exceptions or Boolean Results?
Current mutation methods return boolean for expected rejection:
Already published
Duplicate lesson
Lesson not found
Invalid position
Constructors throw exceptions for invalid object creation।
Another design could return detailed result:
AddLessonResult
with reasons:
ADDED
COURSE_PUBLISHED
DUPLICATE_ID
LIMIT_REACHED
INVALID_LESSON
Boolean is simple but loses failure detail।
Choose based on API needs।
Thread Safety
ArrayList is not thread-safe।
If multiple threads mutate same Course simultaneously:
Duplicate validation may race
Maximum count may be exceeded
Order may be inconsistent
Example:
Thread A checks duplicate → absent
Thread B checks duplicate → absent
Both add same ID
Collection encapsulation alone does not solve concurrency।
Possible solutions depend on application architecture:
- Synchronization
- Single-threaded ownership
- Transactional persistence
- Optimistic locking
- Immutable state replacement
This course currently focuses on single-threaded object correctness।
Persistence Does Not Replace Domain Validation
A database may have:
UNIQUE(course_id, lesson_id)
Still useful for object to reject duplicates early।
Likewise, object validation does not replace database constraints in concurrent systems।
Strong systems often enforce important invariants at multiple appropriate layers।
Collection Choice Review
For course lessons:
List<Lesson>
because:
- Order matters
- Index-based display useful
- Duplicate IDs manually rejected
For completed lesson IDs:
Set<Long>
because:
- Membership matters
- Duplicates meaningless
- Order may not matter
For lesson lookup by ID:
Map<Long, Lesson>
because:
- Direct ID lookup primary operation
One aggregate may use different structures depending on requirements।
Do not automatically replace one structure with multiple redundant indexes।
When a Map May Be Better
If course has thousands of lessons and frequent lookup by ID:
Map<Long, Lesson>
may offer better lookup।
But pure HashMap does not preserve meaningful order।
Possible choices:
LinkedHashMap<Long, Lesson>
or:
List for order
Map for lookup
The second option requires synchronization between both structures।
For typical course sizes, one List with linear search is simpler and sufficient।
Common Mistakes
Returning the Internal List
Caller can bypass rules।
Storing Constructor Input Directly
Caller mutation changes aggregate state।
Assuming List.copyOf() Deep-Copies Elements
It only protects structure।
Allowing Mutable Child Objects to Escape
Caller may bypass aggregate update rules।
Using a Set When Order Matters
Uniqueness alone is not the only requirement।
Using List Index as Stable Identity
Positions change after reordering or removal।
Mutating Before Completing Validation
Batch operations can partially change state।
Validating Add but Not Constructor Input
Invalid objects can enter through another path।
Exposing Every Collection Operation
Aggregate API becomes a disguised List।
Forgetting Lifecycle Rules
Published and draft objects may allow different mutations।
Assuming Encapsulation Solves Concurrency
Multiple threads can still race।
Maintaining Multiple Structures Without Synchronization
List and map may diverge।
Practice Exercises
Exercise 1: Implement Module
Create:
Module
It owns:
List<Lesson>
Rules:
- Module title required
- No null lesson
- No duplicate lesson ID
- Maximum 20 lessons
- Order matters
- Getter returns immutable snapshot
Exercise 2: Batch Add
Add:
boolean addLessons(
List<Lesson> lessons
)
Requirements:
- Validate all before mutation
- Reject duplicates inside input
- Reject duplicates against existing lessons
- Reject maximum-size overflow
- No partial addition
Exercise 3: Immutable Child
Create immutable:
TeamMember
Fields:
id
name
role
Create Team that owns an ordered list of members।
Exercise 4: Choose the Structure
Choose List, Set, or Map:
- Ordered course lessons
- Unique completed lesson IDs
- Lesson ID to lesson lookup
- Ordered unique module codes
- User ID to user
- Activity history with repeated actions
Explain each choice।
Exercise 5: Detect a Leak
Explain why this is unsafe:
public List<Lesson> getLessons() {
return lessons;
}
Show two ways caller can violate course rules।
Exercise 6: Snapshot Behavior
Create:
List<Lesson> snapshot =
course.getLessons();
Then add another lesson to course।
Verify whether the old snapshot changes।
Exercise 7: Add a Minimum Rule
Modify removeLesson() so a published-ready course draft cannot have fewer than three lessons।
Decide whether:
- Removal returns false
- Exception is thrown
- Publication readiness alone should enforce the minimum
Explain the trade-off।
Exercise 8: Detailed Result
Replace:
boolean addLesson(...)
with an enum result:
ADDED
COURSE_PUBLISHED
INVALID_LESSON
DUPLICATE_ID
LIMIT_REACHED
Predict the Result
Question 1
List<Lesson> initial =
new ArrayList<>();
initial.add(
new Lesson(
1L,
"Java"
)
);
Course course =
new Course(
1L,
"Backend",
initial
);
initial.clear();
System.out.println(
course.getLessonCount()
);
Assume constructor defensively copies the list।
Answer
1
Caller list and course internal list are different structures।
Question 2
List<Lesson> snapshot =
course.getLessons();
course.addLesson(
new Lesson(
2L,
"Collections"
)
);
System.out.println(
snapshot.size()
);
Assume getLessons() uses List.copyOf()।
Answer
Snapshot creation-এর সময় যা ছিল, সেই size থাকবে।
Later course mutation old snapshot update করে না।
Question 3
course.getLessons()
.clear();
What happens?
Answer
Runtime-এ:
UnsupportedOperationException
because getter returns immutable snapshot।
Question 4
course.addLesson(
new Lesson(
10L,
"First"
)
);
boolean added =
course.addLesson(
new Lesson(
10L,
"Second"
)
);
Answer
false
Duplicate lesson ID rejected।
Question 5
A batch of three lessons is passed। The third one is invalid।
The method validates all inputs before calling addAll()।
How many lessons are added?
Answer
0
The operation is atomic from the object’s perspective।
Knowledge Check
Question 1
Collection ownership কী?
Question 2
Private field direct getter দিয়ে return করা unsafe কেন?
Question 3
Defensive copying কী?
Question 4
List.copyOf() কী protect করে?
Question 5
Does List.copyOf() deep-copy elements?
Question 6
Collection invariant কী?
Question 7
Why use a List if duplicates are rejected?
Question 8
Why prefer removal by lesson ID over list index?
Question 9
Why validate constructor input and mutation methods?
Question 10
Why should batch operations validate before mutation?
Question 11
Why can an immutable child simplify ownership?
Question 12
Does encapsulation make ArrayList thread-safe?
Question 13
Why avoid exposing all List methods through the aggregate?
Question 14
When might Map be better than List?
Question 15
Why can maintaining both a list and map be risky?
Knowledge Check Answers
Answer 1
Class collection structure, valid elements এবং mutation rules control করে।
Answer 2
Caller same mutable object পায় এবং validation bypass করে internal state change করতে পারে।
Answer 3
External mutable structure থেকে independent internal structure তৈরি করা।
Answer 4
List-এর structural mutation—add, remove এবং replace—prevent করে।
Answer 5
না।
Element references generally shared থাকে।
Answer 6
Valid object state-এর জন্য সবসময় true থাকা rule।
Answer 7
Order এবং index meaningful হতে পারে, while duplicates domain rule দিয়ে rejected হয়।
Answer 8
Index reordering-এর কারণে change হয়; ID stable domain identity।
Answer 9
Every creation এবং update path valid state preserve করতে হবে।
Answer 10
Invalid input পেলে partial state change avoid করতে।
Answer 11
Caller child state direct mutate করতে পারে না।
Answer 12
না।
Concurrent mutations still race করতে পারে।
Answer 13
Aggregate domain contract হারিয়ে generic collection wrapper হয়ে যায়।
Answer 14
Direct key lookup primary হলে এবং order less important বা separately supported হলে।
Answer 15
Every mutation দুটো structures-এ synchronously apply না হলে data diverge করতে পারে।
Lesson Summary
এই lesson-এ আমরা শিখেছি:
- A class can own and protect a collection
- Aggregate object child objectsকে consistent boundary-এর মধ্যে manage করে
- Private collection direct return করলে mutable state leak হয়
List.copyOf()immutable structural snapshot return করে- Snapshots later internal changes reflect করে না
- Collection copies generally shallow
- Immutable child objects aggregate ownership সহজ করে
- Constructor input direct store করলে aliasing problem হয়
- Defensive copy external এবং internal structures separate করে
- Collection invariants every creation এবং mutation path-এ preserve করতে হয়
- Ordered unique lessons
Listplus duplicate validation দিয়ে model করা যায় - Domain methods generic list operations-এর চেয়ে clearer
- Lesson ID index-এর চেয়ে stable identity
- Controlled add, remove, rename এবং reorder operations rules enforce করে
- Published lifecycle collection mutation lock করতে পারে
- Constructor delegation validation duplication কমায়
- Temporary
Setduplicate input detect করতে useful - Batch operations validation-first করলে partial mutation avoid হয়
- Outer collection protection child mutation automatically prevent করে না
- Ownership exclusive বা shared হতে পারে
- Empty collection, minimum size এবং removal behavior domain decisions
- Focused count এবং lookup methods unnecessary copying কমাতে পারে
- Unmodifiable view এবং immutable snapshot different
- Aggregate every collection method expose করা উচিত নয়
- Boolean result simple but failure detail হারায়
- Encapsulation concurrency solve করে না
- Database constraints এবং object invariants complementary
- Collection selection order, uniqueness এবং lookup requirements থেকে আসা উচিত
Next Lesson
পরবর্তী lesson:
Choosing the Right Collection
আমরা শিখব:
List,Set, এবংMapdecision framework- Ordering requirements
- Duplicate semantics
- Membership checks
- Key-based lookup
ArrayListHashSetLinkedHashSetHashMapLinkedHashMapTreeSetএবংTreeMap- Basic performance mental model
- Avoiding premature optimization
- Refactoring when requirements change