Java Getters, Setters, Data Validation & Controlled Access
Getter Methods (Accessors) ยท Setter Methods (Mutators) ยท boolean Getter Naming (isActive) ยท Data Validation in Setters ยท Defensive Copying ยท Chained Setters (Fluent API) ยท Computed Properties ยท When NOT to Use Setters
Mastering controlled object access in Java: designing accessor (getter) and mutator (setter) methods with business validation rules, defensive copying for mutable fields, computed properties that derive values from existing state, and fluent method chaining for readable object construction APIs.
1. Getter Methods (Accessors) โ Controlled Read Access
A Getter provides controlled read-only access to a private field. The Java naming convention is:
- For non-boolean fields: public ReturnType getFieldName()
- For boolean fields: public boolean isFieldName() (not get!)
class Employee {
private String name;
private double salary;
private boolean active;
public String getName() { return name; }
public double getSalary() { return salary; }
public boolean isActive() { return active; } // "is" prefix for booleans!
}
Getter can transform before returning (Computed Property):
private String firstName;
private String lastName;
public String getFullName() {
return firstName + " " + lastName; // Derived/computed property
}
2. Setter Methods (Mutators) โ Controlled Write Access with Validation
A Setter provides controlled write access, allowing you to insert business rule validation before accepting a new value:
class Employee {
private String name;
private double salary;
public void setName(String name) {
if (name == null || name.isBlank())
throw new IllegalArgumentException("Employee name cannot be null or empty.");
this.name = name.trim();
}
public void setSalary(double salary) {
if (salary < 0)
throw new IllegalArgumentException("Salary cannot be negative: " + salary);
if (salary > 10_000_000)
throw new IllegalArgumentException("Salary exceeds maximum policy limit.");
this.salary = salary;
}
}
3. Defensive Copying for Mutable Fields
When a field is a mutable object (like an array or a Date), simply returning it from a getter exposes the internal state to external modification. Defensive copying prevents this:
class Report {
private int[] scores; // Mutable array!
public Report(int[] scores) {
this.scores = scores.clone(); // Copy in: don't trust caller's array!
}
public int[] getScores() {
return scores.clone(); // Copy out: don't expose the internal array!
}
}
Without defensive copying:
int[] data = {90, 80, 70};
Report r = new Report(data);
data[0] = 0; // Would corrupt r.scores if no defensive copy!
r.getScores()[1] = 0; // Would corrupt r.scores on return if no defensive copy!4. Fluent Setter Pattern (Method Chaining)
Setters can return this to enable clean fluent method chaining syntax:
class QueryBuilder {
private String table;
private String condition;
private int limit;
public QueryBuilder from(String table) { this.table = table; return this; }
public QueryBuilder where(String cond) { this.condition = cond; return this; }
public QueryBuilder limit(int n) { this.limit = n; return this; }
public String build() {
return "SELECT * FROM " + table + " WHERE " + condition + " LIMIT " + limit;
}
}
// Fluent API reads like natural language:
String query = new QueryBuilder()
.from("students")
.where("gpa > 3.5")
.limit(10)
.build();
5. When NOT to Provide Setters (Read-Only Properties)
Not every field needs a setter. Consider making fields effectively read-only (no setter) in these cases:
1. Identity fields (ID, account number, creation timestamp) that should never change once set.
2. Derived fields computed from other fields (e.g. age derived from date of birth).
3. Immutable value classes (Money, Point, Color) where the entire object state is fixed.
class Order {
private final String orderId; // Never changes โ NO setter!
private final long createdTimestamp; // Created once โ NO setter!
private int quantity; // Can change โ has setter with validation
Order(int quantity) {
this.orderId = "ORD-" + System.nanoTime();
this.createdTimestamp = System.currentTimeMillis();
setQuantity(quantity);
}
public String getOrderId() { return orderId; }
public long getCreatedTimestamp() { return createdTimestamp; }
public int getQuantity() { return quantity; }
public void setQuantity(int q) {
if (q <= 0) throw new IllegalArgumentException("Quantity must be positive!");
this.quantity = q;
}
}Beginner Example & Code Anatomy
class Employee {
// Private fields
private final String employeeId; // Read-only: no setter!
private String name;
private String email;
private double salary;
private boolean active;
private int[] projectIds; // Mutable array field
public Employee(String name, String email, double salary, int[] projectIds) {
this.employeeId = "EMP-" + System.currentTimeMillis() % 100000;
setName(name);
setEmail(email);
setSalary(salary);
this.projectIds = projectIds != null ? projectIds.clone() : new int[0];
this.active = true;
}
// GETTERS
public String getEmployeeId() { return employeeId; }
public String getName() { return name; }
public String getEmail() { return email; }
public double getSalary() { return salary; }
public boolean isActive() { return active; }
// Defensive copy on getter for mutable array
public int[] getProjectIds() { return projectIds.clone(); }
// Computed (derived) property โ no backing field!
public String getDisplayTitle() {
return (active ? "[ACTIVE] " : "[INACTIVE] ") + name + " <" + email + ">";
}
// SETTERS WITH VALIDATION
public void setName(String name) {
if (name == null || name.isBlank())
throw new IllegalArgumentException("Name cannot be blank.");
this.name = name.trim();
}
public void setEmail(String email) {
if (email == null || !email.matches("^[\w.-]+@[\w.-]+\.\w{2,}$"))
throw new IllegalArgumentException("Invalid email format: " + email);
this.email = email.toLowerCase().trim();
}
public void setSalary(double salary) {
if (salary < 15000)
throw new IllegalArgumentException("Salary below minimum wage: " + salary);
if (salary > 5_000_000)
throw new IllegalArgumentException("Salary exceeds company maximum.");
this.salary = salary;
}
public void setActive(boolean active) { this.active = active; }
public void setProjectIds(int[] projectIds) {
this.projectIds = projectIds != null ? projectIds.clone() : new int[0];
}
@Override
public String toString() {
return String.format("[%s] %-18s | Salary: $%,9.2f | Active: %b",
employeeId, name, salary, active);
}
}
public class Main {
public static void main(String[] args) {
System.out.println("=== 1. User Requested BankAccount Pattern Applied to Employee ===");
Employee emp = new Employee("Ravi Kumar", "ravi.kumar@company.com",
85000.0, new int[]{101, 205, 312});
System.out.println(emp);
System.out.println("Display Title : " + emp.getDisplayTitle());
System.out.println("
=== 2. Validated Setter Updates ===");
emp.setSalary(92000.0);
System.out.println("After raise : " + emp);
System.out.println("
=== 3. Setter Validation Guards ===");
try {
emp.setSalary(-500.0); // Invalid!
} catch (IllegalArgumentException e) {
System.out.println("Salary error : " + e.getMessage());
}
try {
emp.setEmail("not-an-email"); // Invalid!
} catch (IllegalArgumentException e) {
System.out.println("Email error : " + e.getMessage());
}
System.out.println("
=== 4. Defensive Copy Protection ===");
int[] returnedIds = emp.getProjectIds();
returnedIds[0] = 9999; // Attempt to corrupt internal array
System.out.println("Ext. modified [0] : " + returnedIds[0]);
System.out.println("Internal [0] safe : " + emp.getProjectIds()[0]); // Still 101!
System.out.println("
=== 5. Computed Property ===");
emp.setActive(false);
System.out.println("Inactive title : " + emp.getDisplayTitle());
}
}
๐ Line-by-Line Code Explanation
public boolean isActive()
Boolean getters use "is" prefix (not "get") per Java Beans convention, required by frameworks like Spring and Hibernate.
this.projectIds = projectIds.clone();
Defensive copy on both construction and return prevents external code from mutating internal array state.
public String getDisplayTitle()
Computed property derives a formatted display value from multiple private fields without a backing field of its own.
if (!email.matches("^[\\w.-]+@[\\w.-]+\\.\\w{2,}$"))
Regex validation in setter prevents invalid email strings from entering the object state.
Practical Real-World Example
class Temperature {
private double celsius;
public Temperature(double celsius) {
setCelsius(celsius);
}
// Setter with physical validation
public void setCelsius(double celsius) {
if (celsius < -273.15)
throw new IllegalArgumentException("Temperature below absolute zero!");
this.celsius = celsius;
}
// Getter for raw value
public double getCelsius() { return celsius; }
// Computed getters โ derive other scales automatically
public double getFahrenheit() { return (celsius * 9.0 / 5.0) + 32; }
public double getKelvin() { return celsius + 273.15; }
@Override
public String toString() {
return String.format("%.2fยฐC = %.2fยฐF = %.2fK", celsius, getFahrenheit(), getKelvin());
}
}
public class PracticalApplication {
public static void main(String[] args) {
Temperature t1 = new Temperature(100.0);
Temperature t2 = new Temperature(37.0); // Human body temp
System.out.println("=== Temperature Converter ===");
System.out.println("Boiling Point : " + t1);
System.out.println("Body Temp : " + t2);
try {
new Temperature(-300); // Below absolute zero!
} catch (IllegalArgumentException e) {
System.out.println("Physical Error: " + e.getMessage());
}
}
}
- Using
getIsActive()instead ofisActive()for boolean getters โ frameworks like Spring/Hibernate will not recognize the wrong naming. - Returning a mutable array or Date directly from a getter without defensive cloning, allowing external corruption.
- Writing
this.name = this.name;(assigning field to itself) due to missingthis.prefix on one side. - Providing setters for every field without considering whether each field should truly be mutable.
Test your understanding by writing the code directly in your editor or running in our online Java compiler:
// Coding Challenge:
// Build a BankAccount class with:
// 1. private double balance (no direct setter โ only deposit/withdraw control it).
// 2. private String accountHolder (immutable after construction).
// 3. Getter getBalance() returns balance rounded to 2 decimal places.
// 4. deposit(amount): validates amount > 0, adds to balance.
// 5. withdraw(amount): validates 0 < amount <= balance, subtracts from balance.
class BankAccount {
private final String accountHolder;
private double balance;
public BankAccount(String holder, double initialBalance) {
if (holder == null || holder.isBlank()) throw new IllegalArgumentException("Holder required");
this.accountHolder = holder.trim();
this.balance = Math.max(0, initialBalance);
}
public String getAccountHolder() { return accountHolder; }
public double getBalance() {
return Math.round(balance * 100.0) / 100.0;
}
public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive!");
balance += amount;
}
public void withdraw(double amount) {
if (amount <= 0 || amount > balance)
throw new IllegalArgumentException("Invalid withdrawal: " + amount);
balance -= amount;
}
}
public class Challenge {
public static void main(String[] args) {
BankAccount acc = new BankAccount("Priya", 1000.0);
acc.deposit(500.0);
acc.withdraw(200.0);
System.out.println(acc.getAccountHolder() + " Balance: $" + acc.getBalance());
}
}
๐ก Frequently Asked Questions & Interview Insights
โ Should I always generate getters and setters for every field?
No. Only generate them when genuinely needed. Unnecessary setters make objects mutable when they should be immutable, and unnecessary getters expose internal implementation details.
โ What is the Java Beans convention for getters and setters?
Non-boolean getters: `getFieldName()`. Boolean getters: `isFieldName()`. Setters: `setFieldName(value)`. This convention is required by frameworks like Spring, Hibernate, JSP EL, and serialization libraries.
โ Can I have a getter without a matching setter (read-only field)?
Absolutely, and it is encouraged for identity fields. Declare the field `final` in combination with only a getter โ no setter โ to create a clean, immutable public property.
๐ Quick Chapter Recap
- Getters provide controlled read access:
getField()for objects,isField()for booleans. - Setters enforce business rules: validate before accepting new values.
- Defensive copying in constructors and getters protects mutable internal state.
- Computed properties derive values from existing fields without extra backing storage.
- Not every field needs a setter โ prefer immutability for identity fields.