LLD Dojo

Objects that hold their shape · chapter 11 of 33

Encapsulation: deciding who can change your data

Chapter 2.1 · Part 2, Objects that hold their shape · about 30 minutes

What you need before this chapter: everything in Part 1, through chapter 1.10, enum and record. That means classes, objects, fields, constructors, private, and null. It also means references and interfaces. Inheritance and exceptions round out the list.

When you finish this chapter you will be able to:


1. Where chapter 1.1 left off

Chapter 1.1 built a Ticket with two public fields. It warned that this was a real flaw: any code, anywhere, could reach into a ticket and change its plate or its arrival time to nonsense. This chapter is where that gets fixed. The fix is worth building slowly, because the wrong version of it is everywhere in real code.

Start by extending the ticket from 1.1 with two more fields a parking lot actually needs: whether the ticket has been paid, and the minute the vehicle left.

INLINECODE0

FrontDesk is a separate class: a different part of the program, the way a real front-desk till would be, that closes a ticket out when a car leaves.

INLINECODE1

Compile and run it:

INLINECODE2

INLINECODE3

For one clerk closing out one ticket correctly, there is nothing wrong here. The fields hold the values you would expect, and the arithmetic is correct.

2. The new requirement, and the bug it exposes

A parking lot's front desk is not one careful person. It is whatever code path happens to call checkout, and one of those paths has a bug: a typo transposes two digits, and it passes 500 instead of 655 as the exit minute.

INLINECODE4

INLINECODE5

INLINECODE6

The program compiles, runs, and prints a negative stay. Nothing in Ticket or in FrontDesk checked that an exit minute has to be at or after the arrival minute. Nothing stopped a value that makes no physical sense. Whatever computes a fee from stayLength() next has to decide what to do with -55 minutes of parking, and every one of the choices is wrong: charge a negative fee, charge zero, or crash. The bug did not happen because FrontDesk was careless. It happened because Ticket never gave anyone a way to be careful. There was no method to call that could say no.

This is the cost that matters, and it is not really about this one typo. Ticket's two fields are public, which means every file in the program that can see a Ticket can set arrivedAtMinute and exitMinute to anything at all, independently, in any order, from any number of places. A rule like "the exit can't be before the arrival" has nowhere to live, because there is no code between "a caller decides a value" and "the field holds it." Six call sites that each set these fields correctly today are six chances for the seventh one, written in a hurry next month, to get it wrong.

3. The fix: state changes go through a method that can refuse

The rule that an exit minute can never come before the arrival minute belongs to Ticket, because Ticket is the only thing that knows both values at once. Give it a single method that is the only way a ticket's stay ever ends. Every other field becomes impossible to set from outside.

INLINECODE7

plate and arrivedAtMinute are private and final: set once, in the constructor, which is also the one place that checks they make sense. exitMinute and paid are private but not final, because they do change — once, through closeOut, and nowhere else. There is no setExitMinute and no setPaid. The only door in is the one method that enforces the rule.

INLINECODE8

INLINECODE9

The same typo that produced -55 a page ago now produces a thrown exception with a message that names the exact problem. That is the entire difference encapsulation makes. Mistakes do not stop happening. A mistake gets caught at the one place that knows it is a mistake, instead of silently becoming a number a fee calculator has to make sense of later.

That is the principle, stated plainly: a class decides, in its own methods, which changes to its own state are legal, and it refuses every other one. "Who is allowed to change this?" — nobody, directly. Code outside the class asks, through a method, and the class decides whether the answer is yes.

This is one of the four pillars object-oriented languages are usually described by. Two of the four are encapsulation and abstraction. The other two are inheritance and polymorphism. You have already met the other three without the label. Abstraction is Ticket exposing stayLength() and closeOut() without callers needing to know they are backed by two int fields. Inheritance and polymorphism are chapter 1.4's extends, super, and a call that resolves to different code depending on an object's real type at runtime. Interviewers ask for these four by name early, expecting you to place whatever you just did into one of them, so it is worth knowing which one you are looking at.

4. The version of this that is wrong

The instinctive first move, once someone is told "make the fields private," is to keep every field exactly as free to set as it was. The wiring just moves behind a getter and a setter for each one.

INLINECODE10

INLINECODE11

INLINECODE12

Same bug, same wrong number, and the fields are technically private now. This is not encapsulation. It is a public field wearing two extra method calls. A getter-setter pair that does nothing but read and write the field underneath it enforces no rule, because it does not check anything. It is the identical amount of freedom to corrupt the object, spelled with more syntax.

