Generics, Collections, and Core Data Structures

Working with `Map`

ReadingPreview

আপনি একটি free preview lesson দেখছেন।

Lesson Overview

আগের lessons-এ আমরা দুই ধরনের collection শিখেছি:

List → Ordered sequence
Set  → Unique membership

কিন্তু application-এ অনেক সময় একটি value অন্য একটি unique key-এর মাধ্যমে খুঁজে বের করতে হয়।

Examples:

Course code → Course
Learner ID → Learner
Lesson ID → Lesson
Country code → Country name
Configuration key → Configuration value

এই ধরনের relationship model করতে Java provides:

Map<K, V>

এখানে:

K → Key type
V → Value type

Example:

Map<String, Course> coursesByCode;

Meaning:

String key দিয়ে Course value lookup করা যাবে

Map:

  • Key-value pairs store করে
  • Duplicate key রাখে না
  • Different keys same value reference করতে পারে
  • Key দিয়ে efficient lookup support করে
  • Index-based নয়
  • Key equality-এর ওপর depend করে
  • Mutable বা immutable হতে পারে

Common mutable implementation:

HashMap<K, V>

এই lesson-এ আমরা শিখব:

  • Map কী
  • HashMap
  • Key-value pair
  • put(), get(), এবং remove()
  • Duplicate key replacement
  • containsKey() এবং containsValue()
  • Missing key এবং null
  • getOrDefault()
  • putIfAbsent()
  • Keys, values, এবং entries iterate করা
  • entrySet()
  • Map.of() এবং Map.copyOf()
  • Key equality এবং hashCode()
  • Mutable key-এর risk
  • List search থেকে Map lookup-এ refactoring
  • Course catalog এবং learner enrollment lookup design

Learning Objectives

এই lesson শেষ করার পর আপনি পারবেন:

  • Map<K, V> declare এবং initialize করতে
  • HashMap ব্যবহার করতে
  • Key-value pair add এবং retrieve করতে
  • Existing key replacement detect করতে
  • Missing key safely handle করতে
  • Key, value এবং entry iteration করতে
  • putIfAbsent() এবং getOrDefault() ব্যবহার করতে
  • Immutable map তৈরি করতে
  • Custom key-এর equals() এবং hashCode() design করতে
  • Mutable map key-এর risk explain করতে
  • List, Set, এবং Map-এর মধ্যে appropriate choice নিতে

What Is a Map?

Map key এবং value-এর relationship store করে।

Map<String, String> countryNames =
        new HashMap<>();

Add:

countryNames.put(
        "BD",
        "Bangladesh"
);

countryNames.put(
        "EE",
        "Estonia"
);

Conceptually:

BD → Bangladesh
EE → Estonia

Lookup:

String country =
        countryNames.get(
                "BD"
        );

Result:

Bangladesh

Importing Map and HashMap

import java.util.HashMap;
import java.util.Map;

Declaration:

Map<String, Course> courses =
        new HashMap<>();

Variable type:

Map<String, Course>

Concrete implementation:

HashMap<String, Course>

As with List and Set, variable type হিসেবে interface ব্যবহার করা common।


Map Is Not a Collection

Java-তে Map directly Collection interface extend করে না।

কারণ এটি single values-এর collection নয়।

এটি pairs store করে:

Key + Value

Map views provide করে:

keySet()
values()
entrySet()

এগুলোর মাধ্যমে keys, values, অথবা entries collection হিসেবে দেখা যায়।


Key and Value Types

Map<String, Course>

এখানে:

K = String
V = Course

Another example:

Map<Long, Learner>
K = Long
V = Learner

Another:

Map<String, Integer>
K = String
V = Integer

Both key and value type generic arguments।


Creating an Empty Mutable Map

Map<String, Course> coursesByCode =
        new HashMap<>();

Initially:

coursesByCode.isEmpty()

returns:

true

And:

coursesByCode.size()

returns:

0

Adding an Entry with put()

coursesByCode.put(
        "JAVA-OOP",
        javaCourse
);

Another:

coursesByCode.put(
        "BACKEND",
        backendCourse
);

Map now contains two entries।

JAVA-OOP → javaCourse
BACKEND  → backendCourse

Keys Must Be Unique

A map cannot contain duplicate keys।

coursesByCode.put(
        "JAVA-OOP",
        firstCourse
);

coursesByCode.put(
        "JAVA-OOP",
        secondCourse
);

Second put() does not create a second "JAVA-OOP" key।

It replaces the old value।

Result:

JAVA-OOP → secondCourse

put() Returns the Previous Value

Course previous =
        coursesByCode.put(
                "JAVA-OOP",
                newCourse
        );

If key did not exist:

previous = null

If key existed:

previous = old Course value

This return value replacement detect করতে useful।


Detecting Replacement

