Professional Java Practices

Records and Modern Data Models

ReadingPreview

You are viewing a free preview lesson.

Lesson Overview

Java-তে অনেক class আছে যেগুলোর primary কাজ শুধু data hold করা।

Example:

Course summary
Lesson response
Coordinates
Money snapshot
Search result
Configuration
Command input

Traditional class লিখলে অনেক boilerplate লাগে:

private final fields
constructor
getters
equals()
hashCode()
toString()

Modern Java এই ধরনের data-focused types-এর জন্য provide করে:

record

Example:

public record CourseSummary(
        String code,
        String title
) {
}

এই ছোট declaration থেকেই Java automatically generate করে:

  • Private final fields
  • Constructor
  • Accessor methods
  • equals()
  • hashCode()
  • toString()

Records powerful, but তারা every class-এর replacement নয়।

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

  • record কী
  • Record components
  • Generated constructor
  • Accessor methods
  • equals(), hashCode(), toString()
  • Compact constructor
  • Validation
  • Normalization
  • Additional methods
  • Static members
  • Records with collections
  • Defensive copying
  • Record vs normal class
  • Record vs entity
  • Records as DTOs and value objects
  • Common record mistakes

Learning Objectives

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

  • Java record explain করতে
  • Simple record declare করতে
  • Generated methods identify করতে
  • Compact constructor ব্যবহার করতে
  • Record input validate এবং normalize করতে
  • Collection components defensively copy করতে
  • Record-এর value equality explain করতে
  • Record এবং normal class-এর মধ্যে choose করতে
  • Record-কে mutable entity replacement হিসেবে ভুলভাবে ব্যবহার করা avoid করতে

What Is a Record?

A record হলো special Java type designed primarily for:

Immutable data representation

Example:

public record CourseSummary(
        String code,
        String title
) {
}

The values inside parentheses are called:

record components

Here:

code
title

are components।


Traditional Class Equivalent

Without a record, you might write:

public final class CourseSummary {

    private final String code;
    private final String title;

    public CourseSummary(
            String code,
            String title
    ) {
        this.code = code;
        this.title = title;
    }

    public String code() {
        return code;
    }

    public String title() {
        return title;
    }

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

    @Override
    public int hashCode() {
        // implementation
    }

    @Override
    public String toString() {
        // implementation
    }
}

A record removes most of this boilerplate।


Basic Record

public record CourseSummary(
        String code,
        String title
) {
}

Usage:

CourseSummary summary =
        new CourseSummary(
                "JAVA-OOP",
                "Java and OOP Foundation"
        );

Generated Accessors

Record accessor names are the same as component names।

Use:

summary.code();
summary.title();

Not:

summary.getCode();
summary.getTitle();

unless you explicitly add such methods yourself।


Record Components Become Final State

A record is designed so its component fields cannot be reassigned after construction।

Conceptually:

private final String code;
private final String title;

There are no generated setters।


Generated Constructor

For:

public record CourseSummary(
        String code,
        String title
) {
}

Java provides a canonical constructor conceptually similar to:

public CourseSummary(
        String code,
        String title
) {
    this.code = code;
    this.title = title;
}

Generated equals()

Records automatically provide value-based equality across their components।

Example:

CourseSummary first =
        new CourseSummary(
                "JAVA",
                "Java"
        );

CourseSummary second =
        new CourseSummary(
                "JAVA",
                "Java"
        );

System.out.println(
        first.equals(
                second
        )
);

Output:

true

Generated hashCode()

Because records generate hashCode() consistently with equals(), they work naturally with:

HashSet
HashMap

Example:

Set<CourseSummary> summaries =
        new HashSet<>();

summaries.add(
        new CourseSummary(
                "JAVA",
                "Java"
        )
);

summaries.add(
        new CourseSummary(
                "JAVA",
                "Java"
        )
);

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

Output:

1

Generated toString()

Example:

System.out.println(
        summary
);

Possible output:

CourseSummary[code=JAVA-OOP, title=Java and OOP Foundation]

This is much more useful than default Object.toString()


Records Are Value-Oriented

A record's equality is based on all record components।

For:

public record Money(
        long amountInCents,
        String currency
) {
}

These are equal:

new Money(
        1000,
        "EUR"
)

and:

new Money(
        1000,
        "EUR"
)

