Encapsulation
If any part of a program can write anything into a field, an object is easy to break. Encapsulation is the rule "an object's data is changed only by the object itself". From the outside only methods are available, and they keep the data correct.
The problem with open fields
An open field is not protected from wrong values. Nothing stops you from making a balance negative:
class Account {
double balance;
}
void main() {
Account account = new Account();
account.balance = -1000;
IO.println(account.balance);
}Output:
-1000.0The private modifier
The word private in front of a field closes it: the field is visible only to code inside the class. From the outside you can work with it only through methods:
class Account {
private double balance;
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
double getBalance() {
return balance;
}
}
void main() {
Account account = new Account();
account.deposit(50);
account.deposit(-20);
IO.println(account.getBalance());
}Output:
50.0The wrong deposit is simply ignored: the deposit() method itself decides what is allowed.
Getters and setters
A method that returns a field's value is called a getter, and a method that sets it is called a setter. By convention they are named getName and setName after the field:
class User {
private String name;
String getName() {
return name;
}
void setName(String name) {
if (name.isBlank()) {
throw new IllegalArgumentException("Name cannot be empty");
}
this.name = name;
}
}
void main() {
User user = new User();
user.setName("Anna");
IO.println(user.getName());
}Output:
AnnaA setter is not a required part of a class. If a field must not change from the outside, you just do not write one.
final fields
A field marked final is assigned once — in its declaration or in the constructor — and never changes after that. This gives you objects that cannot be spoiled after creation:
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
int getX() {
return x;
}
int getY() {
return y;
}
}
void main() {
Point p = new Point(3, 4);
IO.println(p.getX() + ", " + p.getY());
}Output:
3, 4A note about the examples
In the tutorial examples all classes sit in one file, inside the implicit class Main. Because of this the compiler lets main access private fields — private members are visible within one outer class. In a real project each class lives in its own file, and a line like account.balance = -1000; causes the error balance has private access in Account. We will look at how such projects are organised in the lesson on packages.
Common mistake
A getter that returns an internal list opens the door to changes that bypass the class. The calling code gets a reference to the very same list:
class Order {
private final List<String> items = new ArrayList<>();
void add(String item) {
items.add(item);
}
List<String> getItems() {
return items;
}
}
void main() {
Order order = new Order();
order.add("Tea");
order.getItems().clear();
IO.println(order.getItems());
}Output:
[]Return an unmodifiable copy — return List.copyOf(items); — and then calling clear() from the outside throws an exception, while the order stays intact.