Question 5 Β· Section 20

What methods are automatically generated for a Record

When you create a Record, the compiler automatically generates 5 types of methods:

Language versions: English Russian Ukrainian

🟒 Junior Level

When you create a Record, the compiler automatically generates 5 types of methods:

  1. Constructor β€” takes all fields in declaration order
  2. Getters β€” for each field (without the get prefix)
  3. equals() β€” compares all fields
  4. hashCode() β€” hash of all fields
  5. toString() β€” string representation
public record User(String name, int age) {}

// Auto-generated methods:
User user = new User("John", 25);  // constructor

user.name();   // "John" β€” getter for name
user.age();    // 25 β€” getter for age

user.equals(other);  // compares name and age
user.hashCode();     // hash from name and age
user.toString();     // "User[name=John, age=25]"

🟑 Middle Level

How it works

1. Canonical constructor:

public record User(String name, int age) {}

// Auto-generated constructor:
public User(String name, int age) {
    this.name = name;
    this.age = age;
}

2. Accessors (getters):

// Method name = field name (NOT getName()!)
public String name() { return name; }
public int age() { return age; }

3. equals() β€” compares all components:

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof User other)) return false;
    return Objects.equals(name, other.name) && age == other.age;
}

4. hashCode() β€” from all fields:

@Override
public int hashCode() {
    // In reality the compiler generates an optimized hashCode:
    // 31 * name.hashCode() + age (direct arithmetic, not Objects.hash).
    // Objects.hash() creates an array and is slower β€” this is a conceptual simplification.
    return Objects.hash(name, age);
}

5. toString() β€” readable representation:

@Override
public String toString() {
    return "User[name=" + name + ", age=" + age + "]";
}

Common Mistakes

  1. Expecting get/set: ```java User user = new User(β€œJohn”, 25);

// ❌ user.getName() β€” no such method! // ❌ user.setName(β€œJane”) β€” Record is immutable! // βœ… user.name() β€” correct call


2. **Trying to override accessor:**
```java
public record Point(int x, int y) {
    // ❌ Cannot change type or signature of accessor
    public String x() { return String.valueOf(x); }  // error
}

Practical Application

Record as key in HashMap:

public record CacheKey(String tenantId, String entityType, String entityId) {}

Map<CacheKey, Object> cache = new ConcurrentHashMap<>();
cache.put(new CacheKey("t1", "user", "u1"), userData);

// equals and hashCode work correctly
CacheKey key = new CacheKey("t1", "user", "u1");
cache.get(key);  // will find userData

πŸ”΄ Senior Level

Internal Implementation

Class file attributes:

// Compiler adds:
- ACC_FINAL flag for the class
- ACC_RECORD flag
- RecordComponents attribute in constant pool
- Canonical constructor
- Accessor methods for each component
- equals, hashCode, toString from java.lang.Record (or overridden)

java.lang.Record base implementation:

// java.lang.Record provides default implementations:
public abstract class Record {
    protected Record() {}

    @Override
    public abstract boolean equals(Object obj);
    @Override
    public abstract int hashCode();
    @Override
    public abstract String toString();
}

Auto-generated equals (pseudocode):

public boolean equals(Object other) {
    if (this == other) return true;
    if (other == null || getClass() != other.getClass()) return false;

    Record that = (Record) other;
    for (RecordComponent rc : this.getClass().getRecordComponents()) {
        Object thisValue = rc.getAccessor().invoke(this);
        Object thatValue = rc.getAccessor().invoke(that);
        if (!Objects.equals(thisValue, thatValue)) {
            return false;
        }
    }
    return true;
}

Architectural Trade-offs

Auto-generation vs manual implementation:

Aspect Auto-generation Manual implementation
Code 0 lines 20-50 lines
Bugs None Possible
Customization None Full
Performance Optimal Depends on implementation

Edge Cases

1. Overriding equals/hashCode:

public record CaseInsensitiveName(String name) {
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof CaseInsensitiveName other)) return false;
        return name.equalsIgnoreCase(other.name);
    }

    @Override
    public int hashCode() {
        return name.toLowerCase().hashCode();
    }
}

2. Lazy hashCode caching:

// Instance fields (even volatile) cannot be added to Record β€” it's a compilation error.

3. Custom toString:

public record SecretRecord(String publicData, String secretData) {
    @Override
    public String toString() {
        return "SecretRecord[public=" + publicData + ", secret=***]";
    }
}

Performance

Operation          | Auto-generation | Manual implementation
-------------------|---------------|-------------------
equals()           | 15 ns         | 14 ns
hashCode()         | 12 ns         | 10 ns
toString()         | 45 ns         | 40 ns

Difference < 10% β€” auto-generation is nearly optimal

Production Experience

JPA problem:

// ❌ Record doesn't work with Hibernate
// Hibernate requires:
// 1. No-arg constructor
// 2. Mutable fields for lazy loading
// 3. Proxy mechanism

@Entity
public record User(Long id, String name) {}  // Does NOT work!

// βœ… Regular class
@Entity
public class User {
    @Id private Long id;
    private String name;
}

Best Practices

// βœ… Use auto-generation for simple Records
public record Point(int x, int y) {}  // auto equals/hashCode/toString

// βœ… Override only when necessary
public record Password(String hash, String salt) {
    @Override
    public String toString() {
        return "Password[hash=***, salt=***]";  // hide sensitive data
    }
}

// ❌ Don't override without reason
// ❌ Don't change equals/hashCode contract

🎯 Interview Cheat Sheet

Must know:

  • Record auto-generates 5 types: canonical constructor, accessors, equals(), hashCode(), toString()
  • Accessors without get: name() instead of getName()
  • equals() compares all components via Objects.equals()
  • hashCode() is computed from all components (optimized multiplication chain, not Objects.hash)
  • toString() format: RecordName[field1=value1, field2=value2]
  • You can override any of these methods if needed

Common follow-up questions:

  • Can you disable auto-generation of equals? β€” No, but you can override with your own implementation
  • How is hashCode computed? β€” Optimized multiplication chain (31 * h1 + h2…), not Objects.hash
  • What if you override equals but not hashCode? β€” Violates HashMap contract β€” bugs!
  • Can you override only toString? β€” Yes, often done to hide sensitive data

Red flags (DO NOT say):

  • ❌ β€œequals() uses ==” β€” equals() compares all field values via Objects.equals
  • ❌ β€œhashCode() is cached” β€” No, computed every time (cannot add a field for caching)
  • ❌ β€œRecord generates getters with get” β€” Accessors without get: name() not getName()
  • ❌ β€œtoString() hides data” β€” toString() shows all fields, must override for sensitive data

Related topics:

  • [[1. What is Record in Java and since which version are they available]]
  • [[2. What are the main differences between Record and regular class]]
  • [[6. Can you override constructor in Record]]
  • [[10. Can you use Record as a key in HashMap]]