Course previous =
        coursesByCode.put(
                course.getCode(),
                course
        );

if (previous != null) {
    System.out.println(
            "Existing course was replaced."
    );
}

কিন্তু একটি subtle issue আছে:

Map value itself null হতে পারে।

If null values allowed, previous == null দিয়ে নিশ্চিতভাবে বলা যায় না key absent ছিল।

Better:

boolean existed =
        coursesByCode.containsKey(
                course.getCode()
        );

Course previous =
        coursesByCode.put(
                course.getCode(),
                course
        );

Or use:

putIfAbsent()

when replacement should not happen।


Retrieving a Value with get()

Course course =
        coursesByCode.get(
                "JAVA-OOP"
        );

If key exists:

Returns associated Course

If key does not exist:

Returns null

Missing Key

Course course =
        coursesByCode.get(
                "UNKNOWN"
        );

Result:

null

Then:

course.getTitle();

throws:

NullPointerException

Missing lookup result deliberately handle করতে হবে।


Handling a Missing Value

Course course =
        coursesByCode.get(
                courseCode
        );

if (course == null) {
    System.out.println(
            "Course was not found."
    );

    return;
}

System.out.println(
        course.getTitle()
);

Later Optional শেখার পরে missing result আরও explicitly model করা যাবে।


Checking a Key with containsKey()

boolean containsJava =
        coursesByCode.containsKey(
                "JAVA-OOP"
        );

Use when question is:

Is this key registered?

This is usually better than:

get(key) != null

when null values may exist।


Checking a Value with containsValue()

boolean containsCourse =
        coursesByCode.containsValue(
                javaCourse
        );

This checks whether an equal value exists।

It relies on value’s:

equals()

Unlike key lookup, containsValue() generally needs to inspect values।

Use it when value membership is genuinely required।

Do not use it as a replacement for proper key selection।


Removing an Entry by Key

Course removed =
        coursesByCode.remove(
                "JAVA-OOP"
        );

If key exists:

Returns removed Course

If absent:

Returns null

Map no longer contains the key।


Conditional Removal

boolean removed =
        coursesByCode.remove(
                "JAVA-OOP",
                expectedCourse
        );

This removes only if key currently maps to an equal value।

Useful when you want to avoid removing an entry that has been replaced।


Replacing a Value

Course previous =
        coursesByCode.replace(
                "JAVA-OOP",
                updatedCourse
        );

Difference from put():

  • put() adds or replaces
  • replace() only replaces if key exists

If key absent:

replace() returns null
Map remains unchanged

Conditional Replacement

boolean replaced =
        coursesByCode.replace(
                "JAVA-OOP",
                oldCourse,
                newCourse
        );

Replacement happens only if current mapping matches oldCourse


putIfAbsent()

When duplicate key should not replace existing value:

Course existing =
        coursesByCode.putIfAbsent(
                "JAVA-OOP",
                javaCourse
        );

If key absent:

  • New entry added
  • Returns null

If key already exists:

  • Existing value remains
  • Returns existing value

Using putIfAbsent() for Registration

public boolean registerCourse(
        Course course
) {
    if (course == null) {
        return false;
    }

    Course existing =
            coursesByCode.putIfAbsent(
                    course.getCode(),
                    course
            );

    return existing == null;
}

This prevents accidental replacement।

Again, this assumes null values are not stored।

For domain registries, rejecting null values is usually a strong rule।


getOrDefault()

String country =
        countryNames.getOrDefault(
                "FI",
                "Unknown"
        );

If "FI" exists, associated value returns।

Otherwise:

Unknown

Useful for simple fallback values।


Counting with getOrDefault()

Suppose course tags count করতে হবে।

Map<String, Integer> tagCounts =
        new HashMap<>();

For each tag:

int currentCount =
        tagCounts.getOrDefault(
                tag,
                0
        );

tagCounts.put(
        tag,
        currentCount + 1
);

Example input:

java
backend
java

Result:

java    → 2
backend → 1

Updating a Count with merge()

Modern Java provides:

tagCounts.merge(
        tag,
        1,
        Integer::sum
);

This uses a method reference, formally later শেখানো হবে।

Conceptually:

  • Key absent হলে value 1
  • Key present হলে old value + 1

For now, getOrDefault() version easier to understand।


Map Size

int courseCount =
        coursesByCode.size();

Size means number of key-value mappings।

Replacing an existing key does not increase size।

map.put(
        "JAVA",
        firstCourse
);

map.put(
        "JAVA",
        secondCourse
);

Size remains:

1

Checking Whether a Map Is Empty

if (
        coursesByCode.isEmpty()
) {
    System.out.println(
            "No courses registered."
    );
}

Clearing a Map

coursesByCode.clear();

Removes every mapping।

Domain object-এর internal map directly expose করলে caller rules bypass করে clear করতে পারে।

