انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-026-L05

السجلات (Records) كمعاينة: مساواة مجانية

  • الدرس 5 من 6
  • 10 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

In the last three lessons you wrote equals, hashCode, and toString by hand for a value class. It works, but it is a lot of code for a simple idea: "this object is just its values". Modern Java has a shortcut for exactly that case: the record.

This is a preview. Records get a proper, full treatment later in the course; here you learn only what a record generates and why it matters for the contract you just studied. Do not worry about every detail of the syntax.

In the banknote picture: a record is a note that is defined by its value alone. Two notes of 100 riyals are interchangeable, and Java can work that out for you without a line of equals code.

Why this matters: value-like types such as money, coordinates, ids, and request or response data appear everywhere in a backend. Records let you write them in one line, with a correct equals/hashCode/toString, and the classic bug from Lessons 3 and 6 simply cannot happen.

This lesson builds on Lessons 1 to 4. Check the Java version first: records exist in modern Java, and the Javadoc of Java SE 21, the version used for this course's examples, documents them. If your JDK is much older, the code may not compile.

Learning Objectives

By the end of this lesson you will be able to:

  • Declare a simple record such as record Money(long cents, String currency) { }.
  • List what the compiler generates for a record.
  • Show that two records with the same values are equal and have equal hash codes.
  • Read the generated toString output.
  • Explain when a record's "all fields" equality is the right or wrong choice.
  • Explain why a record cannot have the stale-hash-code problem from Lesson 3 in its basic form.

Written Explanation

One line instead of fifty

A record declares a data carrier by listing its components in the header:

java
record Money(long cents, String currency) { }

The compiler generates, from those components:

  • a private final field for each component;
  • a constructor taking all the components (new Money(100, "SAR"));
  • an accessor method for each component, named like the component: m.cents() and m.currency() (no get prefix);
  • equals, hashCode, and toString.

All fields are final, so a record is immutable: there are no setters, and you cannot reassign a field after creation. That is the same immutability idea from the encapsulation module, built into the language.

What the generated methods do

The Java documentation for java.lang.Record states the rules:

  • equals is true when the other object is of the same record class and each component is equal (object components compare with Objects.equals).
  • hashCode combines the hash codes of all components, and two records made from the same component values must have the same hash code.
  • toString contains the record's class name, each component's name, and each value, for example Money[cents=100, currency=SAR]. The documentation says the exact format may change, so do not parse it.

So the contract from Lesson 3 holds automatically: equal records have equal hash codes, because both come from the same components. It also holds in the sense of Lesson 4: the text is readable out of the box.

Records are not a free pass

Three things to keep in mind:

  1. All components count. Record equality compares every component. If you want equality by an id only (as with Customer(id, name) in Lesson 3), a plain record does not match: two records with the same id and different names are unequal. For that you would write a normal class (or override equals inside the record, which is beyond this preview).
  2. toString shows every component, including secrets. A record Login(String user, String password) would print the password! You can override toString inside the record, but you must remember to.
  3. Shallow immutability. A component that refers to a mutable object (such as a list) can still change inside it; the reference is final, the contents may not be.

Comparing with the Lesson 3 recipe

The generated methods follow the same recipe you used by hand: equality compares the fields, the hash code combines the same fields, and the text lists them. The difference is who writes it. A hand-written pair can drift (someone adds a field to equals and forgets hashCode), while the compiler regenerates the record's methods from the header every time. The header is the single source of truth, so adding a component updates all three methods at once.

A record can still contain methods, so a small value type can keep its own helper such as isZero(). Details of that, and of validating values in a record's constructor, come later.

Where records fit

Use a record when the type is "just data" and every component is part of its identity: money, coordinates, a date range, a response object. Use a normal class when the object has behavior and changing state (an Account with a balance), or when equality is by id only. You will see records again in later modules on data transfer and validation; this lesson only shows that they remove the boilerplate that Lesson 3 asked you to write.

Examples

Example 1: equality for free (simple)

java
record Money(long cents, String currency) { }

Money a = new Money(100, "SAR");
Money b = new Money(100, "SAR");
System.out.println(a);
System.out.println(a == b);
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
System.out.println(a.cents() + " " + a.currency());

Real output:

text
Money[cents=100, currency=SAR]
false
true
true
100 SAR

a == b is false: they are still two objects (identity). equals is true because the values match, and so are the hash codes. We wrote no equals, hashCode, or toString, and the accessors cents() and currency() also exist without being written.

Example 2: record vs hand-written class (practical)

Compare the sizes of the same idea:

java
// Lesson 3 style: about 15 lines
class Money {
    private final long cents;
    private final String currency;
    Money(long cents, String currency) { this.cents = cents; this.currency = currency; }
    @Override public boolean equals(Object o) { /* as in Lesson 3 */ }
    @Override public int hashCode() { return Objects.hash(cents, currency); }
    @Override public String toString() { return "Money[" + cents + " " + currency + "]"; }
}

// Record: one line
record Money(long cents, String currency) { }

Because there is nothing to forget, the "I overrode equals but not hashCode" mistake is gone. Fewer lines also means fewer places for a typo to hide.

Example 3: when "all components" is the wrong equality (realistic)

java
record Customer(long id, String name) { }

Customer x = new Customer(7, "Ana");
Customer y = new Customer(7, "Ana Maria");
System.out.println(x.equals(y));
System.out.println(x);
text
false
Customer[id=7, name=Ana]

