Java Getter Setter: Syntax and Modern Alternatives
java getter setter: Learn how to implement getters and setters in Java, when encapsulation requires them, and how records offer a modern alternative for value objects.
The java getter setter pattern is the standard way to expose private fields through controlled accessor methods. A getter reads a field, a setter writes it, and both form the public contract that other classes depend on. Understanding when this pattern is necessary, and when it is not, is central to designing maintainable Java classes.
The Basic Getter and Setter Pattern in Java
A conventional getter follows the naming pattern getFieldName and returns the field value, while a setter uses setFieldName and assigns a new value:
public class User { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
The fields are private, so external code cannot read or write them directly. The accessor methods are the only path to the state. For boolean fields, the getter conventionally uses is instead of get: isActive() rather than getActive(). This naming convention is recognized by frameworks such as Jackson and Spring, which rely on it for serialization and dependency injection.
Why Encapsulation Depends on Accessor Methods
Direct field access couples every caller to the internal representation. If a class stores a String date and later changes to LocalDate, every direct read breaks. With a getter, the conversion happens in one place:
public class Event { private LocalDate date; public String getDate() { return date.toString(); } public void setDate(String date) { this.date = LocalDate.parse(date); } }
Callers keep using strings, but the internal representation changed without touching their code. Setters also give you a single point for validation:
public void setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("Age must be between 0 and 150"); } this.age = age; }
Without the setter, every caller that assigns age would need to repeat this check, and a missed caller would allow invalid state to enter the object.
Defensive Copying: When Returning the Field Is Wrong
A getter that returns a reference to a mutable field exposes the internal state. A caller can modify the collection or object without going through the setter:
public class Order { private final List<Item> items = new ArrayList<>(); public List<Item> getItems() { return items; // caller can call items.add(...) } }
Returning an unmodifiable view prevents that:
public List<Item> getItems() { return Collections.unmodifiableList(items); }
For arrays, return a copy: return items.clone(). For mutable objects like Date, return a new instance or a defensive copy. The tradeoff is allocation cost: every getter call creates a new object or wrapper. If the collection is large and the getter is called in a hot loop, this cost matters. An alternative is to provide mutator methods on the owning class, like addItem(Item) and removeItem(Item), and keep the collection private.
Performance: What JIT Compilation Does to Accessors
A common concern is that getters and setters add method-call overhead. In practice, the JIT compiler inlines small accessor methods after profiling. A getter that simply returns a field compiles to the same machine code as a direct field read. The same applies to setters. The real performance cost comes from defensive copying, validation logic, or synchronization inside accessors, not from the method call itself.
That said, if a getter performs a non-trivial computation on every call, such as parsing a string into a LocalDate, the cost is repeated on each access. Caching the parsed value inside the class avoids recomputation but introduces a consistency concern: the cache must be invalidated when the underlying field changes. A derived getter that computes a value from another field, like converting Celsius to Fahrenheit, is cheap enough that caching would add complexity without measurable benefit:
public class Temperature { private double celsius; public double getFahrenheit() { return celsius * 9.0 / 5.0 + 32; } public void setFahrenheit(double fahrenheit) { this.celsius = (fahrenheit - 32) * 5.0 / 9.0; } }
Here the getter and setter expose a different unit than the internal field, which is a legitimate use of accessor methods beyond simple field access.
Records: The Modern Alternative
Java 14 introduced records, which generate the accessor methods, constructor, equals, hashCode, and toString from the component list:
public record User(String name, int age) {}
A record is immutable: there are no setters. The accessor is name() rather than getName(). Records fit when the data is a plain value carrier with no validation or derived behavior. They do not replace getters and setters when you need mutable state, validation in the setter, or a different accessor name.
| Concern | Traditional class with getters/setters | Record |
|---|---|---|
| Mutability | Mutable by default | Immutable |
| Validation | In setter or constructor | In compact constructor |
| Accessor syntax | getName() | name() |
| Boilerplate | Manual | Generated |
| Extensibility | Full | Limited to the component list |
Records are a good default for DTOs, API responses, and value objects. Use a traditional class when the object must change after construction or when you need to hide the internal representation behind derived accessors.
Common Pitfalls in Getter and Setter Design
A getter that returns null forces every caller to handle the null case. If the field is optional, consider Optional<T> as the return type, but be aware that Optional itself adds an allocation. For collections, return an empty collection instead of null so callers can iterate safely without null checks.
Setters that accept invalid values silently are another problem. If a setter assigns a value that violates an invariant, the object enters a state that other methods must defend against. Throwing IllegalArgumentException early is usually better than failing later at an unpredictable point.
Finally, avoid exposing internal mutable objects through getters. If a field is a Date or a List, the getter should return a copy or an unmodifiable view, as described earlier. This is a correctness concern, not just a style preference. The same logic applies to constructors: a constructor that stores a caller-supplied mutable reference without copying it allows the caller to mutate the object after construction. Defensive copying in both directions keeps the class in control of its own state.