Controlled operations prefer করুন।


Iterating Over Keys

for (
        String courseCode
        : coursesByCode.keySet()
) {
    System.out.println(
            courseCode
    );
}

keySet() returns a set-like view of keys।

Keys are unique।

For HashMap, order guaranteed নয়।


Iterating Over Values

for (
        Course course
        : coursesByCode.values()
) {
    System.out.println(
            course.getTitle()
    );
}

values() returns a collection view।

Values do not have to be unique।

Different keys may map to equal or same values।


Iterating Over Entries

When both key and value are needed:

for (
        Map.Entry<String, Course> entry
        : coursesByCode.entrySet()
) {
    String courseCode =
            entry.getKey();

    Course course =
            entry.getValue();

    System.out.println(
            courseCode
            + " → "
            + course.getTitle()
    );
}

This is generally better than:

for (
        String key
        : map.keySet()
) {
    Course value =
            map.get(
                    key
            );
}

entrySet() already provides both।


What Is Map.Entry<K, V>?

A map entry represents one pair:

Key + Value

Type:

Map.Entry<String, Course>

Methods:

entry.getKey()
entry.getValue()

Some mutable map entry views may support:

entry.setValue(...)

Use cautiously, because it mutates the map।


forEach() on a Map

coursesByCode.forEach(
        (courseCode, course) ->
                System.out.println(
                        courseCode
                        + " → "
                        + course.getTitle()
                )
);

This uses a lambda।

For now, entrySet() loop বেশি explicit এবং beginner-friendly।


HashMap Does Not Guarantee Order

Map<String, String> countries =
        new HashMap<>();

countries.put(
        "BD",
        "Bangladesh"
);

countries.put(
        "EE",
        "Estonia"
);

countries.put(
        "FI",
        "Finland"
);

Iteration may appear in any order।

Do not depend on:

Insertion order
Alphabetical key order
Stable display order

LinkedHashMap

LinkedHashMap insertion order preserve করে।

import java.util.LinkedHashMap;
Map<String, String> countries =
        new LinkedHashMap<>();

Add:

BD
EE
FI

Iteration follows same insertion order।

Use when:

  • Key lookup প্রয়োজন
  • Unique keys প্রয়োজন
  • Insertion order also meaningful

TreeMap: Brief Introduction

TreeMap keys sorted order-এ রাখে।

import java.util.TreeMap;
Map<String, String> countries =
        new TreeMap<>();

Key iteration:

BD
EE
FI

sorted according to key’s natural ordering।

Keys need comparable ordering or a comparator।

Detailed sorting পরে শেখানো হবে।

High-level comparison:

HashMap       → No order guarantee
LinkedHashMap → Insertion order
TreeMap       → Sorted by key

Creating an Immutable Map with Map.of()

Map<String, String> countryNames =
        Map.of(
                "BD",
                "Bangladesh",
                "EE",
                "Estonia",
                "FI",
                "Finland"
        );

This map:

  • Cannot be modified
  • Rejects duplicate keys
  • Rejects null keys
  • Rejects null values
  • Does not guarantee iteration order

Modifying Map.of()

countryNames.put(
        "SE",
        "Sweden"
);

Throws:

UnsupportedOperationException

Also:

countryNames.remove(
        "BD"
);

fails।


Duplicate Keys in Map.of()

Map<String, String> values =
        Map.of(
                "JAVA",
                "Course 1",
                "JAVA",
                "Course 2"
        );

Throws:

IllegalArgumentException

Null in Map.of()

Invalid:

Map.of(
        "JAVA",
        null
);

Throws:

NullPointerException

Same for null key।


Map.ofEntries()

For larger immutable maps:

Map<String, String> countries =
        Map.ofEntries(
                Map.entry(
                        "BD",
                        "Bangladesh"
                ),
                Map.entry(
                        "EE",
                        "Estonia"
                ),
                Map.entry(
                        "FI",
                        "Finland"
                )
        );

This can be more readable when many pairs exist।


Creating an Immutable Copy

Map<String, Course> snapshot =
        Map.copyOf(
                coursesByCode
        );

The returned map cannot be structurally modified।

It rejects null keys or values in the source।


Immutable Map Does Not Make Values Immutable

Map<String, Course> snapshot =
        Map.copyOf(
                coursesByCode
        );

Caller cannot:

snapshot.put(...)
snapshot.remove(...)

But if Course mutable:

snapshot.get(
        "JAVA-OOP"
).changeTitle(
        "Updated Course"
);

may still mutate the shared course object।

Map immutability protects mappings, not deep object state।


Map Copies Are Shallow

Original map and copied map may refer to same value objects।

Original map ─┐
              ├── Course object
Copied map ───┘

If value object mutates, both maps observe it।


HashMap and Null