The test worth applying to any setter: does it check something, or does it just assign? closeOut checks two things and only then assigns. setExitMinute assigns. If you cannot name the rule a setter enforces, it is not protecting the field. It is decorating it.

The reverse mistake is just as real. A field with no rule to enforce at all, a free-text note field on an internal debugging object, say, does not need a private field and a getter and a setter either. A public field is an honest description of "there is no rule here." Wrapping it in ceremony that enforces nothing does not make the design safer. It only adds two methods a reader has to open before finding out they do nothing.

The threshold: encapsulate a field behind a method when there is a rule the method enforces, or a sequence of steps that has to happen in a fixed order. A constructor validates its arguments before storing them; closeOut refuses an exit time that precedes an arrival time. No rule and no order means there is no need to hide the field behind accessors that do nothing a public field would not.

Your turn

Add a refund() method to the working Ticket from section 3. Calling it should be allowed only before the ticket has been closed out (exitMinute still -1); calling it after closeOut has run should throw IllegalStateException with a message naming the ticket's plate. A successful refund does not set paid — a refunded ticket was never paid for.

Write it, compile it, and try both orders before reading on.

The answer.

INLINECODE13

Running refund() before closeOut() and then trying closeOut() afterward throws ticket for ... is already closed out, because refund() used the same "has this ticket already got an exit minute" check that closeOut uses. That reuse is not an accident. It is the same invariant, asked from a second direction, and it is why the check belongs to the object rather than to whichever method happens to run first.

Going deeper

private is a promise the compiler enforces at compile time. It is not a promise the runtime enforces. Seeing that once matters, because it explains why "the field is private" does not end the case for keeping data safe.

INLINECODE14

INLINECODE15

arrivedAtMinute is private and final, and this code changes it anyway, because java.lang.reflect can ask the JVM directly for a field by name and switch off the access check with setAccessible(true). javac would have refused to compile t.arrivedAtMinute = -999; written directly — that check happens at compile time, against the source you wrote. Reflection asks the runtime for the field by string name instead, and the runtime has no source to check against, only bytecode. Encapsulation, in ordinary Java, is a contract between well-behaved callers and the compiler that checks them. It stops accidents and careless code with total reliability. It does not stop code written specifically to defeat it, and no amount of private changes that.

There is a second, more common way encapsulation quietly fails, and this one has nothing to do with reflection. A getter that hands back a mutable collection field directly looks encapsulated: the field is private, and there is no setter. It is not.

INLINECODE16

INLINECODE17

INLINECODE18

getIssued() returns the ledger's own ArrayList, not a copy of it, so anything the caller does to that list happens to the ledger. Calling .clear() on what looks like a read-only query wipes every ticket the ledger has ever recorded, and nothing about the method signature warned that this was possible. The fix is to hand out a snapshot instead, with return List.copyOf(issued);. That single change turns a silent corruption into a loud, immediate refusal:

INLINECODE19

That copy is not free. lessons/A7 in this app measures it directly: List.copyOf costs 8 bytes per element plus 56 bytes of fixed overhead. That is 136 bytes for a ten-line shopping cart, and 40,056 bytes once a list runs to five thousand elements. For most getters that cost never shows up in a profile, and the copy is the right default.

The lesson from the measurement is not "never copy." A getter returning a live, mutable field is exactly as leaky as a public field. The leak stays invisible until someone calls a mutating method on what they assumed was a read-only view.

Why this matters in an interview

An interviewer designing a system with you watches every field you make public. They are asking, out loud or not, what would stop two pieces of code from disagreeing about that object's state. "It's private" is not an answer if every field still has a setter that assigns without checking anything. The interviewer has seen that pattern called encapsulation in code review, and knows it is not one. The answer they want is the one this chapter built: name the rule, put it in exactly one method, and make that method the only way in.

The reflection example is not interview material on its own. The instinct behind it is: a principle that holds "in the ordinary case" is not the same as one the language enforces absolutely. Knowing which kind you are relying on is the difference between a design decision and a hope.


Next: chapter 2.2, Immutability, and the bugs it deletes — where some of Ticket's fields stop being merely private and become impossible to change at all. A whole category of bug this chapter patched with a check disappears instead.

← 1.10 enum and record: the two types you will reach for most · All chapters · 2.2 Immutability, and the bugs it deletes →