Learn Java

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 Updated 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.

By the end of this lesson
  • Explain why a field that any code can change is a problem, using a broken bank account.
  • Use private and public, and say which code may use a member at each of Java's four access levels.
  • Make a field private and 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.
From before
You can write a class with fields and methods (classes and objects, methods), and a constructor sets an object up the moment it is created. That is everything you need. One word in this lesson, 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.

Main.java
1public class Main {
2
3 public static void main(String[] args) {
4 BankAccount acc = new BankAccount(100.0);
5
6 acc.balance = -999.0;
7
8 System.out.println("Balance: " + acc.balance);
9 }
10}
11
12class BankAccount {
13 double balance;
14
15 BankAccount(double startingBalance) {
16 balance = startingBalance;
17 }
18}
Prints
›Balance: -999.0

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 idea

A 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.

This is not a made-up worry
The bad write does not have to be deliberate. Usually it is a typo, or a programmer at the other end of a large project who did not know the rules of your account. Locking the door makes the mistake impossible, which works far better than asking everyone to be careful.

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
private
Any code in the program can read or write it.
Only code inside the same class can read or write it.
Good for the methods you want others to call.
Good for the data you want to protect.
The front desk of the bank.
The vault behind the front desk.
Written public double balance;
Written private double balance;
Easy to use, and easy to misuse.
Locked by default, opened on purpose.

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:

ModifierIts own classSame packageSubclasses in other packagesEverywhere else
publicyesyesyesyes
protectedyesyesyesno
no modifieryesyesnono
privateyesnonono
Which code may use a field or method at each access level.

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.

Quick check
Inside BankAccount, balance is private and getBalance() is public. Which line, written in main, compiles?
Coming from Python?
Python has no enforced private. A leading underscore, as in _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.

Main.java
1public class Main {
2
3 public static void main(String[] args) {
4 BankAccount acc = new BankAccount(100.0);
5
6 acc.deposit(50.0);
7 acc.deposit(-30.0);
8
9 System.out.println("Balance: " + acc.getBalance());
10 }
11}
12
13class BankAccount {
14 private double balance;
15
16 public BankAccount(double startingBalance) {
17 balance = startingBalance;
18 }
19
20 public void deposit(double amount) {
21 if (amount > 0) {
22 balance = balance + amount;
23 } else {
24 System.out.println("Rejected: deposit must be positive.");
25 }
26 }
27
28 public double getBalance() {
29 return balance;
30 }
31}
Prints
›Rejected: deposit must be positive.
›Balance: 150.0

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:

Main.java (does not compile)
read along
1public class Main {
2 public static void main(String[] args) {
3 BankAccount acc = new BankAccount(100.0);
4
5 acc.balance = -999.0; // not allowed: balance is private
6
7 System.out.println("Balance: " + acc.getBalance());
8 }
9}
10
11class BankAccount {
12 private double balance;
13
14 public BankAccount(double openingBalance) {
15 balance = openingBalance;
16 }
17
18 public double getBalance() {
19 return balance;
20 }
21}
Output
Main.java:5: error: balance has private access in BankAccount
        acc.balance = -999.0;   // not allowed: balance is private
           ^
1 error

The compiler refuses before the program ever runs, and the message names the reason: balance has private access in BankAccount.

The playground is more lenient here
The in-browser engine that runs the examples on this page does not check 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 get followed by the field's name with a capital letter. The getter for balance is getBalance().
  • For a boolean field the getter usually starts with is instead, so a field called frozen gets isFrozen().
  • A setter is named set followed by the field's name. It takes the new value as its only parameter and returns nothing, as in void 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.

Predict the outputThermostat.java
1public class Main {
2 public static void main(String[] args) {
3 Thermostat t = new Thermostat();
4 t.setTarget(21);
5 t.setTarget(45);
6 t.setTarget(-3);
7 System.out.println("Target: " + t.getTarget());
8 }
9}
10
11class Thermostat {
12 private int target = 20;
13
14 public int getTarget() {
15 return target;
16 }
17
18 public void setTarget(int degrees) {
19 if (degrees < 10 || degrees > 30) {
20 System.out.println("Ignored " + degrees);
21 return;
22 }
23 target = degrees;
24 }
25}

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.

Open field
Private field with a setter
Changed with t.target = x;
Changed with t.setTarget(x);
Accepts any number, including nonsense.
Accepts only what the method allows.
Has no place to put a rule.
The rule lives in one spot, the method.
A bad value stays in the object until someone notices.
A bad value never gets in.

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 idea

A 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.

Put the lines in orderPerson.java
This setter must refuse a negative age. Put the lines in order so the program prints 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.

Quick check
A 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:

Main.java
1public class Main {
2 public static void main(String[] args) {
3 BankAccount a = new BankAccount(-50.0);
4 System.out.println("a: " + a.getBalance());
5
6 BankAccount b = new BankAccount(80.0);
7 b.deposit(-5.0);
8 System.out.println("b: " + b.getBalance());
9 }
10}
11
12class BankAccount {
13 private double balance;
14
15 public BankAccount(double openingBalance) {
16 if (openingBalance >= 0) {
17 balance = openingBalance;
18 } else {
19 System.out.println("Rejected: opening balance cannot be negative.");
20 }
21 }
22
23 public void deposit(double amount) {
24 if (amount > 0) {
25 balance = balance + amount;
26 } else {
27 System.out.println("Rejected: deposit must be positive.");
28 }
29 }
30
31 public double getBalance() {
32 return balance;
33 }
34}
Prints
›Rejected: opening balance cannot be negative.
›a: 0.0
›Rejected: deposit must be positive.
›b: 80.0

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 idea

An 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.

ExerciseWithdraw.java
Finish 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.

A habit worth forming now
Make fields 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.

Refusing more firmly
Printing a message is the simplest way for a method to refuse, but the code that called 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.

1
In one sentence, what does private do that public does not?
2
In the running BankAccount, after deposit(50.0) and then deposit(-30.0), what does getBalance() return, and why is the answer not 120.0?
3
A teammate wants other code to be able to read an account's balance but never to set it to any value it likes. What should they provide, and what should they leave out?
4
Why is putting the amount > 0 check inside deposit better than asking every caller to check before calling?
5
Choose an access level for each member of 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.

1
Give 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.
2
Design a 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.
3
Take the three-field 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.
4
Write one short paragraph in your own words. Suppose a 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