A class can hold data in two ways: expose it directly as public fields, or hide it behind methods that read and write the data on the caller's behalf. Java code typically picks the second approach, and the read and write methods have standard names: getX and setX. This lesson covers why hiding fields behind methods is the default, how to write getters and setters cleanly, and the cases where one or both should be left out.
A Product with everything public looks tempting. It's the shortest code possible.
This works. It also fails the moment any of the following happens:
mouse.price = -5.0. Nothing stops it. The catalog now has a product with a negative price.stock should never go below zero. There is no single place to enforce that, because every caller writes directly to the field.price must change.stock recorded for an audit. There is no hook to add the logging to.A getter and a setter sidestep all of these. The field becomes private, callers go through methods, and the methods can validate, transform, log, or change their underlying storage without breaking any caller. The field is an implementation detail. The methods are the contract.
Same behavior at the call site, more flexibility internally. There's no validation yet, but the door is open. The validation section is next.
Three benefits this pattern provides:
getPrice() still returns a double, the caller doesn't care whether the storage is in cents or dollars.The standard setup is private for fields and public for accessor methods.
Java has firm conventions for accessor method names, originally formalized by the JavaBeans specification. Tools, frameworks, and IDEs all expect them.
| Field declaration | Getter name | Setter name |
|---|---|---|
private String name | getName() | setName(String name) |
private double price | getPrice() | setPrice(double price) |
private int stockCount | getStockCount() | setStockCount(int stockCount) |
private boolean available | isAvailable() | setAvailable(boolean available) |
The pattern:
get followed by the field name with its first letter uppercased.void, and is named set followed by the same uppercased field name.is instead of get. isAvailable() reads naturally inside an if: if (product.isAvailable()) { ... }. The setter still uses set.The compiler doesn't enforce these names. fetchName() and assignName(String) would compile fine. But every reflection-based framework (Spring, Jackson, Hibernate, the older JavaBeans tools), every IDE that generates accessors, and every developer reading the code expects the standard names. Deviating from them costs compatibility for no real benefit.
Putting the conventions together, here is the canonical shape of an e-commerce Product. Fields are private, accessors are public, and a small main method exercises them.
The class has a clear external surface: six methods, three pieces of data. A caller can do exactly two things with each piece of data, read it or write it, and the class controls how both operations behave. That control is what the rest of the lesson builds on.
A diagram of the same idea: the fields sit behind a wall of accessors. Callers only touch the methods, never the fields directly.
The fields live in the middle. Setters on the left write to them; getters on the right read from them. Callers stay on the outside. This separation is what people mean when they call this pattern encapsulation.
The first practical benefit of using setters is that they can reject bad input. A setter is the one and only path that writes the field, so validation placed there is impossible to bypass.
A negative price doesn't make sense. Neither does a negative stock count. An email address without an @ is almost certainly a bug. All three checks belong inside the corresponding setters.
Three details:
this.price = price; if (price < 0) throw ..., the bad value would already be in the field by the time the check ran.IllegalArgumentException. It's a standard unchecked exception meant for exactly this situation: "the caller passed an argument the method refuses to accept". The Exception Handling section covers exceptions in depth.Validation in setters runs on every single write. For hot paths that set a field millions of times per second, this might add up. For ordinary object creation and updates, the cost is negligible compared to the cost of having a corrupted object.
Validation is only useful if a rejected setter call is loud. A common mistake is to validate and then do nothing on bad input.
What's wrong with this code?
The setter returns void, so the caller has no way to know whether the assignment happened. A buggy caller passes -5.0, the setter does nothing, the caller moves on assuming the price was set, and the rest of the program runs with a stale value. The bug shows up much later, far from its source.
Fix:
Throwing is louder. The caller can't ignore it; the program either handles the exception explicitly or crashes at the exact point where the bad value showed up. That's much easier to debug than a stale field.
Not every field needs a setter. Some values are set once when the object is created and should never change. A product's permanent identifier is a good example. The system assigns it, it appears on receipts and in databases, and changing it would silently break every reference to that product elsewhere.
For a read-only field, give it a getter and skip the setter. Set the value in the constructor, then leave it alone.
Two things make productId read-only here:
setProductId method. A caller has no way to change the value after construction.final. That tells the compiler the field can only be assigned once, in the constructor. Even code inside the class can't accidentally reassign it later. final isn't strictly required for a read-only field (the absence of a setter is enough for external callers), but it's a useful belt-and-braces signal.For now, treat final as a way to say "this value is set once and never changes".
Read-only fields are common for anything that identifies an object: order IDs, customer IDs, timestamps for when something was created. Once the system assigns them, changing them would break every reference that uses them.
The opposite case, a field with only a setter and no getter, exists but is rare. The classic example is a password reset. The class accepts a new password, but no code should be able to read the stored password back out.
The class accepts a password through setPassword but never lets a caller read it back. Anything that needs to use the password (logging in, for example) does so through other methods like verify, which takes a candidate password and compares it to the stored hash without revealing it.
Write-only setters are a useful sanity check for security-sensitive data: secrets, tokens, internal keys. The pattern is rare because most fields have at least one legitimate reader. Use it only when the data should never leave the object.
A getter doesn't have to return a field directly. It can compute a value on the fly from one or more fields and return that. This is the second big reason to expose state through methods rather than fields: the caller doesn't have to know whether the value is stored or calculated.
A discounted price is a good example. The base price is stored. The discount percentage is stored. The discounted price is whatever those two add up to, computed when someone asks.
getDiscountedPrice isn't backed by a field. Each call recomputes the value. From outside the class, it looks identical to any other getter: take no arguments, return a double. The fact that it's derived is an implementation detail the caller doesn't need to know.
Computed getters are useful for:
A computed getter does its work on every call. If the computation is expensive (loops over a list, walks a tree) and gets called inside a tight loop, consider caching the result or computing it once and storing it. For arithmetic on a couple of doubles, the cost is invisible.
A setter's job is to set a field. Anything beyond that, logging, sending notifications, recalculating other fields, talking to a database, is a side effect. A small amount of side-effect logic is reasonable (the validation we already wrote is technically a side effect). A lot of it is a code smell.
Consider this overcooked setter.
Setting a price now triggers four pieces of business logic. A caller writing product.setPrice(29.99) cannot tell from the call site that they're also triggering a search rebuild. The setter has also become impossible to use in tests or in any context where those side effects shouldn't fire.
The fix isn't to add side effects to a setter; it's to move the side effects out of it.
setPrice is back to its small, predictable shape. The orchestration that wants logging and notifications has its own clearly-named method, updatePriceAndNotify, for callers that want all that to happen.
The rule of thumb: a setter should be safe to call without considering cascading effects. Light validation belongs there. Heavy work, especially anything that talks to the network, the database, or other services, does not.
Two related shapes are worth a brief mention before we wrap up.
Records are a Java feature (introduced in Java 16) for classes that exist mainly to carry data. Fields are declared once, and the compiler generates accessors automatically. The accessor naming differs from the standard convention: a record with a field name exposes it as product.name() rather than product.getName(). When a class is nothing but private fields and standard accessors, a record may be the better tool.
Immutable classes deliberately have no setters at all. Every field is final, set once in the constructor, and there's no way to change it afterward. A new value means a new object. This is the design behind String, LocalDate, and many other core Java types. The takeaway is that "always provide getters and setters" is a starting point, not a law. Sometimes the answer is no setters at all.
A more complete Product class uses every idea from this lesson: a read-only ID, validated setters for price and stock, a computed discounted price, and no surprise side effects.
The constructor calls the setters rather than assigning the fields directly. That way, the validation rules apply to construction too. A CatalogProduct can't be built with a blank name or a negative price, because the constructor would throw the same exception a later setName(null) would throw.
The class has five fields, but its external API is the set of public methods: six getters, four setters, and two boolean / computed helpers. That collection of methods is the contract. The fields behind them are an implementation detail.
5 quizzes