HashMap permits:

  • One null key
  • Multiple null values

Example:

Map<String, String> values =
        new HashMap<>();

values.put(
        null,
        "Unknown key"
);

values.put(
        "A",
        null
);

values.put(
        "B",
        null
);

Technically valid।

But null keys/values often create ambiguity।


Ambiguity of get()

String value =
        map.get(
                "A"
);

If result null, two possibilities:

Key does not exist
Key exists and maps to null

Use:

containsKey()

to distinguish।

Better domain rule:

Avoid null map values unless null has a deliberate meaning.


Empty Map Instead of Null

Weak:

public Map<String, Course> findCourses() {
    return null;
}

Better:

public Map<String, Course> findCourses() {
    return Map.of();
}

Caller safely:

for (
        Course course
        : courses.values()
) {
}

No null check required।


Key Equality

HashMap key lookup uses:

hashCode()
equals()

Same principle as HashSet

When inserting:

map.put(
        key,
        value
);

Map uses key hash and equality to determine:

  • New key
  • Existing equal key
  • Replacement target

Custom Key Example

Suppose course code is a value object।

public final class CourseCode {

    private final String value;
}

If logically equal codes should represent same key, implement:

equals()
hashCode()

correctly।


CourseCode.java

import java.util.Objects;

public final class CourseCode {

    private final String value;

    public CourseCode(
            String value
    ) {
        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        this.value =
                value.strip()
                        .toUpperCase();
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        if (this == other) {
            return true;
        }

        if (
                !(other
                instanceof CourseCode courseCode)
        ) {
            return false;
        }

        return value.equals(
                courseCode.value
        );
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                value
        );
    }

    @Override
    public String toString() {
        return value;
    }
}

Equal Keys Replace the Same Mapping

Map<CourseCode, String> courses =
        new HashMap<>();

CourseCode firstCode =
        new CourseCode(
                "java-oop"
        );

CourseCode secondCode =
        new CourseCode(
                "JAVA-OOP"
        );

courses.put(
        firstCode,
        "First Course"
);

courses.put(
        secondCode,
        "Updated Course"
);

Because keys are equal:

Map size = 1

Lookup:

courses.get(
        new CourseCode(
                "Java-Oop"
        )
);

returns:

Updated Course

Mutable Keys Are Dangerous

Suppose key equality depends on mutable field।

public final class MutableCourseCode {

    private String value;

    public void changeValue(
            String value
    ) {
        this.value = value;
    }

    @Override
    public boolean equals(
            Object other
    ) {
        // Uses value
    }

    @Override
    public int hashCode() {
        return Objects.hash(
                value
        );
    }
}

Insert:

MutableCourseCode code =
        new MutableCourseCode(
                "JAVA"
        );

map.put(
        code,
        course
);

Then mutate:

code.changeValue(
        "SPRING"
);

Now:

map.get(
        code
);

may fail।

The key is stored using old hash location।


Prefer Immutable Map Keys

Strong key types:

CourseCode
LearnerId
EnrollmentKey
CountryCode
LessonId

should generally be immutable।

Fields used by:

equals()
hashCode()

must remain stable while key belongs to a hash-based map।


Values Do Not Need to Be Unique

Map<String, Course> featuredCourses =
        new HashMap<>();

Different keys may map to same object:

featuredCourses.put(
        "JAVA",
        javaCourse
);

featuredCourses.put(
        "FEATURED",
        javaCourse
);

Valid।

Map uniqueness applies to keys, not values।


Choosing the Right Key

Weak key:

Map<String, Course> coursesByTitle

Course titles may:

  • Change
  • Duplicate
  • Differ by case
  • Contain spacing variations

Stronger key:

Map<CourseCode, Course> coursesByCode

if course code is:

  • Unique
  • Stable
  • Normalized
  • Value-based

Key selection should reflect reliable identity or lookup requirement।


List Search vs Map Lookup

List-based lookup:

public Course findByCode(
        List<Course> courses,
        CourseCode courseCode
) {
    for (
            Course course
            : courses
    ) {
        if (
                course.getCode()
                        .equals(
                                courseCode
                        )
        ) {
            return course;
        }
    }

    return null;
}

Every lookup scans elements until match found।

Map-based lookup:

return coursesByCode.get(
        courseCode
);

More direct এবং communicates key-based access intent।


Do Not Maintain Redundant Collections Carelessly

Suppose class stores:

List<Course> courses;
Map<CourseCode, Course> coursesByCode;

Both represent same data।

Then every add/remove/update must keep both synchronized।

Potential bugs:

Added to list, not map
Removed from map, not list
Different Course references
Order mismatch

Only keep both if requirements need both sequence and lookup, and class strictly controls updates।

Otherwise choose one primary structure।


Preserving Order and Lookup

If registration order and key lookup both matter:

