Getters and Setters: Best Practices
Best practices for getters and setters in OOP — validation, defensive copies, and when to omit a setter — with a Java example.
Expected Interview Answer
Good getter/setter practice means not generating them blindly for every field, but instead validating input in setters, returning defensive copies of mutable fields from getters, omitting setters for fields that should be immutable, and naming methods so the exposed behavior matches what the class actually guarantees.
A common anti-pattern is auto-generating a public getter and setter for every field, which effectively makes the class as unsafe as if the fields were public — it defeats the purpose of encapsulation because it exposes both read and write access with no validation or invariant protection. Better practice: only add a setter if external mutation is genuinely needed; validate every setter input against business rules and throw on violation; keep getters free of side effects and have them return defensive copies for mutable types (Date, List, arrays) rather than live internal references; prefer constructing objects fully valid via the constructor and using immutability where the domain allows it; and avoid “anemic” getter/setter-only classes that push all behavior into external service classes, which is itself considered a design smell (Tell, Don’t Ask is the corrective principle).
- Preserves real invariant protection instead of just adding indirection
- Prevents accidental external mutation of shared internal state
- Keeps object behavior colocated with its data
- Makes intent explicit: read-only fields simply have no setter
AI Mentor Explanation
A responsible team doesn’t hand every fan a pen to edit the official scoresheet just because it’s convenient — only the accredited scorer gets write access, and only through the validated recording process. Blindly giving everyone read-and-write access to everything is exactly the anti-pattern good practice avoids: some things (like the match date once play starts) should have no “setter” at all. Good getter/setter design mirrors this selective, validated access rather than universal open access.
Step-by-Step Explanation
Step 1
Question whether a setter is needed at all
Prefer full initialization via the constructor; only add a setter if genuine external mutation is required.
Step 2
Validate every setter
Enforce business rules and throw on invalid input rather than blindly assigning.
Step 3
Keep getters side-effect free and defensive
Return computed or copied values for mutable field types rather than live internal references.
Step 4
Avoid anemic, auto-generated pairs
Do not generate a public getter/setter for every field by rote — that reintroduces the exposure encapsulation is meant to prevent.
What Interviewer Expects
- Recognition that blanket getter/setter generation defeats encapsulation
- Concrete validation logic mentioned for setters, not just assignment
- Awareness of defensive copying for mutable getter return types
- Understanding of when a field should have no setter at all (immutability)
Common Mistakes
- IDE-generating getters and setters for every field without judgment
- Setters that assign without any validation
- Getters that leak a live reference to an internal mutable collection or date
- Believing all classes must expose both a getter and a setter for every field
Best Answer (HR Friendly)
“Best practice with getters and setters isn’t to add them automatically for every field — that just exposes the field with extra steps. I validate setter input against real business rules, keep getters free of side effects, return defensive copies for mutable fields like dates or lists, and simply don’t add a setter for anything that should stay fixed after construction.”
Code Example
import java.util.Date;
public class Employee {
private final String employeeId; // fixed at construction: no setter
private final Date hireDate; // immutable after hire: no setter
private double salary;
public Employee(String employeeId, Date hireDate, double salary) {
this.employeeId = employeeId;
this.hireDate = new Date(hireDate.getTime()); // defensive copy in
setSalary(salary); // validated even at construction
}
public String getEmployeeId() { return employeeId; }
public Date getHireDate() {
return new Date(hireDate.getTime()); // defensive copy out
}
public double getSalary() { return salary; }
// Setter is exposed only where real mutation is needed, and it validates
public void setSalary(double newSalary) {
if (newSalary < 0) {
throw new IllegalArgumentException("Salary cannot be negative");
}
this.salary = newSalary;
}
}Follow-up Questions
- Why is auto-generating getters and setters for every field considered an anti-pattern?
- How do you decide whether a field should have a setter at all?
- What is a defensive copy and why does it matter for getters returning mutable types?
- How does the "Tell, Don’t Ask" principle relate to getter/setter overuse?
MCQ Practice
1. What is the main risk of blindly generating a public getter and setter for every field?
Unvalidated getters and setters for every field give external code the same unrestricted access as public fields would.
2. A getter returning a mutable field (like a Date) should?
Returning a defensive copy prevents external code from mutating the object’s internal state through the returned reference.
3. When should a field deliberately have no setter?
Fields that should not change after object creation should have no setter, enforcing immutability by design.
Flash Cards
Biggest getter/setter anti-pattern? — Auto-generating a public pair for every field with no validation, defeating encapsulation.
What should a setter always do? — Validate input against real business rules before assigning.
What should a getter avoid? — Side effects, and leaking live references to mutable internal state.
When to omit a setter entirely? — When the field should remain immutable after construction.