Encapsulation is an OOP principle that hides the state (field) of an object from the outside and makes it accessible only through designated methods (getter/setter, or domain method). This prevents objects from being broken by incorrect values and allows room to freely change the internal implementation.
Learning Objectives
- Know the difference between
public/private/ package-private access limiters - Write
privatefield + getter/setter pattern - Understand the meaning and advantages of immutable objects
- See how JDK 16+
recordreduces boilerplate - Let’s take a look at the package concept.
Key concepts
1) Access restrictor
| qualifier | same class | Same package | child class | Other packages |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
protected |
✅ | ✅ | ✅ | ❌ |
| (default) | ✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
It is recommended that most fields be left as private.
2) getter/setter
public class Account {
private long balance;
public long getBalance() { return balance; }
public void deposit(long amount) {
if (amount <= 0) throw new IllegalArgumentException("amount > 0");
this.balance += amount;
}
}
Don't necessarily create setters. If there is a separate domain action such as depositing, it is better than a setter.
3) Immutable objects
An object whose value never changes once set is called an immutable object. It is safer and easier to debug even in multithreading.
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) { this.x = x; this.y = y; }
public int getX() { return x; }
public int getY() { return y; }
}
final class + final field + no setter + value determination only in constructor.
4) record (JDK 16+)
An immutable data carrier can be defined in one line:
public record Point(int x, int y) {}
x()·equals/hashCode/toString is automatically created instead of constructor·getX().
5) Package
It is a namespace that groups classes with the same meaning. Declare it once at the top of the file, like package com.codingnow.lecture.oop07;. In this section, packages are not used to simplify the directory structure, but after Part 21, packages are automatically used along with the standard directory structure in Maven projects.
See as example
Example 1 — PrivateField.java: The first step in encapsulation
public class PrivateField {
private int age;
public int getAge() { return age; }
public void setAge(int age) {
if (age < 0) throw new IllegalArgumentException("age >= 0");
this.age = age;
}
public static void main(String[] args) {
PrivateField p = new PrivateField();
p.setAge(21);
System.out.println("age=" + p.getAge());
try {
p.setAge(-1);
} catch (IllegalArgumentException e) {
System.out.println("Reject:" + e.getMessage());
}
}
}
Execution result
age=21
Reject: age >= 0
Memo: Placing validation logic in the setter can prevent incorrect states in advance.
Example 2 — Account.java: Domain Behavior Driven
public class Account {
private long balance;
public Account(long initial) { this.balance = initial; }
public long getBalance() { return balance; }
public void deposit(long amount) {
if (amount <= 0) throw new IllegalArgumentException("amount > 0");
balance += amount;
}
public void withdraw(long amount) {
if (amount <= 0) throw new IllegalArgumentException("amount > 0");
if (amount > balance) throw new IllegalStateException("insufficient balance");
balance -= amount;
}
public static void main(String[] args) {
Account a = new Account(10_000);
a.deposit(5_000);
a.withdraw(3_000);
System.out.println("balance=" + a.getBalance());
}
}
Execution result
Balance=12000
Note: Domain methods such as deposit / withdraw are more meaningful than setters.
Example 3 — ImmutablePoint.java: immutable object
public final class ImmutablePoint {
private final int x;
private final int y;
public ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
public ImmutablePoint translate(int dx, int dy) {
return new ImmutablePoint(x + dx, y + dy);
}
public static void main(String[] args) {
ImmutablePoint p = new ImmutablePoint(1, 2);
ImmutablePoint q = p.translate(10, 20);
System.out.println("p=(" + p.getX() + "," + p.getY() + ")");
System.out.println("q=(" + q.getX() + "," + q.getY() + ")");
}
}
Execution result
p=(1,2)
q=(11,22)
Note: The usage pattern for immutable objects is to return a new object instead of changing state.
Example 4 — PointRecord.java : One line as record
public class PointRecord {
public record Point(int x, int y) {}
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println(a);
System.out.println("a.x = " + a.x());
System.out.println("a.equals(b) = " + a.equals(b));
}
}
Execution result
Point[x=1, y=2]
a.x = 1
a.equals(b) = true
Memo: record automatically creates toString / equals / hashCode.
Common Mistakes
- Create setters for all fields → Think about meaningful domain methods first
- Intended to be immutable, but internal collection is exposed as is (shallow immutability)
- Receiving variable object fields as is within
record(defensive copy required) - Overuse of
publicfield → Encapsulation collapse - Missing validation in setter
Cleanup
- The field is first
private, and the path to the outside is method - Immutable objects are safe and simple
recordreduces boilerplate of data classes
Try it yourself
cd 02_Object-Oriented/07_Encapsulation/src
javac Account.java
java Account