Object-Oriented Programming Basics

Getters, Setters & Validation

15 min Lesson 5 of 14

Getters, Setters & Validation

In the previous lesson you made fields private using encapsulation. That raises an obvious question: if the fields are private, how does outside code read or change them? The answer is accessor methods (getters) and mutator methods (setters). More importantly, setters give you a single place to enforce rules — so a BankAccount can never have a negative balance, and a Person can never have a blank name.

What Is a Getter?

A getter is a public method that reads and returns a private field. By convention its name starts with get followed by the field name in PascalCase:

public class Person { private String name; private int age; // getter for name public String getName() { return name; } // getter for age public int getAge() { return age; } }

Callers write person.getName() instead of person.name. That looks like extra ceremony, but it pays off the moment you need to change how the value is stored or computed — only the getter changes, not every caller.

Boolean getters use is. For boolean fields, convention replaces get with is: isActive(), isEmpty(), isVerified(). Many tools (IDEs, frameworks, JSON libraries) rely on this naming convention.

What Is a Setter?

A setter is a public void method that accepts a new value and assigns it to the private field. Its name starts with set:

public class Person { private String name; private int age; public void setName(String name) { this.name = name; } public void setAge(int age) { this.age = age; } }

Right now these setters blindly accept anything. That is not much better than a public field. The real power comes when you add validation.

Adding Validation Inside Setters

Because the setter is the only route into a private field, every piece of code that changes the field automatically goes through your validation. You write the rule once and it is always enforced:

public class Person { private String name; private int age; public void setName(String name) { if (name == null || name.isBlank()) { throw new IllegalArgumentException("Name cannot be blank."); } this.name = name.trim(); } public void setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("Age must be between 0 and 150."); } this.age = age; } public String getName() { return name; } public int getAge() { return age; } }

Now any caller that tries person.setAge(-5) gets an IllegalArgumentException immediately — not a mysterious bug somewhere else in the program.

Throw early, fail loud. Throwing an exception inside a setter the moment bad data arrives is far easier to debug than letting invalid data silently corrupt your object and cause a failure five method calls later.

Calling the Setter From the Constructor

You already know that constructors initialise fields. A common mistake is to assign fields directly inside the constructor, bypassing validation. Instead, call your own setters so the same rules apply:

public class Person { private String name; private int age; public Person(String name, int age) { setName(name); // reuses the setter's validation setAge(age); } public void setName(String name) { if (name == null || name.isBlank()) { throw new IllegalArgumentException("Name cannot be blank."); } this.name = name.trim(); } public void setAge(int age) { if (age < 0 || age > 150) { throw new IllegalArgumentException("Age must be between 0 and 150."); } this.age = age; } public String getName() { return name; } public int getAge() { return age; } }

With this design, creating a new Person("", 25) throws immediately — invalid state can never even be constructed.

Read-Only Fields

Sometimes a field should be set once during construction and never changed afterward. Provide a getter but no setter. You can also mark the field final to make the compiler enforce this:

public class BankAccount { private final String accountNumber; // set once, never changed private double balance; public BankAccount(String accountNumber, double initialBalance) { if (accountNumber == null || accountNumber.isBlank()) { throw new IllegalArgumentException("Account number is required."); } this.accountNumber = accountNumber; setBalance(initialBalance); } // getter only — no setter public String getAccountNumber() { return accountNumber; } public double getBalance() { return balance; } public void setBalance(double balance) { if (balance < 0) { throw new IllegalArgumentException("Balance cannot be negative."); } this.balance = balance; } }

Because accountNumber is final, the compiler refuses to compile any code that tries to reassign it — even inside the class itself.

Do not provide setters for everything automatically. Many IDEs offer a "generate getters and setters" shortcut. Use it thoughtfully. Adding a setter for every field defeats the purpose of encapsulation. Ask: should this field ever change after construction? If not, omit the setter.

Putting It All Together

Here is a short program that exercises the BankAccount class above:

public class Main { public static void main(String[] args) { BankAccount account = new BankAccount("ACC-001", 500.00); System.out.println(account.getAccountNumber()); // ACC-001 System.out.println(account.getBalance()); // 500.0 account.setBalance(750.00); System.out.println(account.getBalance()); // 750.0 // This line would throw IllegalArgumentException at runtime: // account.setBalance(-100); } }

Summary

Getters expose private field values without handing over direct control. Setters are the single entry point for changes, which means validation lives in one place and applies every time. Calling setters from constructors ensures an object is valid from the moment it is created. Read-only fields use a getter without a setter, and marking them final gets the compiler to enforce immutability for you.