Map<CourseCode, Course> coursesByCode =
        new LinkedHashMap<>();

This gives:

  • Unique key lookup
  • Insertion-order iteration

No separate list may be needed।


Complete Example: Course Catalog

Course.java

public final class Course {

    private final CourseCode code;
    private final String title;

    public Course(
            CourseCode code,
            String title
    ) {
        if (code == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

        if (
                title == null
                || title.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course title is required."
            );
        }

        this.code = code;
        this.title = title.strip();
    }

    public CourseCode getCode() {
        return code;
    }

    public String getTitle() {
        return title;
    }

    @Override
    public String toString() {
        return code
                + " — "
                + title;
    }
}

CourseCatalog.java

import java.util.LinkedHashMap;
import java.util.Map;

public final class CourseCatalog {

    private final Map<CourseCode, Course> coursesByCode;

    public CourseCatalog() {
        this.coursesByCode =
                new LinkedHashMap<>();
    }

    public boolean register(
            Course course
    ) {
        if (course == null) {
            return false;
        }

        Course existing =
                coursesByCode.putIfAbsent(
                        course.getCode(),
                        course
                );

        return existing == null;
    }

    public Course findByCode(
            CourseCode courseCode
    ) {
        if (courseCode == null) {
            return null;
        }

        return coursesByCode.get(
                courseCode
        );
    }

    public boolean contains(
            CourseCode courseCode
    ) {
        if (courseCode == null) {
            return false;
        }

        return coursesByCode.containsKey(
                courseCode
        );
    }

    public boolean remove(
            CourseCode courseCode
    ) {
        if (courseCode == null) {
            return false;
        }

        return coursesByCode.remove(
                courseCode
        ) != null;
    }

    public int getCourseCount() {
        return coursesByCode.size();
    }

    public void printCourses() {
        for (
                Map.Entry<CourseCode, Course> entry
                : coursesByCode.entrySet()
        ) {
            System.out.println(
                    entry.getKey()
                    + " → "
                    + entry.getValue()
                            .getTitle()
            );
        }
    }

    public Map<CourseCode, Course> getCoursesByCode() {
        return Map.copyOf(
                coursesByCode
        );
    }
}

Main.java

public class Main {

    public static void main(
            String[] args
    ) {
        CourseCatalog catalog =
                new CourseCatalog();

        Course javaCourse =
                new Course(
                        new CourseCode(
                                "java-oop"
                        ),
                        "Java and OOP Foundation"
                );

        Course backendCourse =
                new Course(
                        new CourseCode(
                                "backend"
                        ),
                        "Backend Development"
                );

        Course duplicateJavaCourse =
                new Course(
                        new CourseCode(
                                "JAVA-OOP"
                        ),
                        "Another Java Course"
                );

        System.out.println(
                "Java registered: "
                + catalog.register(
                        javaCourse
                )
        );

        System.out.println(
                "Backend registered: "
                + catalog.register(
                        backendCourse
                )
        );

        System.out.println(
                "Duplicate registered: "
                + catalog.register(
                        duplicateJavaCourse
                )
        );

        System.out.println(
                "Course count: "
                + catalog.getCourseCount()
        );

        Course found =
                catalog.findByCode(
                        new CourseCode(
                                "Java-Oop"
                        )
                );

        if (found != null) {
            System.out.println(
                    "Found: "
                    + found.getTitle()
            );
        }

        System.out.println();
        System.out.println(
                "Catalog:"
        );

        catalog.printCourses();

        boolean removed =
                catalog.remove(
                        new CourseCode(
                                "BACKEND"
                        )
                );

        System.out.println();
        System.out.println(
                "Backend removed: "
                + removed
        );

        System.out.println(
                "Course count: "
                + catalog.getCourseCount()
        );
    }
}

Possible output:

Java registered: true
Backend registered: true
Duplicate registered: false
Course count: 2
Found: Java and OOP Foundation

Catalog:
JAVA-OOP → Java and OOP Foundation
BACKEND → Backend Development

Backend removed: true
Course count: 1

Why Use LinkedHashMap?

Catalog registration order print করতে চায়।

new LinkedHashMap<>()

provides:

  • Key uniqueness
  • Direct lookup
  • Insertion-order iteration

If order irrelevant:

HashMap

would be sufficient।


Why Use putIfAbsent()?

Duplicate course code should not replace existing course।

Weak:

coursesByCode.put(
        course.getCode(),
        course
);

would silently replace।

Strong:

putIfAbsent()

preserves first registered course and returns existing value।


Why Is CourseCode a Key Object?

Instead of plain string, it centralizes:

  • Required validation
  • Case normalization
  • Whitespace normalization
  • Equality
  • Hash code
  • Type meaning

This prevents code variation across callers।


Why Does the Getter Return Map.copyOf()?

