Lesson 14Phase 2: Objects
Access modifiers and encapsulation
Hiding the inside of an object, why private fields plus methods beats exposed data, and getters and setters.
By BasithUpdated 55 min
- private
- public
- protected
- encapsulation
- data hiding
- getters
When you first made objects, any line of code could reach into one and change its fields. A program could write account.balance = -999, and the account would accept it. This lesson closes that door. You will lock an object's fields away and give the object a few methods that guard them, so it can refuse a bad value. That arrangement is called encapsulation, and most classes you write from now on will use it.
- Explain why a field that any code can change is a problem, using a broken bank account.
- Use
privateandpublic, and say which code may use a member at each of Java's four access levels. - Make a field
privateand reach it through a getter and a setter with the usual names. - Write a setter and a constructor that refuse bad input, so an object never holds a nonsense value.
- Say what an invariant is, and which parts of a class must protect it.
- Explain encapsulation, and why hiding an object's inside lets you change it later.
protected, only makes full sense once one class can build on another, so it is only named here and explained in inheritance.The open door we left behind#
Here is a small BankAccount. It has a balance, and a constructor that sets the starting amount. Read it, then look at what main does on the line before it prints.
public class Main { public static void main(String[] args) { BankAccount acc = new BankAccount(100.0); acc.balance = -999.0; System.out.println("Balance: " + acc.balance); }} class BankAccount { double balance; BankAccount(double startingBalance) { balance = startingBalance; }}It prints Balance: -999.0. The account now holds an amount that should be impossible: no bank lets a balance of minus 999 appear out of nowhere. Nothing stopped it, because the field balance is open, so any code in the program can reach in with a dot and write whatever it likes. The careful constructor does not help if the very next line can overwrite its work.
Key ideaA field that anyone can write to is a field that anyone can break. An object cannot protect itself while its insides are open to the whole program.
public and private: who is allowed in#
Java lets you put a word in front of a field or a method that controls which code may use it. These words are called access modifiers, because they set who has access. Two of them matter most.
public means “any code, anywhere, may use this”. privatemeans “only code written inside this same class may use this”. From outside the class, a private field might as well not exist.
public double balance;private double balance;Fields with no access word#
The fields you have written so far had neither word in front. A field or method with no access modifier gets package access: any class in the same package may use it. A package is a named group of classes, and you will sort your own code into packages in packages and imports. Every class in one of these programs sits in the same package, so in practice any class could use those fields. That is how main was able to write acc.balance.
With the fourth modifier, protected, that makes four levels of access:
| Modifier | Its own class | Same package | Subclasses in other packages | Everywhere else |
|---|---|---|---|---|
public | yes | yes | yes | yes |
protected | yes | yes | yes | no |
| no modifier | yes | yes | no | no |
private | yes | no | no | no |
A subclass is a class built on top of another one, which is what inheritance is about, and that is where protected earns its place. Until then, one rule serves you well: make fields private, and make the methods that other classes need public.
BankAccount, balance is private and getBalance() is public. Which line, written in main, compiles?_balance, is a polite request that other code stay out, and nothing stops it. In Java the compiler checks private: code outside the class that names a private field does not compile.Locking the field, then opening a window#
Now make balance private. Straight away the line acc.balance = -999.0 stops compiling, because it sits in main, which is in a different class. But reading the balance is blocked too, and reading it is perfectly reasonable. Locking the field shut went further than we wanted. We only meant to control how the balance changes.
The fix is to keep the field private and add small public methods that are the only ways in and out. A method that hands back a field's value is called a getter. A method that changes a field, on the object's own terms, is called a setter. The object sets the rules, and other code goes through these methods or not at all.
public class Main { public static void main(String[] args) { BankAccount acc = new BankAccount(100.0); acc.deposit(50.0); acc.deposit(-30.0); System.out.println("Balance: " + acc.getBalance()); }} class BankAccount { private double balance; public BankAccount(double startingBalance) { balance = startingBalance; } public void deposit(double amount) { if (amount > 0) { balance = balance + amount; } else { System.out.println("Rejected: deposit must be positive."); } } public double getBalance() { return balance; }}Step through it and watch balance in the memory view. The constructor sets it to 100.0. The first deposit(50.0) passes the amount > 0 check, so the balance becomes 150.0. The second call, deposit(-30.0), fails the check: the account leaves its balance alone, prints a refusal and returns. So getBalance() hands back 150.0, and the last line prints Balance: 150.0. The bad amount never reached the field.
And here is what Java says about the old trick, now that the field is private:
public class Main { public static void main(String[] args) { BankAccount acc = new BankAccount(100.0); acc.balance = -999.0; // not allowed: balance is private System.out.println("Balance: " + acc.getBalance()); }} class BankAccount { private double balance; public BankAccount(double openingBalance) { balance = openingBalance; } public double getBalance() { return balance; }}Main.java:5: error: balance has private access in BankAccount
acc.balance = -999.0; // not allowed: balance is private
^
1 errorThe compiler refuses before the program ever runs, and the message names the reason: balance has private access in BankAccount.
private, so in the playground that line would run. A real Java compiler always refuses it, as above. What the engine does run faithfully is the check inside deposit, and that check is what keeps the balance sensible.Getters and setters#
Getters and setters follow naming habits that nearly all Java code shares:
- A getter is named
getfollowed by the field's name with a capital letter. The getter forbalanceisgetBalance(). - For a
booleanfield the getter usually starts withisinstead, so a field calledfrozengetsisFrozen(). - A setter is named
setfollowed by the field's name. It takes the new value as its only parameter and returns nothing, as invoid setTarget(int degrees).
These names are a convention, not a rule of the language, but many tools and libraries look for them, so it pays to stick to them.
A setter is the place where an object can refuse a value. This thermostat accepts targets from 10 to 30 degrees and ignores anything else. Decide what it prints before you run it.
public class Main { public static void main(String[] args) { Thermostat t = new Thermostat(); t.setTarget(21); t.setTarget(45); t.setTarget(-3); System.out.println("Target: " + t.getTarget()); }} class Thermostat { private int target = 20; public int getTarget() { return target; } public void setTarget(int degrees) { if (degrees < 10 || degrees > 30) { System.out.println("Ignored " + degrees); return; } target = degrees; }}The thermostat starts at 20 degrees and is given three new targets. What does the program print?
It prints Ignored 45, then Ignored -3, then Target: 21. The first call passes the check and stores 21. The other two fail it, print a message and return before they reach the line that stores the value, so target stays at 21. Open the program and step through it: the narration shows the check coming out false once and true twice.
A setter checks before it stores#
A setter is an ordinary method, so it can think before it writes. Compare the two ways of changing a value. The open field trusts every caller; the guarded method accepts a value only after it passes the check.
t.target = x;t.setTarget(x);Notice where the rule lives: once, inside the class, next to the data it protects. No code can change the target without going through the check, because the setter is the only way in. If you later decide the limit should be 28 degrees, you change one line in setTarget, and every part of the program follows the new rule without any change to the code that calls it.
Key ideaA getter lets other code read a value. A setter lets other code ask to change it. Because the setter is a method, the object decides whether the change is allowed.
Rejected: -4 and then Age: 30.Tap a line, then tap where it should go. Or use the arrows.
Invariants: rules that always hold#
A bank account has a rule that should be true at every moment: the balance is never negative. A rule like that, which must always hold for an object, is called an invariant. Private fields are how a class keeps its invariants. Only code inside BankAccount can change the balance, so that code is the only code you have to check.
Every way into the object has to respect the rule, and that includes the constructor. Try this one.
BankAccount keeps balance private, and its deposit and withdraw methods both check their amounts. Its constructor stores whatever opening balance it is given. Can an account ever hold a negative balance?So the constructor checks as well. Here the account refuses a negative opening balance, and the balance keeps its default of 0.0:
public class Main { public static void main(String[] args) { BankAccount a = new BankAccount(-50.0); System.out.println("a: " + a.getBalance()); BankAccount b = new BankAccount(80.0); b.deposit(-5.0); System.out.println("b: " + b.getBalance()); }} class BankAccount { private double balance; public BankAccount(double openingBalance) { if (openingBalance >= 0) { balance = openingBalance; } else { System.out.println("Rejected: opening balance cannot be negative."); } } public void deposit(double amount) { if (amount > 0) { balance = balance + amount; } else { System.out.println("Rejected: deposit must be positive."); } } public double getBalance() { return balance; }}It prints the first refusal, then a: 0.0, then the deposit refusal, then b: 80.0. Account a was never negative, not even for a moment: the constructor refused before storing anything.
Key ideaAn invariant is a rule about an object's fields that must always be true. The constructor makes it true at the start, and every method that changes the fields keeps it true.
withdraw so that the account keeps its invariant. An amount that is not above zero is refused with Rejected: amount must be positive., an amount larger than the balance is refused with Rejected: not enough money., and any other amount is taken off the balance.What encapsulation means#
You now have the whole idea, and it has a name. Encapsulationis keeping an object's data together with the methods that guard it, and hiding the data behind those methods. Hiding the fields themselves is often called data hiding. The word encapsulation comes from capsule: the important part is sealed inside, and other code deals only with the surface the object chooses to show. The balance is inside the capsule; deposit, withdraw and getBalance are the surface.
Two payoffs make this worth the extra typing. The first you have seen: the object can never be put into a state that makes no sense, because every path to its data runs through a method that checks. The second matters more as programs grow. Because other code only ever uses the public methods, you are free to change everything behind those methods later, and no caller notices.
private by reflex, the moment you declare them. Then open exactly the methods other code needs with public. Starting locked and opening on purpose is far safer than starting open and trying to remember to lock up later.Constructors and encapsulation work as a team. The constructor makes sure an object starts in a valid state, and the private fields and guarding methods make sure it stays valid for the rest of its life.
deposit never finds out that anything went wrong. The usual Java way to refuse is to throw an exception. You will use one in testing your code with JUnit, and learn them in full in exceptions.Try it
Answer each one in your head or on paper first, then open the answer.
private do that public does not?BankAccount, after deposit(50.0) and then deposit(-30.0), what does getBalance() return, and why is the answer not 120.0?amount > 0 check inside deposit better than asking every caller to check before calling?BankAccount: the balance field, the deposit method, and a helper method isValidAmount(double amount) that only deposit and withdraw call.Practice
Work through these on your own. Each one builds on this page.
BankAccount a boolean frozen field with a getter isFrozen() and two methods, freeze() and unfreeze(). While an account is frozen, deposit and withdraw refuse every amount and print a message. Test it: deposit, freeze, try to deposit, unfreeze, deposit again, and check that the balance only changed when it should.Temperature class with a private field for degrees Celsius. Write a constructor and a setter that refuse any value below -273.15, since nothing can be colder than absolute zero. Add a getter. In a sentence or two, say what invariant your class protects and which methods protect it.Student from constructors and encapsulate it: make name, age and gpa private, add a getter for each, and add setters that refuse an age below 0 and a gpa outside 0.0 to 4.0. Then make the constructor call the setters, so the checks live in one place.BankAccount exposed its balance as a public field and code all over a large program read it directly. Later you want to store the balance as an int number of cents instead of a double number of dollars. Why is that change painful with a public field, and why would a private field with a getBalance() method have made it easy?Further reading#
Spotted a mistake, or a part that did not make sense? Report it by email