But:

new Money(
        1000,
        "USD"
)

is different।


Validation in a Record

A basic record accepts whatever values are passed।

Example:

new CourseSummary(
        null,
        ""
);

This compiles unless you add validation।

Use a:

compact constructor

Compact Constructor

public record CourseSummary(
        String code,
        String title
) {

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

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

Notice:

public CourseSummary {

There is no parameter list।

The record components are automatically available as constructor parameters।


Why Is It Called Compact Constructor?

The full canonical constructor would look conceptually like:

public CourseSummary(
        String code,
        String title
) {
    this.code = code;
    this.title = title;
}

Compact constructor lets you focus on:

Validation
Normalization

while Java still assigns the record components automatically after the constructor body।


Normalize Values

You can reassign constructor parameters inside the compact constructor before Java stores them।

Example:

public record CourseCode(
        String value
) {

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

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

Usage:

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

System.out.println(
        code.value()
);

Output:

JAVA-OOP

Validate Normalized Value

Better:

public record CourseCode(
        String value
) {

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

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

        if (
                !value.matches(
                        "[A-Z0-9-]+"
                )
        ) {
            throw new IllegalArgumentException(
                    "Course code contains unsupported characters."
            );
        }
    }
}

Now every CourseCode is valid and normalized।


Record Accessors Are Methods

For:

public record CourseCode(
        String value
) {
}

access:

code.value();

not:

code.value

because the field itself is private।


You Can Add Methods

Records are not limited to storing data।

Example:

public record Money(
        long amountInCents,
        String currency
) {

    public boolean isFree() {
        return amountInCents
                == 0;
    }
}

Usage:

Money price =
        new Money(
                0,
                "EUR"
        );

System.out.println(
        price.isFree()
);

Records Can Have Static Members

public record Money(
        long amountInCents,
        String currency
) {

    public static final String DEFAULT_CURRENCY =
            "EUR";

    public static Money free() {
        return new Money(
                0,
                DEFAULT_CURRENCY
        );
    }
}

Records Cannot Have Additional Instance Fields

You cannot add arbitrary extra mutable instance state outside the record components।

This is intentionally restrictive।

The record header should describe the complete state representation।


Why This Restriction Is Useful

A record says:

These components define this value.

Generated:

equals
hashCode
toString

all use those components।

Hidden extra instance state would make that model confusing।


Records Are Implicitly Final

You cannot extend a record।

This:

class PremiumSummary
        extends CourseSummary {
}

is not allowed।

Records are effectively final।

This simplifies equality semantics।


Records Can Implement Interfaces

Example:

public interface Identified {

    String id();
}

Record:

public record LessonSummary(
        String id,
        String title
) implements Identified {

}

This is valid।


Records Cannot Extend Ordinary Classes

A record cannot extend your own base class।

It already has its special inheritance relationship defined by Java।

If inheritance is central to the design, a record may not be the right type।


Records with Collections

Consider:

public record CoursePlan(
        String courseCode,
        List<String> lessons
) {
}

Looks immutable.

But is it truly safe?

Not necessarily।


Mutable Collection Leak

List<String> lessons =
        new ArrayList<>();

lessons.add(
        "Variables"
);

CoursePlan plan =
        new CoursePlan(
                "JAVA",
                lessons
        );

lessons.add(
        "Loops"
);

If record stores the same mutable list reference, plan.lessons() now also reflects "Loops"

Record components are final references, but referenced objects can still mutate।


Defensive Copy in Record Constructor

public record CoursePlan(
        String courseCode,
        List<String> lessons
) {

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

        if (lessons == null) {
            throw new IllegalArgumentException(
                    "Lessons are required."
            );
        }

        courseCode =
                courseCode.strip()
                        .toUpperCase();

        lessons =
                List.copyOf(
                        lessons
                );
    }
}

Now the record owns an unmodifiable snapshot of the list structure।


Record Does Not Automatically Deep-Copy Components

If the list contains mutable objects:

List<MutableLesson>

then:

List.copyOf(...)

protects the list structure only।

The elements can still mutate।

Records do not automatically provide deep immutability।


Record with Immutable Elements

Better:

public record LessonSummary(
        long id,
        String title
) {
}

Then:

public record CoursePlan(
        CourseCode courseCode,
        List<LessonSummary> lessons
) {

    public CoursePlan {
        lessons =
                List.copyOf(
                        lessons
                );
    }
}

Now the object graph is much easier to reason about because nested elements are immutable too।


Records and Arrays

Array components remain mutable।

Weak:

public record BinaryContent(
        byte[] data
) {
}

Caller can mutate:

content.data()[0] =
        99;

This changes the array stored by the record।


Defensive Array Record

You can copy on input:

public record BinaryContent(
        byte[] data
) {

    public BinaryContent {
        if (data == null) {
            throw new IllegalArgumentException(
                    "Data is required."
            );
        }

        data =
                data.clone();
    }
}

But there is another problem।

Generated accessor:

data()

returns the internal array directly।


Override an Accessor

You can override a record accessor:

@Override
public byte[] data() {
    return data.clone();
}

Complete:

public record BinaryContent(
        byte[] data
) {

    public BinaryContent {
        if (data == null) {
            throw new IllegalArgumentException(
                    "Data is required."
            );
        }

        data =
                data.clone();
    }

    @Override
    public byte[] data() {
        return data.clone();
    }
}

Now both input and output boundaries are protected।


But Arrays Affect Generated Equality

There is an important issue।

A record containing:

byte[]

uses the component's normal equals() semantics।

Arrays use reference equality for equals()

So:

new BinaryContent(
        new byte[] {1, 2}
)

and another record with a separate equal-content array may not compare as logically equal by content unless you override equality yourself।

This is one reason mutable arrays are often awkward record components।


Prefer Better Value Types Where Possible

Instead of storing mutable arrays directly in a value record, consider whether your design can use:

String
List<Byte>
Immutable wrapper
Domain-specific immutable type

depending on the problem।

Do not force a record when its component semantics do not match value equality naturally।


Records and equals()

Generated record equality includes every component।

Example:

public record CourseSummary(
        String code,
        String title,
        CourseStatus status
) {
}

If status changes conceptually, you create a different record:

new CourseSummary(
        "JAVA",
        "Java",
        CourseStatus.DRAFT
)

is not equal to:

new CourseSummary(
        "JAVA",
        "Java",
        CourseStatus.PUBLISHED
)

This is appropriate for a snapshot representation।


Records Are Excellent for Snapshots

Imagine:

Course

is a mutable domain entity।

You want to expose a read-only summary:

public record CourseSummary(
        String code,
        String title,
        CourseStatus status
) {
}

The summary describes:

Course state at a point in time

If the course later changes, create another summary।


Entity vs Record

Suppose a Course has lifecycle:

DRAFT
→ REVIEW
→ PUBLISHED
→ ARCHIVED

and behavior:

course.addLesson(...)
course.publish()
course.archive()

This is usually better modeled as a normal class।

Why?

Because the object owns:

Identity
Behavior
Controlled state transitions
Invariants

A record is better when the primary purpose is:

Represent a value or snapshot

Do Not Replace Every Domain Class with a Record

Weak thinking:

Records are modern
Therefore every class should be a record

No।

Choose based on semantics।


Good Record Candidates

Common good candidates:

CourseCode
Money
DateRange
Coordinates
CourseSummary
SearchResult
ValidationError
FieldError
Configuration
Command input
Query result

Normal Class Candidates

Common normal-class candidates:

Course
Enrollment
ShoppingCart
BankAccount
Order
Subscription

when they own lifecycle and meaningful mutation।


Record as a Value Object

Our earlier CourseCode class can be greatly simplified।

Traditional:

public final class CourseCode {

    private final String value;

    // constructor
    // getter
    // equals
    // hashCode
    // toString
}

Record:

public record CourseCode(
        String value
) {

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

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

Do You Need to Override toString()?

Generated:

CourseCode[value=JAVA-OOP]

Maybe you prefer:

JAVA-OOP

Then override:

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

Complete:

public record CourseCode(
        String value
) {

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

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

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

Record as DTO

DTO means:

Data Transfer Object

It carries data between boundaries।

Example:

public record CreateCourseRequest(
        String code,
        String title,
        long priceInPaisa
) {
}

This is often a natural record because its main purpose is holding input data।


Validation Responsibility Still Matters

Just because a request is a record does not mean all domain validation belongs there।

Example:

CreateCourseRequest

may validate structural things:

title not null

But business rules such as:

Instructor subscription allows only 10 courses

do not belong in the record constructor।

That requires application/domain context।


Record as Response

Example:

public record CourseResponse(
        String code,
        String title,
        String status
) {
}

This cleanly communicates:

A read-only output shape

Record as Validation Error

public record ValidationError(
        String field,
        String message
) {
}

Great candidate because it simply represents a value pair।


Record as Map Key

Immutable records with stable components can make excellent map keys।

Example:

public record EnrollmentKey(
        long userId,
        CourseCode courseCode
) {
}

Usage:

Map<EnrollmentKey, Enrollment> enrollments =
        new HashMap<>();

Generated equality and hashing include both components।


Validate Primitive Values Too

Example:

public record LessonId(
        long value
) {

    public LessonId {
        if (value <= 0) {
            throw new IllegalArgumentException(
                    "Lesson id must be positive."
            );
        }
    }
}

This creates a strong type instead of passing arbitrary:

long

everywhere।


Record with Derived Behavior

public record Price(
        long amountInCents
) {

    public Price {
        if (amountInCents < 0) {
            throw new IllegalArgumentException(
                    "Price cannot be negative."
            );
        }
    }

    public boolean isFree() {
        return amountInCents
                == 0;
    }
}

A record can contain useful domain behavior।


But Keep It Cohesive

If a record starts accumulating:

20 mutable-looking operations
5 service dependencies
repository calls
network calls
complex lifecycle transitions

it is probably no longer a simple value representation।

A normal class may better communicate the design।


Records and Setters

You cannot sensibly add:

setTitle(...)

that changes the record component because components are final।

If you want a changed record, create a new value।

Example:

public CourseSummary withTitle(
        String newTitle
) {
    return new CourseSummary(
            code,
            newTitle
    );
}

Functional Update Style

Example:

public record CourseSummary(
        String code,
        String title
) {

    public CourseSummary withTitle(
            String newTitle
    ) {
        return new CourseSummary(
                code,
                newTitle
        );
    }
}

Usage:

CourseSummary original =
        new CourseSummary(
                "JAVA",
                "Java"
        );

CourseSummary updated =
        original.withTitle(
                "Java Foundation"
        );

original stays unchanged।


Record Copy Is Explicit

Records do not provide a universal:

copy()

method like some other languages।

You create a new record:

new CourseSummary(
        old.code(),
        newTitle
);

You may add helper methods if they meaningfully improve readability।


Records and Serialization

Libraries such as JSON mappers often support records well in modern Java ecosystems।

But do not confuse:

Record

with:

Serialization format

A record is still just a Java type।

JSON, database rows, files, and network protocols remain separate concerns।


Records and Persistence Entities

Using records directly as database entities can be possible with some tools, but framework requirements vary।

Do not choose a record only because:

It has fewer lines of code

Choose according to lifecycle, persistence tool, and domain semantics।


Records and Framework Boundaries

Records are especially useful at boundaries such as:

Request DTO
Response DTO
Query result
Configuration
Message payload
Application command

because boundary data is often:

Created once
Read many times
Not mutated

Constructor Normalization and Equality

This is powerful:

public record EmailAddress(
        String value
) {

    public EmailAddress {
        if (
                value == null
                || value.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Email is required."
            );
        }

        value =
                value.strip()
                        .toLowerCase();
    }
}

Then:

new EmailAddress(
        "SAKIB@EXAMPLE.COM"
)

and:

new EmailAddress(
        "sakib@example.com"
)

become equal according to the normalization policy।

But normalization rules must match the actual domain।

Do not normalize blindly।


Record Component Order Matters

Given:

public record Money(
        long amount,
        String currency
) {
}

constructor order is:

new Money(
        amount,
        currency
);

With several same-type components:

public record PersonName(
        String firstName,
        String lastName
) {
}

this can compile accidentally:

new PersonName(
        lastName,
        firstName
);

because both are String

Strong types can help if mistakes would be costly।


Example with Strong Types

public record FirstName(
        String value
) {
}

public record LastName(
        String value
) {
}

public record PersonName(
        FirstName firstName,
        LastName lastName
) {
}

Now accidentally swapping them no longer compiles।

Do not introduce tiny wrapper types everywhere, but use them when semantic safety is valuable।


Records Reduce Boilerplate, Not Design Responsibility

This:

public record Course(
        String code,
        String title,
        List<Lesson> lessons,
        CourseStatus status
) {
}

may compile in one line।

But you still need to ask:

Should lessons be mutable?
Can status be arbitrary?
Can an empty course be published?
Who owns course transitions?
Is this an entity or snapshot?

Less code does not mean fewer design decisions।


Example: Good Record Design

public record CourseSummary(
        CourseCode code,
        String title,
        CourseStatus status
) {

    public CourseSummary {
        if (code == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

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

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

        title =
                title.strip();
    }
}

This record represents a stable snapshot and protects its basic invariants।


Complete Example: Domain Entity + Record Snapshot

Course.java

package io.liveklass.course;

public final class Course {

    private final CourseCode code;

    private String title;
    private CourseStatus status;

    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();

        this.status =
                CourseStatus.DRAFT;
    }

    public void changeTitle(
            String newTitle
    ) {
        if (
                newTitle == null
                || newTitle.isBlank()
        ) {
            throw new IllegalArgumentException(
                    "Course title is required."
            );
        }

        title =
                newTitle.strip();
    }

    public void publish() {
        if (
                status
                != CourseStatus.DRAFT
        ) {
            throw new IllegalStateException(
                    "Only draft courses can be published."
            );
        }

        status =
                CourseStatus.PUBLISHED;
    }

    public CourseSummary summary() {
        return new CourseSummary(
                code,
                title,
                status
        );
    }
}

CourseSummary.java

package io.liveklass.course;

public record CourseSummary(
        CourseCode code,
        String title,
        CourseStatus status
) {

    public CourseSummary {
        if (code == null) {
            throw new IllegalArgumentException(
                    "Course code is required."
            );
        }

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

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

        title =
                title.strip();
    }
}

Why This Split Works

Course is:

Entity
Mutable through controlled behavior
Owns lifecycle

CourseSummary is:

Immutable snapshot
Value-based equality
Easy to pass around

These are different responsibilities।


Common Mistakes

Treating Records as Automatically Deeply Immutable

Mutable components can still mutate।


Storing Mutable Collections Without Copying

External mutations can leak into the record।


Assuming Final References Mean Immutable Objects

They do not।


Using a Record for a Complex Mutable Entity

Can fight against the type's intended semantics।


Putting Business Services Inside a Record

Records are best when their primary identity is data/value representation।


Forgetting Constructor Validation

Records still allow invalid values unless you reject them।


Assuming Generated Equality Is Always Correct

It uses every component.

If those components do not define your desired equality, reconsider the record design।


Using Arrays Without Considering Their Equality and Mutability

Arrays are mutable and their default equality is reference-based।


Using Records Just to Reduce Lines

Boilerplate reduction is useful, but semantics come first।


Practice Exercises

Exercise 1: Convert to Record

Convert:

public final class LessonId {

    private final long value;

    // constructor
    // getter
    // equals
    // hashCode
    // toString
}

into a record।

Validate:

value > 0

Exercise 2: Course Summary

Create:

CourseSummary

with:

CourseCode code
String title
CourseStatus status

Validate all components।

Normalize title with:

strip()

Exercise 3: Immutable Course Plan

Create:

CoursePlan(
        CourseCode courseCode,
        List<LessonSummary> lessons
)

using a record।

Defensively copy:

lessons

Exercise 4: Find the Mutation Leak

public record Tags(
        List<String> values
) {
}

Explain why this is not necessarily immutable and fix it।


Exercise 5: Entity or Record?

Choose the better starting design:

A

Course with publish(), archive(), addLesson()

B

CourseSearchResult with code, title, price

C

Money with amount and currency

D

Enrollment with lifecycle and cancellation rules

Suggested Answers

A → Normal class
B → Record
C → Record is a strong option
D → Normal class

Predict the Result

Question 1

public record Point(
        int x,
        int y
) {
}

Point first =
        new Point(
                10,
                20
        );

Point second =
        new Point(
                10,
                20
        );

System.out.println(
        first.equals(
                second
        )
);

Answer

true

Generated record equality compares the components।


Question 2

Point point =
        new Point(
                10,
                20
        );

System.out.println(
        point.x()
);

Answer

10

Question 3

Does a record automatically generate:

getX()

for component:

int x

?

Answer

No।

The generated accessor is:

x()

Question 4

Can a record extend your own:

BaseEntity

class?

Answer

No।


Question 5

Can a record implement an interface?

Answer

Yes।


True or False

  1. Records automatically generate value-based equals().
  2. Record components are mutable fields.
  3. Records automatically deep-copy collection components.
  4. Records can contain methods.
  5. Records can implement interfaces.
  6. Records can extend arbitrary classes.
  7. Compact constructors can validate input.
  8. Record equality includes all components.
  9. Records are always better than normal classes.
  10. Records are good candidates for immutable snapshots and DTOs.

Answers

1. True
2. False
3. False
4. True
5. True
6. False
7. True
8. True
9. False
10. True

Knowledge Check

Question 1

What is a Java record?

Question 2

What is a record component?

Question 3

Which methods does Java generate for records?

Question 4

How are record accessor methods named?

Question 5

What is a compact constructor?

Question 6

Can record values be validated?

Question 7

Can constructor parameters be normalized in a compact constructor?

Question 8

Are records deeply immutable automatically?

Question 9

Why should collection components often use List.copyOf()?

Question 10

Why are arrays awkward record components?

Question 11

Why are records good value objects?

Question 12

Why might a lifecycle-heavy entity be better as a normal class?

Question 13

Can records implement interfaces?

Question 14

What fields participate in generated record equality?

Question 15

What is the most important rule when choosing between a record and normal class?


Knowledge Check Answers

Answer 1

A special Java type designed to concisely represent data-oriented values with generated state access, equality, hashing, and string representation।

Answer 2

A value declared in the record header that forms part of the record's state।

Answer 3

The canonical constructor, component accessors, equals(), hashCode(), and toString()

Answer 4

Using the exact component names, such as:

code()
title()

Answer 5

A shortened canonical constructor syntax used mainly for validation and normalization।

Answer 6

Yes।

Answer 7

Yes. Reassign the constructor parameter before the automatic component assignment happens।

Answer 8

No. Mutable objects referenced by record components may still mutate।

Answer 9

To prevent external mutation of the stored collection structure।

Answer 10

Arrays are mutable and their normal equality is reference-based rather than content-based।

Answer 11

They naturally provide stable component-based equality, final state, and concise representation।

Answer 12

Entities often own identity, controlled mutation, invariants, and state transitions that are better expressed through a normal class।

Answer 13

Yes।

Answer 14

All record components।

Answer 15

Choose based on semantics: use a record when the type fundamentally represents a value or data snapshot; use a normal class when it primarily owns behavior, lifecycle, or controlled mutable state।


Lesson Summary

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

  • record data-oriented Java types concisely represent করে
  • Record header components complete state representation define করে
  • Java automatically constructor, accessors, equals(), hashCode(), and toString() generate করে
  • Record accessors use componentName(), not JavaBean-style getters
  • Records value-based equality provide করে
  • Compact constructors validation এবং normalization support করে
  • Records methods এবং static members contain করতে পারে
  • Records interfaces implement করতে পারে
  • Records arbitrary classes extend করতে পারে না
  • Record components final হলেও referenced objects mutable হতে পারে
  • Collections defensive copy করা প্রয়োজন হতে পারে
  • List.copyOf() structural mutation leak prevent করতে সাহায্য করে
  • Records automatically deep immutability provide করে না
  • Arrays record components হিসেবে extra care require করে
  • Generated equality uses every record component
  • Records excellent value objects, DTOs, query results, validation errors, and snapshots
  • Lifecycle-heavy mutable entities often normal class হিসেবে clearer
  • A normal entity can expose an immutable record snapshot
  • Records boilerplate reduce করে, design responsibility নয়
  • Modern Java type নির্বাচন করার principle:
Value or snapshot → record is a strong candidate
Lifecycle and controlled mutation → normal class is often better

Next Lesson

পরবর্তী lesson:

Clean Methods and Class Design

আমরা শিখব:

  • Single responsibility
  • Cohesion
  • Intention-revealing names
  • Small focused methods
  • Parameter design
  • Avoiding boolean blindness
  • Guard clauses
  • Command-query separation
  • Avoiding god classes
  • Moving behavior to the right object
  • Comments vs expressive code
  • Refactoring long procedural methods into maintainable Java