Encapsulation is one of the four pillars of object-oriented programming, alongside inheritance, polymorphism, and abstraction. The idea is simple to state and surprisingly hard to do well: a class should bundle its data with the operations that work on that data, and outside code should interact with it through a small, controlled set of methods instead of poking at its internals directly. This lesson covers what encapsulation is, why it exists, the basic getter and setter pattern, and how to tell the difference between encapsulation that's pulling its weight and encapsulation that's just noise.
Consider a Product class whose fields other parts of the program touch directly. Code that wants the price writes product.price. Code that wants to set the stock count writes product.stockCount = 5. The fields are public, so anyone with a reference can read or write them.
That style is convenient for a small example. It stops being convenient the moment the class needs to enforce a rule. Suppose stock count must never go negative. With public fields, every place in the codebase that writes to stockCount has to remember that rule. The class itself can't enforce anything; it has no idea when its own fields are being modified.
Encapsulation is the design choice to flip this around. The class controls its own data. Outside code can ask the class to do things, but the class decides what's allowed and how it's done. The internal fields become an implementation detail, and the public methods become the only door in or out.
A short, deliberately verbose definition is useful here:
Encapsulation is bundling the state of an object (its fields) together with the behavior that operates on that state (its methods), and exposing only what callers need while hiding what they don't.
Two words in that definition do most of the work. Bundling means the data and the operations live in the same class, not scattered across the program. Hiding means the class chooses what callers can see and change, instead of letting them reach in and grab whatever they want.
Here's the contrast in code. First, a Product class with no encapsulation:
The output is nonsense. A book with negative stock count doesn't make sense, but nothing stopped the assignment. The class had no opportunity to say "wait, that's not a legal value." Public fields make every caller a co-author of the class's correctness, and any single bug anywhere can corrupt the state.
Now the encapsulated version:
The fields are now private. Outside code can't read or write them directly. It has to go through getName, getPrice, getStockCount, and setStockCount. The class hasn't gained any rule enforcement yet, but it has gained the option to enforce rules. Every read and every write now flows through a method that the class controls. The class is no longer at the mercy of its callers.
That option, the ability to change the rules later without changing every caller, is the central benefit of encapsulation.
The "bundle" part of encapsulation gets less attention than the "hide" part, but it's just as important. A class isn't just a bag of fields; it's a unit that owns both data and the operations on that data.
Consider what happens when bundling is missing. With a cart that needs a free-shipping check (carts over $50 get free shipping), and no encapsulation, the data sits in one place and the logic sits somewhere else:
The code works, but the rule "$50 is the free-shipping threshold" lives outside the Cart class. If three more places in the codebase need the same check, they either call ShippingHelper (fine) or copy the logic (not fine). And if you ever want to add a second rule, like "free shipping requires at least one physical item," you have to find every caller and update each one.
The encapsulated version pulls the behavior into the class that owns the data:
Now the rule lives on Cart. Anyone with a Cart reference can ask the cart itself whether it qualifies. The threshold of $50 is mentioned exactly once, inside the class. If it changes to $75 tomorrow, you change one line.
This is what "bundle state with behavior" looks like in practice. The class that owns the data is the right place for the methods that interpret or mutate that data. Pulling those methods out into separate helper classes is a smell, not a pattern. It usually means the original class is anemic: a record of data without the behavior that should travel with it.
A useful test: when you find yourself writing a helper method that takes one object and a few simple values and does a calculation, ask whether that method belongs on the object itself. Most of the time, it does. The helper class is a sign that the data class is missing methods it should have had from the start.
The diagram captures the shape of an encapsulated class. The caller sends messages to the object through its public methods. The private fields are inside the object, invisible to the outside. The class owns its data and the operations on it.
The case for encapsulation rests on three concrete benefits. None of them are theoretical. They show up the first time you have to change a real codebase.
When a class's fields are public, the class's storage is part of its public API. Any code anywhere can write product.price. If you ever want to change how price is stored, say to track it as cents (an int) instead of dollars (a double) to avoid floating-point rounding, every line in the codebase that mentions product.price has to change. The compiler will find them eventually, but you've turned what should have been a one-class change into a project-wide refactor.
With encapsulation, the storage is private. The public methods are the API:
The class stores the price as an integer number of cents internally, but getPrice still returns a double in dollars. Callers never see the change. The class has freedom to evolve its internals because it controls its boundary.
This is the single biggest reason encapsulation exists in real codebases. Software changes constantly. Classes that hide their internals can evolve. Classes that expose their internals freeze the day they ship.
An invariant is a rule about a class's state that should always be true. Stock count is never negative. A cart's total is the sum of its item prices. A customer's email always contains an @. The class is the only piece of code that can guarantee these rules, because the class is the only piece of code that can intercept every change.
With public fields, the class can't enforce anything. The rule "stock count is never negative" becomes a request to every caller: "please don't set stock count below zero." Some callers will get it right. One will get it wrong. The class can't tell which.
With private fields and public setters, the class gets the chance to check:
The bad call is caught at the door. The stock count stays at 3 because the setter refused the negative value. This is a deliberately minimal example. Real codebases use exceptions instead of println messages. The point here is structural: putting the writes behind a method gives the class the ability to enforce its rules.
The third benefit is more about communication than enforcement. Public methods on a class form its API: the surface that the rest of the program is allowed to use. Anything not on that surface is an implementation detail, free to change.
When fields are public, everything is on the surface. There's no API and no implementation detail; there's just "the class." Readers of the class can't tell which fields are part of the contract and which are temporary scaffolding. Authors of the class can't tell which changes are safe and which will break callers.
When fields are private and only a handful of public methods are exposed, the class declares its boundary explicitly. The methods are the contract. The fields are scaffolding. Readers know which is which. Authors can refactor freely as long as the methods keep their behavior.
The benefits stack:
| Benefit | Without encapsulation | With encapsulation |
|---|---|---|
| Changing storage | Touches every caller | One-line change in the class |
| Enforcing rules | Hopes every caller obeys | Class intercepts every change |
| Communicating intent | Everything looks equally public | Public methods are the contract |
| Tracking down bugs | Field could be modified anywhere | Field is only written inside the class |
The fourth row is worth pausing on. If stockCount is private and only one setter writes to it, then any bug that leaves it in a weird state must be the setter's fault. With a public field, the bug could be anywhere. Encapsulation narrows the search space.
The mechanical recipe for encapsulating a single field has three parts:
private.public method named getFieldName() that returns the field's value. This is the getter.public method named setFieldName(...) that takes a new value and assigns it to the field. This is the setter.For a boolean field, the getter is conventionally named isFieldName() instead of getFieldName(). Most Java tools and libraries (including frameworks like Jackson and Spring) recognize this convention and rely on it.
A class with the pattern applied to every field:
The pattern is mechanical enough that every Java IDE generates it automatically from the field declarations. That's both a feature and a warning. The pattern is so easy to apply that learners often apply it everywhere, including places where it adds nothing but noise.
A few naming details that matter:
get or is followed by the field name with its first letter capitalized. name becomes getName, subscribedToNewsletter becomes isSubscribedToNewsletter. Frameworks that introspect Java classes (serialization libraries, ORMs, template engines) rely on this convention; deviating from it can break those tools.set followed by the field name with its first letter capitalized. setName, setEmail, setSubscribedToNewsletter.this.fieldName = fieldName to disambiguate. this tells the compiler that the left side refers to the field and the right side refers to the parameter.Getters and setters are regular method calls. The JVM inlines them after the method has been called a few times, so the runtime cost is the same as a direct field access. The "performance penalty" of encapsulation is a myth in modern Java.
The shorthand "fat object" refers to a class with too many public fields and nothing protecting them. The class is technically an object, but it offers no encapsulation; it is a data record dressed up in a class declaration. Some books call this an anemic data class or a DTO with no boundary. The exact label does not matter. The shape is the problem.
An example:
Several things are wrong with this design, and they're all symptoms of the same root cause: the class doesn't own its data.
total in sync with subtotal, tax, and shipping. The caller could (and did) overwrite total with -50.00, and the class has no opinion about it.String. Nothing prevents order.status = "banana". The set of valid statuses is implicit and unenforced.String with no shape requirement. Setting it to "not-an-email" works fine.The encapsulated version pulls these decisions into the class:
A few things changed structurally, and all of them follow from "the class owns its data":
getTotal() adds the three components. There's no field to get out of sync, no opportunity for a caller to overwrite it with a wrong value, and no risk of two fields disagreeing.markShipped() is a domain operation, not a generic setter. The class controls which status transitions are even expressible. If "placed → shipped → delivered" is the only allowed sequence, that sequence can be enforced inside markShipped() and other transition methods. The structural move is to expose meaningful operations instead of raw field writes.setOrderId(...) because changing the ID of an existing order doesn't make sense in the domain.The fat object exposes everything because that's the path of least resistance. The encapsulated object exposes only what callers actually need, and it does so through methods that match real operations on the data.
The refactor from fat object to encapsulated object is one of the most common improvements you'll make in a Java codebase. It rarely changes external behavior. It usually changes the surface area of bugs by an order of magnitude.
The mechanical getter-and-setter pattern is useful, but applied without thought it produces a different kind of fat object: one where every field has a getX and setX, the methods do nothing but pass the value through, and the result is a class that's twice as long as the public-fields version while offering exactly the same level of protection.
Here's an example of the pattern gone wrong:
If every getter just returns the field and every setter just assigns it, the class has private fields, but it has no boundary. A caller can read anything, write anything, and put the object in any state. The only thing the class is doing that public fields wouldn't do is forcing callers to type more.
This pattern is sometimes mocked as cargo-cult encapsulation. The mechanics of encapsulation are present, but the meaning is missing. The class isn't enforcing any rule, computing any derived value, or hiding any decision. It's just paying the syntactic tax of methods without getting the semantic benefit.
That said, sometimes the pattern is still worth keeping, even when the methods are pass-through today. Two reasons to defend it:
getPrice() gives the class a place to add logic later (logging, lazy computation, unit conversion, validation) without changing the call sites. With public fields, you'd have to refactor every caller. With pass-through accessors, you change one method.The honest answer is that the cost-benefit shifts depending on the situation. A short rule of thumb:
| Situation | Pass-through accessors are |
|---|---|
| The class will be serialized by a framework that requires JavaBeans | Worth it |
| The class will likely gain validation or derived logic over time | Worth it |
| The class is a small internal helper that nothing else touches | Probably noise |
| The class is a pure data carrier and you control all the callers | Consider record |
The last row points at a feature Java added precisely because pure data classes with trivial accessors were so common. A record declares fields, generates accessors, equals, hashCode, and toString automatically, and signals "this is a transparent carrier of data" without 50 lines of boilerplate. When that fits, use it. When the class needs real behavior, write the methods yourself and skip the accessors that add nothing.
Abstraction can feel close enough to encapsulation that the distinction blurs. They are related, but they're not the same thing. Both involve choosing what to expose, but they answer different questions.
Abstraction asks: what shape does this thing have? It's about defining a type, a set of operations that any concrete implementation must support. An abstract Discount class doesn't care whether a discount is percentage-based or fixed-amount; it defines the common shape so the rest of the program can work with "discounts" generically. Abstraction is about modeling at the right level: ignoring details that don't matter for the operation at hand.
Encapsulation asks: who is allowed to see and change this state? It's about controlling the boundary between a class's internals and its callers. A Cart class with private fields and a small set of public methods isn't necessarily abstract. It's just well-encapsulated: callers go through the front door, and the kitchen stays hidden.
The two often appear together. An abstract Discount class can encapsulate its internal code and description fields with private modifiers and public accessors. A concrete Cart class can encapsulate without any abstract parent. The pillars are independent in principle, even if good design tends to use them together.
A short version: abstraction is what a thing looks like from the outside; encapsulation is who's allowed to touch its insides.
Pulling the threads together, here's a Cart class that uses encapsulation deliberately. State is private. Behavior is bundled. Derived values are computed. The public surface exposes only what callers need.
Walk through the design choices:
customerName. Whether a cart can change owners after creation is a domain question. This design says no, so the field is set in the constructor and never changed.subtotal field that could disagree with the items. getSubtotal() walks the list every call. For a small cart, this is fine; for a large one, you might cache and invalidate, but that's an implementation choice the class can make later without changing its API.getTotalWithTax and qualifiesForFreeShipping use getSubtotal internally. They're methods on the class because the data they need lives on the class.addItem is a verb, not a setter. It describes a domain operation. Internally it appends to the list, but the caller doesn't need to know that.If you compare this to the fat-object Order from earlier, the difference isn't really about "private vs public." It's about whether the class owns its data and the operations that interpret it. The encapsulated Cart is a real object with real responsibilities. A fat Cart with seven public fields would be a struct with a class declaration.
10 quizzes