Weak:

return coursesByCode;

Caller could:

catalog.getCoursesByCode()
        .clear();

and bypass catalog registration rules।

Map.copyOf() protects map structure।


Grouping Values in a Map

Sometimes one key maps to multiple values।

Example:

Learner ID → Enrolled course IDs

Type:

Map<Long, Set<Long>>

Meaning:

Each learner has a unique set of course IDs

Adding to a Nested Collection

Map<Long, Set<Long>> courseIdsByLearner =
        new HashMap<>();

Manual version:

Set<Long> courseIds =
        courseIdsByLearner.get(
                learnerId
        );

if (courseIds == null) {
    courseIds =
            new HashSet<>();

    courseIdsByLearner.put(
            learnerId,
            courseIds
    );
}

courseIds.add(
        courseId
);

computeIfAbsent()

Java provides a shorter pattern:

courseIdsByLearner
        .computeIfAbsent(
                learnerId,
                ignored ->
                        new HashSet<>()
        )
        .add(
                courseId
        );

This uses a lambda expression।

Conceptually:

If learner key is absent:
    create and store a new HashSet

Then:
    return the set
    add course ID

Lambdas later formally শেখানো হবে।

Manual version বুঝে রাখা important।


Nested Collections Increase Complexity

Map<Long, Set<Long>>

useful, but responsibilities include:

  • Creating inner set
  • Preventing null
  • Protecting internal sets
  • Removing empty keys
  • Defensive copying
  • Synchronizing updates

Do not use nested collections casually।

Wrap them in a domain class when behavior and invariants matter।


Complete Example: Learner Enrollment Index

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public final class LearnerEnrollmentIndex {

    private final Map<Long, Set<Long>> courseIdsByLearner;

    public LearnerEnrollmentIndex() {
        this.courseIdsByLearner =
                new HashMap<>();
    }

    public boolean enroll(
            long learnerId,
            long courseId
    ) {
        validateId(
                learnerId,
                "Learner ID"
        );

        validateId(
                courseId,
                "Course ID"
        );

        Set<Long> courseIds =
                courseIdsByLearner.get(
                        learnerId
                );

        if (courseIds == null) {
            courseIds =
                    new HashSet<>();

            courseIdsByLearner.put(
                    learnerId,
                    courseIds
            );
        }

        return courseIds.add(
                courseId
        );
    }

    public boolean isEnrolled(
            long learnerId,
            long courseId
    ) {
        Set<Long> courseIds =
                courseIdsByLearner.get(
                        learnerId
                );

        return courseIds != null
                && courseIds.contains(
                        courseId
                );
    }

    public boolean cancel(
            long learnerId,
            long courseId
    ) {
        Set<Long> courseIds =
                courseIdsByLearner.get(
                        learnerId
                );

        if (courseIds == null) {
            return false;
        }

        boolean removed =
                courseIds.remove(
                        courseId
                );

        if (courseIds.isEmpty()) {
            courseIdsByLearner.remove(
                    learnerId
            );
        }

        return removed;
    }

    public Set<Long> findCourseIds(
            long learnerId
    ) {
        Set<Long> courseIds =
                courseIdsByLearner.get(
                        learnerId
                );

        if (courseIds == null) {
            return Set.of();
        }

        return Set.copyOf(
                courseIds
        );
    }

    public int getLearnerCount() {
        return courseIdsByLearner.size();
    }

    private static void validateId(
            long id,
            String fieldName
    ) {
        if (id <= 0) {
            throw new IllegalArgumentException(
                    fieldName
                    + " must be positive."
            );
        }
    }
}

Why Remove the Empty Inner Set?

After last enrollment cancellation:

courseIds.isEmpty()

If learner key remains mapped to an empty set, map contains a learner with no enrollments।

That may be valid in some domains।

Current design chooses:

No enrollments → No learner entry

The class enforces that representation consistently।


Defensive Copying Nested Collections

Returning:

courseIds

directly would expose internal mutable set।

Instead:

Set.copyOf(
        courseIds
)

Empty result:

Set.of()

This avoids null and protects state।


Choosing Map

Use Map when:

  • Values have unique lookup keys
  • Direct key access is primary
  • Repeated scanning should be avoided
  • Relationship is naturally key → value
  • Each key maps to one current value

Examples:

CourseCode → Course
LearnerId → Learner
LessonId → Lesson
Locale → Translation
ConfigurationKey → Value

When Map May Not Be Best

Use List when:

Sequence and position matter
Duplicates are meaningful

Use Set when:

Only unique membership matters
No associated value required

Do not use a map just to simulate an indexed list:

Map<Integer, Lesson>

unless integer keys have actual domain meaning।

A List<Lesson> is usually clearer for ordered lesson positions।


List, Set, and Map

