# How to create a class - Best Practices

## Introduction

We will explain how to create an object in Java. It has evolved from the old Javabeans to the use of features in the latest versions of the language.

## Setters and getters

We have a Person with three attributes: name, age and gender.

```java
import java.util.Objects;

public class Person {

    private String name;
    private int age;
    private String gender;

    public Person(String name, int age, String gender) {
        this.name = name; 
        this.age = age;
        this.gender = gender;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public Gender getGender() {
       return gender;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + ", gender='" + gender + '\'' + '}';
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.getAge() && name.equals(person.getName()) && gender.equals(person.getGender());
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, gender);
    }
}
```

Any IDE can generate all the above methods. But...... we can do it better.

## Validation

Imagine the client wants to create a Person with a negative age or a with no name. Our API must take care of that in the constructor:

```java
public Person(String name, int age, String gender) {
    if (name == null || name.isBlank()) {
        throw new IllegalArgumentException("Invalid name");
    }
    if (age < 0) {
        throw new IllegalArgumentException("Age cannot be negative");
    }
    if (gender == null || gender.isBlank()) {
        throw new IllegalArgumentException("Invalid gender");
    }
    this.name = name;
    this.age = age;
    this.gender = gender;
}
```

## Static factory methods

Instead of having the client call the traditional constructor, our API can provide a number of descriptive methods, so that the client can choose the most appropriate one. This pattern is very common in the JDK. For example, the `Instance` class (from the Java Time API) includes static factory methods such as `now()`, `of()`, `from()`, and `parse()`.

```java
    public static Person of(String name, int age, Gender gender) {
        return new Person(name, age, gender);
    }

    public static Person createATwentyYearOldMale(String name) {
        return new Person(name, 20, Gender.MALE);
    }

    public static Person createATwentyYearOldFemale(String name) {
        return new Person(name, 20, Gender.FEMALE);
    }
```

```java
public enum Gender {
    MALE,
    FEMALE
}
```

Thanks to the enumeration class, we change the gender parameter from String type to Gender type. This allows to **catch errors in compilation time**.

```java
    Person person = Person.of("John", 25, Gender.MALE);
    var person2 = Person.createATwentyYearOldMale("John");
    var person3 = Person.of("John", 25, "men");     // Compilation error
```

It is important to note that **the constructor is now private**, so that the client cannot create directly objects of this class:

```java
    var person = new Person("Alice", 25, Gender.FEMALE);     // Compilation error
```

Other benefit of having the constructor private is that the client cannot extend the Person class:

```java
    class Woman extends Person {  // Compilation error
        ...................
    }
```

This is a good thing, because we want to be able to control what the client can do with our API.

## Non extensible class

However, to make the intent clear in the Javadoc documentation, we can use the **final** modifier:

```java
    public final class Person {
        ...................
    }
```

We will talk about restricted inheritance later.

## No setters

Although the setter methods were heavily used in the Javabeans days, programmers came to realize that **immutable objects** made in in many cases the code easier to read, especially in concurrent applications. In other words, creating an object every time an attribute changes its value. This is quite similar to how purely functional languages work.

```java
    public Person withName(String newName) {
        return new Person(newName, this.age, this.gender);
    }

    public Person withAge(int newAge) {
        return new Person(this.name, newAge, this.gender);
    }

    public Person withGender(Gender newGender) {
        return new Person(this.name, this.age, newGender);
    }
```

and make the attributes immutable, too:

```java
    private final String name;
    private final int age;
    private final Gender gender;
```

The `with` prefix is a common convention and these methods are called `withers`.

## Records

A record is a class that is **immutable**(its attributes cannot be changed) and **final**. It was introduced as a preview feature in Java 14 and the first LTS version that included it was Java 17.

```java
public record Person(String name, int age, Gender gender) {}
```

The record class automatically generates private final attributes, and implements the `equals()`, `hashCode()`, and `toString()` methods.

We only have to write this code:

```java
public record Person(String name, int age, Gender gender) {
    
    // Validation rules
    public Person {   // The compact constructor doesn't have any parameters
        if (name == null || name.isBlank())
            throw new IllegalArgumentException("Name cannot be null or blank");
        if (age < 0)
            throw new IllegalArgumentException("Age cannot be negative");
        if (gender == null || gender.isBlank()) 
            throw new IllegalArgumentException("Invalid gender");
    }

    // Static factory methods
    public static Person of(String name, int age, Gender gender) {
        return new Person(name, age, gender);
    }

    public static Person createATwentyYearOldMale(String name) {
        return new Person(name, 20, Gender.MALE);
    }

    public static Person createATwentyYearOldFemale(String name) {
        return new Person(name, 20, Gender.FEMALE);
    }

    // Wither-methods
    public Person withName(String newName) {
        return new Person(newName, this.age, this.gender);
    }

    public Person withAge(int newAge) {
        return new Person(this.name, newAge, this.gender);
    }

    public Person withGender(Gender newGender) {
        return new Person(this.name, this.age, newGender);
    }
}
```

The public accessor methods generated for each component have the same name as the corresponding attribute, they are not getters anymore:

```java
    var personJohn = Person.of("John", 25, Gender.MALE);
    System.out.println("Name: " + personJohn.name());     // Accesor method if Person is a record
    System.out.println("Name: " + personJohn.getName());  // Compiler error if Person is a record
```

There are two caveats with records:

* The public constructor of a record, called **canonical constructor**, is part of the interface. It can't be made private. As a consequence, the client is not forced to call the static factories.
    
* The `wither` methods are not yet generated by the compiler.  
    The JEP ("Java Enhancement Proposal") 468 introduces **derived record creation**, which consists of generating “wither” methods automatically. But this technique, as of July 2025, is not even in preview yet.
    

### When not to use records

If we have a class with a complex business logic (many static factories, for example), regular classes are better suited than records, as they are more flexible.

| Feature | Regular Class | Record |
| --- | --- | --- |
| **Purpose** | General-purpose, can encapsulate state and behavior | Immutable data carrier, concise data-only types |
| **Mutability** | Can be mutable or immutable | Immutable by default (all fields are final) |
| **Inheritance** | Can extend another class (single inheritance) and implement interfaces | Cannot extend any class; can only implement interfaces |
| **Constructors** | Any number, including no-arg and overloaded | Canonical constructor auto-generated; can add custom constructors (with restrictions) |
| **Boilerplate code** | Require manual implementation of getters, setters, `equals`, `hashCode`, etc. | Getters, canonical constructor, `equals`, `hashCode`, and `toString` auto-generated |
| **State-changing methods** | Can have methods that mutate fields | Cannot have mutator methods for fields (fields are final) |
| **Field Declaration** | Fields can have any visibility and can be static or final | All fields are implicitly `private` and `final`, based on record components |
| **Use Case** | Complex logic, behaviors, mutable patterns, frameworks requiring mutability | Data transfer, value objects, DTOs, where immutability is desired |
| **Compatibility with JPA** | Fully compatible, allows setters, no-arg constructor | Not suitable for JPA entities (no setters, no-arg constructor not provided) |

## Summary

We have explained the best practices for declaring objects in Java.

Since the (not so good) old Java Beans, the principles of immutability and inheritance prohibition have been applied.

The implementation of records in Java 17 have greatly simplified the creation of objects without having to use libraries similar to **Lombok**.

However, in some cases it is still more convenient to use a regular class.