In Lesson 3 we wanted these two to be equal because they share an id. A plain record says they differ. Neither answer is wrong in general: they are different decisions. If the customer's name can change and the business means "the same customer", an id-only equals in a normal class models it better. If the object is a snapshot of a customer's data at a moment (for example data in a response), the record's behavior is correct. Choosing which one you need is the design skill this module is about.

Backend Insight

A large share of backend code moves small bundles of data around: a request that arrives, a result from a query, a message to publish. These are values, and records are a natural fit. They are immutable, so one part of the code cannot change them behind another part's back, and they can be used safely as keys in maps and sets, because their hash cannot change.

Be careful at the edges, though. A record that is mapped to a database table row or used as an entity with a changing state is a poor match, and the "all fields" equals can surprise you when the object has an id plus other attributes. Frameworks you meet later (such as JPA, which this course covers much later) have their own rules about equality for entities, and plain records are generally used for data transfer, not for entities.

A useful way to remember it: a record is a contract with the compiler that says "this type is exactly its components". The compiler keeps the promise by deriving the methods; you keep yours by not hiding changing state or secrets inside it.

Common Mistakes

Mistake 1: Assuming record equality is "by id"

A record compares all components. If you want identity by id only, write a class with a custom equals/hashCode.

Mistake 2: Putting secrets in a record and printing it

The generated toString prints every component. A password or token component will show up in logs unless you override toString.

Mistake 3: Looking for getters

Record accessors are m.cents(), not m.getCents().

Mistake 4: Thinking a record is deeply immutable

Fields cannot be reassigned, but a component such as a list can still be changed internally.

Mistake 5: Using a record for an object that changes

A bank account with a balance that changes is a class. Records are for values.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. Write a record Point with two int components, x and y.

أظهر الحل A1

A1. record Point(int x, int y) { }

A2. Name five members the compiler generates for a record, besides the fields.

أظهر الحل A2

A2. The constructor, the accessor methods, equals, hashCode, and toString (plus the private final fields).

A3. What is new Point(1, 2).toString() likely to look like?

أظهر الحل A3

A3. Something like Point[x=1, y=2]. The exact format is not guaranteed.

Level B: Application

B1. For record Point(int x, int y), give the result of new Point(1, 2).equals(new Point(1, 2)), of == on the same two objects, and of comparing their hash codes.

أظهر الحل B1

B1. equals is true, == is false (two objects), and the hash codes are equal.

B2. Why does record Login(String user, String password) create a risk when you log an instance?

أظهر الحل B2

B2. The generated toString includes every component, so the password would appear in log text. Override toString to leave it out, or keep passwords out of such a record.

B3. What is the accessor to read y from a Point?

أظهر الحل B3

B3. p.y().

Level C: Challenge

C1. A team proposes record Customer(long id, String name, String email), and wants two customers with the same id to be equal. Explain why the record does not do that and name one alternative.

أظهر الحل C1

C1. A record's equals compares all components, so two customers with the same id but a different name or email are unequal. Alternatives: a normal class with equals and hashCode based on id only (Lesson 3), or keep the record as a snapshot and compare ids explicitly with a.id() == b.id().

C2. Explain why using a record as a key in a hash-based collection is safer than using a mutable class whose hashCode uses a changing field (Lesson 3).

أظهر الحل C2

C2. The record's fields are final, so its hash code cannot change after it is stored. A mutable class can change the hashed field, which moves it to a different bucket and makes it unfindable. (Caveat: if a record holds a mutable component, the hash can still change.)

Mini Challenge

Convert your Money class from Lesson 3 into a record (keep a copy of the old version to compare). Run the checks from Lesson 3: reflexive, symmetric, null, a different class, equal hash codes, and print the object. Then add a record Login(String user, String password), print an instance, and observe the problem. Write one sentence on how you would fix it.

Check your work: all the old checks still pass with far less code, and the Login printout contains the password, proving why toString needs care.

Summary & Next Lesson

Key takeaways

  • A record declares an immutable data carrier in one line, and the compiler generates the constructor, accessors, equals, hashCode, and toString.
  • Records with equal component values are equal and have equal hash codes, so the contract holds automatically.
  • Equality covers all components. For equality by id only, use a normal class.
  • The generated toString shows every component, including secrets.
  • Records are for values, not for objects whose state changes.

Next lesson

The next lesson, "Demo: the broken HashSet and the equals/hashCode fix", is a hands-on demo: you will see duplicates appear in a set, then fix them step by step. It prepares the lab.

Self-check

  1. Which methods does a record generate for you?
  2. Are two records with the same values ==?
  3. What is the accessor for a component named cents?
  4. Why can printing a record be a security risk?
  5. When is a normal class a better choice than a record?
أظهر الإجابات
  1. The constructor, the accessors, equals, hashCode, and toString.
  2. No, == is still identity; equals is true.
  3. cents().
  4. toString includes every component, so secrets would be printed.
  5. When the object changes over time, has behavior, or must be equal by an id only.

References

  • Oracle, Java SE 21 API: java.lang.Record. It specifies the implicit equals, hashCode, and toString: equality compares all components, hash codes are derived from all components, and the toString format (class name, component names, values) may change.
  • Oracle, Java SE 21 API: java.util.Objects. It covers Objects.equals and Objects.hash, used by the hand-written comparison.
  • That the compiler also generates the private final fields, the constructor, and the accessors, and the Java version in which records became standard, were not verified on a fetched page in this lesson. The behavior shown in the examples was confirmed by compiling and running on JDK 21.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.