Generics, Collections, and Core Data Structures
Working with `Map`
আপনি একটি 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 replacesreplace()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
| Requirement | Likely Choice |
|---|---|
| Ordered lesson sequence | List<Lesson> |
| Unique user roles | Set<Role> |
| Course lookup by code | Map<CourseCode, Course> |
| Activity history with repeats | List<Activity> |
| Completed lesson IDs | Set<Long> |
| Learner ID to Learner | Map<Long, Learner> |
| Unique tags in insertion order | LinkedHashSet<String> |
| Courses by code in registration order | LinkedHashMap<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
SEexists - 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
falseon 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:
- Fast course lookup, order irrelevant
- Course lookup while preserving registration order
- Country lookup printed by sorted country code
- 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 value1 - 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 করেMapdirectlyCollectionনয়- Keys unique
- Values duplicate হতে পারে
HashMapcommon mutable implementationput()adds or replaces a mappingput()previous value return করে- Replacing an existing key map size বাড়ায় না
get()key দিয়ে value retrieve করে- Missing key
nullreturn করে 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 করেHashMaporder guarantee করে নাLinkedHashMapinsertion order preserve করেTreeMapkeys 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
Listsequence model করেSetunique membership model করেMapunique-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