RequirementLikely Choice
Ordered lesson sequenceList<Lesson>
Unique user rolesSet<Role>
Course lookup by codeMap<CourseCode, Course>
Activity history with repeatsList<Activity>
Completed lesson IDsSet<Long>
Learner ID to LearnerMap<Long, Learner>
Unique tags in insertion orderLinkedHashSet<String>
Courses by code in registration orderLinkedHashMap<CourseCode, Course>

Performance Mental Model

HashMap is designed for fast average:

put
get
containsKey
remove

But actual performance depends on:

  • Good key hashCode()
  • Equality cost
  • Hash collisions
  • Map size
  • Runtime implementation
  • Key mutability

Choose Map first because key-based lookup matches the domain।

Do not select it only from memorized complexity।


Common Mistakes

Using a Mutable Key

Changing equality-relevant key state can make entries unreachable।


Overriding equals() Without hashCode()

Equal keys may behave as separate map keys।


Assuming put() Rejects Duplicate Keys

It replaces the existing value।

Use putIfAbsent() when duplicates must be rejected।


Assuming get() Returning Null Means Key Is Absent

The key may map to null।

Use containsKey() or reject null values।


Depending on HashMap Order

Iteration order is not guaranteed।


Using containsValue() for Frequent Lookup

If value-based lookup is common, data may need another key structure।


Returning an Internal Mutable Map

Caller can bypass registration rules।


Assuming Immutable Map Makes Values Immutable

Only map structure is protected।


Storing Null Keys and Values Without Clear Meaning

Creates lookup ambiguity and null-related failures।


Choosing an Unstable Key

Mutable titles or display names are often weak identifiers।


Keeping a List and Map Unsynchronized

Redundant indexes require controlled updates।


Using Map When Order Is the Main Requirement

Use a list or ordered map deliberately।


Allowing Nested Collections to Leak

Return immutable copies of inner collections।


Practice Exercises

Exercise 1: Country Lookup

Create:

Map<String, String> countryNames

Add:

BD → Bangladesh
EE → Estonia
FI → Finland

Then:

  • Lookup EE
  • Check whether SE exists
  • Return "Unknown" for missing key
  • Remove FI

Exercise 2: Course Registry

Create:

Map<CourseCode, Course>

Requirements:

  • Duplicate course code must not replace existing course
  • Use putIfAbsent()
  • Return false on duplicate
  • Return immutable map snapshot

Exercise 3: Count Words

Given:

java backend java spring backend java

Build:

Map<String, Integer>

Expected:

java    → 3
backend → 2
spring  → 1

Use getOrDefault()


Exercise 4: Iterate Entries

Print:

<course code>: <course title>

Use:

entrySet()

Do not call get() inside a key loop।


Exercise 5: Choose the Implementation

Choose HashMap, LinkedHashMap, or TreeMap:

  1. Fast course lookup, order irrelevant
  2. Course lookup while preserving registration order
  3. Country lookup printed by sorted country code
  4. Temporary cache with no display order requirement

Explain each choice।


Exercise 6: Mutable Key Problem

Create a mutable key with:

code
equals()
hashCode()

Add it to HashMap, mutate the code, and test:

get()
containsKey()
remove()

Explain the failure।


Exercise 7: Learner Enrollment Lookup

Create:

Map<Long, Set<Long>>

Requirements:

  • One learner can enroll in many courses
  • Same course ID cannot be duplicated for one learner
  • Missing learner returns empty set
  • Internal set must not be exposed
  • Empty learner entry should be removed after final cancellation

Predict the Result

Question 1

Map<String, Integer> values =
        new HashMap<>();

System.out.println(
        values.put(
                "JAVA",
                1
        )
);

System.out.println(
        values.put(
                "JAVA",
                2
        )
);

System.out.println(
        values.size()
);

System.out.println(
        values.get(
                "JAVA"
        )
);

Question 2

Map<String, String> values =
        new HashMap<>();

System.out.println(
        values.get(
                "UNKNOWN"
        )
);

Question 3

Map<String, String> values =
        Map.of(
                "BD",
                "Bangladesh"
        );

values.put(
        "EE",
        "Estonia"
);

What happens?


Question 4

Map<String, String> values =
        Map.of(
                "A",
                "First",
                "A",
                "Second"
        );

What happens?


Question 5

Map<CourseCode, String> values =
        new HashMap<>();

values.put(
        new CourseCode(
                "java"
        ),
        "Course"
);

System.out.println(
        values.get(
                new CourseCode(
                        "JAVA"
                )
        )
);

Assume CourseCode.equals() and hashCode() are implemented correctly।


Question 6

Map<String, String> values =
        new HashMap<>();

values.put(
        "A",
        null
);

System.out.println(
        values.get(
                "A"
        )
);

System.out.println(
        values.containsKey(
                "A"
        )
);

