Generics, Collections, and Core Data Structures

Designing Classes That Own Collections

ReadingPreview

You are viewing a 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:

  • null lesson 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:

  1. Ordered course lessons
  2. Unique completed lesson IDs
  3. Lesson ID to lesson lookup
  4. Ordered unique module codes
  5. User ID to user
  6. 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 List plus 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 Set duplicate 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, এবং Map decision framework
  • Ordering requirements
  • Duplicate semantics
  • Membership checks
  • Key-based lookup
  • ArrayList
  • HashSet
  • LinkedHashSet
  • HashMap
  • LinkedHashMap
  • TreeSet এবং TreeMap
  • Basic performance mental model
  • Avoiding premature optimization
  • Refactoring when requirements change