Predict the Result Answers

Answer 1

null
1
1
2

Explanation:

  • First put() had no previous value
  • Second put() returns previous value 1
  • Same key was replaced, so size remains 1
  • Current value is 2

Answer 2

null

Missing key returns null।

Answer 3

Runtime-এ:

UnsupportedOperationException

Map.of() immutable।

Answer 4

Runtime-এ:

IllegalArgumentException

Duplicate keys are not allowed।

Answer 5

Course

Normalized equal key successfully finds the mapping।

Answer 6

null
true

The key exists and maps to null।


Knowledge Check

Question 1

Map কী store করে?

Question 2

K এবং V কী represent করে?

Question 3

Can a map contain duplicate keys?

Question 4

Can different keys map to the same value?

Question 5

What does put() return?

Question 6

What does get() return for a missing key?

Question 7

Why can get() == null be ambiguous?

Question 8

What does putIfAbsent() do?

Question 9

When should entrySet() be used?

Question 10

Does HashMap preserve insertion order?

Question 11

Which implementation preserves insertion order?

Question 12

Which implementation sorts by key?

Question 13

Does Map.of() allow null?

Question 14

Why should map keys usually be immutable?

Question 15

What methods control hash-based key equality?

Question 16

Is Map.copyOf() a deep copy?

Question 17

Why is an empty map often better than null?

Question 18

When is Map a better choice than List?


Knowledge Check Answers

Answer 1

Unique key এবং associated value pairs।

Answer 2

K হলো key type এবং V হলো value type।

Answer 3

না।

Equal key put করলে existing value replace হয়।

Answer 4

হ্যাঁ।

Value uniqueness required নয়।

Answer 5

Previous associated value, অথবা key absent থাকলে null

Answer 6

null

Answer 7

Key absent হতে পারে অথবা key present থেকে null value store করতে পারে।

Answer 8

Key absent হলে mapping add করে; present হলে existing value রাখে।

Answer 9

When both key and value are needed during iteration।

Answer 10

না।

Answer 11

LinkedHashMap

Answer 12

TreeMap

Answer 13

না।

Null key এবং value reject করে।

Answer 14

Equality-relevant mutation entryকে hash lookup-এর জন্য unreachable করতে পারে।

Answer 15

hashCode() এবং equals()

Answer 16

না।

Mappings copy হয়, কিন্তু key/value object references generally shared থাকে।

Answer 17

Caller null check ছাড়া safely iterate এবং lookup করতে পারে।

Answer 18

When direct lookup by a unique, stable key is the primary requirement।


Lesson Summary

এই lesson-এ আমরা শিখেছি:

  • Map<K, V> key-value relationships store করে
  • Map directly Collection নয়
  • Keys unique
  • Values duplicate হতে পারে
  • HashMap common mutable implementation
  • put() adds or replaces a mapping
  • put() previous value return করে
  • Replacing an existing key map size বাড়ায় না
  • get() key দিয়ে value retrieve করে
  • Missing key null return করে
  • containsKey() key existence explicitly check করে
  • containsValue() equal value membership check করে
  • remove() key mapping delete করে
  • replace() only existing key update করে
  • putIfAbsent() duplicate replacement prevent করতে সাহায্য করে
  • getOrDefault() missing lookup fallback দেয়
  • keySet() keys expose করে
  • values() values expose করে
  • entrySet() key এবং value একসঙ্গে iterate করতে দেয়
  • Map.Entry<K, V> one mapping represent করে
  • HashMap order guarantee করে না
  • LinkedHashMap insertion order preserve করে
  • TreeMap keys sorted রাখে
  • Map.of() immutable map তৈরি করে
  • Map.of() duplicate keys এবং null reject করে
  • Map.copyOf() immutable structural copy তৈরি করে
  • Immutable map mutable values freeze করে না
  • Hash-based key lookup hashCode() এবং equals() use করে
  • Equal custom keys compatible hash code require করে
  • Mutable equality-relevant keys dangerous
  • Stable immutable value objects strong map keys
  • Key selection domain identity এবং lookup requirement reflect করা উচিত
  • List search direct map lookup-এ refactor করা যায়
  • Redundant list/map indexes carefully synchronize করতে হয়
  • Nested maps এবং collections encapsulation require করে
  • Empty maps null-এর চেয়ে safer
  • List sequence model করে
  • Set unique membership model করে
  • Map unique-key lookup model করে

Next Lesson

পরবর্তী lesson:

Iteration and Collection Operations

আমরা শিখব:

  • Enhanced for
  • Index-based loops
  • Iterator
  • Safe removal during iteration
  • Searching
  • Filtering manually
  • Transforming values
  • Aggregation
  • Counting and grouping
  • Nested iteration
  • Early return and break
  • Avoiding accidental quadratic work
  • Choosing readable collection operations